Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  

Large diffs are not rendered by default.

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .agents/design/core/workflow/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

FastGPT 工作流系统是一个基于 Node.js/TypeScript 的可视化工作流引擎,支持拖拽式节点编排、实时执行、并发控制和交互式调试。系统采用队列式执行架构,通过有向图模型实现复杂的业务逻辑编排。

## 专题设计文档

- [Workflow Builder(工作流辅助生成)](./workflow-builder.md):当前工作流辅助生成的功能范围、端到端流程、双仓库职责、Sandbox/CLI/Core 架构、版本应用和测试部署说明。
- [Workflow Builder 评测系统](../../../../pro/admin/test/core/ai/workflowBuilder/README.md):定义工作流自动生成的五项指标、案例数据结构、运行流程、报告和版本归档方式。

## 核心架构

### 1. 项目结构
Expand Down
701 changes: 701 additions & 0 deletions .agents/design/core/workflow/workflow-builder.md

Large diffs are not rendered by default.

138 changes: 138 additions & 0 deletions .agents/issue/workflow-builder-apply-revert-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Workflow Builder「再次应用」后画布被还原 —— 问题审计报告

> 状态:已按方案 A 修复(2026-08-14),本报告保留审计过程与证据
> 日期:2026-08-14
> 涉及分支:`yyh/workflow-core-cli`

## 1. 问题描述

在 Workflow Builder(AI 工作流自动生成)中,工作流生成完成后点击版本卡片「再次应用」:

1. 点击瞬间画布短暂显示新版本内容(本次修改的节点已更新);
2. 约 1 秒后(「已应用到画布」toast 出现时)画布还原到点击前的状态;
3. 每次点击都会重复出现。

## 2. 复现方式

- 前置:AI 生成的工作流中包含一个**带自定义输出(collected results)的 Loop 节点**(本次复现为 `examLoop`,类型 `loopRun`,array 模式);
- 操作:点击版本卡片「再次应用」;
- 现象:画布先显示新内容,约 1 秒后还原。

## 3. 根因结论

**根因:AI 生成端输出的 loopRun 节点文档中,自定义输出 `collectedResults` 只有 outputs 声明,没有配套的 `canEdit: true` 输入声明;画布挂载 Loop 节点时,`NodeLoopRun` 组件的 useEffect 将 `collectedResults` 判定为「未声明的动态输出」,自动执行 `delOutput` 删除该输出并同步 `onDelEdge` 删除其连线,把刚应用的内容改回去。**

由于 S3 归档的始终是 AI 原始文档(含 `collectedResults` 但缺声明),每次重新应用都会重复触发同样的修正,与「每次点击都还原」完全吻合。

## 4. 证据链

### 4.1 一次完整 apply 的日志时序(05:14:28,带埋点复现)

| 时间 | 事件 | 说明 |
|---|---|---|
| 28.492 | `apply start version=AI 生成版本 3` | 点击「再次应用」 |
| 28.656 | `apply loaded document` | 从 S3 加载 AI 归档文档 |
| 28.660 | `apply document loop` | **AI 文档中 examLoop 结构**(见下) |
| 28.697 | `apply before initData nodes=31` | 目标文档 31 个节点 |
| 28.704 | `apply after initData` | 画布已显示新版本(此时用户能看到改动) |
| 29.327 | `examLoop:delOutput:collectedResults` at **NodeLoopRun.useEffect** | **修正触发点** |
| 29.503 | `onDelEdge nodeId=examLoop sh=examLoop-source-collectedResults` | 删除 `examLoop → aggResults` 连线 |
| 30.802 | `apply after autoLayout=applied` | 布局只改位置 |
| 30.868 | `apply before push nodes=31` | 已是修正后的残缺画布 |
| 30.870 | `push SAVED title=AI 生成版本 3 nodes=31` | **残缺画布被存为快照** |
| 30.871 | `apply toast success` | toast 出现,用户感知「还原」 |

### 4.2 AI 文档中 loop 节点原始结构(埋点输出原文)

```json
apply document loop: [
{
"id": "examLoop",
"type": "loopRun",
"mode": "array",
"inputs": ["loopRunMode", "loopRunInputArray", "loopCustomOutputs", "childrenNodeIdList", "nodeWidth", "nodeHeight", "loopNodeInputHeight"],
"outputs": ["system_error_text", "collectedResults"]
},
{
"id": "examLoop__start",
"type": "loopRunStart",
"mode": "array",
"inputs": ["loopRunMode", "loopStartInput", "loopStartIndex"],
"outputs": ["currentIndex", "currentItem"]
}
]
apply document loopEdges: ["examLoop:undefined->aggResults:undefined", "examLoop__start:undefined->readCurrent:undefined"]
```

