From bc3ee495204b7891f6be087aee77e0c76533e293 Mon Sep 17 00:00:00 2001 From: shaohuzhang1 Date: Fri, 13 Nov 2026 19:39:10 +0800 Subject: [PATCH] fix: Optimize AI generation workflows --- ui/src/workflow-canvas/WORKFLOW_README.md | 14 +- .../ai-generate/impl/application/index.ts | 9 +- .../ai-generate/impl/knowledge/index.ts | 9 +- .../ai-generate/impl/shared.ts | 8 +- .../ai-generate/impl/tool/index.ts | 15 +- .../ai-generate/panel/AiGeneratePanel.vue | 42 +++- .../ai-generate/panel/activity.ts | 14 +- .../ai-generate/tools/index.ts | 234 ++++++++++++------ ui/src/workflow-canvas/core/validate.ts | 15 ++ ui/src/workflow-canvas/index.vue | 5 +- .../nodes/application-node/agent.ts | 106 ++++++++ .../nodes/application-node/index.ts | 3 +- .../nodes/document-split-node/agent.ts | 6 +- .../nodes/tool-base-node/agent.ts | 1 + .../nodes/tool-custom-node/agent.ts | 8 +- .../nodes/tool-lib-node/agent.ts | 4 +- .../nodes/tool-workflow-lib-node/agent.ts | 101 ++++++++ .../nodes/tool-workflow-lib-node/index.ts | 3 +- .../store/api/system-resource/index.ts | 1 + .../store/api/workspace/index.ts | 1 + ui/src/workflow-canvas/store/index.ts | 7 + 21 files changed, 493 insertions(+), 113 deletions(-) create mode 100644 ui/src/workflow-canvas/nodes/application-node/agent.ts create mode 100644 ui/src/workflow-canvas/nodes/tool-workflow-lib-node/agent.ts diff --git a/ui/src/workflow-canvas/WORKFLOW_README.md b/ui/src/workflow-canvas/WORKFLOW_README.md index 7f5f136325a..9cdbc653cca 100644 --- a/ui/src/workflow-canvas/WORKFLOW_README.md +++ b/ui/src/workflow-canvas/WORKFLOW_README.md @@ -513,8 +513,9 @@ MkFormList 的排序、增删均以 `cloneDeep` 回写;MkTable 保留普通行 | `validate` | 并行调用节点校验,返回 Promise | | `onmousedown` | 启动节点拖拽或执行节点回调 | -`getGraphData()` 在实例未初始化时返回 `undefined`;`validate()` 此时返回已完成的空数组 Promise, -有实例时汇总节点各自的校验,不额外执行顶层图结构校验。 +`getGraphData()` 在实例未初始化时返回 `undefined`;`validate()` 此时返回已完成的 Promise, +有实例时调用 `core/validate.ts` 的 `validateWorkflow()` 汇总节点各自的校验,不额外执行顶层图结构校验; +AI 生成的 validate 工具复用同一入口(`collectWorkflowValidationErrors()` 取全部失败原因)。 当前 `renderGraphData()` 不主动销毁上一实例;普通数据加载使用 `render()`,不要将前者当作 已封装完整清理的重置方法。画布卸载时销毁当前实例并清理 Teleport。 @@ -565,9 +566,16 @@ MkFormList 的排序、增删均以 `cloneDeep` 回写;MkTable 保留普通行 | panel/ | 将用户需求、文本、思考、工具及委派事件转换为对话内容展示 | 可新增的节点以节点菜单(`node-menu/menu.ts` 的 `getMenuNodes`)为准:节点目录与 add_node 都按画布模式取菜单节点, -另含工具库节点(tool-lib-node);菜单新增节点后 AI 侧自动可用。数据源节点(`kind` 为 DataSource)是知识库工作流入口, +另含工具(tool-lib-node)与智能体(application-node,仅应用工作流)两个资源类节点,对应菜单里的资源页签; +菜单新增节点后 AI 侧自动可用。数据源节点(`kind` 为 DataSource)是知识库工作流入口, 新增时不接上游,其余节点必须建即接。停止后续聊会重建父 agent,并沿用裁剪后的历史与首条需求。 +资源类节点(tool-lib-node、application-node)的签名取决于新增时绑定的资源,不是静态的:get_tools / get_applications +列出可选资源(自定义工具带 inputs;工作流工具与智能体的签名取决于它们自己声明的参数,成本较高不在列表里带), +add_node 时按 tool_id / application_id 查出真实详情、连同输入输出字段一起写入新节点的 node_data,返回结果里的 +`inputs`/`outputs` 就是这份真实签名,不需要再查一次。tool-lib-node 同时覆盖自定义工具与工作流工具(AI 侧只传 +这一种 type,按工具类型选择实际的内部节点),application-node 的入参只能引用上游数据,不支持常量。 + 节点配置 agent 位于节点目录的 agent.ts,通过 index.ts 默认导出的 agent 字段注册; 未注册的节点仍进入 AI 可选目录(说明取菜单文案,输出取模板 config.fields),可新增和连线,但不能 AI 配置。NodeAgentModule 使用 JSON Schema 定义 params、config、returns: params 描述运行时上游引用,config 描述建图时配置,returns 描述固定和动态输出。 diff --git a/ui/src/workflow-canvas/ai-generate/impl/application/index.ts b/ui/src/workflow-canvas/ai-generate/impl/application/index.ts index 099cd511cd9..52076a6eccd 100644 --- a/ui/src/workflow-canvas/ai-generate/impl/application/index.ts +++ b/ui/src/workflow-canvas/ai-generate/impl/application/index.ts @@ -8,6 +8,7 @@ import { getCanvasDetailTool, getNodeDetailTool, getNodeAnchorsTool, + getApplicationsTool, getToolsTool, layoutTool, validateTool, @@ -28,11 +29,12 @@ const GUIDE = `你是 MaxKB「智能体工作流」的搭建助手,在 LogicFl - 画布用 lf_id 标识:主画布 "main"(或省略);循环体 = 该循环节点 id。每个循环体固定有 loop-start-node(输出 index、item)。 - 循环变量:要跨轮次累积 / 传递数据,或循环结束后还要用循环内算出的值时,configure_node 循环节点时在 requirement 写明要声明哪些循环变量、returns 列出变量名。循环体内用 ["loop", 变量名] 引用;循环变量初始为空,修改它只能在循环体内加变量赋值节点(variable-assign-node,target=["loop", 变量名]),其他节点的输出不会自动写入,所以每个要更新的循环变量都要在产出数据的节点之后接一个变量赋值节点;循环结束后下游用 [循环节点ID, 变量名] 引用最终值。 - 目录里标注「暂不支持 AI 配置」的节点可以新增和连线,但不能 configure_node:有可配置的节点能满足需求时优先用后者;确实要用时建好接好,并在最后总结里提示用户手动配置。 +- 智能体节点(application-node)相当于把当前对话的一部分交给另一个已发布的智能体处理:add_node 时传 application_id(get_applications 查询),configure_node 时绑定用户问题与它需要的每个参数(只能引用上游数据,不支持常量)。 【从零搭建】 1. 先调 plan:规划器据需求组合节点函数,写出文字方案 { plan }(用户已在「规划流程」中看到全文)。拿到方案后先不要建图:不复述全文,用两三句概括主链路与关键假设,请用户确认或提出修改,然后直接结束本轮回复(不调用任何工具)。 用户确认后再照方案从第 2 步开始建;用户提出修改时,小改直接说明调整点后再次请用户确认,大改重新调 plan,同样确认后再建。 -2. 用 add_node(type, source_anchor_id) 建节点并接上游:第一个节点接 "start-node";普通链式连接传上游节点 id;判断器 / 意图识别要先 configure_node 配好分支(requirement 写清分几路、各路条件或分类),再 get_node_anchors 取各分支出口接下游;多分支汇合用 add_edge 补入边。 +2. 用 add_node(type, source_anchor_id) 建节点并接上游:第一个节点接 "start-node";普通链式连接传上游节点 id;新增工具节点(tool-lib-node)同时传 tool_id(get_tools 查询),新增智能体节点(application-node)同时传 application_id(get_applications 查询);判断器 / 意图识别要先 configure_node 配好分支(requirement 写清分几路、各路条件或分类),再 get_node_anchors 取各分支出口接下游;多分支汇合用 add_edge 补入边。 3. 循环:主画布 add_node 建 loop-node 并 configure_node 配循环方式;再带 lf_id=该循环节点 id 往循环体建(首节点 source_anchor_id="loop-start-node"),循环体内节点用 lf_id=该循环 id 配置;无限循环必须放 loop-break-node。 4. 节点建好接好后 configure_node(nodeId, args, requirement, returns?) 配置,相当于调用这个函数: - args 传实参:形参名 → 上游引用 [节点ID, 字段](多引用形参传列表),按方案中各节点的输入来源给,框架会校验并绑定; @@ -54,7 +56,7 @@ const PLAN_GUIDE = `你是 MaxKB「智能体工作流」的规划器。下方【 - 按数据依赖排顺序:每个节点的形参都要有上游返回值(或开始节点的 question、全局变量)来提供,类型要对得上(如循环的 array 需要上游返回 array)。 - 需要按条件 / 分类走不同处理时用分支(condition-node / intent-node),需要逐项处理一组数据时用循环(loop-node)。 - 只放需求实际需要的节点;末端必须是能输出给用户的节点,最终回答 is_result=true、中间步骤 false。 -- 需要外部工具时先 get_tools 看有哪些可用工具。需求含糊时按最常见理解规划,并写明假设。 +- 需要外部工具时先 get_tools 看有哪些可用工具;需要把一部分任务交给另一个智能体处理时先 get_applications 看有哪些已发布的智能体可用。需求含糊时按最常见理解规划,并写明假设。 方案格式(Markdown,面向用户可读,同时足够搭建 agent 照着建): - 按执行顺序逐个列节点:节点名称(类型)——做什么与配置要求(AI 对话的角色 / 目标 / 输出、检索什么、判断条件、表单收集哪些字段);输入:形参 ← 上游节点.返回字段(如 问题 ← 开始.question);有 [字段名] 动态返回时写出要产出的字段与类型。 @@ -70,11 +72,12 @@ export function useApplicationAgent(bindings: AgentBindings): RunnableAgent { { prompt: buildParentPrompt(GUIDE, bindings), tools: [ - createPlanTool(bindings, { guide: `${PLAN_GUIDE}\n\n======== 可用节点 ========\n${renderCatalog(bindings)}`, task: PLAN_TASK, tools: [getToolsTool(bindings)] }), + createPlanTool(bindings, { guide: `${PLAN_GUIDE}\n\n======== 可用节点 ========\n${renderCatalog(bindings)}`, task: PLAN_TASK, tools: [getToolsTool(bindings), getApplicationsTool(bindings)] }), getCanvasDetailTool(bindings), getNodeDetailTool(bindings), getNodeAnchorsTool(bindings), getToolsTool(bindings), + getApplicationsTool(bindings), addNodeTool(bindings), addEdgeTool(bindings), deleteNodeTool(bindings), diff --git a/ui/src/workflow-canvas/ai-generate/impl/knowledge/index.ts b/ui/src/workflow-canvas/ai-generate/impl/knowledge/index.ts index 28e40172265..1262b9c4e62 100644 --- a/ui/src/workflow-canvas/ai-generate/impl/knowledge/index.ts +++ b/ui/src/workflow-canvas/ai-generate/impl/knowledge/index.ts @@ -59,7 +59,7 @@ const GUIDE = `你是 MaxKB「知识库工作流」的搭建助手,在 LogicFl 7. 全部完成后调 validate,problems 非空就按提示修复,直到 qualified=true,再调 layout 整理布局。 【写入前加工:标签 / 关联问题 / 元数据】 -需求要给文档打标签、补关联问题、改文档名或补元数据时,在文档分段与知识库写入之间加一段加工链路,由自定义代码(tool-custom-node)把分段结果改写成带这些字段的文档列表,知识库写入改为引用代码节点:documents ← [代码节点ID, "result"]。 +需求要给文档打标签、补关联问题、改文档名或补元数据时,在文档分段与知识库写入之间加一段加工链路。规划阶段应该已经 get_tools 查过现成工具;方案里写了用现成工具就照方案 add_node(type="tool-lib-node", tool_id) 接上,configure_node 时 documents 引用文档分段的 paragraph_list,其余入参按工具自己的签名绑定,输出字段必须是整个文档列表才能直连知识库写入。方案里没有现成工具,或续聊时还没查过,才用自定义代码(tool-custom-node)把分段结果改写成带这些字段的文档列表,知识库写入改为引用代码节点:documents ← [代码节点ID, "result"]。 - 用 AI 生成的(如按内容打标签):文档分段 → AI 对话(ai-chat-node)→ 自定义代码 → 知识库写入。 · AI 对话:materials 传文档分段的 paragraph_list,提示词要求只输出 JSON,不要多余文字,格式固定为 { "文档名": [{ "key": "标签名", "value": "标签值" }] },并写清标签的维度和取值范围。 · 自定义代码:inputs 两个引用参数 documents(array)← 文档分段.paragraph_list、ai_output(string)← AI 对话.answer。代码解析 ai_output(先去掉 Markdown 代码块标记,解析失败时保留原文档不加标签),遍历 documents,按 name 把标签写进对应文档的 tags,原字段一律保留,返回整个 documents 列表。AI 输出的字典只是查找表,不是返回值。 @@ -81,7 +81,8 @@ const GUIDE = `你是 MaxKB「知识库工作流」的搭建助手,在 LogicFl const PLAN_GUIDE = `你是 MaxKB「知识库工作流」的规划器。下方【可用节点】把每个节点写成一个函数(说明、形参、返回字段)。知识库工作流把外部数据源经文档处理写入知识库;它没有固定开始节点,入口是数据源节点。根据用户需求,像写程序一样组合这些函数,写出一份文字方案:先给用户确认,再供搭建 agent 照着建。你只规划、不改画布。 思路: -- 先拆解需求:数据从哪来(本地文件 / Web 站点等数据源)、如何清洗(内容提取、分段)、写到哪(当前知识库)。 +- 规划前必须先调一次 get_tools,不要凭经验判断「大概没有」就跳过:看工具库里有没有现成的工具能直接完成需求里的加工步骤(打标签、提取信息等)。有合适的就在方案里直接用它(tool-lib-node),不要重新搭一条 AI 对话 + 自定义代码的链路;确实没有才按下文的生成式做法写。 +- 拆解需求:数据从哪来(本地文件 / Web 站点等数据源)、如何清洗(内容提取、分段)、写到哪(当前知识库)。 - 入口数据源优先用内置的本地文件(data-source-local-node,画布通常已自带)或 Web 站点(data-source-web-node);只有需求指向其他来源时才 list_datasource 查数据源工具(tool-lib-node)。 - 按数据依赖排顺序:本地文件.file_list → 文档内容提取 → 文档分段 → 知识库写入;Web 站点.document_list 已含内容,直接 → 文档分段 → 知识库写入。每个节点的形参都要有上游返回值来提供,类型要对得上。 - 需要按条件 / 分类走不同处理时用分支(condition-node / intent-node),需要逐项处理一组数据时用循环(loop-node)。 @@ -91,7 +92,7 @@ const PLAN_GUIDE = `你是 MaxKB「知识库工作流」的规划器。下方【 方案格式(Markdown,面向用户可读,同时足够搭建 agent 照着建): - 按执行顺序逐个列节点:节点名称(类型)——做什么与配置要求(数据源抓取 / 上传方式、分段策略、写入哪);输入:形参 ← 上游节点.返回字段;有 [字段名] 动态返回时写出要产出的字段与类型。 - 分支节点(判断器 / 意图识别)逐路写:条件或分类 → 该路的后续节点,最后一路为 else / 其他;循环节点写清循环方式与循环体内的节点。 -- 需求要给文档打标签、补关联问题或元数据时,在文档分段与知识库写入之间加加工链路:AI 生成的用 文档分段 → AI 对话(只输出 JSON)→ 自定义代码(合并进文档的 tags 等字段)→ 知识库写入;规则能确定的用 文档分段 → 自定义代码 → 知识库写入。方案里写清标签的维度和取值。 +- 需求要给文档打标签、补关联问题或元数据时,在文档分段与知识库写入之间加加工链路:get_tools 查到合适的现成工具就用 文档分段 → 该工具(tool-lib-node)→ 知识库写入,方案里写清这个工具叫什么、它的输出字段对应知识库写入的哪个引用;没有合适工具,AI 生成的用 文档分段 → AI 对话(只输出 JSON)→ 自定义代码(合并进文档的 tags 等字段)→ 知识库写入,规则能确定的用 文档分段 → 自定义代码 → 知识库写入,方案里写清标签的维度和取值。 - 最后写「关键假设」:需要用户事先准备或之后补充的内容(如数据源、知识库),以及对含糊需求的理解。 方案写完即结束,之后不再调用任何工具。` @@ -106,7 +107,7 @@ export function useKnowledgeAgent(bindings: AgentBindings): RunnableAgent { createPlanTool(bindings, { guide: `${PLAN_GUIDE}\n\n======== 可用节点 ========\n${renderCatalog(bindings)}`, task: PLAN_TASK, - tools: [listDataSourceTool(bindings)], + tools: [listDataSourceTool(bindings), getToolsTool(bindings)], }), getCanvasDetailTool(bindings), getNodeDetailTool(bindings), diff --git a/ui/src/workflow-canvas/ai-generate/impl/shared.ts b/ui/src/workflow-canvas/ai-generate/impl/shared.ts index e8d887d703e..24fd276548d 100644 --- a/ui/src/workflow-canvas/ai-generate/impl/shared.ts +++ b/ui/src/workflow-canvas/ai-generate/impl/shared.ts @@ -1,5 +1,5 @@ import { getMenuNodes } from '@/workflow-canvas/node-menu/menu' -import { WorkflowNodeType, type ShapeItem } from '@/workflow-canvas/types' +import { WorkflowMode, WorkflowNodeType, type ShapeItem } from '@/workflow-canvas/types' import { loopModeOf } from '@/workflow-canvas/ai-generate/tools/canvas' import { inheritMessages, useAgentConfig } from '@/workflow-canvas/ai-generate/common' import { createAgent } from '@/workflow-canvas/ai-generate/create-agent' @@ -25,7 +25,8 @@ export function nodeLine(node: CatalogNode): string { /** * 可用节点函数目录:与节点菜单(node-menu)一致,按画布模式列出全部菜单节点,循环体专属节点单列; - * 末尾追加「工具」(tool-lib-node)函数,对应菜单的工具 / 数据源页签。 + * 末尾追加「工具」(tool-lib-node,涵盖自定义工具与工作流工具)和「智能体」(application-node,仅应用工作流)函数, + * 对应菜单里的资源页签(工具 / 数据源 / 智能体),这些节点不在 menu.ts 的静态列表里。 */ export function renderCatalog(bindings: AgentBindings): string { const mainGroups = getMenuNodes(bindings.workflowMode) @@ -36,6 +37,9 @@ export function renderCatalog(bindings: AgentBindings): string { const sections = mainGroups.filter((group) => group.list.length).map((group) => `【${group.label}】\n${group.list.map(nodeLine).join('\n\n')}`) const toolLib = nodeAgentFor(WorkflowNodeType.ToolLib) if (toolLib) sections.push(`【工具】\n${renderNodeFunction(WorkflowNodeType.ToolLib, '工具', toolLib)}`) + const isApplicationMode = bindings.workflowMode === WorkflowMode.Application || bindings.workflowMode === WorkflowMode.ApplicationLoop + const application = isApplicationMode ? nodeAgentFor(WorkflowNodeType.Application) : undefined + if (application) sections.push(`【智能体】\n${renderNodeFunction(WorkflowNodeType.Application, '智能体节点', application)}`) if (loopOnly.length) sections.push(`【仅循环体内可用(lf_id=所在循环节点 id)】\n${loopOnly.map(nodeLine).join('\n\n')}`) return sections.join('\n\n') } diff --git a/ui/src/workflow-canvas/ai-generate/impl/tool/index.ts b/ui/src/workflow-canvas/ai-generate/impl/tool/index.ts index 48be99d42ad..ff1f670c9ee 100644 --- a/ui/src/workflow-canvas/ai-generate/impl/tool/index.ts +++ b/ui/src/workflow-canvas/ai-generate/impl/tool/index.ts @@ -28,6 +28,7 @@ const callerGuide = () => `【工具给谁用】工具工作流发布后有两 - 给知识库工作流用(加工待入库的文档,如打标签、补关联问题、改写分段):位置在「文档分段」与「知识库写入」之间。输入一个 array 参数接收文档分段输出的文档列表,输出一个参数返回补好字段的同一个文档列表,原字段一律保留,顶层必须是 list(哪怕只有一篇),这样知识库写入能直接引用。文档列表结构: ${knowledgeDocumentsStructure()} 要 AI 逐篇生成内容时,对文档列表用循环节点逐篇处理(或一次性交给 AI 对话输出 JSON),再由自定义代码按 name 合并回文档列表;AI 输出的 { 文档名: ... } 字典只用于查找,不能直接当返回值。 +- 输入参数和输出参数都是按需声明的,可以没有:不需要调用方提供数据就不声明输入参数(如查询当天天气),只执行动作、不需要把数据交回调用方就不声明输出参数(如发通知、写入数据);不要为了凑签名硬加参数。 - 需求里看得出调用方(如"知识库工作流用的…工具""给智能体调用")就按对应形态设计;看不出来时按智能体调用设计,并在关键假设里写明、请用户确认。` const guide = () => `你是 MaxKB「工具工作流」的搭建助手,在 LogicFlow 画布上按用户需求用建图工具亲手把流程搭出来(含循环体)。工具工作流就是一个函数,发布后被智能体或其他工作流调用:函数签名是「基本信息」里声明的输入参数和输出参数,由你 configure_node("tool-base-node") 声明;输入参数在开始节点提供为全局变量 ["global", 参数名];返回值只取流程里用变量赋值节点写入 ["output", 参数名] 的值,其他节点的输出不会自动返回给调用方。 @@ -38,7 +39,7 @@ ${callerGuide()} 约定: - 画布固定节点:tool-base-node(工具基本信息)与 tool-start-node(开始,id 固定 "tool-start-node"),都不可新增/删除;开始节点不可配置,基本信息节点用 configure_node("tool-base-node", requirement) 声明输入 / 输出参数(无需 args)。开始节点提供输入参数 ["global", 参数名];输出参数 ["output", 参数名] 是待写入的变量,初始为空。 -- 返回结果:每个输出参数都要在产出结果的节点之后接一个变量赋值节点(variable-assign-node),target=["output", 参数名],结果是数组 / 对象时用引用方式原样赋值;有分支时每条会结束的路径都要写入。 +- 返回结果:声明了输出参数时,每个输出参数都要在产出结果的节点之后接一个变量赋值节点(variable-assign-node),target=["output", 参数名],结果是数组 / 对象时用引用方式原样赋值;有分支时每条会结束的路径都要写入。 - 引用格式:[节点ID, 字段];提示词里的变量写 {{节点名称.字段}},输入参数写 {{global.参数名}}。节点名称以 add_node 返回的 name 为准。 - 画布用 lf_id 标识:主画布 "main"(或省略);循环体 = 该循环节点 id。每个循环体固定有 loop-start-node(输出 index、item)。 - 循环变量:要跨轮次累积 / 传递数据,或循环结束后还要用循环内算出的值时,configure_node 循环节点时在 requirement 写明要声明哪些循环变量、returns 列出变量名。循环体内用 ["loop", 变量名] 引用;循环变量初始为空,修改它只能在循环体内加变量赋值节点(variable-assign-node,target=["loop", 变量名]),其他节点的输出不会自动写入,所以每个要更新的循环变量都要在产出数据的节点之后接一个变量赋值节点;循环结束后下游用 [循环节点ID, 变量名] 引用最终值。 @@ -47,7 +48,7 @@ ${callerGuide()} 【从零搭建】 1. 先调 plan:规划器据需求定出工具的签名(输入 / 输出参数)并组合节点函数,写出文字方案 { plan }(用户已在「规划流程」中看到全文)。拿到方案后先不要建图:不复述全文,用两三句概括工具的调用方、输入输出参数、主链路与关键假设,请用户确认或提出修改,然后直接结束本轮回复(不调用任何工具)。 用户确认后再照方案从第 2 步开始;用户提出修改时,小改直接说明调整点后再次请用户确认,大改重新调 plan,同样确认后再建。 -2. 先 configure_node("tool-base-node") 按方案声明输入参数与输出参数,requirement 里逐个写全:输入参数的参数名、显示名称、类型、是否必填、说明;输出参数的参数名、显示名称。它交付最新签名 { inputs, outputs },之后节点才能引用 ["global", 参数名]、变量赋值才能写入 ["output", 参数名]。 +2. 方案里有输入或输出参数时,先 configure_node("tool-base-node") 声明(都没有就跳过这一步),requirement 里逐个写全:输入参数的参数名、显示名称、类型、是否必填、说明;输出参数的参数名、显示名称。它交付最新签名 { inputs, outputs },之后节点才能引用 ["global", 参数名]、变量赋值才能写入 ["output", 参数名]。 3. 用 add_node(type, source_anchor_id) 建节点并接上游:第一个节点接 "tool-start-node";普通链式连接传上游节点 id;判断器 / 意图识别要先 configure_node 配好分支(requirement 写清分几路、各路条件或分类),再 get_node_anchors 取各分支出口接下游;多分支汇合用 add_edge 补入边。 4. 循环:主画布 add_node 建 loop-node 并 configure_node 配循环方式;再带 lf_id=该循环节点 id 往循环体建(首节点 source_anchor_id="loop-start-node"),循环体内节点用 lf_id=该循环 id 配置;无限循环必须放 loop-break-node。 5. 节点建好接好后 configure_node(nodeId, args, requirement, returns?) 配置,相当于调用这个函数: @@ -55,7 +56,7 @@ ${callerGuide()} - requirement 写配置要求(提示词只写要求、模型 / 知识库只写意图),节点配置 agent 会生成内容并查出真实资源; - 节点有 [字段名] 动态返回时,returns 列出要求产出的字段名,下游按这些字段引用。 同一轮可并发配置多个节点;但实参要引用上游的动态返回字段时,先配上游,按其交付的 outputs 再配下游。 -6. 返回结果:在产出结果的节点之后 add_node 变量赋值节点并 configure_node,args 传 { values: [[结果节点ID, 字段]] },requirement 写明把它赋给 ["output", 参数名];每个输出参数都要写入。 +6. 返回结果(没有输出参数则跳过):在产出结果的节点之后 add_node 变量赋值节点并 configure_node,args 传 { values: [[结果节点ID, 字段]] },requirement 写明把它赋给 ["output", 参数名];每个输出参数都要写入。 7. 全部完成后调 validate(会检查输出参数是否都被写入、是否引用了未声明的参数),problems 非空就按提示修复,直到 qualified=true,再调 layout 整理布局。 【续聊修改】先 get_canvas_detail 看骨架(tool_params 当前的输入 / 输出参数,节点、连线、各节点的数据引用 references,不含配置内容),要改或要核对的节点再用 get_node_detail(nodeIds) 取完整配置(可一次传多个),然后用 add_node / add_edge / delete_node / delete_edge / configure_node 就地修改(循环体内传对应 lf_id),要增删改输入 / 输出参数时再次 configure_node("tool-base-node"),requirement 写明要改哪些、其余保留;改名或删除参数后调 validate,按 problems 修正仍引用旧参数的节点。改完同样 validate。 @@ -70,17 +71,17 @@ ${callerGuide()} 根据用户需求,像写程序一样组合这些函数,写出一份文字方案:先给用户确认,再供搭建 agent 照着建。你只规划、不改画布。 思路: -- 先定签名:谁来调用这个工具,据此定输入参数(名称、类型、说明)和输出参数;再从输出参数倒推,为得到它需要哪些中间数据,每份数据由哪个节点函数的返回值提供。 +- 先定签名:谁来调用这个工具,据此判断需不需要输入参数、需不需要输出参数,需要的再定名称、类型、说明;有输出参数时从它倒推,为得到它需要哪些中间数据,每份数据由哪个节点函数的返回值提供;没有输出参数时从要完成的动作倒推。 - 按数据依赖排顺序:每个节点的形参都要有上游返回值(或输入参数 global.参数名)来提供,类型要对得上(如循环的 array 需要上游返回 array)。 - 需要按条件 / 分类走不同处理时用分支(condition-node / intent-node),需要逐项处理一组数据时用循环(loop-node)。 -- 只放需求实际需要的节点;每个输出参数都要有一个变量赋值节点(variable-assign-node)写入,有分支时每条会结束的路径都要写入。 +- 只放需求实际需要的节点;声明了输出参数时,每个输出参数都要有一个变量赋值节点(variable-assign-node)写入,有分支时每条会结束的路径都要写入。 - 需要外部工具时先 get_tools 看有哪些可用工具。需求含糊时按最常见理解规划,并写明假设。 方案格式(Markdown,面向用户可读,同时足够搭建 agent 照着建): -- 开头写「调用方」(智能体 / 知识库工作流 / 应用工作流)与「工具签名」:输入参数逐个列 参数名(类型,是否必填)——说明;输出参数逐个列 参数名——含义与数据结构。 +- 开头写「调用方」(智能体 / 知识库工作流 / 应用工作流)与「工具签名」:输入参数逐个列 参数名(类型,是否必填)——说明;输出参数逐个列 参数名——含义与数据结构;没有的一侧写「无」。 - 按执行顺序逐个列节点:节点名称(类型)——做什么与配置要求(AI 对话的角色 / 目标 / 输出、检索什么、判断条件、表单收集哪些字段);输入:形参 ← 上游节点.返回字段(如 内容 ← global.参数名);有 [字段名] 动态返回时写出要产出的字段与类型。 - 分支节点(判断器 / 意图识别)逐路写:条件或分类 → 该路的后续节点,最后一路为 else / 其他;循环节点写清循环方式与循环体内的节点。 -- 末尾的变量赋值节点写明:输出参数 ← 哪个节点的哪个字段。 +- 有输出参数时,末尾的变量赋值节点写明:输出参数 ← 哪个节点的哪个字段。 - 最后写「关键假设」:需要用户事先准备或之后补充的内容(如知识库、工具),以及对含糊需求的理解(包括调用方是按什么推断的)。 写完方案直接结束,不调用任何工具。` diff --git a/ui/src/workflow-canvas/ai-generate/panel/AiGeneratePanel.vue b/ui/src/workflow-canvas/ai-generate/panel/AiGeneratePanel.vue index 17e57357d8b..5da8b01ec81 100644 --- a/ui/src/workflow-canvas/ai-generate/panel/AiGeneratePanel.vue +++ b/ui/src/workflow-canvas/ai-generate/panel/AiGeneratePanel.vue @@ -8,6 +8,7 @@ import type { DefaultModelSettingPayload, ModelItem, ModelProviderItem } from '@ import SelectModel from '@/components/business/select-model/index.vue' import ContentList from '@/conversation-panel/components/content-list/index.vue' import QuickQuestion from '@/conversation-panel/components/markdown-renderer/QuickQuestion.vue' +import { Scroll } from '@/conversation-panel/core/scroll' import { inputShortcut } from '@/conversation-panel/core/shortcuts' import { MsgError, MsgSuccess } from '@/utils/message' import { useWorkflowStore } from '@/workflow-canvas/store' @@ -91,8 +92,36 @@ function close() { visible.value = false } -function scrollToBottom() { - nextTick(() => scrollbarRef.value?.setScrollTop(Number.MAX_SAFE_INTEGER)) +/* 底部跟随与对话面板共用 Scroll:用户上滑查看时停止跟随,回到底部附近恢复。 */ +let bottomScroll: Scroll | undefined +let bottomScrollElement: HTMLElement | undefined +let followPending = false + +/** 抽屉内容可能重新挂载,滚动容器变化时重建跟随实例 */ +function getBottomScroll() { + const wrap = scrollbarRef.value?.wrapRef + if (!wrap) return undefined + if (wrap !== bottomScrollElement) { + bottomScroll?.destroy() + bottomScroll = new Scroll(wrap) + bottomScrollElement = wrap + } + return bottomScroll +} + +/** 流式增量:仅在用户停留在底部附近时跟随;同一轮渲染内的多次增量只滚动一次 */ +function followBottom() { + if (followPending) return + followPending = true + nextTick(() => { + followPending = false + getBottomScroll()?.scrollBottom() + }) +} + +/** 用户发送需求:回到最新内容并恢复跟随 */ +function forceBottom() { + nextTick(() => getBottomScroll()?.forceBottom()) } /** 会改动画布的顶层工具;本轮未调用时视为仅规划 / 答复(如等待用户确认方案),不提示生成完成 */ @@ -103,7 +132,7 @@ let canvasChanged = false function onEvent(event: AgentEvent) { if (event.type === 'tool_start' && !event.position.length && CANVAS_MUTATING_TOOLS.has(event.name)) canvasChanged = true activity.pushEvent(event) - scrollToBottom() + followBottom() } /** 画布所属资源 id:按模式取路由上的应用 / 工具 / 知识库 id */ @@ -171,7 +200,7 @@ function submit() { activity.reset() } activity.pushQuestion(text) - scrollToBottom() + forceBottom() run(text) } @@ -193,7 +222,10 @@ function handleKeydown(event: KeyboardEvent) { inputShortcut(event, requirement, submit) } -onBeforeUnmount(() => agent?.stop()) +onBeforeUnmount(() => { + agent?.stop() + bottomScroll?.destroy() +}) defineExpose({ open, close }) diff --git a/ui/src/workflow-canvas/ai-generate/panel/activity.ts b/ui/src/workflow-canvas/ai-generate/panel/activity.ts index 0a981bce097..37daf81a8f4 100644 --- a/ui/src/workflow-canvas/ai-generate/panel/activity.ts +++ b/ui/src/workflow-canvas/ai-generate/panel/activity.ts @@ -90,11 +90,6 @@ function spawnKeyOf(toolCall: ToolCall): string | undefined { return undefined } -function tail(text: string) { - const trimmed = text.trim() - return trimmed.length > 40 ? `…${trimmed.slice(-40)}` : trimmed -} - function parentKeyOf(scopeKey: string) { const index = scopeKey.lastIndexOf('/') return index >= 0 ? scopeKey.slice(0, index) : '' @@ -107,7 +102,7 @@ export function createActivity() { const groupIdByScope = new Map() const toolCallById = new Map() /** 各作用域最后一条正文 / 思考:同类增量续写到同一项 */ - const lastStreamByScope = new Map() + const lastStreamByScope = new Map() function reset() { activityItems.value = [] @@ -183,13 +178,12 @@ export function createActivity() { let last = lastStreamByScope.get(scopeKey) if (last?.type !== type) { settle(scopeKey) - last = { type, id: uuidv7(), text: '' } + last = { type, id: uuidv7() } lastStreamByScope.set(scopeKey, last) } - last.text += event.delta emit(scopeKey, { type, id: last.id, content: event.delta, ...(type === 'REASONING' ? { status: 'RUNNING' } : {}) }) - // lastActivity.value = tail(last.text) - lastActivity.value = event.type === 'reasoning' ? tail(last.text) : '' + // mk-dotting 只接受静态文案(见 STYLE_README.md),不能随逐字增量改变,否则文案本身也会跟着抖动 + lastActivity.value = event.type === 'reasoning' ? '思考中' : '生成中' } else if (event.type === 'tool_start') { settle(scopeKey) const delegate = DELEGATE_TOOLS.has(event.name) diff --git a/ui/src/workflow-canvas/ai-generate/tools/index.ts b/ui/src/workflow-canvas/ai-generate/tools/index.ts index 76f46039be6..c668399759e 100644 --- a/ui/src/workflow-canvas/ai-generate/tools/index.ts +++ b/ui/src/workflow-canvas/ai-generate/tools/index.ts @@ -1,10 +1,10 @@ import { cloneDeep } from 'lodash' import { TOOL_TYPE } from '@/api/enums' -import type { ToolItem } from '@/api/types' +import type { ToolItem, ToolType } from '@/api/types' import type LogicFlow from '@logicflow/core' import type { BaseNodeModel } from '@logicflow/core' -import { WorkFlowInstance, KnowledgeWorkFlowInstance, ToolWorkFlowInstance } from '@/workflow-canvas/core/validate' -import { toolLibNode } from '@/workflow-canvas/config/node-data' +import { collectWorkflowValidationErrors } from '@/workflow-canvas/core/validate' +import { applicationNode, toolLibNode, toolWorkflowLibNode } from '@/workflow-canvas/config/node-data' import { useWorkflowStore } from '@/workflow-canvas/store' import { WorkflowKind, WorkflowMode, WorkflowNodeType, type ShapeItem } from '@/workflow-canvas/types' import { @@ -300,14 +300,70 @@ export function getNodeAnchorsTool(bindings: AgentBindings): AgentTool { } } -/** 工具节点的模板:按工具详情补齐参数与名称,与资源菜单拖入的节点一致;数据源工具额外写入 kind / condition */ +/** 自定义工具(tool-lib-node)的入参摘要:node_data.input_field_list 以 name 为键 */ +function toolInputsOf(fields: Array<{ name: string; type?: string; source?: string; is_required?: boolean; desc?: string }> | undefined) { + return (fields ?? []).map((field) => ({ name: field.name, type: field.type, source: field.source, is_required: field.is_required, desc: field.desc })) +} + +/** 工作流工具(tool-workflow-lib-node)的入参摘要:node_data.input_field_list 以 field 为键,与 tool-base-node 的字段协议一致 */ +function workflowToolInputsOf(fields: Array<{ field: string; label?: string; type?: string; source?: string; is_required?: boolean; desc?: string }> | undefined) { + return (fields ?? []).map((field) => ({ name: field.field, label: field.label, type: field.type, source: field.source, is_required: field.is_required, desc: field.desc })) +} + +/** source=custom 的常量初始值:与节点表单 getCustomValue 一致,避免类型不匹配 */ +function workflowToolCustomValueOf(type: string | undefined): unknown { + if (type === 'array') return [] + if (type === 'dict') return {} + if (type === 'int' || type === 'float') return undefined + return '' +} + +/** 工作流工具自己工作流里基本信息节点声明的输入 / 输出参数(即 tool-base-node 的 user_input_field_list / user_output_field_list) */ +function workflowToolBaseProperties(tool: ToolItem) { + const baseNode = tool.work_flow?.nodes?.find((node) => node.type === WorkflowNodeType.ToolBaseNode) + return (baseNode?.properties ?? {}) as { + user_input_field_list?: Array<{ field: string; label?: string; type?: string; desc?: string; is_required?: boolean }> + user_output_field_list?: Array<{ field: string; label?: string }> + } +} + +/** + * 工具节点的模板:按工具详情补齐参数与名称,与资源菜单拖入的节点一致;数据源工具额外写入 kind / condition。 + * 覆盖自定义工具与工作流工具(按 tool_type 选 tool-lib-node / tool-workflow-lib-node),AI 侧统一传 tool_id,不用区分。 + * 工作流工具的签名在它自己工作流的基本信息节点里,这里一并取出补进 node_data 与 config.fields, + * 使 add_node 的返回结果立即带有真实的入参与输出字段,不必另外一次查询。 + */ async function toolNodeTemplate(bindings: AgentBindings, toolId: string): Promise { const tool = await useWorkflowStore(bindings.resourceScope) .force.getToolById(toolId) .catch(() => null) if (!tool?.id) return { error: `工具不存在: ${toolId}` } - const inputFields = (tool.input_field_list ?? []).map((field) => ({ ...field, value: field.source === 'reference' ? [] : '' })) + if (!tool.is_active) return { error: `工具已停用: ${tool.name}` } const dataSource = tool.tool_type === TOOL_TYPE.DATA_SOURCE ? { kind: WorkflowKind.DataSource, condition: 'OR' } : {} + if (tool.tool_type === TOOL_TYPE.WORKFLOW) { + const baseProperties = workflowToolBaseProperties(tool) + if (!tool.work_flow?.nodes?.length) return { error: `工作流工具尚未发布,需发布后才能使用: ${tool.name}` } + const inputFields = (baseProperties.user_input_field_list ?? []).map((field) => ({ + field: field.field, + label: field.label || field.field, + type: field.type ?? 'string', + desc: field.desc ?? '', + is_required: field.is_required ?? true, + source: 'custom' as const, + value: workflowToolCustomValueOf(field.type), + })) + const outputFields = (baseProperties.user_output_field_list ?? []).map((field) => ({ value: field.field, label: field.label || field.field })) + return { + ...cloneDeep(toolWorkflowLibNode), + properties: { + ...cloneDeep(toolWorkflowLibNode.properties), + stepName: tool.name, + config: { ...cloneDeep(toolWorkflowLibNode.properties.config), fields: outputFields }, + node_data: { input_field_list: inputFields, tool_lib_id: tool.id }, + }, + } + } + const inputFields = (tool.input_field_list ?? []).map((field) => ({ ...field, value: field.source === 'reference' ? [] : '' })) return { ...cloneDeep(toolLibNode), properties: { @@ -319,31 +375,83 @@ async function toolNodeTemplate(bindings: AgentBindings, toolId: string): Promis } } -/** 工具列表 → 精简结构,供 get_tools / list_datasource 复用 */ +/** 工具列表 → 精简结构,供 list_datasource 复用(数据源始终是自定义实现,带 inputs) */ function summarizeTools(tools: ToolItem[]) { - return tools.map((tool) => ({ - id: tool.id, - name: tool.name, - desc: tool.desc, - inputs: (tool.input_field_list ?? []).map((field) => ({ - name: field.name, - type: field.type, - source: field.source, - is_required: field.is_required, - desc: field.desc, - })), - })) + return tools.map((tool) => ({ id: tool.id, name: tool.name, desc: tool.desc, inputs: toolInputsOf(tool.input_field_list) })) } -/** 列出可在应用工作流中使用的自定义工具(父 agent 选型用) */ +/** 与节点菜单「工具」页签一致:自定义工具 + 工作流工具(父 agent 选型用) */ export function getToolsTool(bindings: AgentBindings): AgentTool { + const toolTypes: ToolType[] = [TOOL_TYPE.CUSTOM, TOOL_TYPE.WORKFLOW] return { name: 'get_tools', - description: '列出可用的自定义工具 [{id,name,desc,inputs:[{name,type,source,is_required,desc}]}],add_node 新增 tool-lib-node 时传其 tool_id。', + description: + '列出可用工具 [{id,name,desc,type}],范围与节点菜单的「工具」页签一致:自定义工具(type=custom,带 inputs)与工作流工具(type=workflow,签名未知,' + + '因为取决于它自己工作流声明的输入 / 输出参数,add_node 新增后即可在返回结果里看到)。add_node 新增 tool-lib-node 时传其 tool_id,两种工具都用这一种节点类型。', + parameters: { type: 'object', properties: {} }, + apply: async () => { + const tools = await useWorkflowStore(bindings.resourceScope).getToolListWithShared({ tool_type_list: toolTypes }) + const currentToolId = bindings.workflowMode === WorkflowMode.Tool || bindings.workflowMode === WorkflowMode.ToolLoop ? bindings.resourceId : undefined + return tools + .filter((tool) => tool.is_active && tool.id !== currentToolId && toolTypes.includes(tool.tool_type)) + .map((tool) => ({ + id: tool.id, + name: tool.name, + desc: tool.desc, + type: tool.tool_type === TOOL_TYPE.WORKFLOW ? 'workflow' : 'custom', + ...(tool.tool_type === TOOL_TYPE.WORKFLOW ? {} : { inputs: toolInputsOf(tool.input_field_list) }), + })) + }, + } +} + +/** 列出可在应用工作流中添加的已发布智能体(父 agent 选型用),与节点菜单「智能体」页签一致 */ +export function getApplicationsTool(bindings: AgentBindings): AgentTool { + return { + name: 'get_applications', + description: + '列出可添加为智能体节点的已发布智能体 [{id,name,desc}]。add_node 新增 application-node 时传其 application_id,' + + '它的入参(工作流智能体的开始节点字段)在新增后的返回结果里可见,需要逐个用引用绑定(不支持常量)。', parameters: { type: 'object', properties: {} }, apply: async () => { - const tools = await useWorkflowStore(bindings.resourceScope).getToolListWithShared({ tool_type: TOOL_TYPE.CUSTOM }) - return summarizeTools(tools.filter((tool) => tool.is_active && tool.tool_type === TOOL_TYPE.CUSTOM)) + const applications = await useWorkflowStore(bindings.resourceScope).getAllApplication({ publish_status: 'published' }) + const currentApplicationId = bindings.workflowMode === WorkflowMode.Application || bindings.workflowMode === WorkflowMode.ApplicationLoop ? bindings.resourceId : undefined + return applications.filter((application) => application.id !== currentApplicationId).map((application) => ({ id: application.id, name: application.name, desc: application.desc })) + }, + } +} + +/** + * 智能体节点的模板:按应用详情补齐名称与入参,与资源菜单拖入的节点一致。 + * 工作流类型的应用,入参取自它开始节点声明的 api_input_field_list(API 入参)与 user_input_field_list(表单入参); + * 简易应用没有这些入参,只有用户问题(由节点自带的 question_reference_address 绑定,不需要额外参数)。 + */ +async function applicationNodeTemplate(bindings: AgentBindings, applicationId: string): Promise { + const application = await useWorkflowStore(bindings.resourceScope) + .force.getApplicationDetail(applicationId) + .catch(() => null) + if (!application?.id) return { error: `智能体不存在: ${applicationId}` } + const startNode = application.work_flow?.nodes?.[0] + const startProperties = (startNode?.properties ?? {}) as { + api_input_field_list?: Array<{ variable: unknown; label?: unknown; is_required: boolean }> + user_input_field_list?: Array<{ field: string; label: unknown; required: boolean }> + } + const apiInputFieldList = (startProperties.api_input_field_list ?? []).map((field) => ({ ...field, value: [] })) + const userInputFieldList = (startProperties.user_input_field_list ?? []).map((field) => ({ ...field, value: [] })) + return { + ...cloneDeep(applicationNode), + properties: { + ...cloneDeep(applicationNode.properties), + stepName: application.name, + node_data: { + application_id: application.id, + name: application.name, + icon: application.icon, + question_reference_address: [WorkflowNodeType.Start, 'question'], + is_result: false, + api_input_field_list: apiInputFieldList, + user_input_field_list: userInputFieldList, + }, }, } } @@ -362,15 +470,38 @@ export function listDataSourceTool(bindings: AgentBindings): AgentTool { } } +/** 资源类节点(工具 / 智能体)新增后,从已 bake 好的 node_data 里取入参摘要,随 add_node 的结果一并交付,不必再查一次 */ +function resourceNodeInputsOf(node: ReturnType): { inputs?: unknown[] } { + const nodeData = node.properties.node_data as Record | undefined + if (!nodeData) return {} + if (String(node.type) === WorkflowNodeType.ToolLib) return { inputs: toolInputsOf(nodeData.input_field_list as never) } + if (String(node.type) === WorkflowNodeType.ToolWorkflowLib) return { inputs: workflowToolInputsOf(nodeData.input_field_list as never) } + if (String(node.type) === WorkflowNodeType.Application) { + const apiInputs = ((nodeData.api_input_field_list ?? []) as Array<{ variable: unknown; label?: unknown; is_required: boolean }>).map((field) => ({ + name: field.variable, + label: field.label, + is_required: field.is_required, + })) + const userInputs = ((nodeData.user_input_field_list ?? []) as Array<{ field: string; label: unknown; required: boolean }>).map((field) => ({ + name: field.field, + label: field.label, + is_required: field.required, + })) + return { inputs: [...apiInputs, ...userInputs] } + } + return {} +} + export function addNodeTool(bindings: AgentBindings): AgentTool { return { name: 'add_node', description: '新增节点并从上游出口接一条入边(建即接,杜绝孤立节点);可新增的节点与当前画布的节点菜单一致。' + 'source_anchor_id:普通连接传上游节点 id;接判断器/意图识别某分支时传 get_node_anchors 返回的出口 id;循环体首个节点接 "loop-start-node"。' + - 'tool-lib-node 需同时传 tool_id(get_tools 或 list_datasource 查询)。' + + 'tool-lib-node 需同时传 tool_id(get_tools / list_datasource 查询):自定义工具与工作流工具都传这一种 type,框架按工具类型选择实际节点。' + + 'application-node 需同时传 application_id(get_applications 查询)。' + '数据源节点(本地文件 / Web 站点 / 数据源工具)是流程入口,不接上游,必须省略 source_anchor_id。' + - '返回 { nodeId, name, outputs };带 note 表示该节点暂不支持 AI 配置。', + '返回 { nodeId, name, outputs },资源类节点(工具 / 智能体)额外带 inputs;带 note 表示该节点暂不支持 AI 配置。', parameters: { type: 'object', properties: { @@ -378,6 +509,7 @@ export function addNodeTool(bindings: AgentBindings): AgentTool { source_anchor_id: { type: 'string', description: '上游节点 id 或出口 id;仅数据源节点省略' }, name: { type: 'string', description: '节点名称(可选,体现用途,如 "产品知识检索")' }, tool_id: { type: 'string', description: 'type=tool-lib-node 时必填' }, + application_id: { type: 'string', description: 'type=application-node 时必填' }, lf_id: LF_ID_SCHEMA, }, required: ['type'], @@ -391,6 +523,9 @@ export function addNodeTool(bindings: AgentBindings): AgentTool { if (type === WorkflowNodeType.ToolLib) { if (!args.tool_id) return { error: 'tool-lib-node 需要 tool_id,先 get_tools / list_datasource 查询' } template = await toolNodeTemplate(bindings, String(args.tool_id)) + } else if (type === WorkflowNodeType.Application) { + if (!args.application_id) return { error: 'application-node 需要 application_id,先 get_applications 查询' } + template = await applicationNodeTemplate(bindings, String(args.application_id)) } else { template = findNodeTemplate(canvas.mode, type) } @@ -405,7 +540,8 @@ export function addNodeTool(bindings: AgentBindings): AgentTool { name: nodeNameOf(node), lf_id: canvas.lfId, outputs: nodeOutputFields(node), - ...(nodeAgentFor(type) ? {} : { note: '该节点暂不支持 AI 配置:不要对它 configure_node,完成后提示用户手动配置' }), + ...resourceNodeInputsOf(node), + ...(nodeAgentFor(String(node.type)) ? {} : { note: '该节点暂不支持 AI 配置:不要对它 configure_node,完成后提示用户手动配置' }), }) // 数据源节点是流程入口,没有入口锚点:不接上游;其余节点必须建即接 const sourceAnchor = typeof args.source_anchor_id === 'string' ? args.source_anchor_id.trim() : '' @@ -659,59 +795,17 @@ export function configureNodeTool(bindings: AgentBindings): AgentTool { // 校验 // ============================================================ -function graphOf(lf: LogicFlow) { - const graph = lf.getGraphData() as LogicFlow.GraphData - return { - nodes: graph.nodes.filter((node) => node.type !== WorkflowNodeType.LoopBodyNode), - edges: graph.edges.filter((edge) => edge.type !== 'loop-edge'), - } as unknown as ConstructorParameters[0] -} - -/** - * 按画布模式选择图结构校验器:工具 / 知识库主画布有各自的开始节点与流程规则。 - * 循环体与 loop-body-node 视图的校验一致:知识库循环用 KnowledgeWorkFlowInstance,其余(含工具循环)用 WorkFlowInstance, - * 工具循环体的开始节点是 loop-start-node,不能用按 tool-start-node 找开始节点的 ToolWorkFlowInstance。 - */ -function instanceOf(mode: WorkflowMode, graph: ReturnType): WorkFlowInstance { - if (mode === WorkflowMode.Tool) return new ToolWorkFlowInstance(graph, mode) - if (mode === WorkflowMode.Knowledge || mode === WorkflowMode.KnowledgeLoop) return new KnowledgeWorkFlowInstance(graph, mode) - return new WorkFlowInstance(graph, mode) -} - export function validateTool(bindings: AgentBindings): AgentTool { return { name: 'validate', description: - '校验整张工作流:连线完整性(未接入流程的节点、未连接的分支、不能作为结束的节点)、循环体结构与各节点表单必填项;' + + '校验整张工作流,与画布调试 / 发布前的校验是同一个函数:各节点表单必填项与引用有效性、分支是否都已连接、循环体内的节点与循环图结构;' + '工具工作流还检查输出参数是否都有变量赋值写入、是否引用了未声明的输入 / 输出参数。返回 { qualified, problems }。', parameters: { type: 'object', properties: {} }, apply: async () => { - const problems: string[] = [] + // 先展开循环体,保证循环体节点已挂载并注册了 validate const canvases = await listCanvases(bindings.rootLf, bindings.workflowMode) - for (const canvas of canvases) { - const where = canvas.lfId === MAIN_LF_ID ? '主画布' : `循环体(${canvas.lfId})` - try { - const instance = instanceOf(canvas.mode, graphOf(canvas.lf)) - if (canvas.lfId === MAIN_LF_ID) instance.is_valid() - else { - instance.is_loop_valid() - const loopNode = canvases.flatMap((item) => item.lf.graphModel.nodes).find((node) => node.id === canvas.lfId) - if ((loopNode?.properties.node_data as { loop_type?: string } | undefined)?.loop_type === 'LOOP' && !instance.exist_break_node()) { - problems.push(`${where}:无限循环的循环体需要包含 Break 节点`) - } - } - } catch (error) { - problems.push(`${where}:${typeof error === 'string' ? error : formatValidationError(error)}`) - } - const results = await Promise.allSettled( - canvas.lf.graphModel.nodes - .filter((node) => String(node.type) !== WorkflowNodeType.LoopBodyNode) - .map((node) => (node as unknown as { validate?: () => Promise }).validate?.()), - ) - results.forEach((result) => { - if (result.status === 'rejected') problems.push(`${where}:${formatValidationError(result.reason)}`) - }) - } + const problems = (await collectWorkflowValidationErrors(bindings.rootLf)).map((error) => formatValidationError(error)) if (isToolMode(bindings.workflowMode)) { problems.push(...brokenToolParamReferences(bindings.rootLf, canvases)) unwrittenToolOutputs(bindings.rootLf, canvases).forEach((field) => diff --git a/ui/src/workflow-canvas/core/validate.ts b/ui/src/workflow-canvas/core/validate.ts index 680f89ef0a4..1b7e92d9bed 100644 --- a/ui/src/workflow-canvas/core/validate.ts +++ b/ui/src/workflow-canvas/core/validate.ts @@ -396,3 +396,18 @@ export class KnowledgeWorkFlowInstance extends WorkFlowInstance { } } } + +/** + * 画布校验的唯一入口,调试、发布与 AI 生成共用:并行调用各节点注册的 validate(),返回全部失败原因(空数组即通过)。 + * 节点表单必填与引用有效性由各节点负责,循环体内的节点与循环图结构由循环体节点(loop-body-node)的 validate 负责。 + */ +export async function collectWorkflowValidationErrors(lf: LogicFlow): Promise { + const results = await Promise.allSettled(lf.graphModel.nodes.map((node) => (node as unknown as { validate?: () => Promise }).validate?.())) + return results.flatMap((result) => (result.status === 'rejected' ? [result.reason] : [])) +} + +/** 画布校验:通过时完成,否则以第一个失败原因拒绝 */ +export async function validateWorkflow(lf: LogicFlow): Promise { + const errors = await collectWorkflowValidationErrors(lf) + if (errors.length) throw errors[0] +} diff --git a/ui/src/workflow-canvas/index.vue b/ui/src/workflow-canvas/index.vue index 9823c6b30da..8001529a5f1 100644 --- a/ui/src/workflow-canvas/index.vue +++ b/ui/src/workflow-canvas/index.vue @@ -7,6 +7,7 @@ import AppEdge from '@/workflow-canvas/core/edge/index' import LoopEdge from '@/workflow-canvas/core/edge/loop-edge' import { initDefaultShortcut } from '@/workflow-canvas/core/shortcut' import { disconnectAll, getTeleport } from '@/workflow-canvas/core/teleport' +import { validateWorkflow } from '@/workflow-canvas/core/validate' import type { WorkflowNodeModel } from '@/workflow-canvas/core/workflow-node' import Dagre from '@/workflow-canvas/plugins/dagre' import { WorkflowMode, WorkflowNodeType, type ShapeItem } from '@/workflow-canvas/types' @@ -125,8 +126,8 @@ function render(data: LogicFlow.GraphConfigData) { } function validate() { - if (!lf.value) return Promise.resolve([]) - return Promise.all(lf.value.graphModel.nodes.map((node) => (node as unknown as WorkflowNodeModel).validate?.())) + if (!lf.value) return Promise.resolve() + return validateWorkflow(lf.value) } function onmousedown(shapeItem: ShapeItem, event?: PointerEvent) { diff --git a/ui/src/workflow-canvas/nodes/application-node/agent.ts b/ui/src/workflow-canvas/nodes/application-node/agent.ts new file mode 100644 index 00000000000..7a31b01e246 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/application-node/agent.ts @@ -0,0 +1,106 @@ +import { getNode, readNodeData, validateReference, 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' + +/** + * application-node(智能体节点)的 AI 生成配置:把用户问题与智能体需要的参数都绑定成引用。 + * 与工具节点不同,这里的入参只能引用上游数据,不支持常量(节点表单本身只有引用选择器,没有常量输入)。 + * 新增节点(add_node)时已按绑定的智能体的开始节点写入 node_data 的 api_input_field_list / user_input_field_list。 + */ + +interface ApiInputField { + variable: unknown + is_required: boolean + value: string[] +} + +interface UserInputField { + field: string + required: boolean + value: string[] +} + +const params: JsonObjectSchema = { + type: 'object', + properties: { + question: { type: 'array', items: { type: 'string' }, description: '交给智能体的用户问题(引用上游的一个字段)' }, + }, + required: ['question'], + additionalProperties: { type: 'array', items: { type: 'string' }, description: '智能体需要的其余参数(api / 表单入参),键为参数名(add_node 返回的 inputs 的 name)' }, +} + +const config: JsonObjectSchema = { + type: 'object', + properties: { + is_result: { type: 'boolean', description: '是否把智能体的回复输出给用户,默认 false(无下游节点时自动为 true)' }, + }, +} + +const returns: JsonObjectSchema = { + type: 'object', + properties: { result: { type: 'any', description: '智能体的回复内容' } }, + additionalProperties: false, +} + +function useAgent(context: NodeAgentContext) { + return useNodeAgent(context, { + prompt: [ + 'application-node(智能体节点)配置要点:把用户问题与智能体需要的每个参数都绑定成引用。', + '- 入参只能引用上游数据,不支持常量;question 必填,是给智能体的用户问题(常见引用开始节点的用户问题,或上游某个字段)。', + '- 其余入参按 add_node 返回的 inputs 逐个绑定,必填的都要有引用。', + '- is_result 表示是否把智能体的回复输出给用户(默认 false,没有下游节点时自动为 true)。', + ].join('\n'), + tools: [ + { + 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<{ api_input_field_list?: ApiInputField[]; user_input_field_list?: UserInputField[]; question_reference_address?: string[] }>(node) + const question = argReference(context, 'question') + if (question) { + const error = validateReference(context.lf, context.nodeId, question, 'question') + if (error) return new ErrorResult(error) + } + const apiInputFieldList = (nodeData.api_input_field_list ?? []).map((field) => { + const reference = argReference(context, String(field.variable)) + if (!reference && field.is_required) return new ErrorResult(`必填参数 ${String(field.variable)} 需要父节点传入同名入参`) + return reference ? { ...field, value: [...reference] } : field + }) + const apiError = apiInputFieldList.find((item): item is ErrorResult => item instanceof ErrorResult) + if (apiError) return apiError + const userInputFieldList = (nodeData.user_input_field_list ?? []).map((field) => { + const reference = argReference(context, field.field) + if (!reference && field.required) return new ErrorResult(`必填参数 ${field.field} 需要父节点传入同名入参`) + return reference ? { ...field, value: [...reference] } : field + }) + const userError = userInputFieldList.find((item): item is ErrorResult => item instanceof ErrorResult) + if (userError) return userError + writeNodeProperties(node, { + node_data: { + ...nodeData, + ...(question ? { question_reference_address: [...question] } : {}), + api_input_field_list: apiInputFieldList as ApiInputField[], + user_input_field_list: userInputFieldList as UserInputField[], + is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData as { is_result?: boolean }).is_result ?? false, + }, + }) + return finishWithValidate(context) + }, + }, + ], + }) +} + +export default { + description: '调用已发布的智能体,相当于把当前对话的一部分交给另一个智能体处理(新增时需 application_id,用 get_applications 查询);入参只能引用上游数据,输出固定为 result', + params, + config, + returns, + useAgent, +} satisfies NodeAgentModule diff --git a/ui/src/workflow-canvas/nodes/application-node/index.ts b/ui/src/workflow-canvas/nodes/application-node/index.ts index 7246dea7a75..11befe911ca 100644 --- a/ui/src/workflow-canvas/nodes/application-node/index.ts +++ b/ui/src/workflow-canvas/nodes/application-node/index.ts @@ -1,5 +1,6 @@ import ApplicationNode from './index.vue' import ApplicationNodeDetail from './details/index.vue' +import agent from './agent' import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node' import { WorkflowNodeType } from '@/workflow-canvas/types' @@ -9,4 +10,4 @@ class ApplicationNodeView extends WorkflowNodeView { } } -export default { type: WorkflowNodeType.Application, model: WorkflowNodeModel, view: ApplicationNodeView, details: ApplicationNodeDetail } +export default { type: WorkflowNodeType.Application, model: WorkflowNodeModel, view: ApplicationNodeView, details: ApplicationNodeDetail, agent } diff --git a/ui/src/workflow-canvas/nodes/document-split-node/agent.ts b/ui/src/workflow-canvas/nodes/document-split-node/agent.ts index 524f78815fc..ab0a11c50e3 100644 --- a/ui/src/workflow-canvas/nodes/document-split-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/document-split-node/agent.ts @@ -1,7 +1,7 @@ import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas' import { argReference, createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node' import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type' -import { splitPatternOptions } from './constant' +import { DOCUMENT_SPLIT_PATTERN_OPTIONS } from '@/constants/document' /** document-split-node(文档分段)的 AI 生成配置:入参文档列表,配置分段策略及其参数(均按自定义常量写入),返回带分段的文档列表 */ @@ -69,7 +69,7 @@ const config: JsonObjectSchema = { patterns: { type: 'array', items: { type: 'string' }, - description: `分段标识,仅 custom 策略:按先后顺序递归分割,可选 ${splitPatternOptions.map((pattern) => pattern.label).join('、')},也可传自定义正则`, + description: `分段标识,仅 custom 策略:按先后顺序递归分割,可选 ${DOCUMENT_SPLIT_PATTERN_OPTIONS.map((pattern) => pattern.label).join('、')},也可传自定义正则`, }, limit: { type: 'integer', description: `分段长度,仅 custom 策略,${LENGTH_MIN}–${LENGTH_MAX},默认 4096` }, with_filter: { type: 'boolean', description: '自动清洗,仅 custom 策略:去掉重复多余的符号、空格、空行、制表符' }, @@ -121,7 +121,7 @@ function useAgent(context: NodeAgentContext) { const limit = lengthOf(args.limit, 'limit') if (typeof limit === 'string') return limit const patterns = Array.isArray(args.patterns) - ? args.patterns.map((pattern) => splitPatternOptions.find((option) => option.label === pattern)?.value ?? String(pattern)).filter(Boolean) + ? args.patterns.map((pattern) => DOCUMENT_SPLIT_PATTERN_OPTIONS.find((option) => option.label === pattern)?.value ?? String(pattern)).filter(Boolean) : undefined const booleanFields = ['with_filter', 'paragraph_title_relate_problem', 'document_name_relate_problem'].filter( (field) => typeof args[field] === 'boolean', diff --git a/ui/src/workflow-canvas/nodes/tool-base-node/agent.ts b/ui/src/workflow-canvas/nodes/tool-base-node/agent.ts index 228805d553c..fb27c1d2385 100644 --- a/ui/src/workflow-canvas/nodes/tool-base-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/tool-base-node/agent.ts @@ -95,6 +95,7 @@ function useAgent(context: NodeAgentContext) { '- inputs(输入参数):调用方传入的数据。type 按数据形态选:文本 string,数字 int / float,开关 boolean,列表 array,对象 dict。', '- desc 是写给调用方的:智能体调用工具时,模型只能看到每个输入参数的 desc 并据此生成参数值,要写清含义、格式与示例,不超过 128 字;被工作流节点调用时写清需要什么结构的数据。', '- outputs(输出参数):工具的返回字段,只需参数名与显示名称;它的值由流程里的变量赋值节点写入。', + '- 输入参数和输出参数都可以没有:要求里没提到的一侧不要自行添加;要清空某一侧时传空数组。', '- inputs / outputs 各自整体替换:修改其中一项时要带上该侧要保留的其余参数;只改一侧时另一侧省略。', `当前签名:${JSON.stringify(signatureOf(context))}`, ].join('\n'), 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 3a79b0618f8..d378d2f68a9 100644 --- a/ui/src/workflow-canvas/nodes/tool-custom-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/tool-custom-node/agent.ts @@ -28,7 +28,12 @@ const config: JsonObjectSchema = { name: { type: 'string', description: '参数名,与函数形参一致' }, type: { type: 'string', enum: FIELD_TYPES }, source: { type: 'string', enum: ['reference', 'custom'], description: 'reference 由同名入参引用上游,custom 为常量' }, - is_required: { type: 'boolean', description: '是否必填,默认 true' }, + is_required: { + type: 'boolean', + description: + '默认 true。仅对 source=reference 有效:运行时引用取到的值为空(null / 空字符串 / 空数组)会直接中断节点并报错「字段为必填」。' + + '引用的上游字段在正常情况下可能为空(如可选的提取结果、模型可能不输出、循环变量起始为空、分支未覆盖的字段等)时必须设为 false,让空值在代码里按业务逻辑处理,而不是让节点中断;只有确定上游一定有值时才设 true', + }, desc: { type: 'string', description: '参数说明' }, value: { description: 'source=custom 时的常量值' }, }, @@ -55,6 +60,7 @@ function useAgent(context: NodeAgentContext) { 'tool-custom-node(自定义工具)配置要点:', '- code:按需求写一段可直接运行的 Python,只定义一个函数,形参与 inputs 的 name 一一对应,return 结果;只用标准库。', '- inputs:source=reference 的参数由同名入参绑定,无需给 value;source=custom 的参数给常量 value。', + '- is_required 只对 source=reference 的参数有效:引用的上游值运行时为空会直接报错中断节点,不是「代码里判断一下」就能接住的。引用的字段本身可能为空(可选的提取结果、模型可能不输出、循环变量初始为空等)就必须设 is_required=false,代码里按空值正常处理;只有确定上游一定有值才设 true。', '- 示例:def main(text: str, limit: int):\\n return text[:limit]', '- 解析模型输出的 JSON 时先去掉 ```json 代码块标记并做容错(解析失败时返回原数据或空结果),不要让代码直接抛错。', '- 返回值结构以「下游要求」为准:有下游要求时按它逐字段构造返回值;顶层是数组就 return 一个 list,哪怕只有一条也不能返回单个 dict,也不能返回 {名称: 内容} 这样的字典。', 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 b523849dd83..9b72a391cb3 100644 --- a/ui/src/workflow-canvas/nodes/tool-lib-node/agent.ts +++ b/ui/src/workflow-canvas/nodes/tool-lib-node/agent.ts @@ -86,7 +86,9 @@ function useAgent(context: NodeAgentContext) { } export default { - description: '调用工具库中的自定义工具,相当于调用一个外部函数(新增时需 tool_id,用 get_tools 查询);入参为工具中 source=reference 的参数', + description: + '调用工具库中的自定义工具,相当于调用一个外部函数(新增时需 tool_id,用 get_tools 查询;get_tools 同时列出工作流工具,也用这一种节点类型新增,签名在 add_node 的返回结果里);' + + '入参为工具中 source=reference 的参数', params, config, returns, diff --git a/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/agent.ts b/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/agent.ts new file mode 100644 index 00000000000..27138ae4955 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/agent.ts @@ -0,0 +1,101 @@ +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' + +/** + * tool-workflow-lib-node(工作流工具)的 AI 生成配置:为工具自己工作流声明的每个输入参数赋值。 + * 与 tool-lib-node 是同一类节点(都是「工具」),但签名来自绑定的工作流工具自身的基本信息节点, + * 新增节点(add_node)时已按真实签名写入 node_data.input_field_list,这里只负责逐个赋值。 + */ + +interface WorkflowToolInputField { + field: string + source: 'reference' | 'custom' + type?: string + is_required?: boolean + desc?: string + value: unknown +} + +const params: JsonObjectSchema = { + type: 'object', + properties: {}, + additionalProperties: { + type: 'any', + description: '要用引用绑定的输入参数,键为参数名(get_tools / add_node 返回的 inputs 的 name)。工作流工具的每个参数都可以是引用或常量,具体哪些用引用由这里决定,不是固定的', + }, +} + +const config: JsonObjectSchema = { + type: 'object', + properties: { + values: { type: 'object', description: '工作流工具中 source=custom 的输入参数:参数名 → 常量值' }, + is_result: { type: 'boolean', description: '是否把工作流工具的输出结果输出给用户,默认 false' }, + }, +} + +const returns: JsonObjectSchema = { type: 'object', properties: {}, additionalProperties: true } + +function useAgent(context: NodeAgentContext) { + return useNodeAgent(context, { + prompt: [ + 'tool-workflow-lib-node(工作流工具)配置要点:为工作流工具的每个输入参数赋值。与自定义工具不同,这里每个参数都可以是引用或常量,不是工具本身固定的。', + '- 要引用上游数据的参数,在父 agent 调 configure_node 时按参数名传入同名引用(本节点形参即该参数名),这里无需再配置;要给常量的参数在 values 里给值。', + '- 必填参数都要有值(引用或常量二选一);is_result 表示是否把工作流工具的输出结果输出给用户(默认 false)。', + '- 工作流工具的输出字段是它自己声明的输出参数(动态,见本节点的返回字段要求),不是固定的 result。', + ].join('\n'), + tools: [ + { + 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?: WorkflowToolInputField[] }>(node) + const values = (args.values ?? {}) as Record + const inputFields: WorkflowToolInputField[] = [] + for (const field of nodeData.input_field_list ?? []) { + // 每个参数都可能是引用或常量:本次传了同名引用就用引用(不管之前是什么来源); + // 没传引用时,之前已是引用的保持不变(续聊未重新绑定),否则按 values 给常量 + const reference = argReference(context, field.field) + if (reference) { + inputFields.push({ ...field, source: 'reference', value: [...reference] }) + continue + } + if (field.source === 'reference') { + inputFields.push(field) + continue + } + const value = values[field.field] + if (value === undefined) { + if (field.is_required) return new ErrorResult(`必填参数 ${field.field} 未赋值:引用上游数据传 args.${field.field},给常量传 values.${field.field}`) + inputFields.push(field) + continue + } + inputFields.push({ ...field, source: 'custom', 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 finishWithValidate(context) + }, + }, + ], + }) +} + +export default { + description: '调用已发布的工作流工具,相当于调用一个子工作流(新增时需 tool_id,用 get_tools 查询);入参为该工具自己声明的输入参数,输出为它声明的输出参数', + params, + config, + returns, + useAgent, +} satisfies NodeAgentModule diff --git a/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/index.ts b/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/index.ts index c5460dc0ee2..b264869a863 100644 --- a/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/index.ts +++ b/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/index.ts @@ -1,5 +1,6 @@ import ToolWorkflowLibNode from './index.vue' import ToolWorkflowLibNodeDetail from './details/index.vue' +import agent from './agent' import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node' import { WorkflowNodeType } from '@/workflow-canvas/types' @@ -9,4 +10,4 @@ class ToolWorkflowLibNodeView extends WorkflowNodeView { } } -export default { type: WorkflowNodeType.ToolWorkflowLib, model: WorkflowNodeModel, view: ToolWorkflowLibNodeView, details: ToolWorkflowLibNodeDetail } +export default { type: WorkflowNodeType.ToolWorkflowLib, model: WorkflowNodeModel, view: ToolWorkflowLibNodeView, details: ToolWorkflowLibNodeDetail, agent } diff --git a/ui/src/workflow-canvas/store/api/system-resource/index.ts b/ui/src/workflow-canvas/store/api/system-resource/index.ts index e7073e00602..f1ec4b4e8a1 100644 --- a/ui/src/workflow-canvas/store/api/system-resource/index.ts +++ b/ui/src/workflow-canvas/store/api/system-resource/index.ts @@ -24,6 +24,7 @@ const getLatestKnowledgeAction = async (knowledgeId: string) => { export default { getLatestKnowledgeAction, getAllApplication: ApplicationApi.getAllApplication, + getApplicationDetail: ApplicationApi.getApplicationDetail, postPromptGenerate: ModelApi.postPromptGenerate, postChatCompletion: ModelApi.postChatCompletion, getMcpTools, diff --git a/ui/src/workflow-canvas/store/api/workspace/index.ts b/ui/src/workflow-canvas/store/api/workspace/index.ts index 665486f6359..b23ced0bf59 100644 --- a/ui/src/workflow-canvas/store/api/workspace/index.ts +++ b/ui/src/workflow-canvas/store/api/workspace/index.ts @@ -31,6 +31,7 @@ const getLatestKnowledgeAction = async (knowledgeId: string) => { export default { getLatestKnowledgeAction, getAllApplication: ApplicationApi.getAllApplication, + getApplicationDetail: ApplicationApi.getApplicationDetail, postPromptGenerate: modelAPI.postPromptGenerate, postChatCompletion: modelAPI.postChatCompletion, getModelListWithShared: modelAPI.getModelListWithShared, diff --git a/ui/src/workflow-canvas/store/index.ts b/ui/src/workflow-canvas/store/index.ts index e2735d19c9c..c457efea680 100644 --- a/ui/src/workflow-canvas/store/index.ts +++ b/ui/src/workflow-canvas/store/index.ts @@ -21,6 +21,7 @@ export interface McpTool { // 底层 api/*/index.ts 提供的原始接口实现,不含缓存层能力。 type ApiModule = { getAllApplication?: (query?: Dict) => Promise + getApplicationDetail?: (applicationId: string) => Promise getAllTags?: (knowledgeIds: string[]) => Promise getModelListWithShared: (query?: Dict) => Promise getProviderList: () => Promise @@ -37,6 +38,8 @@ type ApiModule = { // 查询默认走缓存,通过 store.force.xxx() 强制刷新;生成请求始终直接转发。 export type WorkflowStoreApi = { getAllApplication: (query?: Dict) => Promise + /** 应用详情;当前范围不支持时返回空对象 */ + getApplicationDetail: (applicationId: string) => Promise getAllTags: (knowledgeIds: string[]) => Promise getModelListWithShared: (query?: Dict) => Promise getProviderList: () => Promise @@ -106,6 +109,10 @@ export function useWorkflowStore(resourceScope: string, getResourceWorkspaceId?: if (!requestApi.getAllApplication) return Promise.resolve([]) return withCache(`application-list:${JSON.stringify(query ?? {})}`, () => requestApi.getAllApplication!(query), force) }, + getApplicationDetail(applicationId: string): Promise { + if (!requestApi.getApplicationDetail) return Promise.resolve({} as ApplicationDetail) + return withCache(`application:${applicationId}`, () => requestApi.getApplicationDetail!(applicationId), force) + }, getAllTags(knowledgeIds: string[]): Promise { if (!requestApi.getAllTags) return Promise.resolve([]) return withCache(`knowledge-tags:${JSON.stringify(knowledgeIds)}`, () => requestApi.getAllTags!(knowledgeIds), force)