From 10c716be0c4cde1923efab1a4a37ebbf23f697d6 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 13 Mar 2026 11:47:31 +0800 Subject: [PATCH] fix: bad model field ref --- src/langbot/pkg/api/http/service/model.py | 15 +++++--- .../templates/default-pipeline-config.json | 5 ++- tests/unit_tests/pipeline/conftest.py | 4 +-- .../dynamic-form/DynamicFormComponent.tsx | 36 +++++++++++++++++-- .../dynamic-form/DynamicFormItemComponent.tsx | 29 ++++++++++++--- 5 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/langbot/pkg/api/http/service/model.py b/src/langbot/pkg/api/http/service/model.py index 15d31f6e..f10dcd02 100644 --- a/src/langbot/pkg/api/http/service/model.py +++ b/src/langbot/pkg/api/http/service/model.py @@ -105,11 +105,16 @@ class LLMModelsService: ) ) pipeline = result.first() - if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '': - pipeline_config = pipeline.config - pipeline_config['ai']['local-agent']['model'] = model_data['uuid'] - pipeline_data = {'config': pipeline_config} - await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data) + if pipeline is not None: + model_config = pipeline.config.get('ai', {}).get('local-agent', {}).get('model', {}) + if not model_config.get('primary', ''): + pipeline_config = pipeline.config + pipeline_config['ai']['local-agent']['model'] = { + 'primary': model_data['uuid'], + 'fallbacks': [], + } + pipeline_data = {'config': pipeline_config} + await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data) return model_data['uuid'] diff --git a/src/langbot/templates/default-pipeline-config.json b/src/langbot/templates/default-pipeline-config.json index 1c6fdb64..be0b6ef8 100644 --- a/src/langbot/templates/default-pipeline-config.json +++ b/src/langbot/templates/default-pipeline-config.json @@ -41,7 +41,10 @@ "runner": "local-agent" }, "local-agent": { - "model": "", + "model": { + "primary": "", + "fallbacks": [] + }, "max-round": 10, "prompt": [ { diff --git a/tests/unit_tests/pipeline/conftest.py b/tests/unit_tests/pipeline/conftest.py index 6cf7385c..a10e0aba 100644 --- a/tests/unit_tests/pipeline/conftest.py +++ b/tests/unit_tests/pipeline/conftest.py @@ -194,7 +194,7 @@ def sample_query(sample_message_chain, sample_message_event, mock_adapter): pipeline_config={ 'ai': { 'runner': {'runner': 'local-agent'}, - 'local-agent': {'model': 'test-model-uuid', 'prompt': 'test-prompt'}, + 'local-agent': {'model': {'primary': 'test-model-uuid', 'fallbacks': []}, 'prompt': 'test-prompt'}, }, 'output': {'misc': {'at-sender': False, 'quote-origin': False}}, 'trigger': {'misc': {'combine-quote-message': False}}, @@ -219,7 +219,7 @@ def sample_pipeline_config(): return { 'ai': { 'runner': {'runner': 'local-agent'}, - 'local-agent': {'model': 'test-model-uuid', 'prompt': 'test-prompt'}, + 'local-agent': {'model': {'primary': 'test-model-uuid', 'fallbacks': []}, 'prompt': 'test-prompt'}, }, 'output': {'misc': {'at-sender': False, 'quote-origin': False}}, 'trigger': {'misc': {'combine-quote-message': False}}, diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx index 7ebfcec1..93cb30dd 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx @@ -34,6 +34,35 @@ export default function DynamicFormComponent({ const previousInitialValues = useRef(initialValues); const { t } = useTranslation(); + // Normalize a form value according to its field type. + // This ensures legacy/malformed data (e.g. a plain string for + // model-fallback-selector) is coerced to the expected shape + // so that downstream components never crash. + const normalizeFieldValue = ( + item: IDynamicFormItemSchema, + value: unknown, + ): unknown => { + if (item.type === 'model-fallback-selector') { + if (value != null && typeof value === 'object' && !Array.isArray(value)) { + const obj = value as Record; + return { + primary: typeof obj.primary === 'string' ? obj.primary : '', + fallbacks: Array.isArray(obj.fallbacks) + ? (obj.fallbacks as unknown[]).filter( + (v): v is string => typeof v === 'string', + ) + : [], + }; + } + // Legacy string format or any other unexpected type + return { + primary: typeof value === 'string' ? value : '', + fallbacks: [], + }; + } + return value; + }; + // 根据 itemConfigList 动态生成 zod schema const formSchema = z.object( itemConfigList.reduce( @@ -116,10 +145,10 @@ export default function DynamicFormComponent({ resolver: zodResolver(formSchema), defaultValues: itemConfigList.reduce((acc, item) => { // 优先使用 initialValues,如果没有则使用默认值 - const value = initialValues?.[item.name] ?? item.default; + const rawValue = initialValues?.[item.name] ?? item.default; return { ...acc, - [item.name]: value, + [item.name]: normalizeFieldValue(item, rawValue), }; }, {} as FormValues), }); @@ -144,7 +173,8 @@ export default function DynamicFormComponent({ // 合并默认值和初始值 const mergedValues = itemConfigList.reduce( (acc, item) => { - acc[item.name] = initialValues[item.name] ?? item.default; + const rawValue = initialValues[item.name] ?? item.default; + acc[item.name] = normalizeFieldValue(item, rawValue) as object; return acc; }, {} as Record, diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx index a00e4294..0b4dd6f0 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx @@ -348,10 +348,31 @@ export default function DynamicFormItemComponent({ {} as Record, ); - const modelValue = field.value as { - primary: string; - fallbacks: string[]; - }; + const rawModelValue = field.value; + const modelValue: { primary: string; fallbacks: string[] } = + rawModelValue != null && + typeof rawModelValue === 'object' && + !Array.isArray(rawModelValue) + ? { + primary: + typeof (rawModelValue as Record).primary === + 'string' + ? ((rawModelValue as Record) + .primary as string) + : '', + fallbacks: Array.isArray( + (rawModelValue as Record).fallbacks, + ) + ? ( + (rawModelValue as Record) + .fallbacks as unknown[] + ).filter((v): v is string => typeof v === 'string') + : [], + } + : { + primary: typeof rawModelValue === 'string' ? rawModelValue : '', + fallbacks: [], + }; const renderModelSelect = ( value: string,