关键点:
- `examLoop.outputs` 含自定义输出 `collectedResults`,但 `inputs` 中**没有** `collectedResults` 对应的 `canEdit: true` 声明(只有 7 个固定 key);
- 连线 `examLoop → aggResults` 在文档中存在,应用后被删。

### 4.3 代码逻辑

**动态输出的声明机制**:loop 节点的自定义输出由 `loopCustomOutputs`(`addInputParam` 渲染类型)驱动的 `canEdit: true` 输入声明,动态输出与 canEdit 输入**成对出现**(见 `DynamicInputs/index.tsx`:`inputs.filter((item) => item.canEdit)` 才是用户可见的动态字段)。

**转换链路**:`applyVersion` → `parseCompatibleWorkflowDocument` → `compileStoreWorkflow` → `parseWorkflowImportConfig` → `initData` → `storeNode2FlowNode`(`projects/app/src/web/core/workflow/utils.ts:163`):
- 模板 `loopRun.ts` 的 outputs 只有 `errorText`;
- store 中多出的 `collectedResults` 走 `.concat()` 分支补为 dynamic 输出(`type: dynamic`),inputs 无对应 canEdit 声明。

**修正触发点**:`Flow/nodes/Loop/NodeLoopRun.tsx`(86-233 行)挂载时两个 useEffect:
- Effect 1(mode sync,86-190 行):`mode=array` 时 `delOutput currentIteration`、`addOutput currentIndex/currentItem` —— 对应 `examLoop__start:delOutput:currentIteration`,属**良性修正**(文档缺 currentIteration,模板补全后按 array 模式删除);
- Effect 2(192-233 行,**根因所在**):
```ts
const declared = inputs.filter((i) => i.canEdit === true);
const currentDynamic = outputs.filter((o) => o.type === FlowNodeOutputTypeEnum.dynamic);
const declaredKeys = new Set(declared.map((i) => i.key));
currentDynamic.forEach((o) => {
if (!declaredKeys.has(o.key)) {
onChangeNode({ nodeId, type: 'delOutput', key: o.key });
}
});
```
`collectedResults` 不在 `declaredKeys` 中 → `delOutput`,`onChangeNode` 内部对删除的输出同步执行 `onDelEdge`(日志 29.503 证实)。

**生成端规范缺失**:`pro/admin/src/service/core/ai/skill/builtin/workflow-builder/SKILL.md` 只写了 "Custom collected outputs are manual dynamic outputs. Add their exact keys and types before downstream references.",**没有要求**在 inputs 中声明对应的 canEdit 输入,导致生成端输出不合规文档。

### 4.4 为什么「先显示后还原」

`initData` 同步覆盖画布(用户看到新版本)→ React 挂载 Loop 节点 → `NodeLoopRun` useEffect 异步修正(delOutput/onDelEdge,约 1 秒后)→ toast 出现。修正发生在 applyVersion 的 await 链返回之后,视觉上表现为「toast 出现时还原」。

## 5. 已排除的假设

| 假设 | 排除依据 |
|---|---|
| 组件重挂载触发 `initData(isInit)` RESTORE past[0] | 画布先显示新版本(日志 28.704 后无 restore 行为),且 Workflow/index.tsx mount 埋点无重复挂载 |
| undo/redo 自动触发 | 日志无 `undo called` / `redo called` |
| 版本切换(onSwitchTmpVersion/onSwitchCloudVersion) | 日志无调用 |
| 自动布局覆盖 | `autoLayout=applied`,只改 position 不改结构;且还原含节点输出与连线,非位置 |
| push 快照回写画布 | push 只记录快照,不回写画布(workflowSnapshotContext) |
| workflowInitContext 监听 appDetail 触发 replaceWorkflowData | replaceWorkflowData 调用栈日志均来自正常编辑流程,无 apply 后的额外覆盖 |

## 6. 衍生问题

1. **快照与 AI 版本不一致**:push SAVED 保存的是被修正后的残缺画布(31 节点、少一条 `examLoop→aggResults` 连线),与 AI 版本卡片内容不一致。用户在「我的编辑」里看到的版本是残缺的。
2. **历史数据无法自愈**:S3 中已归档的 AI 文档均缺 canEdit 声明,仅修复生成端只能保证新生成的文档合规,旧文档重新应用仍会触发修正。

