squash合并: - 意图识别层论证(8维度+10业界佐证) - 多主题上下文管理愿景+并存论证+补充论证(多轮agentic) - 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化) - 前端架构技术债清单归档
28 KiB
AiNode 自审实施方案(②-⑥)
决策基线:决策 a 已定 ——
TaskRecord加output_json字段(task 中心,产出跟 task 走)。① df-storage 迁移(struct 字段 + migrations CREATE/ALTER + crud 白名单 + INSERT/UPDATE/SELECT 列)由同批另一 agent 落地,本方案为前置依赖。范围:仅设计文档(read-only),不改任何 .rs/.ts/.vue 代码。file:line 证据均独立 grep/Read 核验,未采信文档/会话描述声明。
日期:2026-06-16 | 作者:AiNode 自审调研 agent
实施状态(2026-06-18 核对)
②-⑤ 全部已落地(原设计 1.2 表中标 ⚠️ 的 crud/白名单缺口已补全,②③④⑤ 已实施)。
① df-storage 迁移 — 已完成(原 ⚠️ 项已补):
TaskRecord.output_json: Option<String>:crates/df-storage/src/models.rs:72。- 白名单含 output_json:
crates/df-storage/src/crud.rs:344(allowed_columns_for("tasks")已含,注释"ai_execute 写产出 / ai_self_review 读产出自审 / human_review 展示对象")。- migrations:
crates/df-storage/src/migrations.rs:303(注释同上)。② AiNode 持 db + 写产出 — 已落地:
AiNodestruct:crates/df-nodes/src/ai_node.rs:256(持 db 字段,AiNode::new(db)构造)。- state.rs 工厂闭包注入 db:
src-tauri/src/state.rs:351-352(registry.register("ai", ... AiNode::new(ai_db.clone())))。- execute 写产出:
crates/df-nodes/src/ai_node.rs:319-325(若 config 含 task_id →repo.update_field(task_id, "output_json", &json_str))。③ AiSelfReviewNode 独立节点 — 已落地(原设计 2.3 推荐独立节点路径已采纳,非按 node_id 分支):
AiSelfReviewNodestruct:crates/df-nodes/src/ai_node.rs:438。REVIEW_SYSTEM_PROMPT(四维度 prompt 模板):crates/df-nodes/src/ai_node.rs:387。- JSON 兜底解析
parse_review_json(fn 定义,非 JSON/缺 verdict → verdict=unknown):crates/df-nodes/src/ai_node.rs:396,单测:1011-1058(valid/invalid/fence 三场景)。build_review_prompt(fn 定义):crates/df-nodes/src/ai_node.rs:449,单测:1060。- state.rs 注册:
src-tauri/src/state.rs:359-360(registry.register("ai_self_review", ... AiSelfReviewNode::new(review_db.clone())))。④ human_review 展示(经 DAG inputs 透传,HumanNode 零改动)— 已落地:
- review 摘要塞 NodeOutput.data:
crates/df-nodes/src/ai_node.rs:586(注释「NodeOutput.data 塞 review 摘要,供下游 human_review 经 inputs["ai_self_review"] 读」)。- testing 模板 edge ai_self_review → human_review:
crates/df-nodes/src/task_workflow_templates.rs:80,单测:149验证边方向(assert_eq!(edge.source, "ai_self_review"))。- HumanNode 本身零改动(git 历史核验 human_node.rs 在 AiNode 自审批 commit 中无变动)。
⑤ 前端展示 — 已落地:TaskDetail.vue 加 output_json 区块(本轮未逐行核验前端 file:line,但后端 output_json schema 已定型、前端按 schema 渲染)。
超出原设计、后追加的能力 — gate 闸门:
- testing 模板
ai_self_review启用gate:true:crates/df-nodes/src/task_workflow_templates.rs:54-67(阶段3 起 verdict=fail → AiSelfReviewNode 返 Err → 工作流 failed,不经 human_review)。原设计 2.3 标"首版保守,verdict 仅作展示信号",实际已升级为 DAG 节点闸门(激进方案落地):crates/df-nodes/src/ai_node.rs:598-626(gate==true 时 verdict=fail 返 Err)。⑥ 端到端联调 — 代码层完成,实测类待用户:联调依赖 secret 下沉+provider 注入链(
docs/02-架构设计/专项设计/secret下沉与provider注入方案-2026-06-16.md,AiNode/AiSelfReviewNode改经provider_id+ df_storage::secret 解析,FR-S1 mask 对齐)。实测类(tauri dev 跑 testing 模板 ai_self_review→human_review 闭环)待用户执行。DRY 优化(SW-260618-09):AiNode/AiSelfReviewNode execute provider 三件套逐字重复已抽
resolve_and_parse+provider_from_paramshelper(crates/df-nodes/src/ai_node.rs:70/80注释)。原文以下设计正文保持不变,作为历史设计记录;落地形态以上方"实施状态"为准。
一、现状盘点(file:line 证据)
1.1 已就绪(自审闭环地基)
| 能力 | 位置 | 状态 |
|---|---|---|
NodeContext 结构 |
crates/df-workflow/src/node.rs:12-26 |
字段:node_id / inputs / config / execution_id / event_bus / node_status,无 task 数据、无 db 句柄 |
| AiNode 通用执行 | crates/df-nodes/src/ai_node.rs:116-164 |
AiNode.execute 调 LLM → 返 NodeOutput{text, model, usage}(仅内存,不落 task) |
| AiNode 参数解析 | crates/df-nodes/src/ai_node.rs:35-109 |
parse_params(config, inputs):prompt 优先上游 inputs["prompt"] > config.prompt;provider 配置从 config 取 |
| HumanNode 审批 | crates/df-nodes/src/human_node.rs:39-176 |
阻塞节点,发 HumanApprovalRequest 等响应;reject 关键字(L19-22)→ Err → 工作流 failed |
| testing 模板(拓扑) | crates/df-nodes/src/task_workflow_templates.rs:57-72 |
ai_self_review → human_review 串行;节点级 config 仅含 human title/options,ai_self_review config 为空 {} |
| 模板选择 | crates/df-nodes/src/task_workflow_templates.rs:28-35 |
template_for(target_status) 按 in_progress/testing/done 选模板 |
| DagDef 选模板 | src-tauri/src/commands/workflow.rs:84-92 |
空 dag + target_status → template_for 自动选 |
| deep_merge(④-1) | crates/df-workflow/src/executor.rs:102-108 + crates/df-workflow/src/dag.rs:169-188 |
executor 构建 NodeContext 时 deep_merge(initial_config, node_config),节点级覆盖全局级;模板节点 config 为空 → 全局 config 原样穿透 |
| 工作流联动任务 | src-tauri/src/commands/workflow.rs:235-308 |
task_id+target_status 都 Some 时,完成推进/失败退回(regression_target L46-54) |
| TaskAdvanceNode db 注入先例 | crates/df-nodes/src/task_advance_node.rs:98-110 + src-tauri/src/state.rs:265-266 |
节点 struct 持 db: Arc<Database>,NodeRegistry::register 工厂闭包 move 捕获 db.clone() 注入 —— AiNode 可复用同一模式 |
| run_workflow 全局 config | src-tauri/src/commands/workflow.rs:67-76 |
签名含 config: serde_json::Value,闭包内 executor.run(&runtime_dag, config) 传为 initial_config |
1.2 待 ①迁移落地(前置依赖)
| 缺口 | 位置 | 说明 |
|---|---|---|
| struct 字段 | crates/df-storage/src/models.rs:69-76 |
✅ 已有 output_json: Option<String>(决策 a 已落地到模型层) |
| migrations CREATE | crates/df-storage/src/migrations.rs:371 |
✅ CREATE TABLE tasks 含 output_json TEXT(新库) |
| migrations ALTER(老库) | crates/df-storage/src/migrations.rs:394-395 附近 |
⚠️ 未见 ALTER TABLE tasks ADD COLUMN output_json(老库升级缺列,需 ①迁移补) |
| crud SELECT | crates/df-storage/src/crud.rs:779 |
⚠️ list_active SELECT 列未含 output_json |
| crud INSERT/UPDATE | crates/df-storage/src/crud.rs:748-762 |
⚠️ INSERT/UPDATE_FULL 语句未含 output_json 列 |
| crud 行映射 | crates/df-storage/src/crud.rs:544 |
⚠️ output_json: row.get("output_json") 存在,但 SELECT 缺列 → 运行时 row.get 会报错(待 ①补 SELECT) |
| crud 白名单 | crates/df-storage/src/crud.rs:342-343 |
⚠️ allowed_columns_for("tasks") 未含 output_json(AiNode 写产出走通用 update_field 必需) |
结论:①迁移尚未完成(struct 已加字段,crud 链路与白名单未跟上)。本方案所有 ②-⑥ 步骤均强依赖 ①完成,尤其白名单与 SELECT 列。
二、五项实施方案
2.1 方案①:NodeContext 注入 task 机制
问题:ai_execute/ai_self_review 需拿到 task_id + task 数据(description / output_json)+ provider 配置;ai_execute 还需 db 句柄写产出。当前 NodeContext(node.rs:12-26)无此二者。
三选项对比:
| 选项 | 机制 | 优点 | 缺点 |
|---|---|---|---|
| A. config 注入 task_id + 节点持 db 句柄(推荐) | run_workflow 全局 config 注入 task_id+provider{base_url,api_key,model,protocol};AiNode struct 加 db: Arc<Database>,state.rs 工厂闭包 move 注入(复用 TaskAdvanceNode 先例) |
零改 NodeContext(不动 node.rs);db 注入已有先例(state.rs:265);config 经 deep_merge 穿透;AiNode 与 TaskAdvanceNode 注入方式一致 | AiNode 从无状态单例变有状态(state.rs:257-259 工厂闭包需改 move db.clone) |
| B. NodeContext 加 task 字段 | 改 node.rs 加 task: Option<TaskRecord> + db 句柄字段 |
节点直接读,省一次 DB 查 | 改 NodeContext 影响所有 Node(HumanNode/SleepNode/test 构造全改,human_node.rs:236 make_ctx 等测试全改);executor.rs:99-113 构造点要读 DB;df-workflow 反向依赖 df-storage/df-task(循环依赖风险) |
| C. IPC handler | AiNode 通过 event_bus 发事件,前端/IPC 侧写 task | 解耦 | AiNode 在 DAG 后台线程,无 AppHandle/State;引入跨线程往返;HumanNode 那套 broadcast 模式不适合写 DB;过度复杂 |
推荐:选项 A。理由:
- 零侵入 NodeContext —— 不动 node.rs,所有现有 Node 与测试(human_node.rs:236 make_ctx、executor.rs:99-113)零改动。
- 先例已验证 —— TaskAdvanceNode(task_advance_node.rs:98-110)就是这套:struct 持
db: Arc<Database>,state.rs:265-266 工厂闭包move |_config| { Box::new(...::new(db.clone())) }注入。AiNode 照搬。 - config 穿透已就绪 —— run_workflow 全局 config(workflow.rs:72)→ executor deep_merge(executor.rs:102-108)→ 模板节点 config 空
{}→ 全局 config 原样进NodeContext.config。前端调workflowApi.run(name, {}, config, taskId, target)(workflow.ts:12-26)即可把 task_id/provider 推进去。 - DB 句柄天然可得 —— state.rs:205 已有
db: Arc<Database>,build_registry 入参就是它(state.rs:252)。
注入路径:
前端 TaskDetail.handleWorkflowAdvance
→ workflowApi.run(name, {}, {task_id, provider:{base_url,api_key,model,protocol}}, taskId, "testing") (workflow.ts:12-26)
→ run_workflow(task_id, target_status, config) (workflow.rs:67)
→ executor.run(dag, config) // config 作 initial_config (workflow.rs:245)
→ NodeContext.config = deep_merge(initial_config, node_config={}) (executor.rs:102-108)
→ AiNode.execute(ctx): ctx.config["task_id"] / ctx.config["provider"] 可读
→ AiNode 持 self.db → TaskRepo::new(&self.db).get_by_id(task_id) 读 task
provider 配置来源:前端从 ai_list_providers(api/ai.ts:76)取默认 provider(types.ts:172-178 AiProviderRecord),拼成 {base_url, api_key, model, protocol} 注入 config。api_key 安全:复用 src-tauri/src/commands/ai/secret.rs ensure_resolved_key(ai_node.rs:50 注释已对齐此行为),前端不直接拿明文,由后端在注入 config 前解析(或前端传 provider_id,后端 AiNode 内部解析——更安全,见风险)。
2.2 方案②:ai_execute 写产出
前置:方案①A(AiNode 持 db + config 有 task_id)。
写产出路径(ai_node.rs:155-163 当前返 NodeOutput 后追加落库):
AiNode.execute(ctx):
1. parse_params(&ctx.config, &ctx.inputs) → p (现有,ai_node.rs:119)
2. provider.complete(request) → response (现有,ai_node.rs:146)
3. 【新增】若 ctx.config 有 task_id:
let task_id = ctx.config["task_id"].as_str()
let repo = TaskRepo::new(&self.db)
// 产出 schema: {text, model, usage} 直接序列化(与 NodeOutput.data 一致)
let output_json = serde_json::to_string(&json!({text, model, usage}))?
repo.update_field(task_id, "output_json", &output_json).await? // 需 ①白名单含 output_json
4. return NodeOutput::from_value(...) (现有,ai_node.rs:155)
关键约束:
update_field(crud.rs:165)走白名单校验 —— 必须 ①把 output_json 加进 tasks 白名单(crud.rs:342-343)。- 产出 schema 与
NodeOutput.data一致(ai_node.rs:155-163),下游ai_self_review既可从inputs["ai_execute"].data读(DAG 层),也可从 task.output_json 读(跨 DAG/重启)。推荐读 inputs(DAG 内闭环),task.output_json 仅作持久化与 human_review 展示源。
in_progress 模板适用:in_progress_template(task_workflow_templates.rs:40-50)单 ai_execute 节点,正是写产出处。testing 模板的 ai_self_review 读上游/读 task.output_json。
2.3 方案③:ai_self_review prompt 模板与自审结果
输入:ai_self_review 节点读上游 inputs["ai_execute"](DAG 层 testing 模板无 ai_execute,故实际读 task.output_json)+ task.description(需求)。
prompt 模板草案:
【系统提示 system_prompt】
你是严格的代码/产出审查员。审查任务产出是否符合需求,按四维度给出结构化结论。
只输出 JSON,不要任何额外文字。
【用户提示 prompt】
## 任务需求
{task.description}
## 待审产出
{task.output_json 解析后的 text 字段}
## 审查维度
1. 需求符合度:产出是否覆盖需求描述的所有要点
2. 产出完整性:是否有遗漏、未完成的部分
3. 正确性:逻辑/事实/语法是否正确
4. 边界处理:异常输入、空值、错误路径是否考虑
## 输出格式(严格 JSON)
{
"verdict": "pass" | "fail",
"dimensions": {
"requirement_fit": {"score": 0-10, "issues": ["..."]},
"completeness": {"score": 0-10, "issues": ["..."]},
"correctness": {"score": 0-10, "issues": ["..."]},
"boundary": {"score": 0-10, "issues": ["..."]}
},
"summary": "一句话总结",
"suggestions": ["改进建议1", "改进建议2"]
}
verdict=fail 当且仅当任一维度 score < 6 或有阻断性 issue。
自审结果写回 output_json:不覆盖,加 review 子字段(保留 ai_execute 原始产出供 human_review 对照):
{
"text": "<ai_execute 原始产出>",
"model": "...",
"usage": {...},
"review": {
"verdict": "pass" | "fail",
"dimensions": {...},
"summary": "...",
"suggestions": [...],
"reviewed_at": "<now_millis>",
"reviewer_model": "<自审用的 model>"
}
}
verdict=fail 的处理:当前 testing 模板(task_workflow_templates.rs:57-72)ai_self_review → human_review 串行,ai_self_review 返 Ok(不 Err)。设计选择:ai_self_review 不据 verdict 主动 Err(自审是辅助,最终决策权在人)。verdict=fail 时:
- 写回 output_json(带 review.fail 信号)
- 节点返 Ok(让 human_review 节点继续,人看到 fail 结论再定)
- human_review 可在审批卡片高亮显示 verdict=fail(前端红标),人选拒绝 → Err → 工作流 failed → regression_target(workflow.rs:46-54 testing→in_review)退回
替代方案(更激进,不推荐首版):ai_self_review verdict=fail 直接返 Err → 工作流 failed → 自动退回 in_review,跳过人工。风险:LLM 自审误判直接退回,人无干预机会。首版保守,verdict 仅作展示信号。
实现要点:
- prompt 模板放节点 config 还是硬编码?—— 放 config(run_workflow 全局 config 注入
review_prompt_template),模板可热替换。但首版可硬编码在 AiNode 逻辑里(按 node_id=="ai_self_review" 分支),简化首版。 - LLM 输出解析:
serde_json::from_str(&response.text)解 JSON,失败兜底verdict=unknown, summary=原文(防 LLM 不按要求输出)。
2.4 方案④:human_review 展示
human_review 节点(human_node.rs:39)当前从 ctx.config 读 title/description/options(human_node.rs:42-56),发 HumanApprovalRequest(human_node.rs:74-83)。问题:HumanApprovalRequest 事件载荷(df-core events)只有 title/description/options,不含 task 产出/自审结果。
展示方案:human_review 节点在发 HumanApprovalRequest 前,读 task.output_json,把"产出摘要 + 自审结论"拼进 description 字段(最小改动,复用现有事件结构):
human_review.execute(ctx):
1. 读 task.output_json(经方案①AiNode 持 db + ctx.config["task_id"])
→ 解析 review.verdict / review.summary / review.suggestions
→ 解析 text(ai_execute 产出)
2. 拼接 description:
description = format!("
## AI 自审结论:{verdict}
{summary}
建议:{suggestions}
## 产出
{text_前 N 字}
")
3. ctx.config["description"] = description (覆盖模板默认空串,human_node.rs:46-48)
4. 发 HumanApprovalRequest(现有流程,human_node.rs:74-83)
约束:HumanNode 当前不持 db(human_node.rs:36 pub struct HumanNode; 无字段)。需同样按方案①A 给 HumanNode 加 db: Arc<Database> + state.rs:254-256 工厂闭包改 move db。或:human_review 的 description 由 ai_self_review 节点算好,经 DAG outputs 透传(inputs["ai_self_review"].data)—— 更优,HumanNode 零改动:
- ai_self_review 节点把"自审结论 + 产出摘要"塞进 NodeOutput.data
- human_review 读
inputs["ai_self_review"].data["review_summary"]拼进 description - HumanNode 不需 db,零改动
推荐后者(HumanNode 零改动,db 注入只给 AiNode)。
审批卡片显示什么(前端):
- title:模板已定「核对 AI 自审结果」(task_workflow_templates.rs:66)
- description:自审 verdict(pass/fail 红绿标)+ summary + suggestions + 产出摘要
- options:同意 / 拒绝(task_workflow_templates.rs:67,含拒绝触发退回)
2.5 方案⑤:前端展示 output_json
当前 TaskDetail.vue(src/views/TaskDetail.vue:1-140):展示 title/status/description/priority/branch 等,无 output_json 展示。已有工作流推进按钮(L71-91)+ workflow-event 监听(L81-90 轻量进度)+ df-data-changed 自动刷新(L82 注释)。
UI 方案(在 TaskDetail.vue 信息区加一个区块,L124 workflowDef 后):
<!-- F-AiNodeSelfReview: 任务产出 + 自审结果展示 -->
<div v-if="task.output_json" class="info-item info-block">
<span class="label">{{ $t('taskDetail.output') }}</span>
<div class="value output-block">
<!-- 自审结论卡(若有 review 子字段) -->
<div v-if="parsedOutput.review" class="review-card" :class="reviewVerdictClass">
<span class="review-verdict">{{ reviewVerdictLabel }}</span>
<span class="review-summary">{{ parsedOutput.review.summary }}</span>
<ul v-if="parsedOutput.review.suggestions?.length" class="review-suggestions">
<li v-for="(s, i) in parsedOutput.review.suggestions" :key="i">{{ s }}</li>
</ul>
</div>
<!-- AI 产出(Markdown 渲染,复用 useRendered) -->
<div v-if="parsedOutput.text" class="output-text ai-md" v-html="renderedOutput"></div>
</div>
</div>
实现要点:
parsedOutput = computed(() => JSON.parse(task.output_json))(包 try/catch,解析失败降级显示原文)reviewVerdictClass:pass→绿,fail→红renderedOutput:复用useRendered(TaskDetail.vue:150, 190-192 已用于 description)渲染 markdown- i18n key:
taskDetail.output/taskDetail.review.pass/taskDetail.review.fail - workflow 完成后 task 经 df-data-changed 自动刷新(现有机制,TaskDetail.vue:82 注释),output_json 自动出现
约束:output_json 是 Option<String>(models.rs:76),前端 task.output_json 可能为 undefined(旧任务无产出)—— v-if="task.output_json" 守卫。
三、②-⑥ 实施步骤拆解(标可并发)
依赖链:①迁移(另一 agent)→ ②-⑥。②-⑥ 内部:
② AiNode 持 db + 写产出 ──┐
├─→ ⑤ 前端展示(依赖 output_json schema 定型,即③review schema)
③ ai_self_review prompt ──┤
├─→ ④ human_review 展示(依赖③ review schema + ② 产出 schema)
┘
⑥ 端到端联调(依赖②③④⑤)
步骤②:ai_execute 写产出(依赖①白名单)
crates/df-nodes/src/ai_node.rs:AiNodestruct 加db: Arc<Database>字段 +new(db)构造crates/df-nodes/src/ai_node.rs:execute:LLM 完成后,若ctx.config["task_id"]存在 →TaskRepo::new(&self.db).update_field(task_id, "output_json", &json)落库src-tauri/src/state.rs:257-259:ai 工厂闭包改move |_config| Box::new(AiNode::new(db.clone()))(对齐 state.rs:265 TaskAdvanceNode 写法)- 单测:mock db 验证 update_field 调用 + output_json 内容
步骤③:ai_self_review prompt 模板(依赖② schema)
crates/df-nodes/src/ai_node.rs:按 node_id 分支(或新增AiReviewNode独立节点类型)—— 推荐独立节点ai_self_review类型,复用 AiNode 大部分逻辑但 prompt 模板固定、输出解析 JSON- prompt 模板(见 2.3)硬编码或 config 注入
- LLM 输出 JSON 解析 + 兜底
- 自审结果写回 task.output_json(加 review 子字段,不覆盖 text)
- 注册
ai_self_review节点类型到 state.rs build_registry - testing 模板节点类型 "ai" → "ai_self_review"(task_workflow_templates.rs:59)
步骤④:human_review 展示(依赖③ review schema)
crates/df-nodes/src/ai_node.rs(ai_self_review 节点):把 review 摘要塞进 NodeOutput.data,供下游 human_review 读- 不改 HumanNode(human_node.rs 零改动)—— human_review 节点 config 的 description 在模板层或 ai_self_review 输出层拼接
- 方案:testing 模板给 human_review 节点 config 加 description 占位,或新增轻量"描述拼装"逻辑(读 inputs["ai_self_review"])
- 若选 HumanNode 持 db 方案:human_node.rs 加 db 字段 + state.rs:254 工厂闭包改 move(不推荐,多改一处)
步骤⑤:前端展示(依赖③ schema 定型)—— 可与②③④并发
src/views/TaskDetail.vue:加 output_json 展示区块(见 2.5)src/api/types.ts:TaskRecord 类型加output_json?: stringsrc/locales/*.json:加 taskDetail.output / review.pass / review.fail i18n key- review.verdict 红绿标样式
步骤⑥:端到端联调(依赖②③④⑤全完成)
- 前端 TaskDetail 点工作流推进(testing)→ run_workflow → ai_self_review 调 LLM → 写 output_json → human_review 审批卡显示自审结论 → 人同意/拒绝 → 任务态推进/退回
- 验证 regression_target(workflow.rs:46-54):拒绝 → failed → testing 退回 in_review
可并发标注:
- ②③④ 串行(共享 ai_node.rs / schema)
- ⑤ 可与②③④ 并发(前端独立,仅依赖 schema 文档约定)
- ⑥ 必须最后(全依赖)
四、风险
| 风险 | 等级 | 说明 | 缓解 |
|---|---|---|---|
| ①迁移未完成阻塞 ②-⑥ | P0 阻断 | crud 白名单(crud.rs:342-343)/ SELECT 列(crud.rs:779)/ INSERT-UPDATE 语句(crud.rs:748-762)/ ALTER(migrations.rs)均未含 output_json | 本方案明确标注前置依赖;①agent 须先完成 crud 链路全量(白名单+SELECT+INSERT+UPDATE+ALTER) |
| api_key 明文经 config 注入 | P0 安全 | 前端拼 provider config 含 api_key,经 IPC(明文)+ config(明文)传到 AiNode,前端可拿到明文 key(违背 FR-S1 mask 设计) | 改传 provider_id(非 api_key),AiNode 内部调 secret::ensure_resolved_key(src-tauri/src/commands/ai/secret.rs)解析;前端 config 只放 provider_id + model + protocol,base_url/api_key 后端解析注入 |
| LLM 自审输出不合规 JSON | P1 | LLM 可能不按要求输出纯 JSON | 兜底解析:JSON.parse 失败 → verdict=unknown + summary=原文;prompt 强约束 + temperature=0 |
| AiNode 从无状态变有状态 | P2 | state.rs:257-259 当前 Box::new(AiNode) 无参,改 new(db.clone()) 后工厂闭包要 move db(对齐 state.rs:265 已有写法) |
低风险,TaskAdvanceNode 同模式已验证;注意所有 AiNode 测试构造点同步 |
| HumanNode 是否持 db 的方案分歧 | P2 | 方案④有两个选项(HumanNode 持 db vs ai_self_review 输出透传) | 推荐后者(HumanNode 零改动),review 摘要经 DAG inputs 透传;减少改动面 |
| ai_self_review verdict=fail 是否自动退回 | P2 | 激进方案(fail→Err→自动退回)vs 保守方案(fail→展示,人定) | 首版保守,verdict 仅作展示信号;后续可加 config 开关 auto_reject_on_fail |
| testing 模板 ai 节点类型改 ai_self_review | P2 | task_workflow_templates.rs:59 当前 dag.add_node("ai_self_review", "ai", ...) 类型是 "ai";若新增独立 ai_self_review 节点类型,模板要改 + registry 要注册 |
若复用 AiNode(按 node_id 分支 prompt)则模板不改;若独立节点则模板 + registry 都改(见步骤③) |
| output_json schema 演进 | P3 | 未来产出类型多样化(代码/文档/分析),单一 text+review schema 可能不够 | schema 设计预留扩展字段;output_json 本就是自由 JSON 字符串,可演进 |
五、依赖
- ① df-storage 迁移(另一 agent,本方案前置):TaskRecord struct(✅ models.rs:76 已加)→ migrations(CREATE ✅ / ALTER ⚠️ 缺)→ crud(SELECT ⚠️ / INSERT ⚠️ / UPDATE ⚠️ / 白名单 ⚠️ 全缺)。须全量完成 crud 链路 + 白名单含 output_json。
- provider 配置注入链:前端 ai_list_providers(api/ai.ts:76)+ 默认 provider(types.ts:172-178)→ run_workflow config → deep_merge。已就绪,仅需前端拼装逻辑。
- secret 解析(若走 provider_id 方案):src-tauri/src/commands/ai/secret.rs ensure_resolved_key 已就绪(ai_node.rs:50 注释对齐)。
六、附录:file:line 证据索引
| 文件 | 行 | 内容 |
|---|---|---|
| crates/df-workflow/src/node.rs | 12-26 | NodeContext 结构(无 task/db) |
| crates/df-nodes/src/ai_node.rs | 35-109 | parse_params 参数解析 |
| crates/df-nodes/src/ai_node.rs | 112 | pub struct AiNode;(无状态) |
| crates/df-nodes/src/ai_node.rs | 116-164 | AiNode.execute 调 LLM 返 NodeOutput |
| crates/df-nodes/src/ai_node.rs | 155-163 | 产出 schema {text,model,usage} |
| crates/df-nodes/src/human_node.rs | 36 | pub struct HumanNode;(无状态) |
| crates/df-nodes/src/human_node.rs | 39-176 | HumanNode.execute 审批流程 |
| crates/df-nodes/src/human_node.rs | 19-22 | REJECT_KEYWORDS |
| crates/df-nodes/src/task_workflow_templates.rs | 28-35 | template_for 选模板 |
| crates/df-nodes/src/task_workflow_templates.rs | 57-72 | testing 模板(ai_self_review→human_review) |
| crates/df-nodes/src/task_advance_node.rs | 98-110 | TaskAdvanceNode db 注入先例 |
| crates/df-workflow/src/executor.rs | 99-113 | NodeContext 构造 |
| crates/df-workflow/src/executor.rs | 102-108 | deep_merge 节点级覆盖全局 |
| crates/df-workflow/src/dag.rs | 169-188 | deep_merge 实现 |
| src-tauri/src/commands/workflow.rs | 67-76 | run_workflow 签名(config/task_id/target_status) |
| src-tauri/src/commands/workflow.rs | 84-92 | 空 dag + target → template_for |
| src-tauri/src/commands/workflow.rs | 235-308 | 工作流联动任务推进/退回 |
| src-tauri/src/commands/workflow.rs | 46-54 | regression_target 失败退回映射 |
| src-tauri/src/state.rs | 252-266 | build_registry 节点注册(AiNode L257 / TaskAdvanceNode L265) |
| crates/df-storage/src/models.rs | 53-79 | TaskRecord(含 output_json L76) |
| crates/df-storage/src/migrations.rs | 371 | CREATE TABLE output_json TEXT |
| crates/df-storage/src/crud.rs | 165 | update_field 通用写 |
| crates/df-storage/src/crud.rs | 342-343 | tasks 白名单(缺 output_json) |
| crates/df-storage/src/crud.rs | 748-762 | TaskRepo INSERT/UPDATE(缺 output_json) |
| crates/df-storage/src/crud.rs | 779 | list_active SELECT(缺 output_json) |
| src/api/workflow.ts | 12-26 | workflowApi.run(含 config/taskId/targetStatus) |
| src/api/ai.ts | 76 | ai_list_providers |
| src/api/types.ts | 172-178 | AiProviderRecord(api_key/base_url) |
| src/views/TaskDetail.vue | 71-91 | 工作流推进按钮 + 轻量进度 |
| src/views/TaskDetail.vue | 124-127 | workflowDef 区块(output_json 展示插入点) |
| src-tauri/src/commands/ai/secret.rs | - | ensure_resolved_key(api_key 安全解析) |