diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index 2c76c5fc..8fd63aef 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -124,6 +124,12 @@ export default function BotForm({ const currentAdapter = form.watch('adapter'); const currentAdapterConfig = form.watch('adapter_config'); + // Serialize adapter_config to a stable string so it can be used as a + // useEffect dependency without triggering on every render. form.watch() + // returns a new object reference each time, which would otherwise cause + // the filtering effect below to loop indefinitely. + const adapterConfigJson = JSON.stringify(currentAdapterConfig); + useEffect(() => { setBotFormValues(); }, []); @@ -147,7 +153,7 @@ export default function BotForm({ // For non-Lark adapters, show all fields setFilteredDynamicFormConfigList(dynamicFormConfigList); } - }, [currentAdapter, currentAdapterConfig, dynamicFormConfigList]); + }, [currentAdapter, adapterConfigJson, dynamicFormConfigList]); // 复制到剪贴板的辅助函数 - 使用页面上的真实input元素 const copyToClipboard = () => { diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx index 4d9532eb..b7319529 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx @@ -11,7 +11,7 @@ import { FormMessage, } from '@/components/ui/form'; import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent'; -import { useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { extractI18nObject } from '@/i18n/I18nProvider'; export default function DynamicFormComponent({ @@ -146,34 +146,39 @@ export default function DynamicFormComponent({ const onSubmitRef = useRef(onSubmit); onSubmitRef.current = onSubmit; - // 监听表单值变化 - useEffect(() => { - // Emit initial form values immediately so the parent always has a valid snapshot, - // even if the user saves without modifying any field. - // form.watch(callback) only fires on subsequent changes, not on mount. + // Track the last emitted values to avoid emitting identical snapshots, + // which would cause the parent to call setValue with an equivalent object, + // triggering a re-render loop. + const lastEmittedRef = useRef(''); + + const emitValues = useCallback(() => { const formValues = form.getValues(); - const initialFinalValues = itemConfigList.reduce( + const finalValues = itemConfigList.reduce( (acc, item) => { acc[item.name] = formValues[item.name] ?? item.default; return acc; }, {} as Record, ); - onSubmitRef.current?.(initialFinalValues); + const serialized = JSON.stringify(finalValues); + if (serialized !== lastEmittedRef.current) { + lastEmittedRef.current = serialized; + onSubmitRef.current?.(finalValues); + } + }, [form, itemConfigList]); + + // 监听表单值变化 + useEffect(() => { + // Emit initial form values immediately so the parent always has a valid snapshot, + // even if the user saves without modifying any field. + // form.watch(callback) only fires on subsequent changes, not on mount. + emitValues(); const subscription = form.watch(() => { - const formValues = form.getValues(); - const finalValues = itemConfigList.reduce( - (acc, item) => { - acc[item.name] = formValues[item.name] ?? item.default; - return acc; - }, - {} as Record, - ); - onSubmitRef.current?.(finalValues); + emitValues(); }); return () => subscription.unsubscribe(); - }, [form, itemConfigList]); + }, [form, itemConfigList, emitValues]); return (