## 7. 修复方向建议(未实施,供讨论)

### 方案 A(推荐,生成端修复 + 前端兜底)
1. **生成端**:`pro/admin/.../workflow-builder/SKILL.md` 补充规范——loop 自定义输出必须在 `inputs` 中声明同名 `canEdit: true` 输入(valueType 与输出一致),与 `loopCustomOutputs` 的 addInputParam 机制对齐;同时要求已生成的节点 `loopCustomOutputs` 值与 inputs 声明保持同步。
2. **前端兜底(兼容历史数据)**:在 `storeNode2FlowNode` 或 `parseWorkflowImportConfig` 阶段,对 loop 节点「有 dynamic 输出但无对应 canEdit 输入」的情况自动补齐 canEdit 输入声明(key/valueType 从输出推导),避免 NodeLoopRun Effect 2 误判。注意与 `loopCustomOutputs` 的 value 存储结构对齐(需确认 addInputParam 的存储格式)。

### 方案 B(仅前端,覆盖历史数据)
同上 2,不修改生成端。可最快止血(新老版本均不再被修正),但生成文档本身仍不合规,Loop 节点的自定义输出在子工作流引用语义上依赖声明存在,属数据层兜底而非根治。

### 方案 C(不推荐,防御性)
`NodeLoopRun` Effect 2 删除动态输出前检查该输出是否存在连线,有连线则跳过删除。会掩盖生成端问题,且删除/保留逻辑不一致(动态输出依赖声明才能正确渲染),仅作临时止血考虑。

## 8. 结论

- 根因明确:**AI 生成文档 loop 节点缺 canEdit 输入声明 → NodeLoopRun 挂载时 useEffect 误删动态输出及连线 → 每次 apply 重复修正**;
- 建议按「方案 A:生成端规范 + 前端兼容补齐」修复,其中前端补齐是覆盖已归档历史数据所必需的。

## 9. 修复记录(2026-08-14 已实施,方案 A)

1. **前端兜底(覆盖历史数据)**:`projects/app/src/web/core/workflow/utils.ts` `storeNode2FlowNode` 中,对 `loopRun` 节点按动态输出自动补齐缺失的 canEdit 输入声明(key/valueType 从输出推导,`renderTypeList: [reference]`、`canEdit: true`、`required: false`)。所有文档加载路径(initData、版本应用、快照切换、表单预览)均经过该转换,已归档的旧 AI 文档重新应用也会被修正。
2. **生成端规范(根治新文档)**:`pro/admin/src/service/core/ai/skill/builtin/workflow-builder/references/container-nodes.md` Loop run 章节,明确自定义收集输出必须 `input.add`(canEdit 输入)与 `output.add`(动态输出)成对声明、key/valueType 一致。
3. **测试**:`projects/app/test/web/core/app/workflow/utils.test.ts` 新增 3 个用例(自动补齐 / 已有声明不重复 / 非 loopRun 不受影响),全部通过;`utils.test.ts` 108 例、`store2flow.version/deprecated`、`workflowCorePr2/Pr3Adapter`、`localDraft` 共 133 例无回归,`tsc --noEmit` 通过。
4. 未改动项:`NodeLoopRun` 挂载 Effect 的删除逻辑本身保持原样(用户主动删除声明仍会同步删除输出,属正常编辑行为)。
36 changes: 36 additions & 0 deletions .agents/skills/core/workflow-builder-evaluation/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: workflow-builder-evaluation
description: 运行和维护 FastGPT Workflow Builder 自动生成测评系统。用户要求制作或检查 WBE 案例、运行部分或全部案例、计算五项指标、定位案例失败或卡住阶段、查看测评报告、归档某个版本 Benchmark 结果时使用。
---

# Workflow Builder 测评

## 唯一入口

先完整读取 `pro/admin/test/core/ai/workflowBuilder/README.md`,再根据任务读取其中直接链接的对应文档。指标和案例规则以该目录为唯一事实源,不在 Skill 内复述或自行扩展评分口径。

## 任务路由

- 制作或修改案例:读取 `docs/case-authoring.md` 和 `docs/evaluation-standard.md`,修改后执行案例校验。
- 执行测评:读取 `docs/runbook.md`,依次执行案例校验、Preflight 和 Runner。
- 解读结果:读取 `docs/reporting.md`,以运行目录内 `report.json` 为事实源,使用 `summary.md` 和逐案例报告定位问题。
- 归档版本结果:仅在用户明确要求归档时读取 `docs/benchmark-archive.md` 并执行归档命令。

