重构: 文档汇总+进度看板+孤儿任务清理脚本+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:
2026-06-15 05:14:21 +08:00
parent 4b5f096d1c
commit 04032a2a8d
43 changed files with 5372 additions and 163 deletions

View 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 + ActLLM 流式回复 → 调用工具 → 执行工具 → 结果回传 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 → returngenerating 保持 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/limitAI 不知道有数据被漏掉(待改进)
- **"能写不能跑"**AI 有 `write_file` 但无 `run_command`,无法形成"写→跑→改"闭环(待改进)
- **工具集封闭**:编译期硬编码,无 MCP 客户端AI 运行时不能新增/修改工具
---
## 五、审批门控机制
### 流程
```
AI 要执行 create_projectMedium 风险)
process_tool_calls → 不自动执行,写入 ai_tool_executionsstatus=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-rs5MB 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_startinput+ message_deltaoutput |
| output_tokens | 最终值 | **累计值**(非增量),直接覆盖不累加 |
### 跨 loop 实例累加
审批暂停→恢复 spawn 全新 `run_agentic_loop` 实例,新 loop 局部累加器从 0 起。`save_conversation` 的 upsert 路径 token **读旧值叠加**(非覆盖),保证跨 loop 实例的对话总用量正确。
```
loop 实例 1prompt=500, completion=200 → save(旧值 None + 500/200)
│ 审批暂停
loop 实例 2prompt=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-3LIKE 或 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) — 踩坑/约定/技巧

View 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_runningPending→Running
│ ③ 收集上游输出作为 inputs │
│ ④ 构建 NodeContext │
│ ⑤ 创建 async future不立即执行
└──────────────────────────────────────────────────────┘
┌─ 阶段二:并发执行 ───────────────────────────────────┐
│ futures::future::join_all(node_futures).await │
│ │
│ 同层所有节点真正并发跑tokio 异步运行时) │
│ 等待全部完成(或某个失败) │
└──────────────────────────────────────────────────────┘
┌─ 阶段三:收尾 ───────────────────────────────────────┐
│ 遍历结果: │
│ ✅ 成功 → set_completed + emit NodeCompleted + 存输出 │
│ ❌ 失败 → set_failed + emit NodeFailed + 记录错误 │
│ 🚫 取消 → 跳过 set_failedCancelled 是终态) │
│ │
│ 任一失败 → 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** | ✅ 真实可用 | 调用 LLMOpenAI/Anthropicconfig 驱动 providerbase_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-R8P0 |

View File

@@ -1,6 +1,6 @@
# 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 截断,属正常终止而非断连)|
| embed | POST `/v1/embeddings`,响应按 index 排序返回 `Vec<Vec<f32>>` |
| connect timeout | `connect_timeout(30s)`,不设总 timeout避免误砍流式长任务|
| complete 单请求超时 | ✅ FR-R42026-06-14 commit 36d68dd`complete()` 同步路径用 `RequestBuilder::timeout(Duration::from_secs(60))` 设 60s 单请求超时;**仅作用于 complete不影响 stream 流式路径**stream 仍靠上层 idle timeout 兜底)|
> idle timeout120s与断连丢弃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`)。
### tool_use_id 兜底AC1/AC22026-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.rsSprint 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 调用)|
| `all_messages_clone()` | 返回全量消息(用于 save_conversation / 标题生成)|
| `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-D42026-06-14 commit 4a95f6a由正向 `position` 遍历改为反向 `rposition`(审批替换命中最近一条同 id 工具结果;该调用属审批低频路径,不引入索引)|
---
## 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 |
| 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` 表(审计日志)。
### run_commandSprint 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
@@ -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 |
| Reflection自纠| 缺失 | 执行后自检 / 重试 |
详见 [Phase 2 计划 - 决策能力升级](../07-项目管理/Phase2计划.md)。
详见 [Phase 2 计划 - 决策能力升级](../07-项目管理/Phase2计划-2026-06-12.md)。
---
## 相关文档
- [df-storage 存储层](./df-storage-存储层.md)
- [df-workflow 工作流引擎](./df-workflow-工作流引擎.md)
- [df-nodes 节点集合](./df-nodes-节点集合.md)
- [df-storage 存储层](./df-storage-存储层-2026-06-12.md)
- [df-workflow 工作流引擎](./df-workflow-工作流引擎-2026-06-12.md)
- [df-nodes 节点集合](./df-nodes-节点集合-2026-06-12.md)

View File

@@ -130,6 +130,6 @@ Tier 1+ 目标MCP Shell 封装(`mcp-server` 转发 IPC外部工具使
## 相关文档
- [df-storage 存储层](./df-storage-存储层.md) — knowledges 表结构 + 向量工具函数
- [df-ai AI 集成模块](./df-ai-AI集成模块.md) — hybrid_search / generate_embedding / extract
- [功能决策记录](../02-架构设计/功能决策记录.md) — 检索方案演进决策
- [df-storage 存储层](./df-storage-存储层-2026-06-12.md) — knowledges 表结构 + 向量工具函数
- [df-ai AI 集成模块](./df-ai-AI集成模块-2026-06-12.md) — hybrid_search / generate_embedding / extract
- [功能决策记录](../02-架构设计/功能决策记录-2026-06-14.md) — 检索方案演进决策

View File

@@ -71,4 +71,4 @@ crates/df-nodes/src/
## 相关文档
- [df-workflow 工作流引擎](./df-workflow-工作流引擎.md)
- [df-workflow 工作流引擎](./df-workflow-工作流引擎-2026-06-12.md)

View File

@@ -1,6 +1,6 @@
# 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|
| `top_used(limit)` | published 按 reuse_count DESC热门列表 |
| `set_embedding(id, &[f32])` | UPDATE embedding BLOBf32 little-endian|
| `search_vector(query_vec, limit)` | SELECT published + embedding IS NOT NULL → 纯 Rust 余弦批量比较skip 维度不匹配 |
| `search_vector(query_vec, limit)` | ✅ FR-D22026-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>` |
### 向量工具函数
@@ -137,5 +137,5 @@ crates/df-storage/src/
## 相关文档
- [SQLite CRUD 模式](../01-技术文档/SQLite-CRUD模式.md)
- [前后端类型对齐](../02-架构设计/前后端类型对齐.md)
- [SQLite CRUD 模式](../01-技术文档/SQLite-CRUD模式-2026-06-12.md)
- [前后端类型对齐](../02-架构设计/前后端类型对齐-2026-06-12.md)

View File

@@ -1,6 +1,6 @@
# 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)` | 加带条件边 |
| `predecessors` | `(&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-D12026-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` 求值。
@@ -219,9 +219,9 @@ AI ChatB 路线)从单链 ReAct 升级为规划式协作时,本引擎需
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)
- [Phase1 架构决策](../02-架构设计/Phase1架构决策.md)
- [df-nodes 节点集合](./df-nodes-节点集合-2026-06-12.md)
- [Phase1 架构决策](../02-架构设计/Phase1架构决策-2026-06-12.md)