@@ -14,7 +14,7 @@ import {
1414 FormMessage ,
1515} from '@/components/ui/form'
1616import { channelCreateSchema } from '@/validation/channel'
17- import { useChannelTypeMetas , useCreateChannel , useUpdateChannel , useUpdateChannelStatus , useTestChannelPreviewAll , useChannelDefaultModels } from '../hooks'
17+ import { useChannelTypeMetas , useCreateChannel , useUpdateChannel , useUpdateChannelStatus , useTestChannel , useTestChannelPreviewAll , useChannelDefaultModels } from '../hooks'
1818import { useModels } from '@/feature/model/hooks'
1919import { useTranslation } from 'react-i18next'
2020import { ChannelCreateForm } from '@/validation/channel'
@@ -33,6 +33,62 @@ import { DefaultModelsDialog } from './DefaultModelsDialog'
3333import { ChannelConfigEditor } from './ChannelConfigEditor'
3434import { useRuntimeMetrics } from '@/feature/monitor/runtime-hooks'
3535import { getChannelModelMetric } from '@/utils/runtime-metrics'
36+ import { DEFAULT_PRIORITY } from '@/types/channel'
37+
38+ type ComparableChannelPayload = {
39+ type : number
40+ name : string
41+ key : string
42+ base_url : string
43+ proxy_url : string
44+ models : string [ ]
45+ model_mapping : Record < string , string >
46+ sets : string [ ]
47+ priority : number
48+ enabled_no_permission_ban : boolean
49+ warn_error_rate ?: number
50+ max_error_rate ?: number
51+ configs ?: Record < string , unknown >
52+ }
53+
54+ const stableSerialize = ( value : unknown ) : string => {
55+ if ( Array . isArray ( value ) ) {
56+ return `[${ value . map ( stableSerialize ) . join ( ',' ) } ]`
57+ }
58+
59+ if ( value && typeof value === 'object' ) {
60+ const entries = Object . entries ( value as Record < string , unknown > )
61+ . sort ( ( [ left ] , [ right ] ) => left . localeCompare ( right ) )
62+ . map ( ( [ key , nestedValue ] ) => `${ JSON . stringify ( key ) } :${ stableSerialize ( nestedValue ) } ` )
63+ return `{${ entries . join ( ',' ) } }`
64+ }
65+
66+ return JSON . stringify ( value ) ?? 'undefined'
67+ }
68+
69+ const normalizeChannelPayload = (
70+ payload : Partial < ComparableChannelPayload > & {
71+ type : number
72+ name : string
73+ key : string
74+ }
75+ ) : ComparableChannelPayload => ( {
76+ type : payload . type ,
77+ name : payload . name ,
78+ key : payload . key ,
79+ base_url : payload . base_url ?? '' ,
80+ proxy_url : payload . proxy_url ?? '' ,
81+ models : payload . models ?? [ ] ,
82+ model_mapping : payload . model_mapping ?? { } ,
83+ sets : payload . sets ?? [ ] ,
84+ priority : payload . priority ?? DEFAULT_PRIORITY ,
85+ enabled_no_permission_ban : payload . enabled_no_permission_ban ?? false ,
86+ warn_error_rate : payload . warn_error_rate ?? undefined ,
87+ max_error_rate : payload . max_error_rate && payload . max_error_rate > 0
88+ ? payload . max_error_rate
89+ : undefined ,
90+ configs : payload . configs ?? undefined ,
91+ } )
3692
3793interface ChannelFormProps {
3894 mode ?: 'create' | 'update' | 'copy'
@@ -114,15 +170,24 @@ export function ChannelForm({
114170 const { updateStatus, isLoading : isStatusUpdating } = useUpdateChannelStatus ( )
115171
116172 // Test channel hook
173+ const {
174+ testChannel : testSavedChannel ,
175+ cancelTest : cancelSavedChannelTest ,
176+ isTesting : isSavedChannelTesting ,
177+ results : savedChannelTestResults ,
178+ clearResults : clearSavedChannelTestResults
179+ } = useTestChannel ( )
180+
117181 const {
118182 testChannelPreviewAll,
119- cancelTest,
120- isTesting,
121- results : testResults ,
122- clearResults : clearTestResults
183+ cancelTest : cancelPreviewChannelTest ,
184+ isTesting : isPreviewChannelTesting ,
185+ results : previewChannelTestResults ,
186+ clearResults : clearPreviewChannelTestResults
123187 } = useTestChannelPreviewAll ( )
124188
125189 const [ testDialogOpen , setTestDialogOpen ] = useState ( false )
190+ const [ activeTestMode , setActiveTestMode ] = useState < 'saved' | 'preview' | null > ( null )
126191
127192 useEffect ( ( ) => {
128193 setCurrentStatus ( channel ?. status ?? 1 )
@@ -132,6 +197,8 @@ export function ChannelForm({
132197 const isLoading = isCreateLikeMode ? isCreating : isUpdating
133198 const error = isCreateLikeMode ? createError : updateError
134199 const clearError = isCreateLikeMode ? clearCreateError : clearUpdateError
200+ const isTesting = isSavedChannelTesting || isPreviewChannelTesting
201+ const testResults = activeTestMode === 'saved' ? savedChannelTestResults : previewChannelTestResults
135202
136203 // 表单设置
137204 const form = useForm < ChannelCreateForm > ( {
@@ -281,6 +348,49 @@ export function ChannelForm({
281348 }
282349 }
283350
351+ const isChannelFormUnchanged = (
352+ formData : ChannelCreateForm ,
353+ parsedConfigs ?: Record < string , unknown >
354+ ) => {
355+ if ( mode !== 'update' || ! channel ) {
356+ return false
357+ }
358+
359+ const currentPayload = normalizeChannelPayload ( {
360+ type : formData . type ,
361+ name : formData . name ,
362+ key : formData . key ,
363+ base_url : formData . base_url || '' ,
364+ proxy_url : formData . proxy_url || '' ,
365+ models : effectiveUseDefault ? [ ] : ( formData . models || [ ] ) ,
366+ model_mapping : effectiveUseDefault ? { } : ( formData . model_mapping || { } ) ,
367+ sets : formData . sets || [ ] ,
368+ priority : formData . priority ,
369+ enabled_no_permission_ban : formData . enabled_no_permission_ban ?? false ,
370+ warn_error_rate : formData . warn_error_rate ,
371+ max_error_rate : formData . max_error_rate ,
372+ configs : parsedConfigs ,
373+ } )
374+
375+ const originalPayload = normalizeChannelPayload ( {
376+ type : channel . type ,
377+ name : channel . name ,
378+ key : channel . key ,
379+ base_url : channel . base_url || '' ,
380+ proxy_url : channel . proxy_url || '' ,
381+ models : channel . models || [ ] ,
382+ model_mapping : channel . model_mapping || { } ,
383+ sets : channel . sets || [ ] ,
384+ priority : channel . priority ,
385+ enabled_no_permission_ban : channel . enabled_no_permission_ban ?? false ,
386+ warn_error_rate : channel . warn_error_rate ,
387+ max_error_rate : channel . max_error_rate ,
388+ configs : channel . configs || undefined ,
389+ } )
390+
391+ return stableSerialize ( currentPayload ) === stableSerialize ( originalPayload )
392+ }
393+
284394 // 处理测试按钮点击
285395 const handleTestClick = ( ) => {
286396 const formData = form . getValues ( )
@@ -329,9 +439,18 @@ export function ChannelForm({
329439 }
330440 }
331441
332- clearTestResults ( )
333442 setTestDialogOpen ( true )
443+ const useSavedChannelTest = mode === 'update' && ! ! channelId && isChannelFormUnchanged ( formData , parsedConfigs )
334444
445+ if ( useSavedChannelTest && channelId ) {
446+ setActiveTestMode ( 'saved' )
447+ clearSavedChannelTestResults ( )
448+ testSavedChannel ( channelId )
449+ return
450+ }
451+
452+ setActiveTestMode ( 'preview' )
453+ clearPreviewChannelTestResults ( )
335454 testChannelPreviewAll ( {
336455 type : formData . type ,
337456 key : formData . key ,
@@ -346,7 +465,11 @@ export function ChannelForm({
346465
347466 // 处理取消测试
348467 const handleCancelTest = ( ) => {
349- cancelTest ( )
468+ if ( activeTestMode === 'saved' ) {
469+ cancelSavedChannelTest ( )
470+ } else {
471+ cancelPreviewChannelTest ( )
472+ }
350473 setTestDialogOpen ( false )
351474 }
352475
0 commit comments