## 标准执行顺序

1. 检查 `config/eval.local.yaml` 是否存在,但不得输出其中的 Cookie、Token 或其他凭证。
2. 运行 `pnpm --filter @fastgpt/admin workflow-builder:eval:lint`。
3. 运行 `pnpm --filter @fastgpt/admin workflow-builder:eval:preflight`。失败时停止正式计分并报告环境问题。
4. 运行 `pnpm --filter @fastgpt/admin workflow-builder:eval:run`。用户指定案例时先确认本地配置的 `run.caseIds` 与请求一致。
5. 读取新生成的 `report.json`,汇报五项指标的分子、分母、待完成数和无法评价数。
6. 列出每个问题案例的最后完成阶段、失败阶段或卡住阶段、失败代码、原因和证据位置。
7. 不自动归档。只有用户明确要求后才运行 `workflow-builder:eval:archive`。

## 约束

- 不允许自行计算、覆盖或手工修正 `report.json` 中的自动评分。
- 不把环境错误、凭证问题或输入映射问题算成产品失败。
- 不因节点数量、节点命名或汇聚结构不同,判定行为等价的工作流失败,除非用户需求明确规定这些结构。
- 只运行部分案例时,必须说明这是子集结果,不能表述为完整数据集结论。
- 不删除自动创建的 Workflow 应用或运行证据,除非用户明确要求清理并确认目标。
- 归档前检查运行是否完整;使用 `--allow-incomplete` 必须得到用户明确指示。
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: 'Workflow Builder 测评'
short_description: '运行工作流生成测评、定位失败阶段并手动归档版本结果'
default_prompt: '使用 $workflow-builder-evaluation 运行指定 Workflow Builder 案例并汇报五项指标和问题阶段。'
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"version": "4.0",
"private": true,
"scripts": {
"dev:pro": "turbo run dev:pro --filter=@fastgpt/app",
"dev:pro": "pnpm run build:workflow-packages && turbo run dev:pro --filter=@fastgpt/app",
"prepare": "husky install",
"gen:theme-typings": "chakra-cli tokens packages/web/styles/theme.ts --out node_modules/.pnpm/node_modules/@chakra-ui/styled-system/dist/theming.types.d.ts",
"gen:deploy": "node ./deploy/init.mjs",
"postinstall": "pnpm gen:theme-typings && pnpm run build:sdks",
"postinstall": "pnpm gen:theme-typings && pnpm run build:sdks && pnpm run build:workflow-packages",
"initIcon": "node ./scripts/icon/init.js && prettier --config \"./.prettierrc.js\" --write \"packages/web/components/common/Icon/constants.ts\"",
"previewIcon": "node ./scripts/icon/index.js",
"lint": "turbo run lint",
Expand All @@ -29,7 +29,8 @@
"generate:i18n-loaders": "node scripts/generate-i18n-resource-loaders.mjs",
"check:i18n-loaders": "node scripts/check-i18n-resource-loaders.mjs",
"test:vector": "turbo run test:integration --filter=@fastgpt/service",
"build:sdks": "pnpm -r --filter @fastgpt-sdk/storage --filter @fastgpt-sdk/otel --filter @fastgpt-sdk/sandbox-adapter build"
"build:sdks": "pnpm -r --filter @fastgpt-sdk/storage --filter @fastgpt-sdk/otel --filter @fastgpt-sdk/sandbox-adapter build",
"build:workflow-packages": "pnpm --filter @fastgpt/workflow-core build && pnpm --filter @fastgpt/workflow-cli build"
},
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
Expand Down
1 change: 1 addition & 0 deletions packages/global/common/system/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export type FastGPTFeConfigsType = {
show_publish_offiaccount?: boolean;
show_publish_wechat?: boolean;
show_agent_sandbox?: boolean;
show_workflow_builder?: boolean;
pluginRemoteDebug?: boolean;
enable_team_plugin_upload?: boolean;

Expand Down
5 changes: 4 additions & 1 deletion packages/global/core/chat/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ export enum ChatSourceEnum {
* 会话所属资源类型。
*
* `ChatSourceEnum` 表示对话入口来源,例如 test/api/online。
* `ChatSourceTypeEnum` 表示会话归属资源类型,用于在同一套 chat 表中隔离 App 和 Skill Edit。
* `ChatSourceTypeEnum` 表示会话归属资源类型;辅助生成会话使用独立来源,
* 以隔离聊天记录、上传文件和 Sandbox 资源。
*/
export enum ChatSourceTypeEnum {
app = 'app',
/** Workflow Builder 的独立聊天、上传文件和 Sandbox 来源。 */
workflowBuilder = 'workflowBuilder',
skillEdit = 'skillEdit',
chatAgentHelper = 'chatAgentHelper'
}
Expand Down
2 changes: 2 additions & 0 deletions packages/global/core/chat/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
AgentPlanStatusSchema
} from '../ai/agent/type';
import { ObjectIdSchema } from '../../common/type/mongo';
import { WorkflowBuilderVersionSchema } from '../workflow/builder/type';

export const ChatHistoryItemResSchema = DispatchNodeResponseSchema.extend({
nodeId: z.string(),
Expand Down Expand Up @@ -231,6 +232,7 @@ export const AIChatItemValueSchema = z.object({
agentPlanUpdate: AgentLoopPlanUpdateSchema.nullish(),
agentAsk: AgentLoopAskSchema.nullish(),
contextCheckpoint: ContextCheckpointValueSchema.nullish(),
workflowBuilderVersion: WorkflowBuilderVersionSchema.optional(),
tool: ToolModuleResponseItemSchema.nullish().meta({ deprecated: true }),
hideReason: z.boolean().optional(),
hideInUI: z.boolean().optional()
Expand Down
29 changes: 29 additions & 0 deletions packages/global/core/workflow/builder/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defaultAppSelectFileConfig } from '../../app/constants';
import type { AppChatConfigType } from '../../app/type';

export const WORKFLOW_BUILDER_MAX_FILE_AMOUNT = 10;

/**
* Workflow Builder 的独立对话配置。
*
* Builder 可以通过 WorkflowDocument 读取和修改原工作流的 chatConfig,
* 但其自身对话不得继承这些运行前置配置。
*/
export const WORKFLOW_BUILDER_CHAT_CONFIG = {
whisperConfig: {
open: true,
autoSend: false,
autoTTSResponse: false
},
fileSelectConfig: {
...defaultAppSelectFileConfig,
maxFiles: WORKFLOW_BUILDER_MAX_FILE_AMOUNT,
canSelectFile: true,
canSelectImg: true,
customPdfParse: false,
canSelectVideo: true,
canSelectAudio: true,
canSelectCustomFileExtension: true,
customFileExtensionList: []
}
} satisfies AppChatConfigType;
48 changes: 48 additions & 0 deletions packages/global/core/workflow/builder/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { IntSchema } from '../../../common/zod';
import { z } from 'zod';

export const WorkflowChecksumSchema = z
.string()
.regex(/^sha256:[a-f0-9]{64}$/)
.meta({
description: 'WorkflowDocument 的 SHA-256 checksum',
example: `sha256:${'0'.repeat(64)}`
});

/** AI 完成 Workflow Builder 生成后写入 ChatItem 的版本文件信息。 */
export const WorkflowBuilderVersionSchema = z
.object({
versionNo: IntSchema.positive().meta({
description: '当前工作流辅助生成会话中的 AI 生成版本序号',
example: 1
}),
name: z.string().min(1).meta({
description: '版本展示名称',
example: 'AI 生成版本 1'
}),
filename: z.string().min(1).meta({
description: '版本 JSON 文件名',
example: 'AI 生成版本 1.json'
}),
checksum: WorkflowChecksumSchema,
generatedAt: z.string().datetime().meta({
description: 'AI 完成生成的时间',
example: '2026-08-12T10:00:00.000Z'
}),
s3Key: z.string().min(1).optional().meta({
description: '生成校验成功后归档到聊天文件 S3 的对象 key'
}),
expiresAt: z.string().datetime().optional().meta({
description: 'S3 版本过期时间',
example: '2026-08-13T10:00:00.000Z'
}),
appliedAt: z.string().datetime().optional().meta({
description: '版本首次成功应用到画布的时间',
example: '2026-08-12T10:00:00.000Z'
})
})
.strict();

export type WorkflowBuilderVersion = z.infer<typeof WorkflowBuilderVersionSchema>;

export type WorkflowBuilderVersionDisplayState = 'ready' | 'available' | 'expired';
Loading
Loading