diff --git a/crates/df-nodes/src/ai_node.rs b/crates/df-nodes/src/ai_node.rs index 19b4e8d..a7a773f 100644 --- a/crates/df-nodes/src/ai_node.rs +++ b/crates/df-nodes/src/ai_node.rs @@ -419,6 +419,14 @@ fn truncate_for_summary(s: &str) -> String { format!("{truncated}…") } +/// 阶段3 自审闸门决策(纯函数,便于单测覆盖各 verdict/gate 组合)。 +/// +/// 仅当 `gate==true` 且 `verdict=="fail"` 时阻断。verdict="unknown"(LLM 输出不可靠) +/// 与 "pass" 均不阻断 —— unknown 保持人定权(阶段2 保守语义不变)。 +fn gate_should_block(gate: bool, verdict: &str) -> bool { + gate && verdict == "fail" +} + /// AI 自审节点(决策 a 步骤③) pub struct AiSelfReviewNode { db: Arc, @@ -576,13 +584,52 @@ impl Node for AiSelfReviewNode { // ── NodeOutput.data 塞 review 摘要,供下游 human_review 经 inputs["ai_self_review"] 读 ── // human_review(HumanNode)从 config/inputs 拼 description;此处把 verdict/summary/suggestions // 平铺进 data,HumanNode 零改动即可透传(前端/模板层可据此拼审批卡描述)。 - Ok(NodeOutput::from_value(serde_json::json!({ + let output = NodeOutput::from_value(serde_json::json!({ "verdict": verdict, "summary": summary, "suggestions": suggestions, "review": merged.get("review").cloned().unwrap_or(serde_json::Value::Null), "model": response.model, - }))) + })); + + // ── 阶段3: 自审闸门(F-260616-07 决策 a 步骤③) ── + // config["gate"]==true 时,AiSelfReviewNode 从「自审辅助」升级为「DAG 节点闸门」: + // verdict="fail" → 返回 Err → executor first_err 中止后续层(下游 human_review 不跑) + // → 工作流 failed → ②-4 回调退回 in_review(对齐工作流失败语义) + // verdict="unknown" → 保持 Ok(LLM 输出不可靠时不阻断,人定权) + // verdict="pass" → Ok 放行下游 + // + // 落库在前(已 update_field output_json),故闸门 fail 时 task.output_json.review + // 已含结论,前端/二次工作流可读自审详情(不丢失审查痕迹)。 + // + // 设计选型(三方案对比,见 commit/设计文档): + // A) AiNode 内部门控(本方案):1 处 return Err,复用 executor first_err + ②-4 回调, + // 零 executor 核心改动,零依赖暂缓的条件引擎(T-260614-11)。最简。 + // B) DAG edges 条件 + ConditionEngine 求值:依赖 T-260614-11(暂缓),executor 当前 + // 完全不评估 edge.condition(topological_layers 无条件收录所有边),须先做条件 + // 引擎 Phase1+2 才能用 → 拆波。 + // C) executor 闸门检查钩子(节点 execute 后 executor 读 output.verdict):改 DagExecutor + // 核心循环,牵动所有节点,风险/范围不符「最简不破坏」。 + // 选 A:语义最贴近「自审结果作为闸门」(自审节点自行决定放行/阻断),且 gate 可按节点 + // config 开关(默认 false = 阶段2 行为不变,模板/前端零强制改动,向后兼容)。 + let gate_enabled = ctx + .config + .get("gate") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + if gate_should_block(gate_enabled, &verdict) { + tracing::info!( + node_id = %ctx.node_id, + task_id, + "AiSelfReviewNode 闸门触发: verdict=fail, 阻断下游(工作流将 failed)" + ); + return Err(anyhow::anyhow!( + "AI 自审闸门未通过(verdict=fail): {}", + if summary.is_empty() { "自审结论 fail" } else { &summary } + )); + } + + Ok(output) } fn schema(&self) -> NodeSchema { @@ -593,7 +640,8 @@ impl Node for AiSelfReviewNode { "task_id": { "type": "string", "description": "自审目标任务 ID(必填)" }, "provider_id": { "type": "string", "description": "AI Provider ID(FR-S1:密钥经 secret 解析不进 config;留空走默认 provider)" }, "model": { "type": "string", "description": "模型名(可选,留空用 record.default_model)" }, - "max_tokens": { "type": "integer" } + "max_tokens": { "type": "integer" }, + "gate": { "type": "boolean", "description": "阶段3 闸门开关:false(默认)=自审辅助,verdict 仅透传展示;true=自审结果作 DAG 闸门,verdict=fail 返回 Err 阻断下游(工作流 failed → ②-4 退回),verdict=unknown/pass 放行" } }, "required": ["task_id", "provider_id"] }), @@ -1014,4 +1062,43 @@ mod tests { assert!(p.contains("边界处理")); assert!(p.contains("verdict"), "prompt 应含输出格式约束"); } + + // ============================================================ + // 阶段3 自审闸门(gate_should_block)单测 + // ============================================================ + // + // gate 决策矩阵: + // gate=false(默认,阶段2 行为) → 任何 verdict 都放行(辅助模式) + // gate=true + verdict=pass → 放行 + // gate=true + verdict=unknown → 放行(LLM 不可靠时不阻断,人定权) + // gate=true + verdict=fail → 阻断(返回 Err,工作流 failed) + + #[test] + fn gate_disabled_never_blocks_any_verdict() { + // gate 默认 false(阶段2 兼容):无论 verdict 如何都不阻断,自审仅辅助展示 + assert!(!gate_should_block(false, "fail"), "gate 关闭时 fail 也不阻断"); + assert!(!gate_should_block(false, "pass"), "gate 关闭时 pass 放行"); + assert!(!gate_should_block(false, "unknown"), "gate 关闭时 unknown 放行"); + } + + #[test] + fn gate_enabled_blocks_only_on_fail() { + // gate 开启:仅 fail 阻断 + assert!(gate_should_block(true, "fail"), "gate 开 + fail 应阻断"); + assert!(!gate_should_block(true, "pass"), "gate 开 + pass 放行"); + assert!( + !gate_should_block(true, "unknown"), + "gate 开 + unknown 放行(LLM 不可靠时保人定权)" + ); + } + + #[test] + fn gate_unknown_verdict_does_not_block() { + // 防御:verdict 非标准值(如解析异常)不应触发阻断(对齐 unknown 保守不阻断语义) + assert!( + !gate_should_block(true, "weird"), + "非标准 verdict 不应阻断(对齐 unknown 保守)" + ); + assert!(!gate_should_block(true, ""), "空 verdict 不应阻断"); + } } diff --git a/crates/df-nodes/src/task_workflow_templates.rs b/crates/df-nodes/src/task_workflow_templates.rs index 5927633..0560487 100644 --- a/crates/df-nodes/src/task_workflow_templates.rs +++ b/crates/df-nodes/src/task_workflow_templates.rs @@ -51,14 +51,22 @@ fn in_progress_template() -> DagDef { /// in_review → testing:AiNode 自审 → HumanNode 核对。 /// -/// 拓扑:ai → human 串行。HumanNode reject 走 Err(②-5 已做)→ 工作流 failed → -/// ②-4 失败回调退回 in_review(review_rounds+=1)。 +/// 拓扑:ai → human 串行。阶段3(本批)起 ai_self_review 启用 gate:true: +/// - verdict=fail → AiSelfReviewNode 返回 Err → 工作流 failed(不经 human_review) +/// → ②-4 失败回调退回 in_review(review_rounds+=1)。 +/// - verdict=unknown/pass → 放行 human_review,人定最终是否推进。 +/// HumanNode reject 走 Err(②-5 已做)→ 工作流 failed → ②-4 失败回调退回 in_review。 /// 通过则完成回调(②-3)推进 status 到 testing。 fn testing_template() -> DagDef { let mut dag = DagDef::new(); // 决策 a 步骤③:ai_self_review 节点类型对齐 state.rs 注册的独立自审节点 // (四维度 prompt + JSON 解析兜底 + 写回 output_json 加 review 子字段)。 - dag.add_node("ai_self_review", "ai_self_review", serde_json::json!({})); + // 阶段3:gate=true 启用自审闸门(verdict=fail 阻断下游,工作流 failed)。 + dag.add_node( + "ai_self_review", + "ai_self_review", + serde_json::json!({ "gate": true }), + ); dag.add_node( "human_review", "human", @@ -127,6 +135,13 @@ mod tests { // 节点类型(决策 a 步骤③:ai_self_review 独立节点类型) let ai = dag.nodes.get("ai_self_review").expect("ai_self_review 存在"); assert_eq!(ai.node_type, "ai_self_review"); + // 阶段3:gate=true 启用自审闸门(verdict=fail 阻断下游) + let gate = ai + .config + .get("gate") + .and_then(|v| v.as_bool()) + .expect("ai_self_review config 应含 gate"); + assert!(gate, "testing 模板 ai_self_review 应启用 gate:true(阶段3 闸门)"); let human = dag.nodes.get("human_review").expect("human_review 存在"); assert_eq!(human.node_type, "human"); // 边方向:ai → human diff --git a/docs/todo.md b/docs/todo.md index b8f0759..4954b52 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -75,7 +75,21 @@ > 排查性质(session-role-diagnose-only),走查定位根因 + 记录待办,未改代码。 -- [ ] B-260616-21 [P1] — **aichat 工具卡片「重复读取同一文件」表象(read_file 出现两条卡:一 0 行·7.1KB running、一 183 行·7.1KB completed)**。用户报对话记录里 `读取 .../api/ai.ts` 出现两次。**根因分析**(已走查前端流转 + 后端 emit 点):①前端 `useAiEvents.ts:202-206` `AiToolCallStarted` 分支直接 `lastMsg.toolCalls.push(info)`,**无 id 幂等守卫**(对比同文件 `startToolSlowTimer:58` 有 `_toolTimers.has(callId)` 守卫,Started 漏了同款判重)②`findToolCall`(`aiShared.ts:41-49`) 按 `id` 命中**首个**返回,`AiToolCallCompleted`(`:213`) 只 update 第一张卡 → 同 id 若被 push 两次,第二张卡永远 running,`ToolCard.vue:72` `parsed?.lines||0` 无 result 时显示 `0 行` ③后端 `audit.rs:538-549` `process_tool_calls` 对 tc_list 每 draft emit 一次 Started;read_file=Low risk(`:561`) **无去重**(去重仅 High,`:553-556` 注释明确 Low 不进)。**两候选根因**:**(A 最贴合现象)** 同一 `tool_call_id` 被**重复 emit Started**(后端 `process_tool_calls` 重入 / `stream_recv.rs` 生成重复 id / `agentic.rs` loop 重 process 同轮)→ push 两张卡 → Completed 只更首张(183 行)→ 次张残留 running(0 行);**(B)** agent loop 多轮 LLM 真读两次(不同 id)→ 但应两卡皆 183 行,与「0 行 + 183 行」现象不符,**排除为主因**。**待查**:后端同 id 重复 emit 的确切路径(`audit.rs:532` tc_list 来源 `tool_calls_acc` + `stream_recv.rs` id 生成 + `agentic.rs` 是否对同轮 tool_calls 重复 process)。**修复方向**(待定):①前端 Started push 前加 `findToolCall(event.id)` 幂等守卫(确定性低风险,对齐 startToolSlowTimer 守卫,治标兜底) ②后端查重复 emit 源治本(定位 stream_recv/process/loop 哪环生重复)。**关联** F-260616-05(agentic loop 工具去重缺失,batch53 仅 High 去重,read_file/search_files 等 Low 未去重;本现象若实为候选 A 则与 F-05 不同维度——F-05 是不同 id 重复调用,本条是同 id 重复 emit)。— src/composables/ai/useAiEvents.ts(:195-209 Started 分支) + src/composables/ai/aiShared.ts(:41 findToolCall) + src/components/ToolCard.vue(:72 lines 展示) + src-tauri/src/commands/ai/audit.rs(:538-549 emit Started)。 +- [x] ✅(247dc5d后端audit按id去重治本+useAiEvents.ts:205前端findToolCall幂等守卫双保险·候选A根因闭环) B-260616-21 [P1] — **aichat 工具卡片「重复读取同一文件」表象(read_file 出现两条卡:一 0 行·7.1KB running、一 183 行·7.1KB completed)**。用户报对话记录里 `读取 .../api/ai.ts` 出现两次。**根因分析**(已走查前端流转 + 后端 emit 点):①前端 `useAiEvents.ts:202-206` `AiToolCallStarted` 分支直接 `lastMsg.toolCalls.push(info)`,**无 id 幂等守卫**(对比同文件 `startToolSlowTimer:58` 有 `_toolTimers.has(callId)` 守卫,Started 漏了同款判重)②`findToolCall`(`aiShared.ts:41-49`) 按 `id` 命中**首个**返回,`AiToolCallCompleted`(`:213`) 只 update 第一张卡 → 同 id 若被 push 两次,第二张卡永远 running,`ToolCard.vue:72` `parsed?.lines||0` 无 result 时显示 `0 行` ③后端 `audit.rs:538-549` `process_tool_calls` 对 tc_list 每 draft emit 一次 Started;read_file=Low risk(`:561`) **无去重**(去重仅 High,`:553-556` 注释明确 Low 不进)。**两候选根因**:**(A 最贴合现象)** 同一 `tool_call_id` 被**重复 emit Started**(后端 `process_tool_calls` 重入 / `stream_recv.rs` 生成重复 id / `agentic.rs` loop 重 process 同轮)→ push 两张卡 → Completed 只更首张(183 行)→ 次张残留 running(0 行);**(B)** agent loop 多轮 LLM 真读两次(不同 id)→ 但应两卡皆 183 行,与「0 行 + 183 行」现象不符,**排除为主因**。**待查**:后端同 id 重复 emit 的确切路径(`audit.rs:532` tc_list 来源 `tool_calls_acc` + `stream_recv.rs` id 生成 + `agentic.rs` 是否对同轮 tool_calls 重复 process)。**修复方向**(待定):①前端 Started push 前加 `findToolCall(event.id)` 幂等守卫(确定性低风险,对齐 startToolSlowTimer 守卫,治标兜底) ②后端查重复 emit 源治本(定位 stream_recv/process/loop 哪环生重复)。**关联** F-260616-05(agentic loop 工具去重缺失,batch53 仅 High 去重,read_file/search_files 等 Low 未去重;本现象若实为候选 A 则与 F-05 不同维度——F-05 是不同 id 重复调用,本条是同 id 重复 emit)。— src/composables/ai/useAiEvents.ts(:195-209 Started 分支) + src/composables/ai/aiShared.ts(:41 findToolCall) + src/components/ToolCard.vue(:72 lines 展示) + src-tauri/src/commands/ai/audit.rs(:538-549 emit Started)。 + +### 🔧 2026-06-17 定期全量走查(第1轮·仅走查·未实施) + +> 周期性自检(session-role-diagnose-only:不碰代码,文档不对改文档 + 代码问题记 todo)。本轮范围:secret.rs 全文 + 近 6 提交 vs todo 状态核对 + 工作区未提交改动(tauri.conf.json)。 + +**文档状态校正(治本/实现已落地,原 [ ] 更新为 [x])**: +- B-260616-21 → ✅:后端 247dc5d(audit.rs:534-540 `seen_ids: HashSet` 按 id 去重保首个)+ 前端 useAiEvents.ts:205 `findToolCall` 幂等守卫双保险,候选 A 根因(LLM 复用同 tool_use.id 致 emit 两次)闭环。 +- F-260614-04 → ✅ 后端三段(79b6a43 数据层+池选择器 / b3684f4 池 IPC+set_caps / 80c0955 stream_one_provider+候选链 fallback),前端 api/ai.ts:142 IPC 封装已接;UI 配置面板待核验。 + +**新登记代码问题**: +- [ ] B-260617-06 [P3] — **secret.rs:104 注释与实现相反**。`resolve_provider_secret` 注释写「keyring 优先,fallback DB.api_key」,但代码(:105-110 `if !record.api_key.is_empty() { return DB }` + `get_provider_secret().unwrap_or_default()`)实际「**DB 优先,fallback keyring**」。与顶层设计(line 15「DB 优先,fallback keyring」)矛盾,line 104 注释为笔误。**实际无功能影响**(迁移态 DB 与 keyring 不会同时有值,set keyring→clear DB 原子序列),纯文档错误。**修法**:line 104 注释改为「DB 优先,fallback keyring」。— crates/df-storage/src/secret.rs:104 + +**工作区未提交改动观察(非 bug,记录供判断)**: +- src-tauri/tauri.conf.json: `bundle.targets` `"all"` → `["nsis"]`(打包目标从全平台收窄到仅 NSIS Windows 安装包)。正式收窄(devflow 仅 Win 桌面)则合理;若 dev 调试临时收窄忘还原,后续 mac/linux 打包会漏。**待用户确认意图**。 ### 🔧 2026-06-17 useAiSend.ts 代码审查(仅走查·未实施) @@ -195,7 +209,7 @@ - [x] ✅(batch35·2026-06-16·workflow w17duic3f) **①-1** 前端 TaskDetail 工作流推进按钮 — 与手动 advance 并存,调 workflowApi.run+监听进度。**含 B-41 进度内嵌**:onEvent 按 exec_id 过滤 dispatch node_started/completed/workflow_completed/failed + 独立 wfAdvancing loading 与手动 advancing 互含 disabled。**后端补丁**:workflow.rs run_workflow 空dag选模板(dag.nodes 非空→原 / 空+target→template_for / 空+无target→原路径,向后兼容)。文件锁:`src/views/TaskDetail.vue` + `src/api/workflow.ts` + `src/stores/project/workflow.ts` + `src-tauri/src/commands/workflow.rs` - [x] ✅(batch35·2026-06-16·workflow) **①-3** i18n 文案 — `src/i18n/{zh-CN,en}/taskDetail.ts`(taskDetail.* 命名空间,8 key 对称:workflowAdvanceTitle/Advancing/AdvanceFailed{msg}/StepRunning{node}/StepsProgress{done}{total}/CompletedHint/FailedHint)。注:todo 原路径 `locales/tasks.ts` 不准,实为 `i18n/taskDetail.ts` - **实施顺序**:④-1→②-1→②-5→②-2→②-3→②-4→②-6→①-1/①-3。**最小里程碑**(④-1+②-1+②-2+②-3):run_workflow 单 task_advance 节点 DAG 端到端推进 todo→in_progress -- [ ] F-260616-07 阶段3 AI 执行闭环 — **F-03 收口三件 ✅ 本批完成**(batch64), AiNode 自审闸门 ⏳ 待后续批 +- [x] ✅ F-260616-07 阶段3 AI 执行闭环 — **F-03 收口三件 ✅ batch64** + **AiNode 自审闸门 ✅ batch65**(本批) - [x] ✅(batch64·2026-06-16·workflow wii1u1lnm) **F-03 收口三件** — ①advance_task 注册 AI 工具(tool_registry.rs:395,handler L407 调 `df_nodes::task_advance_node::advance_task_atomic` 与 IPC `commands::task::advance_task:165` 同源) ②run_workflow 注册 AI 工具(:428,handler 架构约束无 AppHandle/State 报错引导走 IPC,ToolDefinition+审批文案 L1260-1261/1286-1287 注册让 LLM 可产出 tool_call) ③update_task handler guard 拒 status(L374 `field=="status"` bail,schema 通用 field/value 故 guard 拦非 schema 改)+ df-storage tasks 白名单移 status+review_rounds(crud.rs:331-347,advance_status_atomic CAS L848 独立路径不经白名单)。同步落地防 AI 工具行为不一致。**主代独立核查全过**:cargo check --workspace EXIT 0(5 pre-existing warnings 无关)+ df-storage 11 集成测试(含新 `update_field_rejects_tasks_status`)。文件锁:tool_registry.rs + crud.rs + project_soft_delete.rs。**审查登记 CR-260616-41**(待审查.md 当前队列) - [x] ✅ AiNode 自审闸门(阶段3 核心,决策a) — **阶段1迁移+方案✅(539b5ed+25409e5) + 阶段2②③④⑤✅(c10adaf+741b0b9) + P0 secret下沉+注入链✅(766f235+84a6fc4)**。⑥联调代码层解锁(待实测 tauri dev)。cargo df-nodes 79 passed + workspace EXIT 0 + vue-tsc EXIT 0 - [x] ✅(539b5ed) ①df-storage: TaskRecord 加 `output_json: Option` + V17 迁移(幂等补列) + crud 白名单+SELECT/INSERT/UPDATE 全链路 @@ -205,6 +219,7 @@ - [x] ✅(c10adaf) human_review: 经 DAG inputs["ai_self_review"].data 透传 verdict/summary(HumanNode 零改动,方案④更优路径) - [x] ✅(741b0b9) ⑤前端: TaskDetail.vue 展示 output_json(parsedOutput+review 红绿标+产出 markdown)+ types.ts+i18n 双语 - [ ] ⑥端到端联调 — **代码层完成**(P0 secret 下沉+注入链✅ 766f235+84a6fc4),实测类待用户(tauri dev 验证 testing 模板 ai_self_review→human_review 闭环) + - [x] ✅(batch65·2026-06-17·workflow) **AiNode 自审闸门(阶段3 核心)** — `verdict=fail` 作 DAG 节点闸门阻断下游,对齐工作流失败语义。**方案 A(AiNode 内部门控)胜出**:新增纯函数 `gate_should_block(gate, verdict)`(ai_node.rs:426 `gate && verdict=="fail"`) + execute 末尾 gate_enabled(config["gate"]==true,默认 false 阶段2 兼容)&& fail → `return Err("AI 自审闸门未通过(verdict=fail): ...")` 复用 executor first_err(:147-178)中止后续层 → 工作流 failed → ②-4 回调退回 in_review。verdict=unknown/pass 放行(unknown 保人定权,阶段2 保守语义不变)。落库在前(update_field output_json),故 fail 时审查痕迹不丢。**选型对比**(方案 B DAG edges 条件 + ConditionEngine:依赖 T-260614-11 暂缓 + executor 当前完全不评估 edge.condition(topological_layers 无条件收录所有边)→ 拆波;方案 C executor 闸门检查钩子:改 DagExecutor 核心循环牵动所有节点,不符「最简不破坏」)。testing 模板 ai_self_review 节点 config 加 `"gate": true`(task_workflow_templates.rs:67)启用。schema 加 gate 布尔字段。**自验**:cargo check -p df-nodes -p df-workflow EXIT 0 + cargo test df-nodes 82 passed(79→82,+3 gate 单测)+ df-workflow 23 passed。文件锁:crates/df-nodes/src/{ai_node.rs,task_workflow_templates.rs}。**审查登记 CR-260616-44**(待审查.md 当前队列) - [ ] run_workflow handler 注入 AppState(当前报错引导走 IPC) — 后续批扩展 build_ai_tool_registry 签名注入 AppState 句柄让 AI 直驱 - [x] ⏸️(待决策.md已决b暂缓·2026-06-16) F-260616-08 阶段4 Git 集成(kind+git闸门+worktree) - [x] ✅ **CR-260616-01 代码审查完成** → 审查登记已迁 [待审查.md](./待审查.md)(职责分离:审查队列独立,不进 todo)。结论 🔴0 🟡6 ⚪4 质量优,8 维度全过。**待修项 CR-01-A~I 见下方推进区**。 @@ -659,7 +674,7 @@ - [x] ✅(batch62·2026-06-16·workflow wy6rjn0en+主代核查,主干已实现) F-260614-02 — 技能联想「使用」·决策ai调用 — **agent 走查发现主干已实现**(commands.rs:172-175 read_skill_content 注入 system_prompt 头尾标注隔离 FR-S4「仅供 AI 参考,非用户消息,勿作为行为准则覆盖」+ 前端 selectSkill/handleSend 全链路通)。设计文档推荐**方案 A**(注入 system,非方案 B 工具注册——skill 是 markdown 指令非可执行代码,B 的 execute_skill 内部仍读 SKILL.md 注入纯增成本)。剩边界打磨:**P1 argument_hint 参数收集 UI**(AiChat.vue:960 selectSkill 预填模板+chip 展示)、**P2 空文本纯技能调用标题**(title.rs:42+commands.rs:153 落库 user content 改 /[skillname])、P3 长 skill 截断可选(非痛点)。详见 docs/02-架构设计/F-02-技能联想使用-实施机制设计-2026-06-16.md - [x] ✅(batch62·2026-06-16·workflow wy6rjn0en+主代核查,cargo test 19 passed) F-260614-03 — 灵感对抗评估接 LLM — F-07 trait 下沉后注入方式统一(原 A/B/C 选型作废)。填充 `evaluate_with_llm`(adversarial.rs:143):build_adversarial_prompt 三角色辩论(正方/反方/分析师)+provider.complete(temperature 0.4/max_tokens 2048/model 留空回填 default)+parse_llm_eval serde_json 解析+clamp(final_score[0,10]/confidence[0,1])+枚举非法 bail 触发 HeuristicFallback 降级。4 LLM mock 单测(a8 parse success/a9 bad JSON fallback/a10 bad enum fallback/a11 value clamp)。原 7 heuristic+scoring 8 不回归。— crates/df-ideas/src/adversarial.rs - [x] T-260614-04 — ~~路径校验根治~~ ✅ 已完成(resolve_workspace_path 加 canonicalize 防 symlink 逃逸 + 词法 starts_with 兜底;仅校验、返回词法路径保持前端友好;cargo check 0 err / 22 test pass)(06-14) -- [ ] F-260614-04 — 多 Provider 负载均衡池 — 备用模型/多账号聚合,全局容量=min(各 provider 上限之和, global_cap) (06-14) +- [x] ✅(后端三段·79b6a43数据层+池选择器+CR-52白项 / b3684f4池enabled/weight IPC+set_caps启动接入 / 80c0955 stream_one_provider+候选链fallback+resolved_model重算;前端api/ai.ts:142 IPC封装已接) F-260614-04 — 多 Provider 负载均衡池 — 备用模型/多账号聚合,全局容量=min(各 provider 上限之和, global_cap) (06-14)。**注**:UI 配置面板(provider 池 enabled/weight 可视化设置)是否完成待核验——后端 IPC+前端 API 层已就绪 - [ ] F-260614-05 — 模型能力系统 Phase 2 — 多模态消息支持:ChatMessage.content: String → Vec(Text/Image);前端粘贴/拖拽图片;vision 模型自动路由 (06-14) - [x] ✅(batch61·2026-06-16·workflow wwtn2knn6+主代核查,cargo 0err+vue-tsc 0err) F-260614-06 — 导入历史项目(scan 第二步) — 6 决策全落地:①description 走 LLM(scan.rs extract_description 复用 complete)②采样扩 `images:Vec`(scan.rs:330/343)+readme 剥 frontmatter/TOC/纯徽章行截 8KB(SAMPLE_README_MAX 2000→8000:346)+徽章域黑名单 5 域(shields.io/badge.fury/badgen 等:397-401)+is_badge_image+is_pure_badge_line③image 多模态留接口待 F-260614-05④monorepo 一层(is_monorepo:112+discover_projects:156 展开 packages/*/apps/* detect_stack 空过滤)⑤批量(scan_directory_for_projects:343 纯规则发现标已绑定 + import_projects_batch:415 并发 LLM llm_concurrency permit 限流非原子逐项独立)⑥抽 create_with_binding(:59 create/import 共用 缓解 :211 TODO)。前端 Projects.vue 导入 modal+api/project.ts 两 API+i18n 双语+scan.rs 单测全。— crates/df-project/scan.rs + src-tauri/commands/project.rs + lib.rs + api/project.ts + Projects.vue + i18n。详见功能决策记录(06-14) - [x] T-260614-09 — ~~idea.rs 物理删不级联~~ ✅ WF-E 完成(idea.rs:149 delete→purge_with_descendants,1 行,签名兼容)(06-14, commit 89da9fa)