diff --git a/ui/src/workflow-canvas/config/node-data.ts b/ui/src/workflow-canvas/config/node-data.ts index c6c779824f4..b377bb5a13f 100644 --- a/ui/src/workflow-canvas/config/node-data.ts +++ b/ui/src/workflow-canvas/config/node-data.ts @@ -278,7 +278,8 @@ export const variableAggregationNode = { type: WorkflowNodeType.VariableAggregationNode, text: '按聚合策略聚合每组的变量', label: '变量聚合', - properties: { stepName: '变量聚合', config: { fields: [] } }, + // 分支汇合:各分支互斥执行,需“任一”前置完成即执行,否则会一直等待未执行的分支 + properties: { stepName: '变量聚合', condition: 'OR', config: { fields: [] } }, } /* 变量拆分 */ diff --git a/ui/src/workflow-canvas/nodes/document-extract-node/agent.ts b/ui/src/workflow-canvas/nodes/document-extract-node/agent.ts index c565e75f11a..45ef5bfd2a8 100644 --- a/ui/src/workflow-canvas/nodes/document-extract-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/document-extract-node/agent.ts @@ -1,7 +1,9 @@ -import { createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' +import { argReference, finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' -/** document-extract-node(文档内容提取)的 AI 生成配置:只有入参,无需额外配置 */ +/** document-extract-node(文档内容提取)的 AI 生成配置:只有入参文档(documents),无其他可配置项 */ const params: JsonObjectSchema = { type: 'object', @@ -24,8 +26,28 @@ const returns: JsonObjectSchema = { function useAgent(context: NodeAgentContext) { return useNodeAgent(context, { - prompt: ['document-extract-node(文档内容提取)配置要点:', '- 文档已由入参 documents 绑定,无需其他配置,直接调用 update 完成校验。'].join('\n'), - tools: [createUpdateTool(context, config, () => null)], + prompt: [ + 'document-extract-node(文档内容提取)配置要点:', + '- 文档来自入参 documents(引用上游的文档列表),确认它能解析出要提取的文档;无需其他配置,直接调用 update 完成校验。', + ].join('\n'), + tools: [ + { + name: 'update', + description: + '配置当前文档内容提取节点:把入参 documents 绑定为待提取文档(document_list),其余字段无需配置。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段 content 与 document_list;不通过返回 error 继续修。', + parameters: config, + apply: async () => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const reference = argReference(context, 'documents') + if (!reference) return new ErrorResult('缺少入参 documents:需要上游传入选中的文档列表,请先把 documents 绑定到文档来源节点') + const nodeData = readNodeData>(node) + writeNodeProperties(node, { node_data: { ...nodeData, document_list: [...reference] } }) + return finishWithValidate(context) + }, + }, + ], }) } diff --git a/ui/src/workflow-canvas/nodes/form-node/agent.ts b/ui/src/workflow-canvas/nodes/form-node/agent.ts index c4398ab4b05..4ec485aaeeb 100644 --- a/ui/src/workflow-canvas/nodes/form-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/form-node/agent.ts @@ -1,6 +1,9 @@ +import { randomId } from '@/utils/common' import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' +import type { VisibilityCompareOperator, VisibilityCondition, VisibilityRules } from '@/components/mk-dynamics-form' /** 可由 AI 生成的字段类型,数据结构与 mk-dynamics-form 各字段配置器的 getData 一致 */ const INPUT_TYPES = [ @@ -23,6 +26,57 @@ const OPTION_TYPES: FormInputType[] = ['SingleSelect', 'MultiSelect', 'RadioCard const MULTI_OPTION_TYPES: FormInputType[] = ['MultiSelect', 'MultiRow'] const DATE_FORMATS: Record = { year: 'YYYY', month: 'YYYY-MM', date: 'YYYY-MM-DD', datetime: 'YYYY-MM-DD HH:mm:ss' } +/** 显隐条件的比较运算符,与 VisibilityCompareOperator 一致 */ +const COMPARE_OPS: Array<{ label: string; value: VisibilityCompareOperator }> = [ + { label: '等于', value: 'eq' }, + { label: '不等于', value: 'not_eq' }, + { label: '包含', value: 'contain' }, + { label: '不包含', value: 'not_contain' }, + { label: '为真', value: 'is_true' }, + { label: '为假', value: 'is_not_true' }, + { label: '大于', value: 'gt' }, + { label: '大于等于', value: 'ge' }, + { label: '小于', value: 'lt' }, + { label: '小于等于', value: 'le' }, +] +const COMPARE_VALUES = COMPARE_OPS.map((op) => op.value) + +/** 一条显隐条件的 AI 入参:self=true 时基于本表单另一字段,否则基于上游节点字段 */ +interface VisibilityConditionArgs { + self: boolean + field: string + node_id?: string + compare: VisibilityCompareOperator + value?: unknown +} + +interface VisibilityRulesArgs { + action: 'show' | 'hide' + condition: 'and' | 'or' + conditions: VisibilityConditionArgs[] +} + +/** 把 AI 给出的显隐条件构建为节点持久化结构;self 引用指向本表单另一字段,self_ref = [本节点ID, 字段名] */ +function toVisibilityRules(rules: VisibilityRulesArgs | undefined, nodeId: string): VisibilityRules | null { + if (!rules) return null + const conditions: VisibilityCondition[] = [] + for (const condition of rules.conditions) { + if (!condition || !condition.field || !condition.compare) continue + const fieldRef: [string, string] = condition.self ? [nodeId, condition.field] : [condition.node_id ?? '', condition.field] + if (!fieldRef[0] || !fieldRef[1]) continue + conditions.push({ + id: randomId(), + field: fieldRef, + self: condition.self === true, + compare: condition.compare, + value: condition.compare === 'is_true' || condition.compare === 'is_not_true' ? true : (condition.value ?? ''), + }) + } + return conditions.length + ? { action: rules.action === 'hide' ? 'hide' : 'show', condition: rules.condition === 'or' ? 'or' : 'and', conditions } + : null +} + interface FormFieldArgs { field: string label: string @@ -37,6 +91,7 @@ interface FormFieldArgs { max?: number step?: number date_type?: keyof typeof DATE_FORMATS + visibility?: VisibilityRulesArgs } /** 文本类字段的长度校验规则,与文本配置器一致 */ @@ -47,7 +102,7 @@ function textRules(field: FormFieldArgs, required: boolean, minLength: number, m return [requiredRule, lengthRule] } -function toFormField(field: FormFieldArgs) { +function toFormField(field: FormFieldArgs, nodeId: string) { const required = field.required !== false const base = { field: field.field, @@ -55,7 +110,7 @@ function toFormField(field: FormFieldArgs) { input_type: field.input_type, required, show_default_value: true, - visibility_rules: null, + visibility_rules: toVisibilityRules(field.visibility, nodeId), } if (TEXT_TYPES.includes(field.input_type)) { const minLength = Math.max(0, Number(field.min_length ?? 0)) @@ -123,6 +178,29 @@ const config: JsonObjectSchema = { max: { type: 'number', description: '滑块最大值,默认 20' }, step: { type: 'number', description: '滑块步长,默认 1' }, date_type: { type: 'string', enum: Object.keys(DATE_FORMATS), description: '日期粒度,默认 datetime' }, + visibility: { + type: 'object', + description: '显隐规则(可选):根据其他字段的值控制本字段是否显示', + properties: { + action: { type: 'string', enum: ['show', 'hide'], description: 'show=满足条件时显示本字段(否则隐藏);hide=满足条件时隐藏本字段(否则显示)' }, + condition: { type: 'string', enum: ['and', 'or'], description: '多条条件组合方式:and 全部满足 / or 任一满足' }, + conditions: { + type: 'array', + items: { + type: 'object', + properties: { + self: { type: 'boolean', description: 'true=基于本表单另一字段;false=基于上游节点字段' }, + field: { type: 'string', description: '被引用的字段名:self=true 时为本表单另一字段名,否则为上游字段名' }, + node_id: { type: 'string', description: 'self=false 时上游节点 id' }, + compare: { type: 'string', enum: [...COMPARE_VALUES], description: '比较方式' }, + value: { description: '比较值:is_true / is_not_true 时可不填,其余按字段类型给值' }, + }, + required: ['self', 'field', 'compare'], + }, + }, + }, + required: ['action', 'condition', 'conditions'], + }, }, required: ['field', 'label', 'input_type'], }, @@ -151,42 +229,62 @@ function useAgent(context: NodeAgentContext) { '- fields:要收集的字段,field 为英文变量名(字母开头,下划线分隔),label 为中文显示名。', '- input_type:文本 TextInput / 多行文本 TextareaInput / 密码 PasswordInput;单选下拉 SingleSelect / 多选下拉 MultiSelect / 选项卡 RadioCard / 单行选项卡 RadioRow / 单行多选卡 MultiRow(选项类需给 options);开关 SwitchInput;滑块 Slider(min / max / step);日期 DatePicker(date_type)。', '- 可选:required(默认必填)、tooltip 提示、default_value 默认值、文本类 min_length / max_length。', + '- visibility:显隐规则(可选)。action:show=满足条件时显示本字段,hide=满足条件时隐藏本字段。想“满足某条件就把字段藏起来”用 hide;想“满足某条件才显示”用 show。', '- form_content_format:展示给用户的引导文字,必须包含占位符 {{form}}(表单渲染位置)。', '- 下游可用 [本节点ID, 字段名] 或 {{节点名称.字段名}} 引用各字段。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const fields = (Array.isArray(args.fields) ? args.fields : []) as FormFieldArgs[] - if (!fields.length) return 'fields 至少一个字段' - const names = new Set() - for (const field of fields) { - if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(String(field.field))) return `字段名不合法:${field.field}` - if (names.has(field.field)) return `字段名重复:${field.field}` - if (!INPUT_TYPES.includes(field.input_type)) return `input_type 不支持:${field.input_type}` - if (OPTION_TYPES.includes(field.input_type) && !field.options?.length) return `选项字段 ${field.field} 需要 options` - if (field.input_type === 'Slider' && Number(field.max ?? 20) <= Number(field.min ?? 0)) return `滑块字段 ${field.field} 的 max 需大于 min` - names.add(field.field) - } - const format = - typeof args.form_content_format === 'string' && args.form_content_format.includes('{{form}}') - ? args.form_content_format - : '你好,请先填写下面表单内容:\n{{form}}\n填写后请点击【提交】按钮进行提交。' - writeNodeProperties(node, { - node_data: { - ...readNodeData>(node), - is_result: true, - form_field_list: fields.map(toFormField), - form_content_format: format, - }, - config: { - ...(node.properties.config as Record), - fields: [{ label: '表单全部内容', value: 'form_data' }, ...fields.map((field) => ({ label: field.label, value: field.field }))], - }, - }) - node.clearNextNodeField(true) - return null - }), + { + name: 'update', + description: + '配置当前表单收集节点:字段列表 fields(含显隐 visibility)、引导文字 form_content_format。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const fields = (Array.isArray(args.fields) ? args.fields : []) as FormFieldArgs[] + if (!fields.length) return new ErrorResult('fields 至少一个字段') + const names = new Set() + for (const field of fields) { + if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(String(field.field))) return new ErrorResult(`字段名不合法:${field.field}`) + if (names.has(field.field)) return new ErrorResult(`字段名重复:${field.field}`) + if (!INPUT_TYPES.includes(field.input_type)) return new ErrorResult(`input_type 不支持:${field.input_type}`) + if (OPTION_TYPES.includes(field.input_type) && !field.options?.length) return new ErrorResult(`选项字段 ${field.field} 需要 options`) + if (field.input_type === 'Slider' && Number(field.max ?? 20) <= Number(field.min ?? 0)) { + return new ErrorResult(`滑块字段 ${field.field} 的 max 需大于 min`) + } + names.add(field.field) + } + for (const field of fields) { + for (const condition of field.visibility?.conditions ?? []) { + if (!COMPARE_VALUES.includes(condition.compare)) return new ErrorResult(`字段 ${field.field} 显隐比较符不支持:${condition.compare}`) + if (condition.self !== true && !condition.node_id) return new ErrorResult(`字段 ${field.field} 的显隐条件 self=false 需给 node_id`) + if (condition.self === true && !names.has(condition.field)) { + return new ErrorResult(`字段 ${field.field} 的显隐 self 字段不存在:${condition.field}(只能引用本表单已定义字段)`) + } + } + } + const format = + typeof args.form_content_format === 'string' && args.form_content_format.includes('{{form}}') + ? args.form_content_format + : '你好,请先填写下面表单内容:\n{{form}}\n填写后请点击【提交】按钮进行提交。' + writeNodeProperties(node, { + node_data: { + ...readNodeData>(node), + is_result: true, + form_field_list: fields.map((field) => toFormField(field, node.id)), + form_content_format: format, + }, + config: { + ...(node.properties.config as Record), + fields: [{ label: '表单全部内容', value: 'form_data' }, ...fields.map((field) => ({ label: field.label, value: field.field }))], + }, + }) + node.clearNextNodeField(true) + return finishWithValidate(context) + }, + }, ], }) } diff --git a/ui/src/workflow-canvas/nodes/image-generate-node/agent.ts b/ui/src/workflow-canvas/nodes/image-generate-node/agent.ts index 070334148b8..f0dbb61bbeb 100644 --- a/ui/src/workflow-canvas/nodes/image-generate-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/image-generate-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, validatePromptVariables, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** image-generate-node(图片生成)的 AI 生成配置;node_data 协议以节点视图 index.vue 的 defaultForm 为准 */ @@ -44,28 +45,36 @@ function useAgent(context: NodeAgentContext) { '- is_result:结果直接给用户时为 true。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const modelFields = resolveModelFields(context, args.model, MODEL) - if (typeof modelFields === 'string') return modelFields - const error = - validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ?? - validatePromptVariables(context.lf, context.nodeId, args.negative_prompt, 'negative_prompt') - if (error) return error - const nodeData = readNodeData>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - ...modelFields, - prompt: String(args.prompt ?? nodeData.prompt ?? ''), - negative_prompt: typeof args.negative_prompt === 'string' ? args.negative_prompt : (nodeData.negative_prompt ?? ''), - dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0), - dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), - }, - }) - return null - }), + { + name: 'update', + description: + '配置当前图片生成节点:模型、正向/反向提示词、是否输出给用户等。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const modelFields = resolveModelFields(context, args.model, MODEL) + if (typeof modelFields === 'string') return new ErrorResult(modelFields) + const error = + validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ?? + validatePromptVariables(context.lf, context.nodeId, args.negative_prompt, 'negative_prompt') + if (error) return new ErrorResult(error) + const nodeData = readNodeData>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + ...modelFields, + prompt: String(args.prompt ?? nodeData.prompt ?? ''), + negative_prompt: typeof args.negative_prompt === 'string' ? args.negative_prompt : (nodeData.negative_prompt ?? ''), + dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0), + dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), + }, + }) + return finishWithValidate(context) + }, + }, getModelsTool(context, MODEL), ], }) diff --git a/ui/src/workflow-canvas/nodes/image-to-video-node/agent.ts b/ui/src/workflow-canvas/nodes/image-to-video-node/agent.ts index 6049606a521..283d5d350f1 100644 --- a/ui/src/workflow-canvas/nodes/image-to-video-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/image-to-video-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, validatePromptVariables, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** image-to-video-node(图生视频)的 AI 生成配置;node_data 协议以节点视图 index.vue 的 defaultForm 为准 */ @@ -47,28 +48,36 @@ function useAgent(context: NodeAgentContext) { '- is_result:结果直接给用户时为 true。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const modelFields = resolveModelFields(context, args.model, MODEL) - if (typeof modelFields === 'string') return modelFields - const error = - validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ?? - validatePromptVariables(context.lf, context.nodeId, args.negative_prompt, 'negative_prompt') - if (error) return error - const nodeData = readNodeData>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - ...modelFields, - prompt: String(args.prompt ?? nodeData.prompt ?? ''), - negative_prompt: typeof args.negative_prompt === 'string' ? args.negative_prompt : (nodeData.negative_prompt ?? ''), - dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0), - dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), - }, - }) - return null - }), + { + name: 'update', + description: + '配置当前图生视频节点:模型、正向/反向提示词、是否输出给用户等。首帧/尾帧图片已由入参绑定。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const modelFields = resolveModelFields(context, args.model, MODEL) + if (typeof modelFields === 'string') return new ErrorResult(modelFields) + const error = + validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ?? + validatePromptVariables(context.lf, context.nodeId, args.negative_prompt, 'negative_prompt') + if (error) return new ErrorResult(error) + const nodeData = readNodeData>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + ...modelFields, + prompt: String(args.prompt ?? nodeData.prompt ?? ''), + negative_prompt: typeof args.negative_prompt === 'string' ? args.negative_prompt : (nodeData.negative_prompt ?? ''), + dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0), + dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), + }, + }) + return finishWithValidate(context) + }, + }, getModelsTool(context, MODEL), ], }) diff --git a/ui/src/workflow-canvas/nodes/image-understand-node/agent.ts b/ui/src/workflow-canvas/nodes/image-understand-node/agent.ts index bfbd506ef28..2f295532872 100644 --- a/ui/src/workflow-canvas/nodes/image-understand-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/image-understand-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, validatePromptVariables, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** image-understand-node(图片理解)的 AI 生成配置;node_data 协议以节点视图 index.vue 的 defaultForm 为准 */ @@ -46,37 +47,45 @@ function useAgent(context: NodeAgentContext) { '- is_result:作为最终回答时为 true,作为中间加工步骤时为 false。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const modelFields = resolveModelFields(context, args.model, MODEL) - if (typeof modelFields === 'string') return modelFields - const error = - validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ?? - validatePromptVariables(context.lf, context.nodeId, args.system, 'system') - if (error) return error - const nodeData = readNodeData & { model_setting?: Record }>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - ...modelFields, - system: typeof args.system === 'string' ? args.system : (nodeData.system ?? ''), - prompt: String(args.prompt ?? nodeData.prompt ?? ''), - dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0), - dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), - model_setting: { - reasoning_content_start: '', - reasoning_content_end: '', - ...nodeData.model_setting, - reasoning_content_enable: - typeof args.reasoning_content_enable === 'boolean' - ? args.reasoning_content_enable - : !!nodeData.model_setting?.reasoning_content_enable, + { + name: 'update', + description: + '配置当前图片理解节点:模型、系统/用户提示词、是否输出给用户、是否输出思考过程等。图片已由入参 images 绑定。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const modelFields = resolveModelFields(context, args.model, MODEL) + if (typeof modelFields === 'string') return new ErrorResult(modelFields) + const error = + validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ?? + validatePromptVariables(context.lf, context.nodeId, args.system, 'system') + if (error) return new ErrorResult(error) + const nodeData = readNodeData & { model_setting?: Record }>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + ...modelFields, + system: typeof args.system === 'string' ? args.system : (nodeData.system ?? ''), + prompt: String(args.prompt ?? nodeData.prompt ?? ''), + dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0), + dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), + model_setting: { + reasoning_content_start: '', + reasoning_content_end: '', + ...nodeData.model_setting, + reasoning_content_enable: + typeof args.reasoning_content_enable === 'boolean' + ? args.reasoning_content_enable + : !!nodeData.model_setting?.reasoning_content_enable, + }, }, - }, - }) - return null - }), + }) + return finishWithValidate(context) + }, + }, getModelsTool(context, MODEL), ], }) diff --git a/ui/src/workflow-canvas/nodes/loop-node/agent.ts b/ui/src/workflow-canvas/nodes/loop-node/agent.ts index 1c62d2706e2..2e8b63548ce 100644 --- a/ui/src/workflow-canvas/nodes/loop-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/loop-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { argReference, createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { argReference, finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** loop-node(循环)的 AI 生成配置 */ @@ -29,22 +30,30 @@ function useAgent(context: NodeAgentContext) { '- loop_type:ARRAY 遍历已绑定入参 array;NUMBER 循环 number 次;LOOP 无限循环(循环体内必须放 Break)。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const loopType = String(args.loop_type) - if (!['ARRAY', 'NUMBER', 'LOOP'].includes(loopType)) return `loop_type 不合法:${loopType}` - const array = argReference(context, 'array') - if (loopType === 'ARRAY' && !array) return 'ARRAY 模式需要父节点传入入参 array,请改用 NUMBER / LOOP 或说明缺少该入参' - writeNodeProperties(node, { - node_data: { - ...readNodeData>(node), - loop_type: loopType, - array: loopType === 'ARRAY' && array ? [...array] : [], - number: loopType === 'NUMBER' ? Math.max(1, Number(args.number ?? 1)) : 1, - }, - }) - return null - }), + { + name: 'update', + description: + '配置当前循环节点:循环类型 loop_type 与次数 number。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const loopType = String(args.loop_type) + if (!['ARRAY', 'NUMBER', 'LOOP'].includes(loopType)) return new ErrorResult(`loop_type 不合法:${loopType}`) + const array = argReference(context, 'array') + if (loopType === 'ARRAY' && !array) return new ErrorResult('ARRAY 模式需要父节点传入入参 array,请改用 NUMBER / LOOP 或说明缺少该入参') + writeNodeProperties(node, { + node_data: { + ...readNodeData>(node), + loop_type: loopType, + array: loopType === 'ARRAY' && array ? [...array] : [], + number: loopType === 'NUMBER' ? Math.max(1, Number(args.number ?? 1)) : 1, + }, + }) + return finishWithValidate(context) + }, + }, ], }) } diff --git a/ui/src/workflow-canvas/nodes/mcp-node/agent.ts b/ui/src/workflow-canvas/nodes/mcp-node/agent.ts index bf382e1ea39..d79643a217c 100644 --- a/ui/src/workflow-canvas/nodes/mcp-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/mcp-node/agent.ts @@ -1,6 +1,7 @@ import { TOOL_TYPE } from '@/api/enums' import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { argReference, createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { argReference, finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { AgentTool, JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' import { useWorkflowStore, type McpTool } from '@/workflow-canvas/store' import { WorkflowMode } from '@/workflow-canvas/types' @@ -117,48 +118,58 @@ function useAgent(context: NodeAgentContext) { '- 引用上游数据的参数由同名入参绑定;其余参数在 values 里给常量值,必填参数都要有值。', ].join('\n'), tools: [ - createUpdateTool(context, config, async (args) => { - const node = getNode(context.lf, context.nodeId)! - const mcpToolId = String(args.mcp_tool_id ?? '') - const loaded = await loadMcpTools(mcpToolId) - if (typeof loaded === 'string') return loaded - const tool = loaded.tools.find((item) => item.name === args.mcp_tool) - if (!tool) return `MCP 工具不存在:${String(args.mcp_tool)},可选:${loaded.tools.map((item) => item.name).join('、')}` - const { nested, fields } = paramFieldsOf(tool) - const values = (args.values ?? {}) as Record - const toolParams: Record = {} - const toolFormField = [] - for (const field of fields) { - const reference = argReference(context, field.name) - const value = reference ? [...reference] : values[field.name] - if (value === undefined && field.required) return `必填参数 ${field.name} 未赋值(引用上游请让父节点传入同名入参,常量写在 values)` - if (value !== undefined) toolParams[field.name] = value - toolFormField.push({ - field: field.name, - label: { input_type: 'TooltipLabel', label: field.name, attrs: { tooltip: field.description } }, - input_type: inputTypeOf(field.type), - source: reference ? 'referencing' : 'custom', - required: field.required, + { + name: 'update', + description: + '配置当前 MCP 调用节点:选择要调用的 MCP 工具并为参数赋值。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const mcpToolId = String(args.mcp_tool_id ?? '') + const loaded = await loadMcpTools(mcpToolId) + if (typeof loaded === 'string') return new ErrorResult(loaded) + const tool = loaded.tools.find((item) => item.name === args.mcp_tool) + if (!tool) return new ErrorResult(`MCP 工具不存在:${String(args.mcp_tool)},可选:${loaded.tools.map((item) => item.name).join('、')}`) + const { nested, fields } = paramFieldsOf(tool) + const values = (args.values ?? {}) as Record + const toolParams: Record = {} + const toolFormField = [] + for (const field of fields) { + const reference = argReference(context, field.name) + const value = reference ? [...reference] : values[field.name] + if (value === undefined && field.required) { + return new ErrorResult(`必填参数 ${field.name} 未赋值(引用上游请让父节点传入同名入参,常量写在 values)`) + } + if (value !== undefined) toolParams[field.name] = value + toolFormField.push({ + field: field.name, + label: { input_type: 'TooltipLabel', label: field.name, attrs: { tooltip: field.description } }, + input_type: inputTypeOf(field.type), + source: reference ? 'referencing' : 'custom', + required: field.required, + }) + } + const nodeData = readNodeData>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + mcp_source: 'referencing', + mcp_tool_id: mcpToolId, + mcp_servers: loaded.servers, + mcp_tools: loaded.tools, + mcp_tool: tool.name, + mcp_server: tool.server, + params_nested: nested, + tool_form_field: toolFormField, + tool_params: nested ? { [nested]: toolParams } : toolParams, + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? false), + }, }) - } - const nodeData = readNodeData>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - mcp_source: 'referencing', - mcp_tool_id: mcpToolId, - mcp_servers: loaded.servers, - mcp_tools: loaded.tools, - mcp_tool: tool.name, - mcp_server: tool.server, - params_nested: nested, - tool_form_field: toolFormField, - tool_params: nested ? { [nested]: toolParams } : toolParams, - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? false), - }, - }) - return null - }), + return finishWithValidate(context) + }, + }, mcpListTool, mcpToolsTool, ], diff --git a/ui/src/workflow-canvas/nodes/parameter-extraction-node/agent.ts b/ui/src/workflow-canvas/nodes/parameter-extraction-node/agent.ts index efc786d9575..a7fbd573a17 100644 --- a/ui/src/workflow-canvas/nodes/parameter-extraction-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/parameter-extraction-node/agent.ts @@ -1,6 +1,7 @@ import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' -import { createUpdateTool, getModelsTool, MODEL_FIELD_SCHEMA, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, getModelsTool, MODEL_FIELD_SCHEMA, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' const PARAMETER_TYPES = ['string', 'int', 'number', 'boolean', 'array', 'object'] @@ -56,36 +57,44 @@ function useAgent(context: NodeAgentContext) { '- parameters:要提取的参数,field 英文名,label 中文名,parameter_type 取 string/int/number/boolean/array/object,desc 写清提取规则。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const modelFields = resolveModelFields(context, args.model) - if (typeof modelFields === 'string') return modelFields - const parameters = (Array.isArray(args.parameters) ? args.parameters : []) as Array> - if (!parameters.length) return 'parameters 至少一个参数' - for (const parameter of parameters) { - if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(String(parameter.field))) return `参数名不合法:${parameter.field}` - if (!PARAMETER_TYPES.includes(parameter.parameter_type ?? '')) return `参数类型不合法:${parameter.parameter_type}` - } - const variableList = parameters.map((parameter) => ({ - field: parameter.field, - label: parameter.label, - parameter_type: parameter.parameter_type, - desc: parameter.desc ?? '', - })) - writeNodeProperties(node, { - node_data: { - ...readNodeData>(node), - ...modelFields, - variable_list: variableList, - }, - config: { - ...(node.properties.config as Record), - fields: [{ label: '结果', value: 'result' }, ...variableList.map((parameter) => ({ label: parameter.label, value: parameter.field }))], - }, - }) - node.clearNextNodeField(true) - return null - }), + { + name: 'update', + description: + '配置当前参数提取节点:模型与要提取的参数列表。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const modelFields = resolveModelFields(context, args.model) + if (typeof modelFields === 'string') return new ErrorResult(modelFields) + const parameters = (Array.isArray(args.parameters) ? args.parameters : []) as Array> + if (!parameters.length) return new ErrorResult('parameters 至少一个参数') + for (const parameter of parameters) { + if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(String(parameter.field))) return new ErrorResult(`参数名不合法:${parameter.field}`) + if (!PARAMETER_TYPES.includes(parameter.parameter_type ?? '')) return new ErrorResult(`参数类型不合法:${parameter.parameter_type}`) + } + const variableList = parameters.map((parameter) => ({ + field: parameter.field, + label: parameter.label, + parameter_type: parameter.parameter_type, + desc: parameter.desc ?? '', + })) + writeNodeProperties(node, { + node_data: { + ...readNodeData>(node), + ...modelFields, + variable_list: variableList, + }, + config: { + ...(node.properties.config as Record), + fields: [{ label: '结果', value: 'result' }, ...variableList.map((parameter) => ({ label: parameter.label, value: parameter.field }))], + }, + }) + node.clearNextNodeField(true) + return finishWithValidate(context) + }, + }, getModelsTool(context), ], }) diff --git a/ui/src/workflow-canvas/nodes/question-node/agent.ts b/ui/src/workflow-canvas/nodes/question-node/agent.ts index 497eaa08d3b..ba22e20b2f0 100644 --- a/ui/src/workflow-canvas/nodes/question-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/question-node/agent.ts @@ -1,13 +1,14 @@ import { getNode, readNodeData, validatePromptVariables, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' import { argReference, - createUpdateTool, + finishWithValidate, getModelsTool, MODEL_FIELD_SCHEMA, promptVariableOf, resolveModelFields, useNodeAgent, } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** question-node(问题优化)的 AI 生成配置 */ @@ -54,26 +55,34 @@ function useAgent(context: NodeAgentContext) { '- 优化结果一般作为中间结果,is_result=false。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const modelFields = resolveModelFields(context, args.model) - if (typeof modelFields === 'string') return modelFields - const error = validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') - if (error) return error - const nodeData = readNodeData>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - ...modelFields, - ...(typeof args.system === 'string' && args.system ? { system: args.system } : {}), - prompt: typeof args.prompt === 'string' && args.prompt ? args.prompt : defaultPrompt, - dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 1), - dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? false), - }, - }) - return null - }), + { + name: 'update', + description: + '配置当前问题优化节点:模型、系统/用户提示词、历史对话、是否输出给用户等。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const modelFields = resolveModelFields(context, args.model) + if (typeof modelFields === 'string') return new ErrorResult(modelFields) + const error = validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') + if (error) return new ErrorResult(error) + const nodeData = readNodeData>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + ...modelFields, + ...(typeof args.system === 'string' && args.system ? { system: args.system } : {}), + prompt: typeof args.prompt === 'string' && args.prompt ? args.prompt : defaultPrompt, + dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 1), + dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? false), + }, + }) + return finishWithValidate(context) + }, + }, getModelsTool(context), ], }) diff --git a/ui/src/workflow-canvas/nodes/reply-node/agent.ts b/ui/src/workflow-canvas/nodes/reply-node/agent.ts index f52c1e212a0..88052bb2c58 100644 --- a/ui/src/workflow-canvas/nodes/reply-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/reply-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, validatePromptVariables, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { argReference, createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { argReference, finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** reply-node(指定回复)的 AI 生成配置 */ @@ -16,6 +17,7 @@ const config: JsonObjectSchema = { type: 'object', properties: { reply_type: { type: 'string', enum: ['content', 'referencing'], description: 'content 输出文本,referencing 直接输出入参 value' }, + content_type: { type: 'string', enum: ['TEXT', 'HTML', 'TOOL'], description: '回复内容格式(reply_type=content 时):TEXT 文本,HTML 富文本,TOOL 工具调用结果 JSON' }, content: { type: 'string', description: 'reply_type=content 时的回复文本,可内嵌入参 materials 的 {{节点名称.字段}}' }, is_result: { type: 'boolean', description: '是否输出给用户,默认 true' }, }, @@ -36,30 +38,44 @@ function useAgent(context: NodeAgentContext) { 'reply-node(指定回复)配置要点:', '- reply_type="content":输出 content 文本,可内嵌已绑定入参 materials 的 {{节点名称.字段}}。', '- reply_type="referencing":直接输出已绑定入参 value。', + '- content_type(reply_type=content 时):TEXT 纯文本 / HTML 富文本 / TOOL 工具结果 JSON,默认 TEXT。', '- 默认 is_result=true(输出给用户)。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const referencing = args.reply_type === 'referencing' - const value = argReference(context, 'value') - if (referencing && !value) return 'reply_type=referencing 需要父节点传入入参 value,请改用 content 或说明缺少该入参' - if (!referencing) { - const error = validatePromptVariables(context.lf, context.nodeId, args.content, 'content') ?? (args.content ? null : 'content 不能为空') - if (error) return error - } - const nodeData = readNodeData>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - reply_type: referencing ? 'referencing' : 'content', - content: referencing ? '' : String(args.content), - fields: referencing && value ? [...value] : [], - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), - }, - }) - return null - }), + { + name: 'update', + description: + '配置当前指定回复节点:直接输出文本(content)或上游变量(referencing)、回复内容格式(TEXT/HTML/TOOL),以及是否输出给用户。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const referencing = args.reply_type === 'referencing' + const value = argReference(context, 'value') + if (referencing && !value) { + return new ErrorResult('reply_type=referencing 需要父节点传入入参 value,请改用 content 或说明缺少该入参') + } + if (!referencing) { + const error = validatePromptVariables(context.lf, context.nodeId, args.content, 'content') ?? (args.content ? null : 'content 不能为空') + if (error) return new ErrorResult(error) + } + const nodeData = readNodeData>(node) + const content_type = + args.content_type === 'HTML' || args.content_type === 'TOOL' ? args.content_type : (nodeData.content_type ?? 'TEXT') + writeNodeProperties(node, { + node_data: { + ...nodeData, + reply_type: referencing ? 'referencing' : 'content', + content: referencing ? '' : String(args.content), + fields: referencing && value ? [...value] : [], + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), + ...(referencing ? { content_type: 'TEXT' } : { content_type }), + }, + }) + return finishWithValidate(context) + }, + }, ], }) } diff --git a/ui/src/workflow-canvas/nodes/reranker-node/agent.ts b/ui/src/workflow-canvas/nodes/reranker-node/agent.ts index 48a8cc192f2..efa0ac9a63e 100644 --- a/ui/src/workflow-canvas/nodes/reranker-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/reranker-node/agent.ts @@ -1,5 +1,6 @@ -import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { getNode, getDefaultModelConfig, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' +import { argReference, finishWithValidate, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** reranker-node(多路召回)的 AI 生成配置;node_data 协议以节点视图 index.vue 的 defaultForm 为准 */ @@ -11,6 +12,11 @@ const params: JsonObjectSchema = { properties: { contents: { type: 'array', items: {}, description: '要重排的多路检索结果,常为多个知识库检索的 paragraph_list', 'x-bind': 'reranker_reference_list' }, question: { type: 'string', description: '检索问题,通常为用户问题', 'x-bind': 'question_reference_address' }, + model_reference: { + type: 'array', + description: '引用上游变量作为重排模型(model_type=reference 时绑定)', + 'x-bind': 'reranker_model_id_reference', + }, }, required: ['contents', 'question'], } @@ -18,13 +24,18 @@ const params: JsonObjectSchema = { const config: JsonObjectSchema = { type: 'object', properties: { + model_type: { + type: 'string', + enum: ['default', 'custom', 'reference'], + description: '重排模型来源:default 用工作流默认重排模型,custom 用下方 model 选具体模型,reference 引用上游变量', + }, model: MODEL_FIELD_SCHEMA, top_n: { type: 'number', description: '保留的分段数,默认 3' }, similarity: { type: 'number', description: '分数阈值,默认 0' }, max_paragraph_char_number: { type: 'number', description: '引用分段最大字符数,默认 5000' }, show_knowledge: { type: 'boolean', description: '是否把结果展示给用户,默认 false' }, }, - required: ['model'], + required: ['model_type'], } const returns: JsonObjectSchema = { @@ -38,37 +49,65 @@ const returns: JsonObjectSchema = { } function useAgent(context: NodeAgentContext) { + const { source, id, reference } = MODEL.fields return useNodeAgent(context, { prompt: [ 'reranker-node(多路召回)配置要点:', - '- model:工作流已配置默认重排模型时传 "default",否则先 get_models 选真实模型 id。', + '- model_type:default 用工作流默认重排模型(已配置时首选);custom 用 get_models 选取的模型 id;reference 引用上游模型变量(需绑定入参 model_reference)。', '- 重排内容与检索问题已由入参 contents / question 绑定,无需配置。', '- top_n、similarity、max_paragraph_char_number 按需求设置,未提及时保持默认。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const modelFields = resolveModelFields(context, args.model, MODEL) - if (typeof modelFields === 'string') return modelFields - const nodeData = readNodeData & { reranker_setting?: Record }>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - ...modelFields, - show_knowledge: typeof args.show_knowledge === 'boolean' ? args.show_knowledge : (nodeData.show_knowledge ?? false), - reranker_setting: { - top_n: 3, - similarity: 0, - max_paragraph_char_number: 5000, - ...nodeData.reranker_setting, - ...(args.top_n !== undefined ? { top_n: Number(args.top_n) } : {}), - ...(args.similarity !== undefined ? { similarity: Number(args.similarity) } : {}), - ...(args.max_paragraph_char_number !== undefined ? { max_paragraph_char_number: Number(args.max_paragraph_char_number) } : {}), + { + name: 'update', + description: + '配置当前多路召回(重排)节点:模型来源与召回参数。重排内容与问题已由入参绑定。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + + const modelType = args.model_type === 'custom' ? 'custom' : args.model_type === 'reference' ? 'reference' : 'default' + let modelFields: Record + let defaultMissing = false + if (modelType === 'reference') { + const modelRef = argReference(context, 'model_reference') + if (!modelRef) return new ErrorResult('model_type=reference 需要父节点传入入参 model_reference,请先把重排模型引用绑定到上游变量') + modelFields = { [source]: 'reference', [id]: '', [reference]: [...modelRef] } + } else if (modelType === 'custom') { + if (!args.model) return new ErrorResult('model_type=custom 需要传 model,请先 get_models 选择模型 id') + modelFields = { [source]: 'custom', [id]: String(args.model), [reference]: [] } + } else { + modelFields = { [source]: 'default', [id]: '', [reference]: [] } + defaultMissing = !getDefaultModelConfig(context.lf, MODEL.type)?.model_id + } + + const nodeData = readNodeData & { reranker_setting?: Record }>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + ...modelFields, + show_knowledge: typeof args.show_knowledge === 'boolean' ? args.show_knowledge : (nodeData.show_knowledge ?? false), + reranker_setting: { + top_n: 3, + similarity: 0, + max_paragraph_char_number: 5000, + ...nodeData.reranker_setting, + ...(args.top_n !== undefined ? { top_n: Number(args.top_n) } : {}), + ...(args.similarity !== undefined ? { similarity: Number(args.similarity) } : {}), + ...(args.max_paragraph_char_number !== undefined ? { max_paragraph_char_number: Number(args.max_paragraph_char_number) } : {}), + }, }, - }, - }) - return null - }), + }) + if (defaultMissing) { + return new ErrorResult( + `当前工作流未配置默认${MODEL.label}(需要在默认模型设置中配置 RERANKER),所以节点无法使用默认模型。请改用 model_type=custom 选取具体模型,或提醒用户先配置默认重排模型后重试`, + ) + } + return finishWithValidate(context) + }, + }, getModelsTool(context, MODEL), ], }) diff --git a/ui/src/workflow-canvas/nodes/search-document-node/agent.ts b/ui/src/workflow-canvas/nodes/search-document-node/agent.ts index cf544f64edd..9a21003be4d 100644 --- a/ui/src/workflow-canvas/nodes/search-document-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/search-document-node/agent.ts @@ -1,6 +1,7 @@ import { cloneDeep } from 'lodash' import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { argReference, createUpdateTool, getKnowledgeListTool, loadKnowledge, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { argReference, finishWithValidate, getKnowledgeListTool, loadKnowledge, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { AgentTool, JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' import { useWorkflowStore } from '@/workflow-canvas/store' import { compareOptions } from './constant' @@ -71,44 +72,63 @@ function useAgent(context: NodeAgentContext) { return useNodeAgent(context, { prompt: [ 'search-document-node(文档标签检索)配置要点:', - '- 检索范围默认 search_scope_type="custom",先 get_knowledge_list 选 knowledge_ids;已绑定入参 scope 时才用 "referencing",并给 search_scope_source。', + '- 检索范围默认 search_scope_type="custom",先 get_knowledge_list 选 knowledge_ids。', + '- 用户要求按上游变量确定检索范围时,用 search_scope_type="referencing",并让父 agent 把入参 scope 绑定到一个上游数组(知识库 id 或文档 id 列表,配 search_scope_source=knowledge/document);绑定成功前引用范围无法生效。', '- search_mode="auto" 按已绑定入参 question 自动匹配文档标签;需求明确给出标签过滤条件时用 "custom",先 get_knowledge_tags 查标签,再写 conditions。', ].join('\n'), tools: [ - createUpdateTool(context, config, async (args) => { - const node = getNode(context.lf, context.nodeId)! - const nodeData = readNodeData>(node) - const referencing = args.search_scope_type === 'referencing' - const scope = argReference(context, 'scope') - if (referencing && !scope) return 'search_scope_type=referencing 需要父节点传入入参 scope,请改用 custom 或说明缺少该入参' - const searchMode = args.search_mode === 'custom' && !referencing ? 'custom' : 'auto' - if (searchMode === 'auto' && !argReference(context, 'question')) return 'search_mode=auto 需要父节点传入入参 question,请改用 custom 或说明缺少该入参' - const ids = !referencing && Array.isArray(args.knowledge_ids) ? args.knowledge_ids.map(String) : [] - const knowledgeList = await loadKnowledge() - const selected = ids.map((id) => knowledgeList.find((knowledge) => knowledge.id === id)) - if (selected.some((knowledge) => !knowledge)) return `知识库不存在:${ids.filter((_, index) => !selected[index]).join(', ')}` - const conditions = (Array.isArray(args.conditions) ? args.conditions : []) as Array> - if (searchMode === 'custom') { - if (!conditions.length) return 'search_mode=custom 至少一条 conditions' - const invalid = conditions.find((condition) => !condition.key || !COMPARES.includes(condition.compare as never)) - if (invalid) return `条件不合法:${JSON.stringify(invalid)}` - } - writeNodeProperties(node, { - node_data: { - ...nodeData, - knowledge_id_list: ids, - knowledge_list: cloneDeep(selected), - search_scope_type: referencing ? 'referencing' : 'custom', - search_scope_source: referencing && args.search_scope_source === 'document' ? 'document' : 'knowledge', - search_scope_reference: referencing && scope ? [...scope] : [], - search_mode: searchMode, - search_condition_type: args.condition_type === 'OR' ? 'OR' : 'AND', - search_condition_list: - searchMode === 'custom' ? conditions.map((condition) => ({ key: condition.key, compare: condition.compare, value: String(condition.value ?? '') })) : [], - }, - }) - return null - }), + { + name: 'update', + description: + '配置当前文档标签检索节点:检索范围、标签过滤条件等。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const nodeData = readNodeData>(node) + const referencing = + args.search_scope_type === 'referencing' || + (args.search_scope_type === undefined && nodeData.search_scope_type === 'referencing') + const scope = argReference(context, 'scope') + const searchMode = args.search_mode === 'custom' && !referencing ? 'custom' : 'auto' + if (searchMode === 'auto' && !argReference(context, 'question')) { + return new ErrorResult('search_mode=auto 需要父节点传入入参 question,请改用 custom 或说明缺少该入参') + } + const ids = !referencing && Array.isArray(args.knowledge_ids) ? args.knowledge_ids.map(String) : [] + const knowledgeList = await loadKnowledge() + const selected = ids.map((id) => knowledgeList.find((knowledge) => knowledge.id === id)) + if (selected.some((knowledge) => !knowledge)) { + return new ErrorResult(`知识库不存在:${ids.filter((_, index) => !selected[index]).join(', ')}`) + } + const conditions = (Array.isArray(args.conditions) ? args.conditions : []) as Array> + if (searchMode === 'custom') { + if (!conditions.length) return new ErrorResult('search_mode=custom 至少一条 conditions') + const invalid = conditions.find((condition) => !condition.key || !COMPARES.includes(condition.compare as never)) + if (invalid) return new ErrorResult(`条件不合法:${JSON.stringify(invalid)}`) + } + writeNodeProperties(node, { + node_data: { + ...nodeData, + knowledge_id_list: ids, + knowledge_list: cloneDeep(selected), + search_scope_type: referencing ? 'referencing' : 'custom', + search_scope_source: referencing && args.search_scope_source === 'document' ? 'document' : 'knowledge', + search_scope_reference: referencing && scope ? [...scope] : [], + search_mode: searchMode, + search_condition_type: args.condition_type === 'OR' ? 'OR' : 'AND', + search_condition_list: + searchMode === 'custom' ? conditions.map((condition) => ({ key: condition.key, compare: condition.compare, value: String(condition.value ?? '') })) : [], + }, + }) + if (referencing && !scope) { + return new ErrorResult( + '已将检索范围设为“引用”,但缺少可引用的入参 scope:需要父 agent 把 scope 绑定到一个上游的文档 id 列表或知识库 id 列表,否则该节点校验不通过(页面已显示为“引用”,请补充引用变量)', + ) + } + return finishWithValidate(context) + }, + }, getKnowledgeListTool, knowledgeTagsTool(context), ], diff --git a/ui/src/workflow-canvas/nodes/search-knowledge-node/agent.ts b/ui/src/workflow-canvas/nodes/search-knowledge-node/agent.ts index d37d7fabd4a..cd8a4d82bdc 100644 --- a/ui/src/workflow-canvas/nodes/search-knowledge-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/search-knowledge-node/agent.ts @@ -1,7 +1,8 @@ import { cloneDeep } from 'lodash' import { KNOWLEDGE_SEARCH_MODE } from '@/api/enums/knowledge' import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { argReference, createUpdateTool, getKnowledgeListTool, loadKnowledge, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { argReference, finishWithValidate, getKnowledgeListTool, loadKnowledge, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' import { defaultSearchSetting } from './constant' @@ -54,40 +55,56 @@ function useAgent(context: NodeAgentContext) { '- 检索参数:top_n(默认 3)、similarity(0-1,默认 0.6)、search_mode(embedding/keywords/blend)、max_paragraph_char_number(默认 5000)。', ].join('\n'), tools: [ - createUpdateTool(context, config, async (args) => { - const node = getNode(context.lf, context.nodeId)! - const nodeData = readNodeData & { knowledge_setting?: Record }>(node) - const referencing = args.search_scope_type === 'referencing' - const scope = argReference(context, 'scope') - if (referencing && !scope) return 'search_scope_type=referencing 需要父节点传入入参 scope,请改用 custom 或说明缺少该入参' - const ids = !referencing && Array.isArray(args.knowledge_ids) ? args.knowledge_ids.map(String) : [] - const knowledgeList = await loadKnowledge() - const selected = ids.map((id) => knowledgeList.find((knowledge) => knowledge.id === id)) - if (selected.some((knowledge) => !knowledge)) return `知识库不存在:${ids.filter((_, index) => !selected[index]).join(', ')}` - if (new Set(selected.map((knowledge) => knowledge!.embedding_model_id)).size > 1) { - return '所选知识库的向量模型不一致,只能选择向量模型相同的知识库' - } - writeNodeProperties(node, { - node_data: { - ...nodeData, - knowledge_id_list: ids, - knowledge_list: cloneDeep(selected), - search_scope_type: referencing ? 'referencing' : 'custom', - search_scope_source: referencing && args.search_scope_source === 'document' ? 'document' : 'knowledge', - search_scope_reference: referencing && scope ? [...scope] : [], - show_knowledge: typeof args.show_knowledge === 'boolean' ? args.show_knowledge : (nodeData.show_knowledge ?? false), - knowledge_setting: { - ...defaultSearchSetting, - ...nodeData.knowledge_setting, - ...(args.top_n !== undefined ? { top_n: Number(args.top_n) } : {}), - ...(args.similarity !== undefined ? { similarity: Number(args.similarity) } : {}), - ...(args.search_mode ? { search_mode: String(args.search_mode) } : {}), - ...(args.max_paragraph_char_number !== undefined ? { max_paragraph_char_number: Number(args.max_paragraph_char_number) } : {}), + { + name: 'update', + description: + '配置当前知识库检索节点:检索范围、检索参数、是否展示结果等。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const nodeData = readNodeData & { knowledge_setting?: Record }>(node) + const referencing = + args.search_scope_type === 'referencing' || + (args.search_scope_type === undefined && nodeData.search_scope_type === 'referencing') + const scope = argReference(context, 'scope') + const ids = !referencing && Array.isArray(args.knowledge_ids) ? args.knowledge_ids.map(String) : [] + const knowledgeList = await loadKnowledge() + const selected = ids.map((id) => knowledgeList.find((knowledge) => knowledge.id === id)) + if (selected.some((knowledge) => !knowledge)) { + return new ErrorResult(`知识库不存在:${ids.filter((_, index) => !selected[index]).join(', ')}`) + } + if (new Set(selected.map((knowledge) => knowledge!.embedding_model_id)).size > 1) { + return new ErrorResult('所选知识库的向量模型不一致,只能选择向量模型相同的知识库') + } + writeNodeProperties(node, { + node_data: { + ...nodeData, + knowledge_id_list: ids, + knowledge_list: cloneDeep(selected), + search_scope_type: referencing ? 'referencing' : 'custom', + search_scope_source: referencing && args.search_scope_source === 'document' ? 'document' : 'knowledge', + search_scope_reference: referencing && scope ? [...scope] : [], + show_knowledge: typeof args.show_knowledge === 'boolean' ? args.show_knowledge : (nodeData.show_knowledge ?? false), + knowledge_setting: { + ...defaultSearchSetting, + ...nodeData.knowledge_setting, + ...(args.top_n !== undefined ? { top_n: Number(args.top_n) } : {}), + ...(args.similarity !== undefined ? { similarity: Number(args.similarity) } : {}), + ...(args.search_mode ? { search_mode: String(args.search_mode) } : {}), + ...(args.max_paragraph_char_number !== undefined ? { max_paragraph_char_number: Number(args.max_paragraph_char_number) } : {}), + }, }, - }, - }) - return null - }), + }) + if (referencing && !scope) { + return new ErrorResult( + '已将检索范围设为“引用”,但缺少可引用的入参 scope:需要父 agent 把 scope 绑定到一个上游知识库 id 列表或文档 id 列表,否则该节点校验不通过(页面已显示为“引用”,请补充引用变量)', + ) + } + return finishWithValidate(context) + }, + }, getKnowledgeListTool, ], }) diff --git a/ui/src/workflow-canvas/nodes/speech-to-text-node/agent.ts b/ui/src/workflow-canvas/nodes/speech-to-text-node/agent.ts index b4ed0759028..f012132777e 100644 --- a/ui/src/workflow-canvas/nodes/speech-to-text-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/speech-to-text-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** speech-to-text-node(语音转文本)的 AI 生成配置;node_data 协议以节点视图 index.vue 的 defaultForm 为准 */ @@ -40,20 +41,28 @@ function useAgent(context: NodeAgentContext) { '- is_result:结果直接给用户时为 true,作为中间步骤时为 false。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const modelFields = resolveModelFields(context, args.model, MODEL) - if (typeof modelFields === 'string') return modelFields - const nodeData = readNodeData>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - ...modelFields, - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), - }, - }) - return null - }), + { + name: 'update', + description: + '配置当前语音转文本节点:模型与是否输出给用户。要识别的音频已由入参 audio 绑定。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const modelFields = resolveModelFields(context, args.model, MODEL) + if (typeof modelFields === 'string') return new ErrorResult(modelFields) + const nodeData = readNodeData>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + ...modelFields, + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), + }, + }) + return finishWithValidate(context) + }, + }, getModelsTool(context, MODEL), ], }) diff --git a/ui/src/workflow-canvas/nodes/text-to-speech-node/agent.ts b/ui/src/workflow-canvas/nodes/text-to-speech-node/agent.ts index 4749bcdbe2a..2d8cbfa8eaa 100644 --- a/ui/src/workflow-canvas/nodes/text-to-speech-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/text-to-speech-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** text-to-speech-node(文本转语音)的 AI 生成配置;node_data 协议以节点视图 index.vue 的 defaultForm 为准 */ @@ -40,20 +41,28 @@ function useAgent(context: NodeAgentContext) { '- is_result:结果直接给用户时为 true,作为中间步骤时为 false。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const modelFields = resolveModelFields(context, args.model, MODEL) - if (typeof modelFields === 'string') return modelFields - const nodeData = readNodeData>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - ...modelFields, - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), - }, - }) - return null - }), + { + name: 'update', + description: + '配置当前文本转语音节点:模型与是否输出给用户。要朗读的文本已由入参 text 绑定。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const modelFields = resolveModelFields(context, args.model, MODEL) + if (typeof modelFields === 'string') return new ErrorResult(modelFields) + const nodeData = readNodeData>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + ...modelFields, + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), + }, + }) + return finishWithValidate(context) + }, + }, getModelsTool(context, MODEL), ], }) diff --git a/ui/src/workflow-canvas/nodes/text-to-video-node/agent.ts b/ui/src/workflow-canvas/nodes/text-to-video-node/agent.ts index 7277a60cfff..ed398bfe5df 100644 --- a/ui/src/workflow-canvas/nodes/text-to-video-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/text-to-video-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, validatePromptVariables, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** text-to-video-node(文生视频)的 AI 生成配置;node_data 协议以节点视图 index.vue 的 defaultForm 为准 */ @@ -43,28 +44,36 @@ function useAgent(context: NodeAgentContext) { '- is_result:结果直接给用户时为 true。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const modelFields = resolveModelFields(context, args.model, MODEL) - if (typeof modelFields === 'string') return modelFields - const error = - validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ?? - validatePromptVariables(context.lf, context.nodeId, args.negative_prompt, 'negative_prompt') - if (error) return error - const nodeData = readNodeData>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - ...modelFields, - prompt: String(args.prompt ?? nodeData.prompt ?? ''), - negative_prompt: typeof args.negative_prompt === 'string' ? args.negative_prompt : (nodeData.negative_prompt ?? ''), - dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0), - dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), - }, - }) - return null - }), + { + name: 'update', + description: + '配置当前文生视频节点:模型、正向/反向提示词、是否输出给用户等。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const modelFields = resolveModelFields(context, args.model, MODEL) + if (typeof modelFields === 'string') return new ErrorResult(modelFields) + const error = + validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ?? + validatePromptVariables(context.lf, context.nodeId, args.negative_prompt, 'negative_prompt') + if (error) return new ErrorResult(error) + const nodeData = readNodeData>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + ...modelFields, + prompt: String(args.prompt ?? nodeData.prompt ?? ''), + negative_prompt: typeof args.negative_prompt === 'string' ? args.negative_prompt : (nodeData.negative_prompt ?? ''), + dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0), + dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), + }, + }) + return finishWithValidate(context) + }, + }, getModelsTool(context, MODEL), ], }) diff --git a/ui/src/workflow-canvas/nodes/tool-custom-node/agent.ts b/ui/src/workflow-canvas/nodes/tool-custom-node/agent.ts index f559949c6bb..d40a09df250 100644 --- a/ui/src/workflow-canvas/nodes/tool-custom-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/tool-custom-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { argReference, createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { argReference, finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** tool-custom-node(自定义工具)的 AI 生成配置:生成 Python 代码与输入参数 */ @@ -57,41 +58,49 @@ function useAgent(context: NodeAgentContext) { '- 示例:def main(text: str, limit: int):\\n return text[:limit]', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const code = typeof args.code === 'string' ? args.code : '' - if (!/^\s*def\s+\w+\s*\(/m.test(code)) return 'code 需要定义一个 Python 函数' - const inputs = (Array.isArray(args.inputs) ? args.inputs : []) as Array> - const names = new Set() - const inputFieldList = [] - for (const input of inputs) { - const name = String(input.name ?? '') - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) return `参数名不合法:${name}` - if (names.has(name)) return `参数名重复:${name}` - names.add(name) - const type = FIELD_TYPES.includes(String(input.type)) ? String(input.type) : 'string' - const isRequired = input.is_required !== false - const field = { name, type, is_required: isRequired, desc: String(input.desc ?? '') } - if (input.source === 'reference') { - const reference = argReference(context, name) - if (!reference && isRequired) return `参数 ${name} 为引用来源,需要父节点传入同名入参` - inputFieldList.push({ ...field, source: 'reference', value: reference ? [...reference] : [] }) - } else { - const value = input.value - inputFieldList.push({ ...field, source: 'custom', value: value === undefined ? '' : typeof value === 'string' ? value : JSON.stringify(value) }) + { + name: 'update', + description: + '配置当前自定义工具节点:Python 代码与输入参数、是否输出给用户。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const code = typeof args.code === 'string' ? args.code : '' + if (!/^\s*def\s+\w+\s*\(/m.test(code)) return new ErrorResult('code 需要定义一个 Python 函数') + const inputs = (Array.isArray(args.inputs) ? args.inputs : []) as Array> + const names = new Set() + const inputFieldList = [] + for (const input of inputs) { + const name = String(input.name ?? '') + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) return new ErrorResult(`参数名不合法:${name}`) + if (names.has(name)) return new ErrorResult(`参数名重复:${name}`) + names.add(name) + const type = FIELD_TYPES.includes(String(input.type)) ? String(input.type) : 'string' + const isRequired = input.is_required !== false + const field = { name, type, is_required: isRequired, desc: String(input.desc ?? '') } + if (input.source === 'reference') { + const reference = argReference(context, name) + if (!reference && isRequired) return new ErrorResult(`参数 ${name} 为引用来源,需要父节点传入同名入参`) + inputFieldList.push({ ...field, source: 'reference', value: reference ? [...reference] : [] }) + } else { + const value = input.value + inputFieldList.push({ ...field, source: 'custom', value: value === undefined ? '' : typeof value === 'string' ? value : JSON.stringify(value) }) + } } - } - const nodeData = readNodeData>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - code, - input_field_list: inputFieldList, - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? false), - }, - }) - return null - }), + const nodeData = readNodeData>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + code, + input_field_list: inputFieldList, + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? false), + }, + }) + return finishWithValidate(context) + }, + }, ], }) } diff --git a/ui/src/workflow-canvas/nodes/tool-lib-node/agent.ts b/ui/src/workflow-canvas/nodes/tool-lib-node/agent.ts index 5e409fec117..b523849dd83 100644 --- a/ui/src/workflow-canvas/nodes/tool-lib-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/tool-lib-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { argReference, createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { argReference, finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' interface ToolNodeInputField { @@ -43,35 +44,43 @@ function useAgent(context: NodeAgentContext) { '- 必填参数都要有值;is_result 表示是否把工具结果输出给用户(默认 false)。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const nodeData = readNodeData & { input_field_list?: ToolNodeInputField[] }>(node) - const values = (args.values ?? {}) as Record - const inputFields: ToolNodeInputField[] = [] - for (const field of nodeData.input_field_list ?? []) { - if (field.source === 'reference') { - const reference = argReference(context, field.name) - if (!reference && field.is_required) return `必填参数 ${field.name} 需要父节点传入同名入参` - inputFields.push(reference ? { ...field, value: [...reference] } : field) - continue + { + name: 'update', + description: + '配置当前工具节点:为工具的每个输入参数赋值、是否输出给用户。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const nodeData = readNodeData & { input_field_list?: ToolNodeInputField[] }>(node) + const values = (args.values ?? {}) as Record + const inputFields: ToolNodeInputField[] = [] + for (const field of nodeData.input_field_list ?? []) { + if (field.source === 'reference') { + const reference = argReference(context, field.name) + if (!reference && field.is_required) return new ErrorResult(`必填参数 ${field.name} 需要父节点传入同名入参`) + inputFields.push(reference ? { ...field, value: [...reference] } : field) + continue + } + const value = values[field.name] + if (value === undefined) { + if (field.is_required) return new ErrorResult(`必填参数 ${field.name} 未赋值`) + inputFields.push(field) + continue + } + inputFields.push({ ...field, value: typeof value === 'string' ? value : JSON.stringify(value) }) } - const value = values[field.name] - if (value === undefined) { - if (field.is_required) return `必填参数 ${field.name} 未赋值` - inputFields.push(field) - continue - } - inputFields.push({ ...field, value: typeof value === 'string' ? value : JSON.stringify(value) }) - } - writeNodeProperties(node, { - node_data: { - ...nodeData, - input_field_list: inputFields, - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? false), - }, - }) - return null - }), + writeNodeProperties(node, { + node_data: { + ...nodeData, + input_field_list: inputFields, + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? false), + }, + }) + return finishWithValidate(context) + }, + }, ], }) } diff --git a/ui/src/workflow-canvas/nodes/variable-aggregation-node/agent.ts b/ui/src/workflow-canvas/nodes/variable-aggregation-node/agent.ts index 5b31c64d6d3..be19e8c7e92 100644 --- a/ui/src/workflow-canvas/nodes/variable-aggregation-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/variable-aggregation-node/agent.ts @@ -1,6 +1,7 @@ import { randomId } from '@/utils/common' import { getNode, readNodeData, validateReference, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** variable-aggregation-node(变量聚合)的 AI 生成配置 */ @@ -60,51 +61,64 @@ function useAgent(context: NodeAgentContext) { 'variable-aggregation-node(变量聚合)配置要点:', '- 汇合多个分支的输出时用 first_non_null:一个分组里放各分支同类的输出,运行时取实际执行分支的那个值。', '- 组内 variables 使用已绑定入参 variables 中的引用;variable_to_dict 时用 keys 给出每个变量的键名。', + '- 本节点只做数据合并,不会向用户输出内容(is_result 无效)。若合并结果要给用户看,必须在其下游接一个「指定回复」或「AI 对话」节点引用合并出的字段;分支最终要给用户的回复,也可直接把各分支 reply 节点设为输出。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const strategy = STRATEGIES.includes(String(args.strategy)) ? String(args.strategy) : 'first_non_null' - const groups = (Array.isArray(args.groups) ? args.groups : []) as Array> - if (!groups.length) return 'groups 至少一个分组' - const names = new Set() - const groupList = [] - for (const [index, group] of groups.entries()) { - const field = String(group.field ?? '') - if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(field)) return `groups[${index}].field 不合法:${field}` - if (names.has(field)) return `组变量名重复:${field}` - names.add(field) - const variables = Array.isArray(group.variables) ? (group.variables as unknown[]) : [] - if (!variables.length) return `groups[${index}].variables 至少一个变量` - const keys = Array.isArray(group.keys) ? group.keys.map(String) : [] - const variableList = [] - for (const [variableIndex, variable] of variables.entries()) { - const error = validateReference(context.lf, context.nodeId, variable, `groups[${index}].variables[${variableIndex}]`) - if (error) return error - variableList.push({ - v_id: randomId(), - variable: (variable as unknown[]).map(String), - ...(strategy === 'variable_to_dict' ? { key: keys[variableIndex] ?? `key${variableIndex + 1}` } : {}), - }) + { + name: 'update', + description: + '配置当前变量聚合节点:聚合方式与分组。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const strategy = STRATEGIES.includes(String(args.strategy)) ? String(args.strategy) : 'first_non_null' + const groups = (Array.isArray(args.groups) ? args.groups : []) as Array> + if (!groups.length) return new ErrorResult('groups 至少一个分组') + const names = new Set() + const groupList = [] + for (const [index, group] of groups.entries()) { + const field = String(group.field ?? '') + if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(field)) return new ErrorResult(`groups[${index}].field 不合法:${field}`) + if (names.has(field)) return new ErrorResult(`组变量名重复:${field}`) + names.add(field) + const variables = Array.isArray(group.variables) ? (group.variables as unknown[]) : [] + if (!variables.length) return new ErrorResult(`groups[${index}].variables 至少一个变量`) + const keys = Array.isArray(group.keys) ? group.keys.map(String) : [] + const variableList = [] + for (const [variableIndex, variable] of variables.entries()) { + const error = validateReference(context.lf, context.nodeId, variable, `groups[${index}].variables[${variableIndex}]`) + if (error) return new ErrorResult(error) + variableList.push({ + v_id: randomId(), + variable: (variable as unknown[]).map(String), + ...(strategy === 'variable_to_dict' ? { key: keys[variableIndex] ?? `key${variableIndex + 1}` } : {}), + }) + } + groupList.push({ id: randomId(), field, label: String(group.label ?? field), variable_list: variableList }) } - groupList.push({ id: randomId(), field, label: String(group.label ?? field), variable_list: variableList }) - } - writeNodeProperties(node, { - node_data: { ...readNodeData>(node), strategy, group_list: groupList }, - config: { - ...(node.properties.config as Record), - fields: groupList.map((group) => ({ label: group.label, value: group.field })), - }, - }) - node.clearNextNodeField(true) - return null - }), + writeNodeProperties(node, { + // 分支汇合需要“任一”前置完成即执行,避免等待未执行的分支导致节点永远不执行 + condition: 'OR', + node_data: { ...readNodeData>(node), strategy, group_list: groupList }, + config: { + ...(node.properties.config as Record), + fields: groupList.map((group) => ({ label: group.label, value: group.field })), + }, + }) + node.clearNextNodeField(true) + return finishWithValidate(context) + }, + }, ], }) } export default { - description: '把多个变量按分组聚合成一个输出;最常用于分支汇合:各分支的输出放进同一组,下游只引用这一组的结果', + description: + '把多个变量按分组聚合成一个输出;最常用于分支汇合:各分支的输出放进同一组,下游只引用这一组的结果。' + + '本节点只做数据合并、不向用户输出内容,需要展示时在其下游接「指定回复」或「AI 对话」节点引用合并字段', params, config, returns, diff --git a/ui/src/workflow-canvas/nodes/variable-assign-node/agent.ts b/ui/src/workflow-canvas/nodes/variable-assign-node/agent.ts index ee531c412a5..7181c4441db 100644 --- a/ui/src/workflow-canvas/nodes/variable-assign-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/variable-assign-node/agent.ts @@ -1,6 +1,7 @@ import { randomId } from '@/utils/common' import { getFieldOptions, getNode, readNodeData, validateReference, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' const VALUE_TYPES = ['string', 'num', 'json', 'bool'] @@ -47,40 +48,48 @@ function useAgent(context: NodeAgentContext) { '- type:string / num / json / bool。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const variables = Array.isArray(args.variables) ? (args.variables as Array>) : [] - if (!variables.length) return 'variables 至少一项' - const groups = getFieldOptions(context.lf, context.nodeId) - const variableList = [] - for (const [index, variable] of variables.entries()) { - const target = Array.isArray(variable.target) ? variable.target.map(String) : [] - if (!['global', 'chat', 'loop', 'output'].includes(target[0] ?? '')) return `variables[${index}].target 只能是 global/chat/loop 变量` - const error = validateReference(context.lf, context.nodeId, target, `variables[${index}].target`) - if (error) return error - const referencing = variable.source === 'referencing' - if (referencing) { - const referenceError = validateReference(context.lf, context.nodeId, variable.reference, `variables[${index}].reference`) - if (referenceError) return referenceError + { + name: 'update', + description: + '配置当前变量赋值节点:给全局 / 会话 / 循环变量赋值。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const variables = Array.isArray(args.variables) ? (args.variables as Array>) : [] + if (!variables.length) return new ErrorResult('variables 至少一项') + const groups = getFieldOptions(context.lf, context.nodeId) + const variableList = [] + for (const [index, variable] of variables.entries()) { + const target = Array.isArray(variable.target) ? variable.target.map(String) : [] + if (!['global', 'chat', 'loop', 'output'].includes(target[0] ?? '')) return new ErrorResult(`variables[${index}].target 只能是 global/chat/loop 变量`) + const error = validateReference(context.lf, context.nodeId, target, `variables[${index}].target`) + if (error) return new ErrorResult(error) + const referencing = variable.source === 'referencing' + if (referencing) { + const referenceError = validateReference(context.lf, context.nodeId, variable.reference, `variables[${index}].reference`) + if (referenceError) return new ErrorResult(referenceError) + } + const type = VALUE_TYPES.includes(String(variable.type)) ? String(variable.type) : 'string' + variableList.push({ + id: randomId(), + fields: target, + name: groups.find((group) => group.nodeId === target[0])?.fields.find((field) => field.value === target[1])?.label ?? target[1], + source: referencing ? 'referencing' : 'custom', + type, + value: referencing + ? null + : type === 'json' && typeof variable.value !== 'string' + ? JSON.stringify(variable.value) + : (variable.value ?? ''), + reference: referencing ? (variable.reference as unknown[]).map(String) : [], + }) } - const type = VALUE_TYPES.includes(String(variable.type)) ? String(variable.type) : 'string' - variableList.push({ - id: randomId(), - fields: target, - name: groups.find((group) => group.nodeId === target[0])?.fields.find((field) => field.value === target[1])?.label ?? target[1], - source: referencing ? 'referencing' : 'custom', - type, - value: referencing - ? null - : type === 'json' && typeof variable.value !== 'string' - ? JSON.stringify(variable.value) - : (variable.value ?? ''), - reference: referencing ? (variable.reference as unknown[]).map(String) : [], - }) - } - writeNodeProperties(node, { node_data: { ...readNodeData>(node), variable_list: variableList } }) - return null - }), + writeNodeProperties(node, { node_data: { ...readNodeData>(node), variable_list: variableList } }) + return finishWithValidate(context) + }, + }, ], }) } diff --git a/ui/src/workflow-canvas/nodes/variable-splitting-node/agent.ts b/ui/src/workflow-canvas/nodes/variable-splitting-node/agent.ts index bc5718362d0..63706936458 100644 --- a/ui/src/workflow-canvas/nodes/variable-splitting-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/variable-splitting-node/agent.ts @@ -1,5 +1,6 @@ import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** variable-splitting-node(变量拆分)的 AI 生成配置 */ @@ -51,32 +52,40 @@ function useAgent(context: NodeAgentContext) { '- variables:每项一个拆出的变量,field 英文变量名,label 显示名称,expression 为 JSONPath(如 $.name)。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const variables = (Array.isArray(args.variables) ? args.variables : []) as Array> - if (!variables.length) return 'variables 至少一个变量' - const names = new Set() - for (const variable of variables) { - if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(String(variable.field))) return `变量名不合法:${variable.field}` - if (names.has(variable.field!)) return `变量名重复:${variable.field}` - if (!String(variable.expression ?? '').trim()) return `变量 ${variable.field} 缺少 expression` - names.add(variable.field!) - } - const variableList = variables.map((variable) => ({ - field: variable.field, - label: variable.label || variable.field, - expression: variable.expression, - })) - writeNodeProperties(node, { - node_data: { ...readNodeData>(node), variable_list: variableList }, - config: { - ...(node.properties.config as Record), - fields: [{ label: '结果', value: 'result' }, ...variableList.map((variable) => ({ label: variable.label, value: variable.field }))], - }, - }) - node.clearNextNodeField(true) - return null - }), + { + name: 'update', + description: + '配置当前变量拆分节点:要拆出的变量与 JSONPath 表达式。要拆分的变量已由入参 input 绑定。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const variables = (Array.isArray(args.variables) ? args.variables : []) as Array> + if (!variables.length) return new ErrorResult('variables 至少一个变量') + const names = new Set() + for (const variable of variables) { + if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(String(variable.field))) return new ErrorResult(`变量名不合法:${variable.field}`) + if (names.has(variable.field!)) return new ErrorResult(`变量名重复:${variable.field}`) + if (!String(variable.expression ?? '').trim()) return new ErrorResult(`变量 ${variable.field} 缺少 expression`) + names.add(variable.field!) + } + const variableList = variables.map((variable) => ({ + field: variable.field, + label: variable.label || variable.field, + expression: variable.expression, + })) + writeNodeProperties(node, { + node_data: { ...readNodeData>(node), variable_list: variableList }, + config: { + ...(node.properties.config as Record), + fields: [{ label: '结果', value: 'result' }, ...variableList.map((variable) => ({ label: variable.label, value: variable.field }))], + }, + }) + node.clearNextNodeField(true) + return finishWithValidate(context) + }, + }, ], }) } diff --git a/ui/src/workflow-canvas/nodes/video-understand-node/agent.ts b/ui/src/workflow-canvas/nodes/video-understand-node/agent.ts index cd6243c1c73..632ebb75777 100644 --- a/ui/src/workflow-canvas/nodes/video-understand-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/video-understand-node/agent.ts @@ -1,5 +1,13 @@ import { getNode, readNodeData, validatePromptVariables, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' -import { createUpdateTool, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' +import { + finishWithValidate, + getModelsTool, + MODEL_FIELD_SCHEMA, + modelSlot, + resolveModelFields, + useNodeAgent, +} from '@/workflow-canvas/ai-generate/tools/node' +import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' /** video-understand-node(视频理解)的 AI 生成配置;node_data 协议以节点视图 index.vue 的 defaultForm 为准 */ @@ -46,37 +54,46 @@ function useAgent(context: NodeAgentContext) { '- is_result:作为最终回答时为 true,作为中间加工步骤时为 false。', ].join('\n'), tools: [ - createUpdateTool(context, config, (args) => { - const node = getNode(context.lf, context.nodeId)! - const modelFields = resolveModelFields(context, args.model, MODEL) - if (typeof modelFields === 'string') return modelFields - const error = - validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ?? - validatePromptVariables(context.lf, context.nodeId, args.system, 'system') - if (error) return error - const nodeData = readNodeData & { model_setting?: Record }>(node) - writeNodeProperties(node, { - node_data: { - ...nodeData, - ...modelFields, - system: typeof args.system === 'string' ? args.system : (nodeData.system ?? ''), - prompt: String(args.prompt ?? nodeData.prompt ?? ''), - dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0), - dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), - is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), - model_setting: { - reasoning_content_start: '', - reasoning_content_end: '', - ...nodeData.model_setting, - reasoning_content_enable: - typeof args.reasoning_content_enable === 'boolean' - ? args.reasoning_content_enable - : !!nodeData.model_setting?.reasoning_content_enable, + { + name: 'update', + description: + '配置当前视频理解节点:模型、系统/用户提示词、是否输出给用户、是否输出思考过程等。视频已由入参 videos 绑定。只传要设置的字段,其余保持不变。' + + '写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。', + parameters: config, + apply: async (args) => { + const node = getNode(context.lf, context.nodeId) + if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`) + const modelFields = resolveModelFields(context, args.model, MODEL) + if (typeof modelFields === 'string') return new ErrorResult(modelFields) + const error = + validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ?? + validatePromptVariables(context.lf, context.nodeId, args.system, 'system') + if (error) return new ErrorResult(error) + const nodeData = readNodeData & { model_setting?: Record }>(node) + writeNodeProperties(node, { + node_data: { + ...nodeData, + ...modelFields, + system: typeof args.system === 'string' ? args.system : (nodeData.system ?? ''), + prompt: String(args.prompt ?? nodeData.prompt ?? ''), + dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0), + dialogue_type: + args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'), + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true), + model_setting: { + reasoning_content_start: '', + reasoning_content_end: '', + ...nodeData.model_setting, + reasoning_content_enable: + typeof args.reasoning_content_enable === 'boolean' + ? args.reasoning_content_enable + : !!nodeData.model_setting?.reasoning_content_enable, + }, }, - }, - }) - return null - }), + }) + return finishWithValidate(context) + }, + }, getModelsTool(context, MODEL), ], })