新增: Phase2 阶段收尾(Sprint 1-20)
重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录 功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore 修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸 文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新 详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
@@ -10,29 +10,70 @@ DevFlow 使用 SQLite (rusqlite) 作为本地存储引擎。df-storage 负责 SQ
|
||||
|
||||
## 当前状态
|
||||
|
||||
- SQLite 连接池: 已实现
|
||||
- Schema 迁移 (6 张表 + 4 索引): 已实现
|
||||
- CRUD 层: **待实施**
|
||||
- SQLite 连接管理: 已实现(`Arc<Mutex<Connection>>` 单连接,非连接池)
|
||||
- Schema 迁移 (V1-V9 累计 11 张表 + 9 索引): 已实现
|
||||
- CRUD 层: 已实现(`impl_repo!` 宏 + 11 个 Repo + 向量检索)
|
||||
|
||||
## 设计要点
|
||||
|
||||
### 待实施内容
|
||||
### 已实施内容
|
||||
|
||||
1. **泛型 CRUD trait** — 定义统一的 `Repository<T>` 接口
|
||||
2. **SQL 构建** — 参数化查询,防止 SQL 注入
|
||||
3. **事务支持** — 跨表操作的原子性保证
|
||||
4. **批量操作** — `insert_batch` / `update_batch`
|
||||
5. **查询构建器** — 条件查询、分页、排序
|
||||
1. **`impl_repo!` 宏 CRUD** — 一次性为各表生成 insert / get_by_id / list_all / query / update_field / update_full / delete(当前 11 个 Repo,非 trait 抽象)
|
||||
2. **参数化查询** — `?1`/`?2` 占位符 + `params![]` 绑定,防止 SQL 注入
|
||||
3. **列名白名单校验** — `ALLOWED_COLUMNS` 校验 query / update_field 的动态列名
|
||||
4. **`update_full(record)`** — 整体更新全可变字段(保留 id 与 created_at)的原子写
|
||||
5. **向量检索** — KnowledgeRepo 余弦相似度 top-N(纯 Rust,数据量 <50k 暴力遍历)
|
||||
|
||||
### 仍待实施
|
||||
|
||||
1. **泛型 `Repository<T>` trait** — 当前用宏非 trait,无统一接口抽象
|
||||
2. **事务支持** — 跨表操作的原子性保证(当前单连接 + Mutex,无显式事务)
|
||||
3. **批量操作** — `insert_batch` / `update_batch`
|
||||
|
||||
### 约定
|
||||
|
||||
- ID 字段统一使用 `TEXT` (UUID v4)
|
||||
- 时间字段使用 `INTEGER` (Unix timestamp)
|
||||
- 时间字段使用 `TEXT` (毫秒时间戳字符串,`now_millis_str()` 返回 String)
|
||||
- JSON 字段使用 `TEXT` 存储 JSON 字符串
|
||||
- 所有写操作返回 `Result<(), df_core::error::Error>`
|
||||
- 所有写操作统一返回 `df_core::error::Error` 错误类型:insert 返 `Result<String>`(id),update/delete 返 `Result<bool>`(是否影响行)
|
||||
|
||||
## update_field 的强制时间戳约束
|
||||
|
||||
`impl_repo!` 宏为各 Repo 统一生成的 `update_field(id, field, value)` 生成 SQL:
|
||||
```sql
|
||||
UPDATE {table} SET {field} = ?1, updated_at = ?2 WHERE id = ?3
|
||||
```
|
||||
**总是连带刷新 updated_at = now**,不可关闭。
|
||||
|
||||
### 适用与不适用
|
||||
|
||||
- 适用:内容更新(标题、状态、描述等)——这些变更本就该反映为新近修改时间。
|
||||
- 不适用:纯元数据标记切换(如归档 archived)。归档是元数据,不应改对话的"最近活跃时间",否则侧栏相对时间会跳变为"刚刚"。
|
||||
|
||||
### 例外模式:专用方法走原生 SQL
|
||||
|
||||
对元数据类切换,新增专用方法绕过 update_field,直接写原生 SQL 只动目标列。例如 AiConversationRepo::set_archived:
|
||||
```sql
|
||||
UPDATE ai_conversations SET archived = ?1 WHERE id = ?2
|
||||
```
|
||||
不带 updated_at。
|
||||
|
||||
同类专用原生 SQL 方法(均不调 update_field),按「是否刷 updated_at」分两类:
|
||||
|
||||
- **不刷**(与 set_archived 同策略,只改目标列):
|
||||
- `KnowledgeRepo::set_embedding` — `UPDATE knowledges SET embedding = ?1 WHERE id = ?2`(向量写库,非业务修改)
|
||||
- **刷**(策略相反,刷新时间反映复用行为):
|
||||
- `KnowledgeRepo::increment_reuse_count` — `UPDATE knowledges SET reuse_count = reuse_count + 1, updated_at = ?1 WHERE id = ?2`(原子自增 + 刷时间,因复用确属"近期活跃")
|
||||
|
||||
> 取舍:是否刷 updated_at 取决于语义——元数据/内部字段切换不刷(避免污染相对时间),反映业务行为变更的刷。
|
||||
|
||||
### 相关机制
|
||||
|
||||
- `update_full(record)`:整体更新全可变字段,保留 id 与 created_at,用于 save_conversation 落库对话内容(此时确实要刷 updated_at)。
|
||||
- `ALLOWED_COLUMNS` 白名单:update_field / query 的列名经白名单校验防注入。set_archived 因列名硬编码无需白名单。
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `crates/df-storage/src/lib.rs` — 存储层入口
|
||||
- `crates/df-storage/src/schema.rs` — Schema 定义与迁移
|
||||
- `crates/df-storage/src/migrations.rs` — Schema 定义与迁移
|
||||
- `crates/df-core/src/error.rs` — 统一错误类型
|
||||
|
||||
248
docs/02-架构设计/B-03-人工审批响应机制.md
Normal file
248
docs/02-架构设计/B-03-人工审批响应机制.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# B-03 人工审批响应机制设计
|
||||
|
||||
> **真相源**(本文档唯一展开完整设计)。功能决策记录仅放摘要 + 指针。
|
||||
>
|
||||
> 背景:B-260614-03 — df-workflow `HumanNode` 假实现(`human_node.rs:55` 注释"等待审批"但首次迭代直接 return "同意")。
|
||||
> 状态:📐 **设计完成,未实施** | 创建:2026-06-14 | 来源:多代理探索
|
||||
> 依赖:B-260614-06(execution_id 硬编码)、B-260614-07(每节点全新空 StateMachine)
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与问题
|
||||
|
||||
`HumanNode` 是工作流中唯一的阻塞节点,用于在 DAG 执行链路上插入人工确认门控(如"发布前确认""删除前确认")。当前实现 `crates/df-nodes/src/human_node.rs` 已正确发送 `WorkflowEvent::HumanApprovalRequest` 到事件总线,但**紧接着直接 `return NodeOutput { decision: "同意" }`**,从不等待前端审批响应。这导致:
|
||||
|
||||
1. 人工审批门控形同虚设——工作流永远按"同意"放行,无人工拦截能力。
|
||||
2. 与 `ai.rs` 的 `ai_approve` 严谨审批链路(Low 自动 / Medium+High 暂停等审批)矛盾——同一项目两套审批机制,一严谨一形同虚设。
|
||||
3. 前端 `approve_human_approval` IPC、`HumanApprovalResponse` 事件、`stores/project.ts` 监听链路均已接通,却被 HumanNode 的假返回架空。
|
||||
|
||||
## 二、现状勘察:链路 90% 已通,缺口仅 1 处
|
||||
|
||||
经代码勘察,端到端审批响应链路的基础设施**已全部就位**,唯一缺口在 HumanNode 本身。
|
||||
|
||||
| 组件 | 位置 | 状态 |
|
||||
|------|------|------|
|
||||
| `WorkflowEvent::HumanApprovalRequest` / `HumanApprovalResponse` 事件 | `df-core/src/events.rs:60-73` | ✅ 已定义 |
|
||||
| `EventBus`(tokio broadcast,capacity 256,`subscribe()`) | `df-workflow/src/eventbus.rs` | ✅ 可用 |
|
||||
| `approve_human_approval` IPC(前端响应回总线) | `src-tauri/src/commands/workflow.rs:161` | ✅ 已实现 |
|
||||
| `AppState.event_bus` 单一全局总线 | `src-tauri/src/state.rs:149` | ✅ run_workflow 与 NodeContext 共享同一 sender |
|
||||
| 前端监听 `workflow-event` + 捕获 Request + 调 IPC | `src/stores/project.ts:214, 228` | ✅ 已接通 |
|
||||
| run_workflow 转发**所有**事件(含 Request/Response)到前端 | `src-tauri/src/commands/workflow.rs:83` | ✅ |
|
||||
| **HumanNode.execute 订阅 Response 等待审批** | `crates/df-nodes/src/human_node.rs:55` | ❌ **缺口:发完直接 return** |
|
||||
|
||||
### 端到端路径验证
|
||||
|
||||
```
|
||||
HumanNode.execute
|
||||
→ ctx.event_bus.send(HumanApprovalRequest) # 同一 broadcast bus
|
||||
→ run_workflow 转发器(独立 receiver) emit "workflow-event" 到前端
|
||||
→ 前端 stores/project.ts 捕获 Request,存 pendingApproval,渲染审批 UI
|
||||
→ 用户点"同意/拒绝"
|
||||
→ invoke('approve_human_approval', { execution_id, node_id, decision, comment })
|
||||
→ workflow.rs:161 构造 HumanApprovalResponse,state.event_bus.send(Response)
|
||||
→ 同一 broadcast bus
|
||||
→ HumanNode 的 receiver 收到 Response ✓
|
||||
→ 过滤 execution_id + node_id 命中 → 返回 NodeOutput
|
||||
```
|
||||
|
||||
`AppState.event_bus` 在 `run_workflow`(`workflow.rs:100` 取 `state.event_bus.clone()` 传入 `DagExecutor::new`)与 `NodeContext.event_bus`(`executor.rs:77` 传入 `self.event_bus.clone()`)之间共享同一 `broadcast::Sender`(Clone 仅复制 sender 句柄,底层通道同一)。`approve_human_approval` 发往 `state.event_bus`,即发往 HumanNode 订阅的同一通道。**路径闭环成立**。
|
||||
|
||||
## 三、B-06 / B-07 前置依赖的真实影响
|
||||
|
||||
todo.md 标 B-03 依赖 B-06/B-07。核对后**分级澄清**,避免误解为硬阻塞:
|
||||
|
||||
### B-06(execution_id 硬编码 "dummy-execution-id")
|
||||
|
||||
- **单工作流场景**:B-03 **照常工作**。Request/Response 两端都取 `ctx.execution_id`(当前 = "dummy"),过滤匹配。
|
||||
- **多工作流并发场景**:所有 execution_id 相同,跨工作流的 Response 会错配到同 node_id 的别的工作流实例 → **必须 B-06 修复**(execution_id 从 run_workflow 已生成的真 ID 下沉到 executor 再到 NodeContext)才能正确隔离。
|
||||
- **结论**:B-06 是**并发正确性**前置,非单流功能性前置。B-03 实现完成后,单工作流可用;并发安全等 B-06。
|
||||
|
||||
### B-07(每节点全新空 StateMachine)
|
||||
|
||||
- HumanNode 取消检查 `ctx.node_status.is_cancelled(&ctx.node_id)` 恒 false(空状态机 `get()` 返回 `Pending`)。
|
||||
- **即使 B-07 修复**(共享 `self.state_machine`),取消仍不生效——因为 `StateMachine`(`state.rs`)**无 `set_cancelled` 方法**,也无 `cancel_workflow_node` IPC、无前端取消按钮。
|
||||
- **结论**:B-07 是取消机制的**必要非充分**条件。取消要真正端到端生效,还需另补三件(见第七节)。B-03 的响应等待 + 超时核心功能不依赖 B-07。
|
||||
|
||||
## 四、核心机制设计
|
||||
|
||||
`human_node.rs::execute` 改为:**先订阅 → 发请求 → `select!` 循环等响应**。
|
||||
|
||||
### 4.1 订阅时序铁律
|
||||
|
||||
tokio `broadcast` 通道**不回放历史消息**——`subscribe()` 调用之后发送的消息才进入该 receiver 的队列。因此必须:
|
||||
|
||||
```
|
||||
subscribe() ← 必须先于 send(Request)
|
||||
send(Request)
|
||||
select! { rx.recv() | timeout | cancel }
|
||||
```
|
||||
|
||||
若顺序颠倒(subscribe 在 send 之后),HumanNode 的 receiver 在 Response 发出时尚不存在,Response 丢失,HumanNode 死等到超时。
|
||||
|
||||
### 4.2 execute 实现骨架
|
||||
|
||||
```rust
|
||||
async fn execute(&self, ctx: NodeContext) -> NodeResult {
|
||||
let config = ctx.config.as_object().cloned().unwrap_or_default();
|
||||
let title = config.get("title").and_then(|v| v.as_str()).unwrap_or("请确认");
|
||||
let description = config.get("description").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let options: Vec<String> = config.get("options")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||
.unwrap_or_else(|| vec!["同意".into(), "拒绝".into()]);
|
||||
let timeout_secs = config.get("timeout_secs").and_then(|v| v.as_u64()).unwrap_or(3600);
|
||||
|
||||
// 1. 先订阅,再发请求(broadcast 不回放历史)
|
||||
let mut rx = ctx.event_bus.subscribe();
|
||||
|
||||
// 2. 发审批请求
|
||||
ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest {
|
||||
execution_id: ctx.execution_id.clone(),
|
||||
node_id: ctx.node_id.clone(),
|
||||
title: title.into(),
|
||||
description: description.into(),
|
||||
options: options.clone(),
|
||||
}).await;
|
||||
|
||||
// 3. select! 循环:Response / 超时 / 取消
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs);
|
||||
let mut cancel_tick = tokio::time::interval(Duration::from_millis(500));
|
||||
cancel_tick.tick().await; // 丢弃首个立即触发
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
recv = rx.recv() => match recv {
|
||||
Ok(WorkflowEvent::HumanApprovalResponse {
|
||||
execution_id, node_id, decision, comment
|
||||
}) if execution_id == ctx.execution_id && node_id == ctx.node_id => {
|
||||
// decision 合法性校验
|
||||
if !decision.is_empty() && (options.is_empty() || options.contains(&decision)) {
|
||||
return Ok(NodeOutput::from_value(serde_json::json!({
|
||||
"decision": decision,
|
||||
"comment": comment.unwrap_or_default(),
|
||||
})));
|
||||
}
|
||||
return Err(anyhow::anyhow!("审批决策非法: {}", decision));
|
||||
}
|
||||
Ok(_) => continue, // 其他节点/类型的事件,忽略
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
tracing::warn!("HumanNode {} 漏收 {} 条事件(可能错过自身响应,继续)", ctx.node_id, n);
|
||||
continue; // 风险:若恰好漏收自身 Response,本节点将等到超时
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
return Err(anyhow::anyhow!("事件总线关闭,审批无法完成"));
|
||||
}
|
||||
},
|
||||
_ = tokio::time::sleep_until(deadline) => {
|
||||
return Err(anyhow::anyhow!("人工审批超时({}s)", timeout_secs));
|
||||
}
|
||||
_ = cancel_tick.tick() => {
|
||||
if ctx.node_status.is_cancelled(&ctx.node_id) {
|
||||
return Err(anyhow::anyhow!("人工审批被取消"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 决策点
|
||||
|
||||
| 决策 | 取值 | 原因 |
|
||||
|------|------|------|
|
||||
| `options` 校验 | 空数组时不校验(允许自由文本决策);非空时强制 `decision ∈ options` | 空数组语义 = 自由文本审批;非空 = 枚举选项,非法值应报错而非静默放行 |
|
||||
| `Lagged` 处理 | 警告日志 + continue | capacity 256 + 审批低频,漏自身 Response 概率极低;丢弃则误判超时更糟 |
|
||||
| 超时来源 | 配置 `timeout_secs`,默认 3600s | 保留现状默认,支持节点级配置(如"删除确认"给更长超时) |
|
||||
| 取消检查频率 | 500ms interval 轮询 `is_cancelled` | 当前无主动取消信号机制,轮询是 B-07 修复前的过渡;B-07 + `set_cancelled` 后仍需轮询(除非引入 `Notify`) |
|
||||
| 过滤键 | execution_id + node_id 双键 | node_id 单键不够(跨工作流可能重复);execution_id 单键不够(同工作流同层多 HumanNode) |
|
||||
|
||||
## 五、关键时序
|
||||
|
||||
```
|
||||
HumanNode.execute run_workflow 转发器 前端 store approve_human_approval
|
||||
│ │ │ │
|
||||
│ subscribe() (rx 建位) │ │ │
|
||||
│ send(Request) ──broadcast──┤ │ │
|
||||
│ ├─emit workflow-event──→│ │
|
||||
│ │ │ pendingApproval=… │
|
||||
│ (select! 阻塞等 rx) │ │ (UI 渲染审批卡片) │
|
||||
│ │ │ 用户点"同意" │
|
||||
│ │ │──── invoke ──────────┤
|
||||
│ │ │ │ send(Response)
|
||||
│ │ │ │ └─broadcast─┐
|
||||
│ rx.recv() = Response ✓ ←──┼───────────────────────┼──────────────────────┼──────────────┘
|
||||
│ 过滤 exec_id+node_id 命中 │ │ │
|
||||
│ return NodeOutput │ │ │
|
||||
```
|
||||
|
||||
**说明**:`run_workflow` 转发器是独立的 broadcast receiver,它收到 Response 后会再 emit 一次到前端(`workflow.rs:83` 无差别转发所有事件)。这是**无害 echo**——前端 store 在调 IPC 后已本地清 `pendingApproval`,重复的 Response 事件不影响状态。
|
||||
|
||||
## 六、并发边界
|
||||
|
||||
| 场景 | 处理 |
|
||||
|------|------|
|
||||
| 同层多个 HumanNode 并行 | 各自独立 receiver,各收全量 Response,靠 `node_id` 过滤互不干扰(execution_id 同层相同,隔离靠 node_id) |
|
||||
| 跨工作流并发 HumanNode | node_id 可能重复,**必须 B-06 真 execution_id 隔离**;未修前并发场景有错配风险 |
|
||||
| 前端未渲染审批 UI | 现有 store 已接通捕获 Request;UI 组件渲染属前端独立工作,B-03 后端不阻塞 |
|
||||
| 前端审批后转发器 echo Response | 无害,store 已清 pendingApproval |
|
||||
| 事件总线容量 | capacity 256;审批事件低频,正常不触发 Lagged |
|
||||
| 审批超时无响应 | `select!` 的 `sleep_until(deadline)` 分支返回 Err,节点置 Failed,工作流中止后续层 |
|
||||
|
||||
## 七、取消机制范围界定(B-03a / B-03b 拆分)
|
||||
|
||||
取消要端到端生效,当前缺三件,均不在 B-03 响应等待核心内:
|
||||
|
||||
1. **`StateMachine::set_cancelled()` 方法** — `state.rs` 当前只有 `is_cancelled` 查询,无对应 setter(`set_waiting`/`set_skipped` 不经转换校验,Cancelled 同理可加)
|
||||
2. **`cancel_workflow_node` IPC** — 前端触发取消的入口(当前无)
|
||||
3. **前端取消按钮 + 调 IPC** — UI 触发点
|
||||
|
||||
**建议拆分**:
|
||||
|
||||
| 子任务 | 范围 | 依赖 |
|
||||
|--------|------|------|
|
||||
| **B-03a** | HumanNode 响应等待 + 超时(本设计第四节) | 无硬依赖,单工作流即可用 |
|
||||
| **B-03b** | 取消机制:`set_cancelled` + cancel IPC + 前端按钮 | B-07(共享 StateMachine) + 上述三件 |
|
||||
|
||||
B-03a 不依赖 B-07 即可工作(取消分支恒 false,等价无取消,功能不残)。todo.md 原文"B-06/B-07 修了 is_cancelled 才有意义"应理解为:B-07 是取消的必要前提,但取消本身需独立补全(归入 B-03b)。
|
||||
|
||||
## 八、改动清单
|
||||
|
||||
| 文件 | 改动 | 风险 |
|
||||
|------|------|------|
|
||||
| `crates/df-nodes/src/human_node.rs` | 重写 `execute`:subscribe → send → `select!` 循环 | 低,单文件,无外部接口变更 |
|
||||
| `crates/df-workflow/src/eventbus.rs`(可选) | 删 `try_recv_human_approval` 死代码 TODO(`eventbus.rs:49-53`,零调用) | 低,零调用方 |
|
||||
|
||||
**不改动**:`df-core/events.rs` / `df-workflow/state.rs` / `src-tauri/commands/workflow.rs` IPC / 前端 store —— 全部基础设施复用,零侵入。
|
||||
|
||||
### B-03b 额外改动(取消机制,后续)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `crates/df-workflow/src/state.rs` | 加 `set_cancelled` 方法(不经转换校验,同 `set_waiting`) |
|
||||
| `crates/df-workflow/src/executor.rs` | B-07:`NodeContext.node_status` 传 `self.state_machine.clone()` 而非 `StateMachine::new()` |
|
||||
| `src-tauri/src/commands/workflow.rs` | 新增 `cancel_workflow_node` IPC |
|
||||
| `src/stores/project.ts` | 审批 UI 加"取消"按钮,调 cancel IPC |
|
||||
|
||||
## 九、测试设计
|
||||
|
||||
| 用例 | 方法 | 期望 |
|
||||
|------|------|------|
|
||||
| 正常审批:发匹配 Response → 收到决策 | 构造 EventBus + NodeContext,spawn execute,另起 task 发匹配(exec_id+node_id) Response | 返回 NodeOutput.decision = 发送的 decision |
|
||||
| execution_id 不匹配:Response 被过滤 | 发不匹配 execution_id 的 Response | 节点继续阻塞,短超时验证 → Err "超时" |
|
||||
| node_id 不匹配:Response 被过滤 | 发不匹配 node_id 的 Response | 同上 |
|
||||
| 超时:无 Response | `timeout_secs=1`,不发 Response | Err "人工审批超时(1s)" |
|
||||
| decision 非法:options 内无该决策 | 配置 options=["同意","拒绝"],发 decision="随便" | Err "审批决策非法: 随便" |
|
||||
| options 空允许自由文本 | 配置 options=[],发任意 decision | 返回该 decision(不校验) |
|
||||
| broadcast 关闭 → Err | drop 所有 sender 后 recv | Err "事件总线关闭" |
|
||||
| Lagged 容忍(可选) | 构造小容量 bus 灌满跳过,验证不 panic | warn 日志 + 继续 |
|
||||
|
||||
**取消分支测试**(B-03b):依赖 `set_cancelled` + B-07,本阶段跳过,或 mock `is_cancelled` 返回 true 验证分支可达。
|
||||
|
||||
---
|
||||
|
||||
**相关**:
|
||||
- 功能决策记录「工作流人工审批节点(B-03)」— 设计摘要
|
||||
- `docs/todo.md` B-260614-03 — 任务看板
|
||||
- `crates/df-nodes/src/human_node.rs` — 实施位置
|
||||
- `crates/df-workflow/src/eventbus.rs` — EventBus 基础设施
|
||||
- `src-tauri/src/commands/workflow.rs:161` — `approve_human_approval` IPC
|
||||
407
docs/02-架构设计/功能决策记录-归档.md
Normal file
407
docs/02-架构设计/功能决策记录-归档.md
Normal file
@@ -0,0 +1,407 @@
|
||||
# 功能决策记录 — 归档
|
||||
|
||||
> 从 [功能决策记录.md](./功能决策记录.md) 归档的条目——纯实现流水、老 Sprint 决策、UX 微调、已被取代或合并的细节。这些条目在「3 个月回看是否仍影响系统/功能设计理解」判断下已不再需要常驻主文档,但完整保留以备回溯。
|
||||
>
|
||||
> 创建:2026-06-14 | 性质:归档只读,不再维护更新
|
||||
|
||||
## 归档判断标准
|
||||
|
||||
- 一次性代码审查流水(甄别落地、骨架删除、列表查询过滤)
|
||||
- UX 微调(工具卡片折叠、对话滚动、错误友好化、空白屏修复)
|
||||
- 实现细节(参数解析抽函数、token 记录策略、model 追溯)
|
||||
- 老 Sprint 决策已被新设计取代或合并
|
||||
|
||||
> 其中**架构级结论**已提炼一句留在主文档对应小节,归档条目为细节展开。
|
||||
|
||||
---
|
||||
|
||||
## 一、AI Chat 上下文窗口与并发控制 [任务 #43 系列]
|
||||
|
||||
> 主文档保留一句架构结论:**ContextManager 类型替换为 messages 真相源 + 双层 Semaphore 并发控制(全局 3 / 单对话 2)**。以下为实现细节归档。
|
||||
|
||||
### ContextManager 接线方式:类型替换而非 Wrapper [任务 #43]
|
||||
|
||||
- **决策**:`AiSession.messages` 字段类型从 `Vec<ChatMessage>` 直接改为 `ContextManager`,后者成为消息的唯一持有者(内含 `Vec<TrackedMessage>` + token 缓存 + 裁剪能力)。不引入 Wrapper 包装层。
|
||||
- **原因/取舍**:Wrapper 方案下 Vec 是真相、ContextManager 是临时视图——每次 `build_for_request` 都要 clone 给 ContextManager,token 缓存永远滞后一轮或每次重建(双重复制)。类型替换让 push 即刻更新 token 计数、零额外 clone、单一数据源。代价是 `save_conversation` 等需全量的场景要显式调 `all_messages_clone()`(多一行但语义明确)。13 处操作点中 6 处签名不变(push/clear)、4 处微调(clone→all_messages_clone/iter)、2 处需重写。
|
||||
- **状态**:✅ 已落地(2026-06-13,cargo check + vue-tsc 通过,待 tauri dev 实测)
|
||||
|
||||
### Token 计数方案:字符粗估零依赖 [任务 #43]
|
||||
|
||||
- **决策**:用 `chars_count × 0.35` 粗估单条消息 token 数(~2.8 字符/token),每条消息 +4 token 固定开销(role 标记),每个 tool_call +30 token(JSON 结构)。不引入 tiktoken-rs 或任何 tokenizer 依赖。
|
||||
- **原因/取舍**:用途是「发送前判断是否超限」做预算控制,误差 ±15% 完全可接受;tiktoken-rs 引入 BPE 数据文件约 5MB,对 Tauri 桌面应用打包不友好;provider 返回的 usage 可用于事后校准闭环暂不实施(原 calibrate 方法未接线,Review 时已删,留待未来按 provider usage 重做)。chars_ratio=0.35 对中英混合文本偏保守(纯英文 ~0.25,纯中文 ~0.5-0.7),宁可多算不少算。
|
||||
- **状态**:✅ context.rs 已落地(2026-06-13,校准闭环暂缓)
|
||||
|
||||
### 淘汰算法:分组滑动窗口 + 三元组保护 [任务 #43]
|
||||
|
||||
- **决策**:超预算时从最旧消息开始按「淘汰单元」丢弃。Standalone 消息单独成单元;`Assistant(tool_calls) + Tool(result)* + Assistant(final_text)` 工具调用三元组作为原子整体要么全保留要么全丢弃。最后 6 条消息(≈2 个完整用户轮次)设为保护区永不淘汰。
|
||||
- **原因/取舍**:工具调用三元组若拆散会导致 LLM 看到工具调用但找不到对应结果(或反之),产生幻觉重复调用。保护区防止丢失即时上下文。替代方案是直接按消息数截断(简单但破坏三元组),或按 token 截断到某位置(可能从三元组中间切断)。分组滑动窗口在安全性和信息保留间取平衡。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### 裁剪时机:build_for_request 时裁剪,push 不触发 [任务 #43]
|
||||
|
||||
- **决策**:token 预算检查和消息裁剪仅在 `build_for_request()` 构建请求时执行。`push()` 只做追加+计缓存,不做任何淘汰。
|
||||
- **原因/取舍**:Agentic Loop 一轮执行中会多次 push(assistant 回复 → tool_result → 可能再 assistant),这些属于当前活跃轮次的消息绝不能被中途裁剪掉。只在「即将发给 LLM」这个时间点评估并裁剪旧消息,语义清晰且安全。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### 持久化策略:裁剪仅影响发送视图,DB 存全量 [任务 #43]
|
||||
|
||||
- **决策**:`all_messages_clone()` 返回全量未裁剪消息用于 save_conversation 落库;`build_for_request()` 返回裁剪后版本发给 LLM。两者解耦。
|
||||
- **原因/取舍**:用户切换对话回来期望看到完整历史,不应因自动裁剪而永久丢失。裁剪是临时的「发送时压缩」类似 gzip。未来若要做永久摘要压缩(将旧对话提炼为 summary 消息插入),那是独立 feature 不影响此设计。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### 多工具调用并行化:join_all 无上限 [任务 #43]
|
||||
|
||||
- **决策**:`process_tool_calls` 中 Low 风险工具收集后用 `futures::future::join_all` 并行执行,Med/High 审批工具仍串行(需用户交互)。工具执行本身不限并发数(本地操作)。
|
||||
- **原因/取舍**:LLM 一次返回 N 个独立工具调用(如同时读 3 个文件)时,串行执行 = O(N×T),并行 = O(T)。N=3 时节省 ~600ms/轮。工具执行是本地 I/O(read_file/grep 等),无外部限流风险故不加 Semaphore。仅 LLM 调用受并发控制。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### LLM 并发控制:双层 Semaphore [任务 #43]
|
||||
|
||||
- **决策**:AppState 新增 `LlmConcurrency`(封装两个双层 `Arc<Mutex<Arc<Semaphore>>>`——全局并发默认 3 / 单对话默认 2)。permit 在 3 个叶子 LLM 调用点 acquire(`run_agentic_loop` 内 stream_llm 前、`generate_title_via_llm`、`extract_knowledge_from_conversation`),作用域结束自动释放;工具执行不受控。`LlmConcurrency: Clone`(两 Arc,cheap)作为参数串到底,spawn 函数收 by-value move、叶子收 `&ref`。acquire 顺序固定 global→per_conv,所有调用点一致防死锁。
|
||||
- **原因/取舍**:多对话场景下同时跑 2-3 个对话可能撞 provider RPM 限制导致 429。双层控制:全局防总并发失控,单对话防单对话独占(标题生成 + 主循环 + 提炼并发)。用 `Arc<Mutex<Arc<Semaphore>>>` 双层包装而非裸 `Arc<Semaphore>`——tokio Semaphore permits 构造时固定不可增减,替换内层 Arc 即重建,已持有旧 permit 不受影响。permit 放叶子调用点(最接近真实 HTTP 调用)而非 command 入口,限流粒度精准且不阻塞非 LLM 路径。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### per_conv 实为应用级单一信号量(非 per-conv map)[任务 #43 / Review 修正]
|
||||
|
||||
- **决策**:`LlmConcurrency.per_conv` 字段命名暗示「单对话」并发,但实现是应用级**单一** `Semaphore`(非 `HashMap<conv_id, Semaphore>`)。当前不修实现,仅在 `state.rs` 结构体 doc 标注真实语义 + 未来重构路径。
|
||||
- **原因/取舍**:`AiSession` 为 `Arc<Mutex<AiSession>>` 单例,且 `generating` 互斥保证同一时刻仅一个对话的 `run_agentic_loop` 在跑。故 per_conv 实际退化为「单对话内并发」(主循环 stream_llm + 标题生成 + 知识提炼三者受限流约束)——命名虽宽泛但当前语义恰好正确,非 bug。改 HashMap 是过度设计(单例会话下多 map 项永不被并发访问)。风险留待未来:若支持多对话并发 loop,per_conv 需随之改 `HashMap<conv_id, Semaphore>` 才名副其实。揭示 `LlmConcurrency` 与 `AiSession` 单例的隐式耦合——本次 review 最有价值的发现。
|
||||
- **状态**:✅ 注释留痕(2026-06-13 Review 修正,state.rs 结构体 doc 标注;实现未改,非 bug,多对话路线时重构)
|
||||
|
||||
### 裁剪策略与模型选择正交 [任务 #43 / 架构边界]
|
||||
|
||||
- **决策**:上下文窗口管理的 `ContextConfig` 不含 `mode`/模型选择字段。「高精度/低精度对话」(深度思考/reasoning_effort/模型选择)属于 LLM 调用层参数(`CompletionRequest` 层面),与裁剪策略(sliding_window / 未来 summarization)是**正交维度**,不混入 ContextManager。未来摘要压缩作为独立模块实现。
|
||||
- **原因/取舍**:避免把不同层面的控制拧到一个配置对象里——ContextConfig 只管「窗口多大、怎么裁」,模型/推理模式由调用方在构建 `CompletionRequest` 时决定。职责单一,后续扩展任一维度不影响另一侧。
|
||||
- **状态**:✅ 架构边界已划定(2026-06-13 讨论确认)
|
||||
|
||||
### save_conversation 异步化 [任务 #43]
|
||||
|
||||
- **决策**:循环结束时 spawn 异步落库,先释放 `generating=false` + emit `AiCompleted`,不阻塞前端收到完成事件。
|
||||
- **原因/取舍**:当前 save_conversation 同步执行(upsert + JSON 序列化),阻塞 Completed 事件几十~几百毫秒。落库失败不影响已完成的结果展示,异步化提升用户感知响应速度。代价是进程崩溃时最后一轮可能未落库(概率极低且下次启动可从 LLM provider 侧无法恢复 anyway)。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### build_for_request 只传 token 数值,不传 system_prompt 文本 [任务 #43 / Review 修正]
|
||||
|
||||
- **决策**:`build_for_request(sys_tokens: u32) -> (Vec<ChatMessage>, bool)` 只接收 system prompt 的预估 token 数,不接收 `&str` 文本。system_prompt 的 token 估算在调用方(`ai.rs`)完成。
|
||||
- **原因/取舍**:职责单一——ContextManager 负责裁剪和消息管理,不需要知道 system prompt 的文本内容。避免每次调用传递可能很长的字符串(含知识库+技能注入后可达几千字符),且 `build_for_request` 内部不混入估算逻辑。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### join_all 不对 tool_calls 做 sort,保留原始 index [任务 #43 / Review 修正]
|
||||
|
||||
- **决策**:收集 Low 风险工具时保留原始 `(index, draft)` 元组,不额外 `sort_unstable_by_key`。LLM 返回的 tool_calls 已按 index 有序,sort 是多余且可能打乱语义顺序。
|
||||
- **原因/取舍**:`futures::join_all` 保证结果顺序与输入一致,只要输入有序输出就有序。去掉 sort 减少一次 O(n log n) 且避免意外重排。回填 session.messages 时按原始 tool_call_id 匹配即可,不依赖数组位置。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### build_for_request 视图裁剪,不 mutate self.messages [任务 #43 / Review 修正]
|
||||
|
||||
- **决策**:`build_for_request(&self)` 改不可变借用,超预算时构造裁剪**视图**返回(`self.messages[trim_end..]` clone),不 `drain` 修改自身。删除原会 `self.messages.drain(0..trim_end)` 的 `trim_to_budget` 方法。
|
||||
- **原因/取舍**:原 mutate 实现违背「裁剪仅影响发送视图」契约——`drain` 后 `all_messages_clone()`(save_conversation 数据源)也返回裁剪版,长对话每轮丢历史、累积性数据丢失。视图裁剪每轮 build 重算 trim_end(O(n) 遍历淘汰单元,n 通常 <50 可忽略),换全量持久化不被破坏。
|
||||
- **状态**:✅ context.rs 已落地(2026-06-13 Review 修正,5 单测通过)
|
||||
|
||||
### AiSession.messages 类型替换为 ContextManager + 13 处接线 [任务 #43 / Part A 第二块]
|
||||
|
||||
- **决策**:`AiSession.messages` 从 `Vec<ChatMessage>` 直接替换为 `ContextManager`(类型替换,非 wrapper 包装层)。13 处操作点适配:push/clear/len/iter 签名天然兼容零改动;switch 对话改 `restore_from_messages(Vec)`;`replace_tool_result` 自由函数删除改走 `ContextManager::replace_tool_result_content` 方法(DRY 收敛,方法已含 token 重估);save_conversation 与 ensure_conversation_title 改 `all_messages_clone()` 取全量。
|
||||
- **原因/取舍**:类型替换而非 wrapper 层——消息真相源唯一,避免 Vec + ContextManager 双存导致状态分裂;ContextManager 方法签名刻意与原 Vec 操作对齐(push/clear/len/iter),13 处中 6 处零改动,改动面最小。核心改造点 run_agentic_loop 由 `session.messages.clone()` 全量塞入改为 `build_for_request(sys_tokens)`:调用方用 `TokenEstimator::estimate_text(&system_prompt)` 估算 system prompt token,ContextManager 只接收数值不接触 prompt 文本。
|
||||
- **状态**:✅ ai.rs 已落地(2026-06-13,cargo check 通过 + df-ai context 5 单测绿)
|
||||
|
||||
### 裁剪触发时不 emit 前端事件 [任务 #43 / Part A]
|
||||
|
||||
- **决策**:`build_for_request` 返回的 `trimmed: bool` 暂忽略(`let (history_msgs, _trimmed) = ...`),裁剪发生时不向前端 emit 任何事件。
|
||||
- **原因/取舍**:裁剪是无损优化——内存 `all_messages_clone` 与 DB 持久化均保留全量历史,仅发送给 LLM 的视图裁掉旧消息。计划原拟用 `AiError` 通知前端,但 AiError 语义是「错误」,裁剪不是错误会误导用户以为出错;前端无需感知裁剪(对 UX 透明)。未来若需「已压缩早期历史」提示条,新增专用事件(如 AiContextTrimmed)而非复用 AiError。
|
||||
- **状态**:✅ ai.rs 已落地(2026-06-13)
|
||||
|
||||
### B1 Low 风险工具 join_all 并行 + 串行回填 [任务 #43 / Part B]
|
||||
|
||||
- **决策**:`process_tool_calls` 重写——批量发 Started 后,Low 风险工具 `futures::future::join_all` 并行 execute(闭包内完成即 emit Completed/Error,不持 session 锁),`join_all` 返回后串行 push tool_result + audit(持锁)。Med/High 审批逻辑不变。
|
||||
- **原因/取舍**:原 for 循环逐个串行 execute,N 个独立 Low 工具 = N 倍等待;并行化总耗时 ≈ 最慢一个。execute + emit 放闭包内(完成即通知前端,体感逐个出结果),push/audit 必须持 session 锁故留 join_all 后串行。`join_all` 保序——结果顺序 = 输入顺序 = tc_list sort 后的原始 index 顺序,tool_result 回填不乱序。
|
||||
- **状态**:✅ ai.rs 已落地(2026-06-13)
|
||||
|
||||
### B1 附注:tool_result push 顺序变化无语义影响 [任务 #43 / Part B]
|
||||
|
||||
- **决策**:Med/High 占位 tool_result 先于 Low 结果 push(分类阶段先处理审批占位,Low 结果 join_all 后回填),与原「按 tc_list 交错顺序 push」不同。
|
||||
- **原因/取舍**:LLM 按 `tool_call_id` 关联 tool_result,不看消息绝对位置;连续 tool_result 都是 ContextManager 的 ToolResultTail、归同一三元组,顺序不影响语义。两阶段(占位先行 + 结果后填)比交错处理实现简单。
|
||||
- **状态**:✅ ai.rs 已落地(2026-06-13)
|
||||
|
||||
### B2+B3 正常完成:save/title/extract 打包后台 spawn [任务 #43 / Part B]
|
||||
|
||||
- **决策**:`run_agentic_loop` 正常完成段,把 save_conversation + maybe_spawn_extraction + ensure_conversation_title 三者打包进**同一** `tauri::async_runtime::spawn` 后台 task,主流程只做 `generating=false` + emit Completed。stop 路径(2 处)save 保持同步 await,title 改 `spawn_ensure_title` 后台。
|
||||
- **原因/取舍**:① 计划原拟裸 spawn save,但 `maybe_spawn_extraction` 明示「需在 save 之后(读已落库消息)」——裸 spawn save 与 extract 各自独立 task 顺序不保证,extract 可能读到旧 DB;打包同一 task 内 `save → extract → title` 串行 await 保顺序。② stop 路径 save 保持同步:stop 后用户可能立刻发新消息触发新 loop,两 loop 的 save 并发 upsert 会竞态(读旧值叠加丢 token);正常完成段同风险但频次低、最多丢少量 token 累加(非功能错误),可接受。③ `ensure_conversation_title` 签名从 `provider: &dyn LlmProvider` 改收 `provider_config: &AiProviderRecord`、内部自建 provider——`&dyn` 非 'static 无法 move 进 spawn,收 config 克隆进 task 后自建。
|
||||
- **状态**:✅ ai.rs 已落地(2026-06-13;并发场景待实测)
|
||||
|
||||
### Semaphore 重建采用「软收敛」策略 [任务 #43]
|
||||
|
||||
- **决策**:用户在 Settings 调整并发上限时,通过 `*semaphore = Arc::new(Semaphore::new(n))` 替换整个 Arc 内部值。已持有旧 permit 的任务不受影响,新请求走新限制。缩并发时实际并发 = 旧持有数 + 新上限(软收敛非硬切断)。
|
||||
- **原因/取舍**:tokio Semaphore 的 permits 数只能在构造时设定,运行时无法增减,这是标准限制。替代方案(如用 Mutex+计数器手动实现)复杂度高且易出错。「软收敛」行为可接受——用户调低并发后,进行中的请求不会被中断,只是新请求受控;待旧请求释放后新限制完全生效。UI 可提示「已有 N 个进行中请求」改善体验。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### protect_count=6 保护最近约 2 个用户轮次 [任务 #43]
|
||||
|
||||
- **决策**:淘汰算法保护最后 6 条消息不纳入淘汰单元。依据:每轮典型产生 User + Assistant[±tools] + Tool* ≈ 2~5 条,6 条 ≈ 覆盖 2 个完整轮次。
|
||||
- **原因/取舍**:固定数值简单可靠。不改为动态轮次检测(增加复杂度且轮次边界模糊——Assistant 纯文本 vs 带 tools 的 Assistant 消息算同一轮还是不同轮?)。6 是保守值,宁可多保几条也不要误裁活跃上下文。未来可根据实测调整。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### syncConcurrencyConfig 加 debounce 防快速连续 IPC [任务 #43]
|
||||
|
||||
- **决策**:Settings 页面修改并发数值后,通过 debounce(~300ms)延迟调用 `ai_set_concurrency_config` IPC,防止快速连续拖动滑块/按键时频繁重建 Semaphore。
|
||||
- **原因/取舍**:`@change` 在 input[type=number] 上只在失焦时触发已比 `@input` 好,但用户可能快速点「保存」或连续调整两个值。Semaphore 重建虽轻量(Arc::new)但不该无节制地做。手写简易 debounce(~5 行)即可,不需引入 lodash-es 依赖。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
### spawn 异步 save_conversation 加 warn 日志 [任务 #43]
|
||||
|
||||
- **决策**:`tauri::async_runtime::spawn(save_conversation_inner)` 内部用 `if let Err(e) = ... .await` 捕获错误并 `tracing::warn!` 记录,不静默吞掉。
|
||||
- **原因/取舍**:spawn 的 task 错误默认被 tokio 静默丢弃,调试时完全看不到落库失败。加一行 warn 零成本,出问题时能从日志定位。不影响用户体验(warn 不是 error)。
|
||||
- **状态**:✅ 已落地(2026-06-13)
|
||||
|
||||
---
|
||||
|
||||
## 二、AI Chat 工具卡片折叠 [Sprint 10 + 2026-06-13]
|
||||
|
||||
> 纯 UX 微调,全部归档。
|
||||
|
||||
### 双层折叠:卡片级 + 内容级分离
|
||||
|
||||
- **决策**:`expandedCards`(整卡 body 显隐)与 `expandedTools`(read_file 代码预览级)两套独立状态。
|
||||
- **原因**:卡片折叠和文件内容预览是两个维度,合并会互相干扰。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
### running/pending_approval 强制展开
|
||||
|
||||
- **决策**:执行中、待审批卡片不可折叠,始终展开。
|
||||
- **原因**:用户需看到骨架屏(执行中)和审批按钮(待操作)。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
### 新内容追加自动收起旧卡
|
||||
|
||||
- **决策**:deep watch `messages` + 轻量 JSON snapshot diff 检测新内容(新消息 / toolCall 状态变化 / 文本增长)→ 清除旧 completed/rejected 展开态,保留活跃卡。
|
||||
- **原因**:多步调用时旧结果折叠为单行 header,界面紧凑(类 ChatGPT/Cursor)。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
### 首次加载 / 切对话不触发收起
|
||||
|
||||
- **决策**:`isFirst` guard,首次 snapshot 赋值后直接 return。
|
||||
- **原因**:防切换对话或初次加载时把当前可见卡片误折叠。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
### 短结果保持展开(审查①)
|
||||
|
||||
- **决策**:`rejected`(拒绝原因)/ `write_file`(写入路径)短结果用 `shouldKeepOpen(tc)` 强制展开,仅 `read_file`/`list_directory`/通用 JSON 大体量结果折叠。
|
||||
- **原因**:短结果折叠无紧凑收益,反隐藏关键信息(拒绝原因/写入路径)。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
### 卡片宽度对齐气泡(审查②)
|
||||
|
||||
- **决策**:工具卡片 `max-width: 90%`,与 AI 文本气泡(`.ai-msg-bubble` max-width 90%)一致,不撑满 content 区。
|
||||
- **原因**:卡片原默认 stretch 撑满 100%、气泡 90%,视觉宽度不一致(list projects 等卡片比回复气泡宽一截)。选限制卡片对齐气泡(非撑满气泡),保留气泡式留白;数据卡片 90% content 宽通常够展示文件树/代码(横向 `overflow-x:auto` 兜底)。
|
||||
- **状态**:✅ 2026-06-13
|
||||
|
||||
### 折叠态 header 显示结果摘要 [2026-06-13]
|
||||
|
||||
- **决策**:completed 工具卡片在 header 的 `.ai-tool-sub` 槽位(原仅 running 显示「执行中...」)追加结果摘要 `toolResultSummary(tc)`,按工具返回结构提取一句话:list_tasks→「12 项任务」、list_projects→「5 个项目」、list_ideas→「8 条想法」、create_project→「已创建:xxx」、create_idea/task→「已创建:title」、update_project→「已更新 status」、delete_project→「已删除」、run_workflow→「请到工作流页面运行」。`.ai-tool-sub` 加 ellipsis 截断防长标题撑破。
|
||||
- **原因**:list_tasks/list_projects/list_ideas/run_workflow 等工具无路径参数(不像 read_file/list_directory header 带路径),折叠态 header 仅剩工具名,光秃秃、信息密度低。摘要放 header(非 body)使折叠态也能一眼看到核心信息,不必先展开。list_* 返回裸数组取 `.length`;create 取 `.name`(project)/`.title`(idea/task)——字段不对称是后端 model 既定,摘要层各自适配。
|
||||
- **状态**:✅ 2026-06-13
|
||||
|
||||
### 工具调用前 AI 总结文字去气泡 [2026-13](已回滚)
|
||||
|
||||
- **决策**:AI 消息同时含文字 content 和 toolCalls 时,文字气泡加 `.ai-msg-bubble--plain`(去背景/边框/padding/圆角/max-width)变紧凑纯文本紧贴卡片;纯文字消息(无工具卡片)保留完整气泡。→ **已回滚**:恢复所有 AI 消息统一气泡。
|
||||
- **原因**:原想条件去气泡兼顾紧凑与长回复可读性。→ 回滚原因:用户实测后强调「所有 AI 消息都应在气泡里」,条件去气泡造成两种 AI 消息外观(纯文字有气泡、工具总结无气泡)不一致、突兀。**紧凑不该靠去气泡实现**——牺牲一致性换紧凑得不偿失。未来若要紧凑走「气泡与卡片视觉一体」(卡片纳入气泡 / 连一体块 / 仅压间距)。
|
||||
- **状态**:✅ 2026-06-13 落地 → 🔄 2026-06-13 回滚(一致性优先,布局方案待用户再定)
|
||||
|
||||
### 工具卡片区拆子组件:ToolCard + ToolCardList [2026-06-13]
|
||||
|
||||
- **决策**:工具卡片区从 AiChat.vue(原 1983 行)拆为两个子组件——`ToolCard.vue`(单卡纯展示,props: `tc`/`isExpanded`/`isContentExpanded`;emits: `toggle`/`expand-content`/`approve`)+ `ToolCardList.vue`(列表容器,持有折叠态 `expandedCards`/`expandedTools`,`defineExpose` 出 `collapseInactive`)。AiChat.vue 仅 `import` + `ref` 持有列表 + 转发 `@approve`→`store.approveToolCall`。
|
||||
- **原因/取舍**:工具卡片占 AiChat.vue ~650 行(模板 107 + script 150 + CSS 392),是高修改频率区(折叠/摘要/时间轴等 UI 调整频发)。拆出后卡片相关变更隔离在两文件,1983 行主文件不再因 UI 调整反复动。auto-collapse 桥接选 `expose`+`ref`(方案 B)而非 prop+emit / provide+inject——前者是 Vue3 标准模式、子组件自治折叠态、父级只调一个 `collapseInactive(activeIds)`。附带:ToolCard 内用 `parsed` computed 缓存 `parseResult`(原模板 6 次重复 `JSON.parse`,每次 patch 都重解析);气泡与卡片的 `max-width:90%` 提取为全局 `--df-msg-max-width` 变量(原跨文件靠注释维系对齐,改一处忘一处即错位,变量化单一来源)。**深化(可维护性)**:ToolCard.vue 再拆双 `<script>` 块——14 纯函数 + `ToolResult` 类型提模块顶层(只建一次,`setup` 聚焦 props/parsed),`parseResult` `any`→`ToolResult|null`(防模板字段名打错编译期不报),`toolDisplayName` 去 i18n fallback(path 几乎总有,属过度防御)。取舍:为可维护性/易迭代,不为微性能(省闭包可忽略)。
|
||||
- **状态**:✅ 2026-06-13 落地(重构 Step 1-5 完成,AiChat.vue 净减 ~650 行)
|
||||
|
||||
---
|
||||
|
||||
## 三、AI Chat Token 用量与模型记录 [2026-06-13]
|
||||
|
||||
> 主文档保留一句设计结论:**Token 分账本——对话 / 工作流节点 / 标题生成三处独立记账**。其余实现细节归档。
|
||||
|
||||
### 流式 token 落库走累加模式(非覆盖)
|
||||
|
||||
> 已移入 [经验记录.md](./经验记录.md)「约定」分组。
|
||||
|
||||
### Anthropic 流式 output_tokens 当累计值直接覆盖
|
||||
|
||||
> 已移入 [经验记录.md](./经验记录.md)「约定」分组。
|
||||
|
||||
### Token 展示默认关闭 + Settings 开关 [2026-06-13]
|
||||
|
||||
- **决策**:AI 气泡底部 token 条默认不显示,加 `df-show-token-usage` 开关放 Settings 通用设置区,默认 `false`。
|
||||
- **原因**:token 是计量信息非核心;用户对「头顶信号」敏感度不一,日常默认隐藏减干扰,需要时开。开关关闭时 store 不写 `lastTokenUsage`、DOM 不渲染,零视觉干扰。
|
||||
- **状态**:✅ 2026-06-13
|
||||
|
||||
### 前端 token 双状态(单次 + 对话累计)[2026-06-13]
|
||||
|
||||
- **决策**:store 维护 `lastTokenUsage`(最近一条回复)+ `convTokenTotal`(当前对话累计)两个状态,而非单一状态。
|
||||
- **原因**:单状态切对话会错乱——实时值(当前回复产生的 token)vs 历史总值(切回老对话应从 DB 读累计)混一起。双状态各司其职:切换时 `convTokenTotal` 从 summary 加载、`lastTokenUsage` 清空。
|
||||
- **状态**:✅ 2026-06-13
|
||||
|
||||
### model 记录策略:取配置值 + 补填不覆盖 [2026-06-13]
|
||||
|
||||
- **决策**:记录的 model 取 `provider_config.default_model`(非 stream 响应解析);`save_conversation` insert 时写、update 时仅 `rec.model` 为 None 才补填。
|
||||
- **原因**:① stream chunk 未必带 model 字段;`default_model` 是确定配置值且即请求所用(`request.model` 就用它发),配置名比响应里的具体版本号(如 `gpt-4o-2024-xx`)对用户更有意义。② model 字段 DB 早存在但老代码 insert 一直填 None,补填兼容历史对话(下次活动自动回填),已有值不覆盖。
|
||||
- **状态**:✅ 2026-06-13
|
||||
|
||||
### token 分账本:对话 / 工作流节点 / 标题生成 [2026-06-13]
|
||||
|
||||
> 此为设计结论,**已在主文档保留一句**:三处分属不同成本中心,混进 `ai_conversations` 会让对话 token 失真。
|
||||
- **决策**:① 对话主流程 token(`run_agentic_loop` 流式)落 `ai_conversations`;② 工作流 AI 节点 token 进 `NodeOutput.usage`(写 `node_executions.output_json`),不进对话表;③ 标题生成 token 刻意不记(`generate_title_via_llm` 的 `resp.usage` 丢弃)。
|
||||
- **原因**:三处分属不同成本中心——工作流节点消耗属工作流执行成本,与对话 token 是两个账本,混进 `ai_conversations` 会让对话 token 失真;标题生成是系统附带行为(max 30 token,量小),计入对话总量会让用户困惑(「只问一句怎么这么多 token」)。非流式 `complete()` 的 token Provider 层已返回,仅消费侧按账本分流。
|
||||
- **状态**:✅ 2026-06-13
|
||||
|
||||
### model UI 展示暂缓,留后续升级 [2026-06-13]
|
||||
|
||||
- **决策**:model 已落库(`ai_conversations.model` + 前端 `AiConversationSummary.model` 字段就绪),但**暂不在对话旁展示**,留作后续升级项。
|
||||
- **原因**:数据通路已打通(后端写值、前端字段在),展示是纯前端增量、随时可加;当前先把 token 用量展示做稳,model 展示留到后续 UI 升级(如对话头部 / token 条旁)统一考虑,避免零散加。
|
||||
- **状态**:📐 待实施(UI 展示,数据已就绪)
|
||||
|
||||
### model 追溯:消息级 + 对话级多值(C+B)[2026-06-13]
|
||||
|
||||
- **决策**:消息级 `ChatMessage` 加 `model` 字段(每条 assistant 消息记生成它的 model)+ 对话级加 `models` 字段(JSON 数组,去重存对话用过的所有 model)。`model` 单值字段保留存首个(兼容已就绪的前端字段 + 补填不覆盖策略不动)。
|
||||
- **演进**:[2026-06-13 初版 📋] 现状为对话级单值 + 补填不覆盖,中途切换只留首个、消息级无字段无法追溯 → [2026-06-13 同日落地] 用户拍板 C+B。A(对话级覆盖记最近)未选——中途历史仍丢;纯 C 不便快速看「用过哪些」,加 B(对话级聚合)互补。
|
||||
- **原因**:「每一条对话都能有效记录」需消息级追溯(每条 assistant 消息知道谁生成);对话级 models 聚合便于不解析整条 messages 就知用过哪些(中途切换场景全覆盖)。`model` 单值保留是渐进兼容(前端 `AiConversationSummary.model` 已就绪,展示首个),不强删避免迁移破坏。
|
||||
- **落地细节**:① `ChatMessage.model: Option<String>`(serde default 兼容历史消息);② `run_agentic_loop` 构造 assistant 消息时 `msg.model = Some(provider_config.default_model)`;③ `save_conversation` update 路径 models 去重追加(读旧 JSON 数组 + 新值去重)、insert 初始化 `[model]`;④ V6 迁移加 `models` 列;⑤ 前端 `AiMessage.model?` + `Summary.models?`,switchConversation 解析历史消息 model。展示仍暂缓(见上条)。
|
||||
- **记录时机澄清 → [2026-06-13]**:models 只追加**实际生成过内容**(本轮 `stream_llm` 跑过、产生 assistant 消息)的 save 调用,不记「配置但未实际用于生成」的 model。需求来自用户澄清:「对话集应该不是切换过都留住吧,只有实际发送过消息的才留」——首轮即 stop(无 stream)路径传 `Some(model)` 会把未生成的 model 误写进 models 数组。正确做法:`run_agentic_loop` 入口 `stop_flag` 路径(未 stream)save 传 `None`;仅 stream 后的退出路径(正常完成/stream 后 stop/审批暂停)传 `Some(model)`。
|
||||
- **状态**:✅ 2026-06-13(编译/类型双过,未 tauri dev 实测) | ✅ 记录时机已落地(2026-06-13):入口 stop 路径 save 改传 `None`,cargo check + vue-tsc 双过
|
||||
|
||||
---
|
||||
|
||||
## 四、AI Chat 对话与滚动 UX
|
||||
|
||||
> 滚动/错误友好化/空白屏修复/markdown 缓存为 UX 微调,归档。审批「删全屏 Modal 保行内」为设计决策,**已留在主文档**。
|
||||
|
||||
### 智能滚动:`isNearBottom` < 80px
|
||||
|
||||
- **决策**:仅当用户在底部 80px 内才自动滚动;上滑时不强制拉回。
|
||||
- **原因**:用户回看历史时被强制拉回底部体验差。
|
||||
- **状态**:✅ Sprint 8
|
||||
- **演进** [2026-06-13]:上滑看历史时新消息/流式不滚 → 补「回到底部」浮动按钮。`showBackToBottom`(内容溢出 >100px 且不在底时显)+ `onMessagesScroll` 刷新 + `onContentChange`(在底则滚、上滑则只刷按钮不强制拉回)。沿用「不打断回看」原则,补一键回底入口。
|
||||
|
||||
### 错误友好化:`friendlyError` 正则映射
|
||||
|
||||
- **决策**:404/401/timeout/network 正则映射为中文提示 + `isError` 红色气泡。
|
||||
- **原因**:原始错误信息对用户不友好。
|
||||
- **状态**:✅ Sprint 8
|
||||
|
||||
### 审批卡片显示参数内容 + 防双击 [2026-06-13]
|
||||
|
||||
- **决策**:审批卡片(pending_approval)在按钮上方渲染工具参数键值对(`toolArgsEntries` 遍历 args,`formatArgValue` 截断超长)+ 风险提示(reason);`AiToolCallInfo` 加 `reason?` 字段,AiApprovalRequired 回填。点击批准/拒绝后 store 乐观置 `status='running'` + 按钮 `:disabled="status!=='pending_approval'"`,等后端事件转 completed/rejected,IPC 失败回滚 pending_approval。
|
||||
- **原因/取舍**:原审批卡片只有「批准/拒绝」按钮 + 工具名(update_project/run_workflow),用户**盲批**——参数不可见等于放行未知操作。双击无防护则连发 IPC(后端 `pending_approvals.remove()` 有幂等不重复执行,但第二次报错弹窗)。乐观置 running 即时禁用按钮 + 回显参数,审批从「盲批」变「知情决策」。
|
||||
- **状态**:✅ 2026-06-13
|
||||
|
||||
### 空白屏修复:防 FOUC
|
||||
|
||||
- **决策**:`index.html` 加主题防闪烁脚本(渲染前置 `data-theme`)+ `__APP_T0` 启动埋点 + body 背景色。
|
||||
- **原因**:Vite 启动慢(122s)期间 webview 白屏;前置主题脚本消除 FOUC。
|
||||
- **状态**:✅ Sprint 7
|
||||
|
||||
### 流式 markdown 渲染加结果缓存 [2026-06-13]
|
||||
|
||||
- **决策**:`renderMd` 加 `Map<text, html>` 缓存(限 200 项超出清空);`mdReady` 翻转(marked+DOMPurify 加载完成)时清缓存重渲。
|
||||
- **原因/取舍**:`v-html="renderMd(...)"` 每次响应式触发(折叠/状态变化/新 delta)都跑 marked.parse + DOMPurify.sanitize 全文。历史消息文本不变却重复全量解析(O(n) × 触发次数)。缓存后历史消息命中 O(1);流式 currentText 中间态各缓存一项(≤200 后清)。**未做**「流式中降级纯文本」——会让流式无格式、完成后突变格式,体验差;缓存方案保留流式格式且消除重复解析。
|
||||
- **状态**:✅ 2026-06-13
|
||||
|
||||
---
|
||||
|
||||
## 五、AI Chat 对话管理 [Sprint 10]
|
||||
|
||||
### 侧边栏时间分组:日历日桶(today/yesterday/earlier)
|
||||
|
||||
- **决策**:活跃对话按日历日分桶(今天/昨天/更早),归档对话单列可折叠区,而非滚动 24h 或纯时间倒序线。
|
||||
- **原因**:用户心智「今天的对话」指当天而非过去 24 小时;归档是冷区,单列折叠减少对活跃区的视觉干扰。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
### 归档分组默认折叠 + 持久化
|
||||
|
||||
- **决策**:`archivedCollapsed` 默认 `true`(折叠),折叠态写入 `df-ai-ui` 持久化。
|
||||
- **原因**:归档为冷区,默认折叠让活跃对话优先可见;持久化记住用户展开偏好,不每次重启都收回。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
### 归档绕过 update_field(set_archived 走原始 SQL)
|
||||
|
||||
- **决策**:归档用专用 `set_archived` 方法直接 `UPDATE ai_conversations SET archived = ?1`,不复用 `update_field` 宏。
|
||||
- **原因**:`update_field` 对任何字段操作都强制连带 `SET updated_at = now`,归档是元数据切换不应改对话时间——否则归档后侧栏时间全跳成「刚刚」,破坏时间分组排序。归档与内容更新是两类操作,时间戳语义须区分。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
### 恢复活跃对话加守卫 `!state.activeConversationId`
|
||||
|
||||
- **决策**:`loadConversations` 自动恢复上次活跃对话时,加守卫(仅首次加载、ID 仍有效、当前无活跃对话才恢复)。
|
||||
- **原因**:若用户已手动 new/switch,恢复逻辑不应覆盖其当前意图;守卫保证恢复只在「冷启动无状态」时介入。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
---
|
||||
|
||||
## 六、AI Node(工作流节点)[任务 #44]
|
||||
|
||||
### AI Node 参数解析抽纯函数,单测不 mock LLM [任务 #44]
|
||||
|
||||
- **决策**:`AiNode::execute` 内的参数解析(config + 上游 inputs → `AiNodeParams`)剥离成独立纯函数 `parse_params(config, inputs)`,execute 调用后再 `build_provider` + `complete`。`AiNodeParams` 加 `#[derive(Debug)]`(unwrap_err 断言需要)。
|
||||
- **原因/取舍**:execute 原本 162 行硬编码 `build_provider`,直接单测会触真 HTTP——需 mock `LlmProvider` trait + 构造完整 `NodeContext`(event_bus/node_status/NodeId),重且脆弱。抽纯函数只接 `config` + `inputs`(execute 实际用到的),参数解析/缺参报错/prompt 上游优先回退/默认值(model 空→`gpt-4o-mini`、protocol→`openai_compat`)全可单测,零 mock 零网络。`complete` 调用正确性归 df-ai provider 层测试(职责分层)。副作用:execute 瘦身,parse 与 LLM 调用分离,可读性提升。
|
||||
- **状态**:✅ 已落地(2026-06-13,7 纯函数单测全过:缺参×3 / prompt 取值×2 / 默认值 / 显式参数;GLM 真调集成测试 `glm_live_complete`(`#[ignore]`+env var)通过:glm-4-flash 返回「通过」/usage 正确;GUI DAG 触发实测转 → #54 跟踪)
|
||||
|
||||
### AI Node 参数边界值契约:空 model 走 provider 兜底 / prompt 双源 [任务 #44]
|
||||
|
||||
- **决策**:config 无 model 时 `parse_params` 给空 `model` + `default_model="gpt-4o-mini"` 占位字段;`build_provider` 用 `default_model`(构造需非空),`CompletionRequest.model` 忠实传空串,由 provider `convert_request`(`if req.model.is_empty() { self.default_model }`)兜底。schema `required` 仅 `["base_url","api_key"]`,不含 `prompt`(prompt 双源:上游 `inputs["prompt"]` > `config.prompt`)。
|
||||
- **原因/取舍**:① `model`/`default_model` 双字段分离「忠实值」与「构造兜底」——provider 构造要非空 model(空则后续无兜底锚点),故 default_model 占位防 panic;但 request 仍传真实 model(空)让 provider 兜底单点收敛(两 provider convert_request 统一 `is_empty→default_model`),避免 AiNode 自猜默认与 provider 不一致。② prompt 双源支持「prompt 由上游节点产出」(如 read_file→ai 分析),schema required 含 prompt 会误拦此合法配置。
|
||||
- **状态**:✅ 已落地(2026-06-13,两 provider convert_request 兜底经 review 核实自洽;schema required 改后 7 单测全过)
|
||||
|
||||
---
|
||||
|
||||
## 七、代码审查甄别(2026-06-13 一次性审查流水)
|
||||
|
||||
> 全模块代码审查后的甄别落地。原则性结论(「真实 bug 修、简单清理做、规模不到位的优化先不动」)**已在主文档「需求澄清」保留**。以下是具体落地条目。
|
||||
|
||||
### df-evolve 骨架物理删除(连带三件套)
|
||||
|
||||
- **决策**:删 `KnowledgeStore` + `PatternExtractor` + `EvolveEngine` 三件全 TODO 空壳;同步删 df-nodes 5 个空节点(docker/git/http/notify/subflow)。
|
||||
- **原因/取舍**:KnowledgeStore 被 EvolveEngine 唯一引用、PatternExtractor 同理——删前两者必连带删 EvolveEngine(编译依赖),原「删 2 个」甄别后扩为三件套。src-tauri Cargo.toml 声明依赖 df-evolve 但源码零 `use df_evolve`,整坨孤立骨架。`Knowledge`/`PromptTemplate`/`ReviewRule` 领域类型保留(有实现 + 测试,Tier 1 Service 复用)。
|
||||
- **状态**:✅ 2026-06-13(cargo check 通过)
|
||||
|
||||
### 列表查询加过滤(内存策略)
|
||||
|
||||
- **决策**:`ai_conversation_list` 加 limit(默认 50)+ include_archived(默认 false);AI 工具 `list_projects`/`list_tasks`/`list_ideas` 闭包加 `truncate(50)`;`build_system_prompt` 项目注入 `take(20)`;`list_ideas` 命令加 status 可选过滤。均用内存 filter/take 或现成 `query`,不加新 SQL 方法。
|
||||
- **原因/取舍**:dev 阶段数据量小,内存过滤零触碰通用 `impl_repo!` 宏(改宏风险高)。数据真到成千上万再加 Repo 的 `list_limited` SQL 方法。`list_ideas` 复用现成 `query("status", v)` 走白名单,零改 Repo。前端 API 加可选参数,旧无参调用兼容(后端默认值兜底)。
|
||||
- **状态**:✅ 2026-06-13
|
||||
|
||||
### node_executions「全表 list」甄别为伪命题
|
||||
|
||||
- **决策**:审查提出的「node_executions 全表 list 改按 execution_id 过滤」**不成立**——全仓仅 state.rs 注册 NodeExecutionRepo,无任何 list/query 命令对外暴露(只写不读,cargo `dead_code` warning 印证)。
|
||||
- **原因**:不存在「废弃全表 list」的对象。若未来前端要看某次工作流执行的节点明细,需**新增** `list_node_executions(execution_id)` 命令,属新功能非清理。
|
||||
- **状态**:📐 待需求驱动新增
|
||||
|
||||
### ALLOWED_COLUMNS 从全局共享演进为按表隔离
|
||||
|
||||
> 已移入 [经验记录.md](./经验记录.md)「约定」分组。
|
||||
|
||||
---
|
||||
|
||||
## 八、状态持久化(UX 偏好部分)
|
||||
|
||||
### UI 布局:localStorage + 模块级恢复
|
||||
|
||||
- **决策**:面板布局(panelOpen/maximized/sidebarOpen/archivedCollapsed)写 `df-ai-ui`;`restoreUiState()` 放模块顶层(state 单例定义后)执行一次,而非 `useAiStore()` 内部。
|
||||
- **原因**:`state` 是模块级单例,`useAiStore()` 被 App.vue/AiChat.vue/AiDetached.vue 多处调用——放内部会重复执行恢复、覆盖用户当次操作。模块级只跑一次才对。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
> 注:窗口位置/大小用 tauri-plugin-window-state、detached/docked 不持久化两条**已在主文档保留**(属设计决策)。
|
||||
|
||||
---
|
||||
|
||||
## 九、i18n(实现细节)
|
||||
|
||||
### i18n 模块必须命名空间化导出
|
||||
|
||||
> 已移入 [经验记录.md](./经验记录.md)「踩坑」分组。
|
||||
|
||||
### 状态枚举 i18n:constants 存 key,view 包 $t
|
||||
|
||||
- **决策**:`constants/project.ts` 的 `PROJECT_STATUS_LABELS`/`TASK_STATUS_LABELS` 值从中文文案改存 i18n key(`planning: 'projects.status.planning'`);`projectStatusLabel`/`taskStatusLabel` 返回 key;view 显示处包 `$t(projectStatusLabel(x))`。`PRIORITY_LABELS`(P0/P1)是代号非文案,不动。
|
||||
- **原因**:constants 是纯数据/结构层,不应含展示文案(违反分层);文案归 locale,constants 管映射结构。
|
||||
- **状态**:✅ 已落地
|
||||
|
||||
---
|
||||
|
||||
**相关文档**:
|
||||
- [功能决策记录](./功能决策记录.md) — 需求规格 + 设计决策规格(当前真相源)
|
||||
- [经验记录](./经验记录.md) — 踩坑/约定/技巧/bug 排查教训
|
||||
472
docs/02-架构设计/功能决策记录.md
Normal file
472
docs/02-架构设计/功能决策记录.md
Normal file
@@ -0,0 +1,472 @@
|
||||
# 功能决策记录
|
||||
|
||||
> 日常开发中对各功能做的**需求规格 + 设计决策规格**(功能粒度,补 [Phase 1 架构决策](./Phase1架构决策.md) 之下的实现层选择)。聚焦「要做什么 / 为什么这么定」,便于日后回溯。
|
||||
>
|
||||
> 创建:2026-06-12 | 范围:Sprint 5–10 | 维护:随开发追加
|
||||
|
||||
## 约定
|
||||
|
||||
**判断标准**:3 个月后回看,这条是否仍影响对系统/功能设计的理解?是 → 留本文档;否 → 分流。
|
||||
|
||||
**本文档只记两类**:
|
||||
- **设计决策规格**(✅ 已落地 / 🚧 待实测 / 📐 设计未实施)——「为什么这么定」。三要素:决策 → 原因/取舍 → 状态。来源标 `[Sprint N]` 或 `[日期]`。
|
||||
- **需求规格 / 待办**(📋)——「要做什么 / 为什么需要」。与 PROGRESS 流水区分:这里记「要做什么 / 为什么需要」,PROGRESS 记「做了啥」。
|
||||
|
||||
**经验性内容**(踩坑 / 约定 / 技巧 / bug 排查教训)→ [经验记录.md](./经验记录.md)。
|
||||
**老条 / 纯流水 / UX 微调 / 已被取代** → [功能决策记录-归档.md](./功能决策记录-归档.md)。
|
||||
|
||||
记录规则见 [文档记录规范](./文档记录规范.md)。
|
||||
|
||||
## 人机协同设计基准
|
||||
|
||||
### AI coding 下的「过度设计」判断基准 [2026-06-13]
|
||||
- **决策**:项目为一人开发 + 全程 AI coding(人定方向/审查,AI 实现),代码被 AI 反复读写。权衡设计时采用新基准——**AI 反复读写的代码,「结构清晰」和「隐性耦合显式化」权重高于传统判断**;但「过度抽象」(多层 trait/Builder/工厂)仍不做,因 AI 读简单直白代码 > 读层层抽象。
|
||||
- **原因/取舍**:① AI 每次理解代码靠注释和结构(比人更依赖),1600 行单文件(如 AiChat.vue)消耗大量 context,拆分反而降低 AI 理解成本;② 隐性依赖(如分离窗口 localStorage 跨 webview)人凭经验避开、AI 易踩坑。故:组件拆分从「不做」升为「值得做」;隐性耦合早修或显式标注。③ 规模没到的优化(list_all LIMIT、白名单拆表)仍按数据量客观判断,不因 AI coding 而变。
|
||||
- **状态**:📐 基准原则(指导后续取舍)
|
||||
|
||||
## AI Chat 可靠性
|
||||
|
||||
### 流式可靠性三重保险 [Sprint 6 + 2026-06-13]
|
||||
- **决策**:① reqwest Client 加 `connect_timeout(30s)`,**不设请求总 timeout**;② `stream_llm` 用 `tokio::time::timeout(120s)` 包**单个** `stream.next()`(idle 间隔),不包整个流;③ 前端 stores/ai.ts 加 streaming watchdog——发送启动 60s 计时,收到 AiTextDelta/工具事件/审批结果重置,AiApprovalRequired 暂停(审批等待不计),AiCompleted/AiError 清除;60s 无活跃事件则置 streaming=false + push 错误「响应中断」。
|
||||
- **原因/取舍**:连接阶段防无限 hang;总 timeout 会误砍流式长生成任务(流式可持续数分钟)。idle 120s 防「连上后中途静默」无限 hang;只卡单 chunk 间隔,不卡总时长。前端 60s 独立计时(短于后端 120s)双保险——后端任何路径漏发收尾(崩溃/事件丢失/agent loop 异常退出)前端永久 streaming=true 卡死。审批等待暂停(不计超时)避免误判用户思考。
|
||||
- **边界**:审批组件未渲染(见「需求与待办·审批可见性缺口」)时,AiApprovalRequired 暂停后永久卡,watchdog 救不了,需审批可见性兜底。
|
||||
- **状态**:✅ Sprint 6 + 2026-06-13
|
||||
|
||||
### 断连丢弃残缺响应
|
||||
- **决策**:维护 `finished_received` 标志;流尽未收到 finished 信号 → emit AiError 并**丢弃残缺响应,不当完整入库**。
|
||||
- **原因**:防脏历史污染对话记录(半截回复入库后无法续接)。
|
||||
- **状态**:✅ Sprint 6
|
||||
|
||||
### 停止生成:保留已生成文本
|
||||
- **决策**:`AiSession.stop_flag: Arc<AtomicBool>` + 多检查点响应;停止后**保留已生成文本**。
|
||||
- **原因**:用户主动停止 ≠ 丢弃成果,已输出内容有价值。idle(无输出)时最多等 120s(P2 可用 `tokio::sync::Notify` 优化为绝对即时)。
|
||||
- **状态**:✅ Sprint 6(运行时待实测)
|
||||
|
||||
### 切对话:从「拒绝切换」演进到「不中断路由」
|
||||
- **决策**:Sprint 6 生成中**拒绝切换**(防 `active_conversation_id` 被改致旧 loop 串台写库)→ Sprint 8 改为**后台对话按 `conversation_id` 路由,生成中可切换不打断**。
|
||||
- **原因**:拒绝切换体验差;后端给所有 event 加 `conversation_id` + spawn 前快照 conv_id + 前端按 id 路由(后台对话事件不污染当前视图),既不串台又不打断。
|
||||
- **状态**:✅ Sprint 8(部分场景待实测)
|
||||
|
||||
## AI Chat 工具调用与审批
|
||||
|
||||
### Agentic Loop 最多 10 轮
|
||||
- **决策**:`run_agentic_loop` 上限 10 轮(`MAX_AGENT_ITERATIONS`)。
|
||||
- **原因**:防失控循环;单链 ReAct 10 轮覆盖绝大多数任务。超出需规划式(B 路线)。
|
||||
- **状态**:✅ Sprint 5
|
||||
|
||||
### 风险门控:Low 自动 / Medium+High 审批
|
||||
- **决策**:工具按 `RiskLevel` 分级,Low 自动执行,Medium/High 暂停等人工审批。
|
||||
- **原因**:读操作放行,写/删操作把关——可靠性 vs 效率的平衡点。
|
||||
- **状态**:✅ Sprint 5
|
||||
|
||||
### `tool_calls` 按 index 排序
|
||||
- **决策**:assistant 消息与 tool_result 两处均按 `index` 排序。
|
||||
- **原因**:消除 HashMap 迭代乱序致多工具结果错位。
|
||||
- **状态**:✅ Sprint 6
|
||||
|
||||
### 路径校验:拒 `..` 遍历 + 扩敏感目录
|
||||
- **决策**:正斜杠→反斜杠规范化 + 拒 `..` 路径遍历 + 扩 `.aws`/`.gnupg`。
|
||||
- **原因**:最小加固防越权读写;根治级(workspace 白名单 + canonicalize)待边界明确后再做。
|
||||
- **状态**:✅ Sprint 6(边界加固待续)
|
||||
|
||||
### list_directory 递归防爆:噪音目录剪枝 + 条目上限 + skip_noise_dirs 开关 [2026-06-14]
|
||||
- **决策**:`list_dir_recursive` 递归时跳过噪音目录(`.git`/`node_modules`/`target`/`dist`/`build`/`.next`/`.cache`/`__pycache__`/`.venv`/`venv`/`.idea`)——**列出但不深入内部**;硬上限 1000 条 + `truncated` 标志;默认 `max_depth` 3→2;加 `skip_noise_dirs` 参数(默认 `true`)。
|
||||
- **原因**:AI 广扫项目根传 `recursive:true` 时,`.git`/`node_modules`/`target` 铺平致 13782 项塞进对话 message(UI 卡 + token 爆)。剪枝防爆炸,但保留访问能力:① 噪音目录仍列出(看得见存在 + 大小);② 想看内部时 `list_directory` 直接指向该目录(depth=0 起算),或传 `skip_noise_dirs:false` 强制递归进去(仍受 1000 上限 + truncated 保护,适合看编译产物 dist / 运行结果 target 做比对)。1000 上限 + truncated 让"想全扫"退化为"分层定点查",不丢信息。
|
||||
- **状态**:✅ 2026-06-14 落地(cargo check 通过)
|
||||
|
||||
### `max_tokens` 8192 + `length` 算 finished
|
||||
- **决策**:max_tokens 4096→8192;`finish_reason="length"`(截断)纳入 finished。
|
||||
- **原因**:大任务输出撞 4096 上限被误判断连、丢弃整段响应;8192 贴合实际,截断视为正常完成。
|
||||
- **状态**:✅ Sprint 6
|
||||
|
||||
### 审批:删全屏 Modal 保留行内卡片 [Sprint 8]
|
||||
- **决策**:移除全屏 Tool Approval Modal,保留工具卡片内联审批按钮。
|
||||
- **原因**:全屏 Modal 打断对话流,行内审批更轻量。
|
||||
- **状态**:✅ Sprint 8
|
||||
|
||||
## AI Chat Provider 协议
|
||||
|
||||
### 按 `provider_type` 路由 OpenAI / Anthropic
|
||||
- **决策**:新增 `anthropic_compat.rs` 实现 Anthropic Messages API,按 `provider_type` 分发到 OpenAICompat 或 AnthropicCompat;分发处用 `Box<dyn LlmProvider>` trait object。
|
||||
- **原因**:GLM 等订阅端点走 Anthropic 协议(`x-api-key` + 顶层 `system` + 必填 `max_tokens` + SSE content_block);统一 Provider trait 屏蔽差异,上层 Agentic Loop / AiNode 零改动(不感知协议)。
|
||||
- **状态**:✅ Sprint 8(用户实测对话流式 OK)
|
||||
|
||||
### 端点 URL 三段智能拼接
|
||||
- **决策**:`messages_url()` 按 base_url 末段判断——已含 `/v1/messages` 直用;以 `/v1` 结尾补 `/messages`;仅域名(如 `…/api/anthropic`、`api.anthropic.com`)补 `/v1/messages`。
|
||||
- **原因**:GLM 订阅端点 `open.bigmodel.cn/api/anthropic` 与 Claude 官方 `api.anthropic.com` 约定不同(前者无 `/v1`,后者需补);不强制用户填全路径,降低配置门槛。GLM 端点已实测。
|
||||
- **状态**:✅ Sprint 8
|
||||
|
||||
### `max_tokens` 必填兜底 4096 / tool_result 连续合并为一条 user
|
||||
- **决策**:① Anthropic 协议 `max_tokens` 必填(协议无默认),`DEFAULT_MAX_TOKENS=4096` 兜底(OpenAI 协议 max_tokens 可选,缺则报错);② 连续多条 `role=Tool`(tool_result)累积,遇非 Tool 消息 flush 为单条 user 消息含多个 `tool_result` 块。
|
||||
- **原因**:① 统一兜底避免上层每个调用点都要传值(与 OpenAI Provider 的 8192 上限独立,此处仅缺省兜底)。② Anthropic 要求 tool_result 必须在 user 角色内;多工具并发结果合并为一条 user 而非一对一,贴合协议「一回合一组结果」语义,减少消息碎片。
|
||||
- **状态**:✅ Sprint 8
|
||||
|
||||
### 默认标识:is_default 落库为真相源 [Sprint 10 → 2026-06-13]
|
||||
- **决策**:`ai_set_provider` 互斥写 DB(目标 `is_default=true`、其余 `false`,仅写变化记录);`ai_save_provider` 新建时若全表尚无默认则自动设为默认(首个);`ai_list_providers` 直接返 DB 值。`session.active_provider_id` 降为运行时缓存,由 `set_provider` 同步,重启清零不影响——`get_active_provider` 兜底取 DB `is_default`。
|
||||
- **原因/取舍**:Sprint 10 原 active 作真相源 → 致「重启默认丢失」bug:active 是内存态重启清零,而 `is_default` 字段恒写 false,重启后无默认可恢复。`is_default` 字段本为持久化默认而存在,回归本职最自然;session 持久化需额外存储,重复造轮子。互斥写库保证「唯一默认」语义。
|
||||
- **边界**:互斥写库逐条 `update_full` **不加事务**——repo 未暴露事务接口;桌面单用户无并发触发,失败即报错、重试自愈。多用户/高并发场景需给 repo 补 `execute_transaction`。
|
||||
- **状态**:✅ 2026-06-13 落地(重启保默认 / 互斥写库 / 首个自动默认实测待补)
|
||||
|
||||
### 📋 delete_provider IPC 缺失(前端假删除)[Sprint 10]
|
||||
- **需求**:Settings 删 Provider 当前只前端 filter 移除,不调后端(无 `ai_provider_delete` 命令),重启后配置回归。
|
||||
- **原因**:交互欺骗——用户以为删除成功,DB 实际未动。需补 `ai_provider_delete` IPC(`lib.rs` 注册 + `ai.rs` 实现 + 前端真调),并加二次确认。
|
||||
- **状态**:📐 待实施 → ✅ Sprint 10 已实现(`ai_delete_provider` 命令 + 删默认清 active + 自建 `confirmDialog` 二次确认)
|
||||
|
||||
## 模型能力与路由(Model Capability & Auto-Routing)
|
||||
|
||||
### 能力声明:复用 `ai_providers.models` JSON 字段,不建新表 [2026-06-13]
|
||||
- **决策**:每个 Provider 下可选模型的能力声明(模态/功能/成本等级)存入已有 `ai_providers.models` 列(JSON 数组),**不新建独立表**。新增 `crates/df-ai/src/model_capability.rs` 定义 `ModelCapability`(name / modalities / functions / max_tokens / cost_tier)+ `TaskRequirements` + `Modality` / `CostTier` 枚举。
|
||||
- **原因/取舍**:模型能力是 Provider 配置的内在属性,非独立实体——无生命周期管理、无跨表 JOIN 需求,JSON 嵌入单行足够(Provider 通常 1-5 个);`models` 列已在 V9 建表且全链路预留,复用零 schema 变更;独立表需外键/级联/JOIN,对桌面应用过度工程;备份迁移友好(配置自包含一行)。代价:无法 SQL 查询「所有有 vision 的模型」,但此查询当前和近期均不需要。
|
||||
- **状态**:📐 设计未实施(Phase 1:数据模型 + 场景级路由)
|
||||
|
||||
### 路由层:纯函数 ModelRouter,重写现有骨架 [2026-06-13]
|
||||
- **决策**:重写 `crates/df-ai/src/router.rs` 已有但空的 `ModelRouter`——从 `TaskRequirements` + Provider model_pool → 按硬性要求筛选候选 → 按 cost_tier 升序取最便宜。路由是同步纯函数(无 I/O、无全局可变态),可单测。7 个 LLM 调用点统一接入。`override_model` 字段支持用户显式指定(聊天手动切)和工作流节点 config.model(节点作者指定)两种覆盖,优先级最高。
|
||||
- **原因**:路由是确定性计算(静态配置→模型名),无需 async/service 化;纯函数可单测;统一入口避免散装 if-else。
|
||||
- **各调用点路由策略**:主对话 `chat(has_image)`→Standard/Premium;标题生成 `title_generation()`→Economy 最便宜;知识提炼 `knowledge_extraction()`→Economy/Standard;工作流 AiNode `workflow_node()` + override=config.model→节点指定优先;**Embedding (×2) 不走路由器**(独立路径,不同模型类)。
|
||||
- **向后兼容**:`models=None`(老记录)→ model_pool 空 → 所有 route 返回 default_model → 行为不变。
|
||||
- **状态**:📐 设计未实施(Phase 1)
|
||||
|
||||
### Embedding 不进通用路由器 [2026-06-13]
|
||||
- **决策**:Embedding 模型不走 ModelRouter,继续走独立路径(`KnowledgeConfig.embedding_model` + `embedding_provider_id`)。
|
||||
- **原因**:Embedding 是完全不同的模型类——API 不同(`/v1/embeddings` vs `/v1/chat/completions`)、用途不同(向量化 vs 生成)、通常更小更专用。混入通用模型池会混淆用户并增加路由分支复杂度。
|
||||
- **状态**:📐 设计未实施(Phase 1 确认不动 embedding 路径)
|
||||
|
||||
### 分阶段实施路线 [2026-06-13]
|
||||
- **Phase 1**(本次):数据模型 + ModelRouter 重写 + 7 调用点接入 + Settings 模型池编辑 UI + AiChat 模型下拉。ChatMessage.content 保持 String 不改。
|
||||
- **Phase 2**(后续):多模态消息——ChatMessage.content: String → Vec<ContentPart>(Text/Image);前端粘贴/拖拽图片;vision 模型自动路由。
|
||||
- **Phase 3**(后续):Agent 内智能路由——Agentic Loop 每轮按子任务构造不同 TaskRequirements;成本预算控制;模型级联降级;跨 Provider 搜索。
|
||||
- **状态**:📐 设计未实施
|
||||
|
||||
### 📋 模型能力系统 — 完整改动文件清单 [2026-06-13]
|
||||
- **后端 Rust**:`crates/df-ai/src/model_capability.rs`(**新增** ModelCapability / TaskRequirements / Modality / CostTier)/ `router.rs`(**重写** ModelRouter 匹配逻辑)/ `lib.rs`(re-export)/ `df-storage/src/migrations.rs`(V10 版本号推进,无需 ALTER)/ `src-tauri/src/commands/ai.rs`(7 调用点加 router;ai_save_provider 加 models 参数;新增 ai_set_chat_model_override)
|
||||
- **前端 TS/Vue**:`src/api/types.ts`(新增 ModelCapability / Modality / FunctionCapabilities / CostTier 类型)/ `src/api/ai.ts`(saveProvider 加 models 参数;新增 setChatModelOverride())/ `src/stores/ai.ts`(availableModels / activeModelOverride 状态 + setModelOverride action)/ `src/views/Settings.vue`(Provider 表单增加模型池编辑区)
|
||||
- **状态**:📐 待实施(Phase 1 全量清单)
|
||||
|
||||
## 灵感模块(评估闭环)
|
||||
|
||||
### 启发式评分维度
|
||||
- **决策**:三维固定 5.0 → 内容启发式(priority / 描述充实度 / tags / 中英关键词),clamp 0-10。
|
||||
- **原因**:固定值无区分度;启发式基于 idea 内容给差异化评分。
|
||||
- **状态**:✅ Sprint 9(启发式,未接 LLM)
|
||||
|
||||
### 对抗评估:启发式 fallback 待接 LLM
|
||||
- **决策**:正反方论点/evidence 基于真实 idea 内容生成,confidence 由评分驱动;未接 df-ai LlmProvider。UI 诚实标注——评估区标题加「启发式」黄标(`.eval-mode-tag`),对齐实现深度避免名实不符,接 LLM 后摘除。
|
||||
- **原因**:先打通评估闭环;LLM 生成论点 + 启发式 fallback 为后续增强。
|
||||
- **→ 接入方式已定**:走全局 AI trait 下沉(见 [crate 治理 — AI trait 下沉拆 df-ai-core](#ai-trait-下沉拆-df-ai-core-轻层确立全局-ai-接入标准-2026-06-14-))。`evaluate()` 加 `LlmProvider` 注入参数,启发式降级为 fallback;F-03 原 A/B/C 选型据此收敛为「全局统一 trait」。
|
||||
- **状态**:📐 设计未实施(LLM 接入)
|
||||
|
||||
### 前后端标签对齐
|
||||
- **决策**:`recommendation` 全小写空格(后端原 "With Resources" 不匹配前端 map key);`assessmentClass` 映射到 CSS 类名(`.immediate`/`.soon`/...)。
|
||||
- **原因**:大小写/类名不一致致中文标签不显示、badge 无色。
|
||||
- **状态**:✅ Sprint 9
|
||||
|
||||
### 评分 0-10(crate)→ 0-100(前端)IPC 缩放 + 多字段写回用单事务 [Sprint 9/10]
|
||||
- **决策**:① crate 内 IdeaScores 维持 0-10;IPC 层 evaluate_idea 组装 scores JSON 时 *10 缩放为 0-100,并用中文维度键(可行性/影响力/紧急度/综合)。② evaluate_idea / promote_idea 写回想法用 `update_full`(单事务覆盖整条记录),放弃多次 `update_field`。
|
||||
- **原因**:① 前端雷达图直接当百分比渲染、零前端改动;crate 内 0-10 符合评分直觉,IPC 层做单位适配。② 多次 update_field 各自独立连接,中途失败致数据半成品;update_full 原子。
|
||||
- **状态**:🚧 Sprint 9/10(编译/构建通过,未 tauri dev 实测)
|
||||
|
||||
## 想法立项(promotion)
|
||||
|
||||
### 复用 df-project 领域层,crate do_promote 留纯决策 TODO [Sprint 10]
|
||||
- **决策**:`promote_idea` IPC 复用 `df_project::manager::ProjectManager::create_from_idea` 构造项目实体 + 映射 ProjectRecord 持久化 + update_full 回写想法;df-ideas crate 内 `IdeaPromoter/do_promote` 保留纯决策 TODO,不真正创建项目。
|
||||
- **原因**:crate 不依赖 df-storage/df-project(避免循环依赖、保持可单测);手动立项无需 Auto/Manual/SemiAuto 策略判断(用户点即确认),副作用放 IPC 组合,与 evaluate_idea 同模式;crate 纯决策留待自动/半自动晋升场景复用。
|
||||
- **状态**:🚧 Sprint 10(编译/构建通过,未 tauri dev 实测)
|
||||
|
||||
### promoted_to 非空拒绝重复立项 [Sprint 10]
|
||||
- **决策**:promote_idea 取想法后校验 promoted_to 已存在则返回错误,不创建新项目。
|
||||
- **原因**:防同一想法多次点「立项」生成多个项目;幂等保护。
|
||||
- **状态**:🚧 Sprint 10
|
||||
|
||||
## 项目管理(删除 / 回收站)
|
||||
|
||||
### 项目删除:软删回收站(deleted_at + 应用层级联),非物理删 [2026-06-13]
|
||||
- **决策**:删项目改为**软删**——`projects` 加 `deleted_at TEXT` 列(V11 迁移),`delete_project` 置 `deleted_at`(进回收站,可恢复);`list_projects` 过滤 `deleted_at IS NULL`;回收站(`list_deleted_projects`)可「恢复」(清 deleted_at)或「彻底删除」(`purge_project` 事务级联物理删 branches→releases→tasks→projects,不可逆)。子表软删时不动,FK 仍满足,项目数据完整保留待恢复。
|
||||
- **原因/取舍**:① 用户要求「可恢复」——纯 CASCADE 物理删不可逆,误删难挽回;软删 + 回收站给反悔余地。② SQLite `ALTER TABLE` 改不了已有表 FK 约束,给老库 projects 加 `ON DELETE CASCADE` 须重建表(高风险),故不走 DDL 级联,改**应用层级联**(purge 时事务内顺序删子表),语义等价且可测、不依赖 `PRAGMA foreign_keys`。③ 软删只标记 projects 行、子表不动——恢复时项目连同历史任务/分支/发布完整还原。④ 两级风险分级:日常软删可逆 / 回收站 purge 二次确认后物理删不可逆。
|
||||
- **边界**:`ProjectRecord` 不带 `deleted_at` 字段,纯靠 SQL `WHERE deleted_at IS NULL` 过滤,models/types 零变更。`ai.rs build_system_prompt` 同步改用 `list_active`(防软删项目泄漏进 AI 上下文)。`soft_delete`/`restore` 守卫对称,重复操作幂等。
|
||||
- **状态**:✅ 2026-06-13 落地(V11 迁移 + ProjectRepo 5 方法 + 3 IPC 命令 + 回收站 modal + 删除入口;cargo check + vue-tsc + 11 integration test 全绿)
|
||||
|
||||
## 项目管理(目录绑定 / 技术栈探测)
|
||||
|
||||
### 项目绑定真实代码目录:扩 ProjectRecord + df-project scan [2026-06-13]
|
||||
- **决策**:`ProjectRecord` 加 `path`(绑定目录绝对路径)+ `stack`(技术栈 JSON 数组字符串)两字段(V12 迁移,nullable);技术栈探测逻辑放**新建 `df-project/src/scan.rs::detect_stack`**(纯函数,浅读根目录标志文件识别 rust/go/python/java/csharp/vue/react/angular/svelte/next/vite/typescript/node/tauri);commands 层薄封装 4 命令(`scan_project_stack`/`check_path_binding`/`relocate_project_path`/`check_path_exists`),`check/relocate` 前 `canonicalize` 规范化路径防绕过重复检查。新建项目可选绑定目录→自动探测栈,详情页支持重定位 + 目录失联检测 + 防重复绑定。
|
||||
- **原因/取舍**:① **扩 df-storage ProjectRecord 而非激活 df-project ProjectContext 空壳**——`ProjectContext` 虽早设计 `root_path`/`tech_stack`/`repo_url`/`ai_context` 字段,但 `manager.rs` 标注 TODO 从未接存储/运行时;激活需新建 `project_contexts` 表 + 填充暂不需要字段,第一步过重,守 YAGNI。ProjectRecord 是实际运行链路,直接扩最快见效。② **scan 放 df-project 而非 commands 层**——`ProjectContext.tech_stack` 本就是 df-project 职责字段,scan 是其天然能力且可被 df-ai/df-workflow 复用,放 commands 变一次性代码。③ **migration nullable**——老项目 path/stack=NULL 零影响。④ **一致性原则**:先在「新建流」验证,第二步「导入历史项目」复用同一套 scan/relocate/checkBinding。
|
||||
- **边界**:`path` 规范化(canonicalize)仅用于比较,存库保留用户输入的原始可读路径。程序化创建项目(想法晋升、AI 工具)path/stack = None(不绑定目录)。`df-project` 的 `ProjectContext` 刻意未激活(ai_context/repo_url 等暂留空)。
|
||||
- **状态**:✅ 2026-06-13 落地(V12 迁移 + detect_stack + 4 IPC 命令 + 选目录/查重/卡片栈 + 重定位/目录状态;cargo build + scan 4 单测 + vue-tsc 全绿)。📋 导入历史项目(第二步):复用 scan/relocate/checkBinding,加 monorepo 子目录识别 + README 首段抽 description + 批量。
|
||||
|
||||
### 导入/绑定项目 AI 扫描填信息:规则探测兜底 + LLM 增强,采样与 LLM 调用分层 [2026-06-14]
|
||||
- **决策**:导入/绑定项目时规则 `detect_stack`(快/免费/准)必跑兜底 + LLM 分析采样(README+目录树+清单,不读源码)产出 description 摘要与 stack 细化;LLM 失败降级纯规则。采样逻辑放 df-project(纯 IO),LLM 调用放 commands 层。
|
||||
- **原因/取舍**:① 规则兜底保证 LLM 不稳定时仍有基础信息,LLM 只补规则搞不定的摘要。② 采样与 LLM 分层——采样纯 IO 属 df-project 职责可复用,LLM 调用依赖 df-ai,放 commands 使 df-project 保持无 LLM 依赖(防循环)。③ 不读源码控 token+隐私。④ 结果预览让用户把关防 LLM 瞎编。
|
||||
- **状态**:✅ 2026-06-14 落地(scan_project_with_ai 命令 + 前端 AI 扫描预览;编译/单测/类型全绿)。
|
||||
|
||||
### AI 工具绑定目录:bind_directory 专用工具 + 工具层白名单同步 + prompt 禁冒充 [2026-06-14]
|
||||
- **决策**:① update_project 工具白名单补 path/stack(同步 db 新字段);② 新增 bind_directory 专用工具(绑定目录不走通用 update);③ 系统 prompt 加约束:工具失败须明说,禁用替代操作冒充原意图成功。
|
||||
- **原因/取舍**:① review AI 对话发现 update_project(path) 被工具层白名单拒(db 字段加了但工具层漏同步),AI 转而改写 description 却回复「已记录」冒充绑定成功误导用户。② 专用工具语义清晰,防 AI 走通用 update 捷径冒充。③ prompt 约束防单链 ReAct「自我圆场」幻觉(失败时用替代谎报成功)。
|
||||
- **状态**:✅ 2026-06-14 落地(白名单同步 / bind_directory / prompt 中英约束;编译全绿)。📋 待清:工具层白名单与 crud 白名单双份去重(详见经验记录)。
|
||||
|
||||
## 知识库(df-evolve / 共享记忆层)
|
||||
|
||||
> 核心定位与设计决策。详细字段/参数级决策见各条目。
|
||||
|
||||
### 定位 + 被动 Service 退化 [2026-06-13]
|
||||
- **决策**:知识库(df-evolve)定位为**整个 DevFlow 的共享记忆层**——每个模块(idea/task/workflow/review/chat)既是知识生产者也是消费者,而非孤立展示功能页。df-evolve 褫夺「自动进化引擎」角色(Sprint 2 对抗论证已砍,自用阶段 ROI 低/过度工程),**退化为被动 Service 层**,只暴露 `search/retrieve/record_reuse/feedback/save` 供各模块调用;被动 Service 复用 df-ai 检索做消费侧,EventBus 做事件驱动提示(非自动抓取)。
|
||||
- **原因**:手脑(各业务模块)分离无学习能力;知识库做「肌肉记忆」中枢系统才越用越懂你。砍的是自动挖矿,非知识库本身。
|
||||
- **状态**:📐 设计未实施(用户拍板定位)
|
||||
|
||||
### 沉淀审核机制:知识状态机,AI 只产草稿 [2026-06-13]
|
||||
- **决策**:知识加 `status` 字段,状态机 `candidate → pending_review → published → archived`;**AI 提炼的知识一律进 candidate,绝不直接入正式库**;草稿进「待审核收件箱」,人工逐条编辑(内容/分类/标签)→ 发布或丢弃。`verified` = 发布审核时一次性人工标(intake 决断动作,非 ongoing 评分)。沿用 IdeaRecord 的 `pending_review` 状态机模式。
|
||||
- **原因**:沉淀必须有人工把关(用户要求「人工能够编辑或调整,必须有这些过程」)——AI 提议、人裁决、系统如实记,非黑箱自动学习。
|
||||
- **状态**:📐 设计未实施(用户明确要求加审核机制)
|
||||
|
||||
### AI 提炼产出:字段集 + 置信度(唯一指标)+ 查重 [2026-06-13]
|
||||
- **决策**:AI 提炼一条 candidate 时产出——**内容字段**:`kind`(分类,AI 判定 7 类之一)、`title`、`content`、`tags`、`source_ref`(原始证据片段);**质量指标**:`confidence`(High/Medium/Low,AI 自评,**唯一质量指标**);提炼时另做**查重**(比对 published 库,重复则不产/标合并,非存储字段)。**克制边界**:质量指标只留 confidence——不加 generality/specificity/novelty 等维度(过工程化 + 多耗 token),适用范围并入 `tags` 不单列 scope 字段。
|
||||
- **原因**:`kind` 和 `source_ref` 是 AI 必填但易漏的两项;confidence 服务降噪 + 审核分诊 + 透明,但**不绕过人审门**(high 也不自动发布)。
|
||||
- **状态**:📐 设计未实施
|
||||
|
||||
### 透明化:provenance 溯源 + 收件箱 + 注入告知 [2026-06-13]
|
||||
- **决策**:① 每条知识标来源(哪次 Chat/task/review 产出 + 原始片段),可跳回——复用现有 `source_project` + `source_ref` 字段;② 「待审核收件箱」作明确信息渠道;③ 复用时显式告知本次注入了哪几条、为什么命中。
|
||||
- **原因**:用户要求「透明化让人们有很好的信息获取渠道」——不黑箱。provenance 字段现有模型已有,零新增成本。
|
||||
- **状态**:📐 设计未实施
|
||||
|
||||
### 克制原则:宁缺毋滥,小步迭代 [2026-06-13]
|
||||
- **决策**:① **检索注入保守**——精确匹配(标签/关键词)优先,语义模糊匹配**后做**,top-N 限 1-3 条,置信不够一条都不塞;② **关联不自动推断**——IdeaGraph 自动聚类/关联发现**先不做**,只支持人工标注;③ **沉淀不主动监听全量事件**——仅「一键沉淀」或事件提示后「确认」才产 candidate;④ **从小到大**——先 AI Chat 单点双向跑通验证手感,再串 review/idea。
|
||||
- **原因**:用户要求「尽可能克制,不要做大胆的连接,从小到大」——贯彻 Sprint 2「scope 砍 60%」精神到知识库,避免重蹈「自动进化」过度工程覆辙。
|
||||
- **状态**:📐 设计未实施(用户明确要求克制)
|
||||
|
||||
### 指标客观化:reuse_count 唯一信号,撤销 effectiveness 人工评分 [2026-06-13]
|
||||
- **决策**:知识排名/淘汰**只用 `reuse_count` 一个客观信号**(检索注入自动 +1);**撤销 `effectiveness` 的人工 👍/👎 评分**(主观、有摩擦、信号不准);淘汰改客观——reuse_count=0 且超 N 天未用 → 提示归档。
|
||||
- **演进**:初版三指标(reuse_count + effectiveness + verified)共同排序权重 → 同日修正:用户指出 👍/👎 是「人为、主观、非准确」的非必要干预,撤销。「用过 ≠ 有用」的质量顾虑改由两层客观兜底——① intake 审核门(一次性决断)② 发现噪音直接删。召回不准根因在标签/搜索质量,靠 intake 打准标签解决。
|
||||
- **状态**:📐 设计未实施
|
||||
|
||||
### 📋 分层落地 Tier 1/2/3 + 来源/去向审查 [2026-06-13]
|
||||
- **需求**:知识库联动分三层——**Tier 1(必做)**:AI Chat ↔ 知识库双向 + 手动录入(沉淀 + 检索注入 + reuse_count + 审核收件箱 + 状态机,**无人工评分**);**Tier 2(串创作流,带前置)**:ai_node 检索 prompt_template、工作流 NodeFailed → pitfall(**仅失败时**)、决策记录 → architecture_pattern(**前置:先补 df-traceability 持久化**);**Tier 3(砍)**:evolve_engine 自动挖事件。
|
||||
- **审查(来源)**:① **/review 源移出**——devflow 无代码审查功能(df-stages/coding.rs 审查节点是 TODO 空壳);② **想法评估源存疑**——对抗论点是「一次性结论」非可复用知识;③ **决策记录源标前置**——`DecisionJournal` 所有 SQLite 查询 TODO 未持久化;④ **工作流源限定失败时**——运行日志≠提炼知识。
|
||||
- **审查(去向)**:**Chat 轴是唯一 Tier 1 就绪消费端**(检索→注入对话/提示词);ai_node prompt 注入属 Tier 2;工作流无主动消费(仅被动产 pitfall);决策溯源消费半残。→ 来源/去向双收敛到 Chat 轴。
|
||||
- **状态**:📐 待实施(先做 Tier 1:Chat + 手动录入)
|
||||
|
||||
### 对外暴露:MCP Server 双向协议 [2026-06-13]
|
||||
- **决策**:知识库对外 API 采用 **MCP Server** 形式暴露,**双向**(读+写)。Tier 1 先做 DevFlow 内部闭环(Tauri IPC),**命令层设计完全对齐 MCP 语义**;Tier 1+ 套 MCP server(封装已有 command)。MCP 暴露:① **Resource (读)**:list/search/get;② **Tool (写)**:create_candidate;③ **Tool (计数)**:record_reuse。
|
||||
- **原因**:① Claude Code 原生吃 MCP——本机主力工具零集成成本;② Cursor 也支持 MCP;③ **双向价值**:外部工具(尤其 Claude Code 做代码审查/重构时)是高质量知识来源——审查结论→review_rule、踩坑经历→pitfall,接 MCP 自动回流知识库等于开「第二来源入口」。克制:Tier 1 不实现 MCP 本身,但 6 个核心命令(search/list/get/create/update_status/record_reuse)全部按可暴露设计。
|
||||
- **状态**:📐 设计未实施(Tier 1+ 事项)
|
||||
|
||||
### 矛盾知识处理:纯标签+内容自述,source_project 仅溯源 [2026-06-13]
|
||||
- **决策**:矛盾知识**不建冲突关系表、不加 scope 字段、不做 access control**。消歧靠 tags(如 `["Go","微服务"]` vs `["Go","单体"]`)+ content 自述适用范围 + source_project 仅作来源溯源展示(不参与检索过滤)。AI 提炼 prompt 加约束:「适用范围有限制必须在 content 或 tags 中标注」。零数据结构变更。
|
||||
- **原因/取舍**:矛盾是少数场景,为 minority 建关系系统是过度工程;source_project 若做绑定/过滤会提高维护门槛 + 降低通用知识复用率;检索 top-N≤3 返回时内容本身场景描述足够消费者判断。
|
||||
- **状态**:📐 设计未实施
|
||||
|
||||
### AI 提炼触发 + 知识注入:可配置 [2026-06-13]
|
||||
- **决策**:① AI 提炼**默认自动触发**(后台 detached task),4 个配置项:`auto_extract`(default true)/ `trigger_mode`(on_complete|on_idle|manual_only,default on_complete)/ `min_messages`(default 4)/ `idle_timeout_ms`(default 30000)。② Chat system prompt 知识注入**加开关**(`auto_inject: bool`,default true,关闭时首行返回空字符串零开销)。
|
||||
- **原因/取舍**:手动按钮依赖用户记得点→遗忘→空库死循环;自动触发保证持续流入候选。但用户控制欲不同——4 配置项覆盖从「全自动」到「全手动」。每次 complete() <500 input/<200 output token,可关零成本。注入开关给用户「先积累再开启 / 调试不被干扰」的控制权。
|
||||
- **状态**:📐 提炼设计未实施 / ✅ 注入已实施(Tier 1)
|
||||
|
||||
### 检索方案:LIKE + top-N,向量检索提前到 Tier 1(Phase 5.5)[2026-06-13]
|
||||
- **决策**:Tier 1 检索用 SQLite `title/content LIKE '%query%'` + `ORDER BY reuse_count DESC LIMIT 3`;收件箱排序用 `CASE WHEN confidence 'high'→3/'medium'→2/'low'→1`(非纯 TEXT 字典序)。**向量检索从 Tier 2 提前到 Tier 1 同步实施**(Phase 5.5),加 **Settings 开关**(`vector_enabled`,默认 false)。
|
||||
- **原因/取舍**:知识库核心消费场景是 AI 自动注入(拿用户自然语言 query 检索),非关键词精确搜索——「部署后白屏」匹配不到「Nginx SPA 路由」,纯 LIKE 语义盲区从第一天就存在。开关化解「本地优先/零依赖」哲学冲突:默认关闭纯 LIKE 零外部调用,开启后才走 embed API。
|
||||
- **实施细节**:① `LlmProvider` trait 加 `embed()`(OpenAICompat 实现,Anthropic 不支持);② V8 幂等补 `embedding BLOB` 列(f32 小端序列化,NULL=未嵌入走 LIKE);③ **嵌入时机=发布时**(candidate 不浪费 embed,published 才参与检索);④ 纯 Rust 余弦(<50k 条暴力遍历够用,不引 sqlite-vec 避免 Windows C 扩展编译风险);⑤ 三层降级链:开关关→LIKE;开但 provider 缺/embed 失败→自动回 LIKE;正常→混合检索(双信号>LIKE 单>向量单,cos≥0.3 滤噪)。
|
||||
- **状态**:✅ Tier 1 LIKE + 向量混合检索(Phase 5.5)均已实施,编译通过待实测
|
||||
|
||||
### 知识生命线:独立 knowledge_events 表(非 JSON 嵌主表)[2026-06-13]
|
||||
- **决策**:知识产生/审核/引用/归档四类审计事件存**独立 `knowledge_events` 表**(V10 迁移),而非塞进 `knowledges.context_json` 字段。
|
||||
- **原因/取舍**:事件是追加型(只增不改删),语义与主表 CRUD 完全不同;一条知识可被引用数百次,JSON 嵌主表致行膨胀 + 写更新竞争(每次引用都重写整行)。独立表可建 `(knowledge_id,event_type)` 复合索引;事件表写失败只丢审计、不影响知识本身(fire-and-forget 隔离)。未来加新事件类型只加一行 insert,不动主表 schema。
|
||||
- **状态**:✅ 已实施(V10 迁移 + KnowledgeEventsRepo + 前端生命线时间线)
|
||||
|
||||
### 📋 知识详情页 + 编辑能力(candidate 审核闭环)[2026-06-13]
|
||||
- **需求**:卡片点不开详情、不能编辑、看不到「为什么产生」。详情页需呈现完整生命线 + candidate 可编辑修正后发布。
|
||||
- **决策**:Knowledge.vue 重构为 Ideas 式左右分栏(左卡片列表 @click 选中 / 右详情面板四分区:①基本信息 ②溯源 ③引用记录 ④生命周期时间线);可编辑字段 title/content/tags/confidence/reasoning 走 `knowledge_update`(部分更新)。
|
||||
- **原因/取舍**:candidate 编辑是审核闭环刚需——AI 提炼必有水分/措辞瑕疵,只能原样发布或整条拒绝会让审核空转。详情复用 Ideas 已验证的 master-detail 模式。published 编辑不做(有归档+重提炼替代)。
|
||||
- **状态**:✅ 已实施(编译+vue-tsc+df-storage 21 单测全绿,GUI 实测待 #54)
|
||||
|
||||
## AI Chat 上下文窗口与并发控制(架构结论)
|
||||
|
||||
> 实现细节见 [归档文档](./功能决策记录-归档.md)「AI Chat 上下文窗口与并发控制」。
|
||||
|
||||
### 设计决策 [2026-06-13]
|
||||
- **ContextManager 类型替换为 messages 真相源**:`AiSession.messages` 从 `Vec<ChatMessage>` 改为 `ContextManager`(非 wrapper 包装层),消息真相源唯一,避免 Vec + ContextManager 双存导致状态分裂。裁剪仅影响发送视图(`build_for_request` 返回裁剪版,`all_messages_clone` 返全量落库)。
|
||||
- **淘汰算法:分组滑动窗口 + 三元组保护**:`Assistant(tool_calls) + Tool(result)* + Assistant(final_text)` 工具调用三元组作为原子整体;最后 6 条(≈2 个完整用户轮次)设为保护区永不淘汰。
|
||||
- **Token 计数零依赖**:`chars_count × 0.35` 粗估(误差 ±15% 可接受),不引入 tiktoken-rs(5MB BPE 数据文件对 Tauri 打包不友好)。
|
||||
- **双层 Semaphore 并发控制**:AppState `LlmConcurrency`——全局并发默认 3 / 单对话默认 2;permit 在 3 个叶子 LLM 调用点 acquire(`run_agentic_loop` stream_llm 前、`generate_title_via_llm`、`extract_knowledge_from_conversation`),工具执行不受控。`per_conv` 实为应用级单一信号量(因 AiSession 单例 + generating 互斥,命名宽泛但当前语义正确,多对话路线时改 HashMap)。Semaphore 重建用「软收敛」策略(替换内层 Arc,旧 permit 不受影响)。
|
||||
- **裁剪策略与模型选择正交**:ContextConfig 不含 mode/模型选择字段;「高精度/低精度对话」属 LLM 调用层参数,与裁剪策略是正交维度。
|
||||
- **状态**:✅ 已落地(2026-06-13,cargo check + vue-tsc 通过,待 tauri dev 实测)
|
||||
|
||||
## i18n
|
||||
|
||||
### legacy:false + zh-CN 默认 + locale 拆分 + glob 聚合 [Sprint 7 + 2026-06-13]
|
||||
- **决策**:① `legacy:false` / `globalInjection` / zh-CN 默认 + en fallback / `localStorage df-language` 持久化。② `zh-CN.ts`/`en.ts` 单文件 → `zh-CN/*.ts` + `en/*.ts` 按模块拆分(nav/dashboard/ai/common/settings/ideas/knowledge/projects/projectDetail/tasks/aiChat/aiTool),`index.ts` 用 `import.meta.glob('./*.ts', { eager, import: 'default' })` 自动聚合(排除 index 自身)。新增模块文件即生效,不改 index。
|
||||
- **原因**:① Composition API 模式;默认中文贴合自用,英文兜底。② 全量 i18n 接入(8 view ~640 处中文)用多代理并行,模块隔离零冲突(每代理建自己模块 + 改自己 view,不动共享 index);比单文件扩 key(多代理改同一 `zh-CN.ts` 冲突)更适合并行。glob eager 运行时聚合,动态新增模块即拾取。
|
||||
- **状态**:✅ Sprint 7 + 2026-06-13(curl 验证 vite 正确展开 glob,vue-tsc PASS)
|
||||
|
||||
### 状态枚举 i18n:constants 存 key,view 包 $t
|
||||
- **决策**:`constants/project.ts` 的 `PROJECT_STATUS_LABELS`/`TASK_STATUS_LABELS` 值从中文文案改存 i18n key(`planning: 'projects.status.planning'`);`projectStatusLabel`/`taskStatusLabel` 返回 key;view 显示处包 `$t(projectStatusLabel(x))`。`PRIORITY_LABELS`(P0/P1)是代号非文案,不动。
|
||||
- **原因**:constants 是纯数据/结构层,不应含展示文案(违反分层);文案归 locale,constants 管映射结构。
|
||||
- **状态**:✅ 已落地
|
||||
|
||||
### 📋 项目 status 字段语义混乱(生命周期 vs 开发阶段)
|
||||
- **现象**:后端 `projects.status DEFAULT 'active'` + `list_active`/软删除(active/deleted 生命周期),但前端 `PROJECT_STATUS_LABELS` 是 planning/in_progress/paused/completed/cancelled(开发阶段),两套塞一个 status 字段。DB 实际只有 `status='active'`(新建默认,阶段值从没产生),前端 map 不认 → 显示英文 "active"。
|
||||
- **治标**:补 `projects.status.active`(🚀进行中),不再显示英文。→ 根本未除。
|
||||
- **治理方向**:① 后端支持阶段流转(planning→in_progress…);② 前端 map 对齐后端真实值(active/deleted/archived);③ 拆双字段(status 生命周期 + stage 开发阶段)。
|
||||
- **状态**:📐 待治理(治标已落地,根本病根未除)
|
||||
|
||||
## 状态持久化
|
||||
|
||||
### 窗口位置/大小:用 tauri-plugin-window-state(纯 Rust 层)
|
||||
- **决策**:窗口位置/大小/最大化用 `tauri-plugin-window-state` 插件(Rust 层自动接管),而非前端 localStorage + setPosition/restore 方案。
|
||||
- **原因**:插件自动覆盖主窗口 + 动态创建的 `ai-detached` 子窗口,零前端代码、零竞态;前端方案需手动同步且对子窗口生命周期处理复杂。需配套 `window-state:default` capability 权限。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
### detached/docked 不持久化
|
||||
- **决策**:UI 布局持久化,但 `detached`/`docked` 重启后强制回 `false`,不随 `df-ai-ui` 落盘。
|
||||
- **原因**:重启后分离窗口必然不存在,若恢复为 `true` 会让 UI 状态指向不存在的窗口(按钮失灵、panelOpen 错乱)。这两个态是运行时临时态,不属可恢复布局。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
> 注:UI 布局 localStorage + 模块级恢复的细节见 [归档文档](./功能决策记录-归档.md)。
|
||||
|
||||
## 应用启动 / 数据库配置
|
||||
|
||||
### Dev 与 Build 拆分独立数据库 [2026-06-13]
|
||||
- **决策**:`lib.rs` 启动时按 `cfg!(debug_assertions)` 选 DB 文件名——debug(Dev 模式)用 `devflow-dev.db`,release(Build 模式)用 `devflow.db`,两库同处 `app_data_dir()`(`top.1216.devflow`)下,靠文件名区分。
|
||||
- **原因/取舍**:原启动代码无编译模式分支,Dev 与 Build 共用一个 `devflow.db`——Dev 频繁改动/清空会污染 Build 侧真实运行数据。拆分后 Dev 库可随意折腾,Build 库长期保留作运行效果基线。**文件名区分而非子目录**——改动最小(lib.rs 一行 if),两库平铺同目录便于备份/查看。**现有 `devflow.db` 文件名未变归 Build**,零迁移零数据丢失;Dev 首次启动自动建空库。
|
||||
- **边界**:`app_data_dir()` 由 `tauri.conf.json` 的 `identifier` 决定、与编译模式无关,故拆分前两种模式确读同一文件。docs/使用手册备份命令 `cp devflow.db` 仍正确(备份 Build 真实数据)。
|
||||
- **状态**:✅ 2026-06-13 落地(`lib.rs:24`)
|
||||
|
||||
## UI 反馈与弹层
|
||||
|
||||
### toast/confirm 自建,不引 Arco / 不用 window.confirm [Sprint 10]
|
||||
- **决策**:Settings 页轻量提示与删除确认用自建 `toast`(顶部 fixed,3s 自动消失)+ `confirmDialog`(遮罩 + 卡片,Promise 化),而非引入 Arco Message/Modal 或原生 `window.confirm`。
|
||||
- **原因**:① `@arco-design/web-vue` 虽在依赖但 `main.ts` 未 `app.use` 注册,引 Message/Modal 要补全局注册 + 样式加载,过重违反做减法;② `window.confirm` 在 Tauri webview2 带「来自 localhost:端口」来源信息,无法去除,体验差。自建零依赖、样式可控(主题色)、`await confirmDialog()` 语义贴近原生 confirm。
|
||||
- **演进** [2026-06-13]:AiChat 删对话需确认 → 第二处复用落地。抽成 `src/components/ConfirmDialog.vue`(`visible`/`msg`/`dangerLabel` props + `@result` emit)。按钮样式内联自包含,不依赖外部 `.btn-*`——因 Settings 是 `scoped`,组件拿不到其内定义的 `.btn-danger`。选 SFC 组件而非 `useConfirm()` composable:模板/遮罩/Transition 动画/CSS 才是真正重复主体,Promise 封装留在调用方(~8 行)。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
## 技能 / 联想
|
||||
|
||||
### 首批 Claude 3 类 + path 去重
|
||||
- **决策**:技能联想首批数据源 = Claude skills / commands / plugins 三类(SKILL.md frontmatter),按 path 去重(`cache/` 与 `marketplaces/` 重复)。
|
||||
- **原因**:frontmatter 格式统一(`name`/`description`/`user_invocable`),可统一解析;Codex frontmatter 一致后续可扩展,openclaw 属 agent 选择层不纳入。
|
||||
- **状态**:✅ Sprint 8(待实测)
|
||||
|
||||
## 决策治理产品化评估(2026-06-13)
|
||||
|
||||
> 审视 DevFlow 是否应把决策治理(记录/锚点/完成度/自检/漂移)做成产品功能。机制设计详见 [规格契约自检机制.md](./规格契约自检机制.md)。
|
||||
|
||||
### 5 痛点产品内未覆盖,真实运转的寄生 Claude Code 层 [2026-06-13]
|
||||
- **决策**:DevFlow 产品内对决策治理 5 痛点「设计满格、代码两极」——df-traceability 死代码(无表/无 IPC/无前端);契约锚点/AI 自检/漂移检测=规格契约自检机制.md 纯设计稿(0 行代码);完成度无聚合视图。唯一真实运转的(功能决策记录.md + decision-record skill + dr-check hook)寄生在 Claude Code 协作层,未沉淀进产品。
|
||||
- **原因/取舍**:没用 Claude Code 的用户,DevFlow 给不了任何决策治理能力。这套能力寄生在协作工具上,核心价值未进产品。
|
||||
- **状态**:📐 待产品定位决策
|
||||
|
||||
### df-traceability 是锚点雏形,Sprint 2 被砍(死代码可复活)[2026-06-13]
|
||||
- **决策**:`crates/df-traceability/` 的 `Annotation.location`(文件路径+行号)= 规格契约自检机制设计的「代码锚点」雏形,`Decision` struct 精确对应决策记录痛点。但 Sprint 2 对抗论证时被砍/降级,此后无表、无 IPC,query 方法全 `vec![]`。
|
||||
- **原因/取舍**:非显然关联——文档层设计的活契约+锚点机制,本质是产品外部用更轻方式重发明被砍的 df-traceability 轮子。是否复活取决于产品定位抉择;Sprint 2 砍的理由(优先级低/过度设计)现需重新评估。
|
||||
- **状态**:📐 待评估
|
||||
|
||||
### 产品化推荐路径 C 混合,完成度驾驶舱起步 [2026-06-13]
|
||||
- **决策**:三路径——A 全产品化(复活 df-traceability 全栈+spec 自检 AI,成本大/重蹈 Sprint 2 覆辙风险);B 纯寄生(承认是 Claude Code 协作层,只优化 skill/hook,产品核心价值存疑);**C 混合(推荐)——产品做数据底座(decisions 表+完成度聚合+视图),AI 验证/漂移留协作层**。最小起步:只做完成度驾驶舱(痛点 3)。
|
||||
- **原因/取舍**:C 分离「确定的数据层」与「不确定的智能层」,先做确定的低风险项。完成度驾驶舱起步:①最痛(记不住做了/没做)②技术已存在(tasks/ideas 有 status,缺聚合 IPC+Dashboard 视图)③立刻可见④验证真会用再扩(避免 Sprint 2 式膨胀)。
|
||||
- **状态**:📐 待用户拍板(DevFlow 要否成为「决策治理/完成度驾驶舱」产品)
|
||||
|
||||
## crate 治理 / 模块结构(2026-06-14)
|
||||
|
||||
> 跨 crate 的删留与拆分决策。涉及 df-evolve 领域保留决策的推翻、coordinator 空壳的去留、ai.rs god file 的拆分方式。
|
||||
|
||||
### 删除 5 个零引用 crate(推翻 df-evolve 领域保留决策)[2026-06-14]
|
||||
- **决策**:整删 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` 但源码零用。整坨孤立骨架。
|
||||
- **推翻归档决策**:`功能决策记录-归档.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-traceability 整删**:原被记为「锚点雏形可复活」(见上方「决策治理产品化评估」)。本次决定删除——如未来真需锚点机制,重新评估而非保留死码。死码「可复活」是一种伪期权,实际价值是误导后续维护者以为它在运转。
|
||||
- **df-plugin / df-stages 属过早设计**:df-plugin(WASM/动态库)、df-stages(11 阶段节点 `execute()` 全空壳)未启动即删,与「人机协同设计基准」中「AI 反复读写的代码不养空壳」一致。
|
||||
- **影响**:① `src-tauri/Cargo.toml` 需移除 `df-evolve` 依赖声明;② 「决策治理产品化评估」中 df-traceability 相关条目(锚点雏形 / 完成度驾驶舱数据底座)状态需重新标注为「无现存代码可复用」;③ 知识库功能域(`## 知识库`)所有决策继续适用,但实现载体明确为 `KnowledgeRecord` 而非 df-evolve 领域类型。
|
||||
- **状态**:✅ 2026-06-14 落地(用户拍板删 5 crate + 领域类型)
|
||||
|
||||
### coordinator.rs 保留(B 路线占位空壳,不删)[2026-06-14]
|
||||
- **决策**:`crates/df-ai/src/coordinator.rs` 的 `AgentCoordinator`(多 agent 协作占位空壳,`run()` 返回硬编码 TODO)**保留不删**,加注释标注「B 路线待立项」。
|
||||
- **原因/取舍**:与 aichat 升级 A/B 路线拆分一致——A 路线先做 UX 快赢(已进行),B 路线单独立项补多 agent 协作决策能力。coordinator 是 B 路线的入口锚点,**有意保留占位**而非删除:① B 路线立项时有现成挂载点(trait + 结构骨架),不必从零设计;② 与上述 5 crate 删除不矛盾——5 crate 是「无路线图占位的纯死码」,coordinator 是「有明确后续路线(B 路线)的占位」,二者判断标准不同。区别在「是否绑定明确的演进路线」。
|
||||
- **状态**:📐 B 路线待立项(保留空壳 + 加注释)
|
||||
|
||||
### ai.rs 拆分:低风险子 module 而非下沉 crate [2026-06-14]
|
||||
- **决策**:`src-tauri/src/commands/ai.rs`(2663 行 god file)拆成 `commands/ai/` 子 module 目录(11 个文件:mod + commands + agentic + stream_recv + conversation + title + audit + skills + prompt + tool_registry + knowledge_inject),用 `pub use` 保持 `state.rs`/`lib.rs`/`knowledge.rs` 引用路径零改动。**采用低风险子 module 方案而非下沉到 df-ai crate**。
|
||||
- **原因/取舍**:① 2663 行单文件消耗大量 AI context(每次理解靠结构),与「人机协同设计基准」中「组件拆分从『不做』升为『值得做』」一致。② **选子 module 不选下沉 crate**——下沉到 df-ai 需动 crate 依赖图(df-ai 反向依赖 src-tauri 的 state/types),引入循环依赖风险 + 跨 crate 重构成本;子 module 仅在同 crate 内切目录,`pub use` 保持对外 API 不变,零引用路径改动,重构面最小。③ 子 module 仍能达成「职责单一」的 AI 可读性目标,与下沉 crate 收益相当但风险低一个数量级。
|
||||
- **实施验证(2026-06-14 落地)**:
|
||||
- 11 文件合计 2833 行,最大 knowledge_inject.rs 557 行(含测试),commands.rs 529 行(17 个 IPC 命令),其余均 < 400 行。
|
||||
- 路径契约全保:`commands::ai::{AiSession, build_ai_tool_registry, restore_pending_approvals, spawn_embedding_for_knowledge, trigger_extraction_now}` + 17 个 invoke 命令,state.rs/lib.rs/knowledge.rs/commands/mod.rs **零改动**。
|
||||
- `cargo check` 0 error,`cargo test ai::` 19 passed。剩 3 warning 全为预存非本次引入。
|
||||
- 顺带修 bug:`ai_conversation_list` 的 `models` 字段从 JSON 字符串直塞改为解析为 `Vec<String>` 数组下发(前端期望数组,原代码传字符串)。
|
||||
- **状态**:✅ 2026-06-14 落地(11 子 module + glob 重导出 + models bug 修复,cargo check/test 通过)
|
||||
|
||||
### 前端 ai.ts 拆分:路线A(store 留 state 单例 + composable),不引入 Pinia [2026-06-14]
|
||||
- **决策**:`src/stores/ai.ts`(758 行 god store)拆成 store 骨架(留 reactive state 单例 + 模块级私有变量)+ `src/composables/ai/` 下 6 个 composable(`useAiEvents`/`useAiStream`/`useAiSend`/`useAiConversations`/`useAiWindow`/`useAiPanel`)。`useAiStore()` 统一入口展开所有 composable 方法,**返回 shape 不变,组件零改动**。
|
||||
- **原因/取舍**:
|
||||
1. **与项目既有 store 风格一致**——project/knowledge/settings 全是「手写 reactive + 工厂函数」模式,引入 Pinia 会破坏一致性、增加心智负担。
|
||||
2. **组件零改动**——AiChat.vue/AiDetached.vue 等用 `const { state, sendMessage } = useAiStore()` 解构,保持 `useAiStore` 返回 shape 不变即可。
|
||||
3. **选路线 A(composable 挪逻辑、store 留 state)而非路线 B(Pinia 多 store)**——后者要改 19 个 state 字段归属和所有组件 import,风险远大于收益。
|
||||
4. **与后端 ai.rs 子 module 拆分配对**——前后端 god file/god store 同步拆解,采用各自生态的惯用拆法(Rust 子 module / Vue composable),不强求统一模式。
|
||||
- **影响**:确立项目 store 架构约定(手写 reactive + composable,不用 Pinia),影响所有未来 store 设计。
|
||||
- **状态**:🚧 执行中(workflow w20yb6n6b 编排,vue-tsc 自验证)
|
||||
|
||||
### AI trait 下沉:拆 df-ai-core 轻层,确立全局 AI 接入标准 [2026-06-14 📐]
|
||||
|
||||
- **决策**:将 `LlmProvider` trait + AI 数据类型(`ChatMessage` / `CompletionRequest` / `ToolDefinition` 等)从 `df-ai` 拆出,下沉到新轻量 crate `df-ai-core`(零 http 依赖);`df-ai` 保留为「实现 + `ModelRouter` + provider 工厂」hub(持有 `reqwest`/`openai_compat`/`anthropic_compat`);所有消费 crate(`df-ideas` 接对抗评估、未来 `df-knowledge`/`df-workflow` 等)**只依赖 `df-ai-core` 的 trait,不直接依赖 `df-ai`**;真实 provider 由 `src-tauri` 最上层装配注入。**F-260614-03 及后续所有 AI 接入点统一照此,不再 per-module 自定义 trait。**
|
||||
- **原因/取舍**(纯 ai-coding 工作模式下重算 F-03 原 A/B/C 选型):
|
||||
1. **砍「人审查的契约面」而非「打字量」**——一份全局 trait = 一份契约给人过目 + 规格契约 self-check 锚一点;per-module 自定义 trait(原 A)= N 份发散契约,审查负担与漂移风险同涨。纯 ai-coding 下接线/mock 全由 AI 吸收,故 A 的「适配器成本」、B 的「mock 成本」论点作废。
|
||||
2. **保住纯逻辑 crate 零摩擦自检**——`df-ideas`/`df-storage` 只依赖 trait 不拖 `reqwest`,规格契约机制 A/B 测试自动跑无 http 依赖;裸 B(df-ideas 直接依赖 df-ai)会污染纯逻辑 crate、自检摩擦上升。
|
||||
3. **与现有 roadmap 对齐**——`ModelRouter`(F-260614-01)、多 Provider 负载均衡池(F-260614-04)、provider 工厂(已落地)本就在 df-ai 内走「gateway」方向,消费方选型应顺此而非另起 N 个 trait。
|
||||
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 污染、自检摩擦上升。
|
||||
- **退路**:若不愿加新 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。
|
||||
|
||||
## 工作流人工审批节点(B-03)
|
||||
|
||||
### HumanNode 审批响应机制:subscribe→send→select! 广播过滤等待 [2026-06-14 📐]
|
||||
- **决策**:df-workflow `HumanNode.execute` 改为「先 `subscribe()` → 发 `HumanApprovalRequest` → `tokio::select!` 循环等 `HumanApprovalResponse`」,按 `execution_id + node_id` 双键过滤命中后返回 NodeOutput;`select!` 三分支 = 响应 / 超时(配置 `timeout_secs` 默认 3600s)/ 取消(500ms 轮询 `is_cancelled`)。复用既有 `EventBus`(broadcast) / `HumanApprovalResponse` 事件 / `approve_human_approval` IPC / 前端 store——零新增基础设施,仅改 HumanNode 一处。
|
||||
- **原因/取舍**:
|
||||
1. **订阅时序铁律**:tokio broadcast 不回放历史,必须 `subscribe()` 先于 `send(Request)`,否则 receiver 错过 Response 死等超时。
|
||||
2. **双键过滤**:node_id 单键不够(跨工作流可能重复)、execution_id 单键不够(同层多 HumanNode),双键才完备。
|
||||
3. **Lagged 容忍**:capacity 256 + 审批低频,漏自身 Response 概率极低;`continue` 优于丢弃(丢弃误判超时更糟)。
|
||||
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。
|
||||
- **边界**:取消分支在 B-07 + `set_cancelled` 补齐前恒 false(等价无取消,功能不残);跨工作流并发 HumanNode 在 B-06 修前有 Response 错配风险。
|
||||
- **状态**:📐 设计完成(2026-06-14),未实施。详见 [B-03-人工审批响应机制.md](./B-03-人工审批响应机制.md)。
|
||||
|
||||
## 需求与待办
|
||||
|
||||
> 汇集散落于各决策条目状态(📐/🚧)的待办 + 新增需求细节 + 需求澄清。单一清单,避免遗漏。
|
||||
|
||||
### 📋 待做需求
|
||||
|
||||
| 需求 | 功能域 | 来源 | 优先级 |
|
||||
|---|---|---|---|
|
||||
| Sprint 9/10 多项编译过未 tauri dev 实测(评分 IPC 缩放 / update_full / promote_idea / Store getter) | 灵感/立项/Store | Sprint 9–10 🚧 | P1 |
|
||||
| 切对话不中断路由:部分场景运行时实测 | AI Chat 可靠性 | Sprint 8 🚧 | P1 |
|
||||
| 技能联想「使用」:首批 3 类联想已做,联想后实际触发/执行技能未实现 | 技能/联想 | Sprint 8 | P2 |
|
||||
| 灵感对抗评估接 LLM:论点/evidence 由 df-ai LlmProvider 生成(现启发式 fallback) | 灵感模块 | Sprint 9 📐 | P2 |
|
||||
| 知识库 Tier 1:AI Chat ↔ 知识库双向闭环(沉淀+检索注入+reuse_count+审核收件箱+状态机+provenance 溯源+克制检索,无人工评分) | 知识库 | 2026-06-13 ✅ 已实施(Sprint 15) | P1 |
|
||||
| 路径校验根治:workspace 白名单 + canonicalize(现仅拒 `..` + 敏感目录) | 工具调用 | Sprint 6 | P2 |
|
||||
| 停止生成 idle 即时优化:`tokio::sync::Notify` 替代 120s 轮询 | AI Chat 可靠性 | Sprint 6 | P3 |
|
||||
| 多 Provider 负载均衡池(备用模型/多账号聚合,全局容量=min(各 provider 上限之和, global_cap)) | AI Chat 并发控制 | 2026-06-13 📐 | P2 |
|
||||
| 裁剪/压缩消息按需召回(Query Function + 分层存储: TrimRecord 追踪被移除范围 → DB 全量归档按需检索 → 精准注入 build_for_request;触发方式待定:自动/手动/语义检索) | 上下文窗口管理 | 2026-06-13 📐 | P3 |
|
||||
| ✅ 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 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 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 |
|
||||
| 🟡 promote_idea 两步写非事务:INSERT project 成功后若 UPDATE idea 失败,项目存在但想法状态未变,补偿删除可修 | 灵感/立项 | 2026-06-13 代码审查 | P1 |
|
||||
| 🔴 分离窗口(detached)跨窗口状态失效:用 localStorage 传递生成态快照(df-ai-gen/text),Tauri 多 webview 不共享 localStorage 致静默失效;用户点 X 关闭(非 closeDetachedWindow)后主窗口 `detached` 永真卡死(reattachPanel 死代码未接线)。需改 Tauri 全局 emit/listen 同步 + 窗口销毁事件复位 | AI Chat 分离窗口 | 2026-06-13 代码审查 | P1 |
|
||||
| 模型能力声明与自动路由系统 Phase 1:ModelCapability 数据模型 + ModelRouter 重写 + 7 调用点接入 + Settings 模型池编辑 UI + AiChat 模型下拉。核心:按任务需求(模态/功能/成本)自动匹配合适模型,不再所有场景共用 default_model | 模型能力与路由 | 2026-06-13 📐 设计完成 | P1 |
|
||||
| 模型能力系统 Phase 2:多模态消息支持——ChatMessage.content: String → Vec<ContentPart>(Text/Image);前端粘贴/拖拽图片;vision 模型自动路由 | 模型能力与路由 | 2026-06-13 📐 | P2 |
|
||||
| 模型能力系统 Phase 3:Agent 内智能路由——Agentic Loop 每轮按子任务构造不同 TaskRequirements;成本预算控制;模型级联降级;跨 Provider 搜索 | 模型能力与路由 | 2026-06-13 📐 | P3 |
|
||||
| 📋 待澄清「显示多开」:用户报"设置里勾选'显示多开'但 AiChat 未显示"。全 src grep `多开\|多窗口\|multi\|multiInstance` 零命中;Settings.vue `settings` 对象仅 8 字段无此项。AiChat 唯一相关的是常驻「分离窗口」按钮(不受设置控制)。疑似:① 用户指分离窗口按钮(本就常驻不需设置);② 看的是打包旧版本界面;③ 想新增"允许分离窗口"设置开关。待用户截图/确认位置再定 | AI Chat | 2026-06-13 需求澄清 | — |
|
||||
| 🔴 待审批持久化根治(重启恢复)未生效——两处逻辑断裂致恢复链路跑不通:① `ai_conversation_switch` 无条件 `pending_approvals.clear()` 清空 `restore_pending_approvals`(init)重建的内存 HashMap,而 `ai_pending_tool_calls`/`ai_approve` 均依赖内存态 → 重启后前端 `switchConversation` 触发 clear → 审批卡片查空永不显示、审批报"未找到挂起的审批";② `ai_approve` 的 recovered 守卫跳过 `save_conversation`(注释称"防空 messages 污染老对话")前提不成立——switch 时 `restore_from_messages` 已载完整历史,审批时 messages 非空 → 执行的工具结果不落库,重启后 toolCard 显示 completed 但 result 仍是占位"需要用户审批,等待确认"。修复方向:pending 恢复链路改查 DB(`ai_tool_executions` WHERE status='pending' 持久化真相源)绕过内存 clear;`ai_approve` 内存 miss 时 fallback DB 单条重建再执行;recovered 审批通过后正常 save。可顺带删 `restore_pending_approvals`(DB 即真相源)。阻断用户"功能逻辑层面解决"诉求——现"根治"实为表面修复 | AI Chat 审批持久化 | 2026-06-13 /review 审查①② | P0 |
|
||||
| 📋 审批可见性缺口:pendingApprovals 无兜底渲染→卡死 [2026-06-13]:AI 发起 Med/High 工具审批(AiApprovalRequired)后暂停等审批不发 delta;前端审批唯一出口是 ToolCard 的 pending_approval 内联卡片(靠 findToolCall 置 tc.status),但 state.pendingApprovals 数组有数据却零渲染(AiChat.vue 仅 @approve 转发,无 pendingApprovals 模板)。若 tc 卡片未显示审批,用户看不到审批按钮 → AI 永久等 → 文字停卡死。待修:A. AiChat.vue 加 pendingApprovals 醒目渲染(顶部条/浮层)兜底审批可见性;或 B. 运行时确认 tc 卡片是否渲染。配套:watchdog 在 AiApprovalRequired 暂停,审批没弹则 watchdog 盲点,需加"审批超时未响应"提示 | AI Chat 审批 | 2026-06-13 | 📋 A/B 待定 |
|
||||
| 📋 node_executions 全表 list:当前只写不读,若未来前端要看某次工作流执行的节点明细,需**新增** `list_node_executions(execution_id)` 命令 | 工作流引擎 | 2026-06-13 代码审查 | 📐 待需求驱动 |
|
||||
|
||||
### 📋 需求澄清
|
||||
|
||||
- **「决策」术语边界**(2026-06-12):devflow 语境「决策/决策需求点」= 日常开发功能细节取舍(为什么这么定),**非** aichat 决策能力升级(B 路线 coordinator/conditions)。后者属架构层,记 Phase2计划/模块文档,不混入本文档。
|
||||
- **代码审查甄别原则**(2026-06-13):审查发现问题时,按「运行时失败/数据损坏 → 简单清理 → 记录不动 → 不做」四档甄别。当前项目规模下,list_all 无 LIMIT、ALLOWED_COLUMNS 不分表、bool→int 重复等属「记录不动」——个人工具表不超千行,加分页/拆白名单是过度设计,维护成本 >> 收益。原则:**真实 bug 修、简单清理做、规模不到位的优化先不动**,保持全局简洁和扩展容易。
|
||||
|
||||
**相关文档**:
|
||||
- [Phase 1 架构决策](./Phase1架构决策.md) — 架构级决策(ADR)
|
||||
- [经验记录](./经验记录.md) — 踩坑/约定/技巧/bug 排查教训
|
||||
- [功能决策记录-归档](./功能决策记录-归档.md) — 纯流水/老 Sprint/UX 微调/已被取代
|
||||
- `PROGRESS.md` — 各 Sprint 工作流水与遗留
|
||||
- [Phase 2 计划](../07-项目管理/Phase2计划.md)
|
||||
167
docs/02-架构设计/文档记录规范.md
Normal file
167
docs/02-架构设计/文档记录规范.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# 文档记录规范
|
||||
|
||||
> 写文档 / 更新文档时的**路由规则**:记到哪、优先写哪、怎么避免重复。
|
||||
> 创建:2026-06-12 | 维护:文档结构变化时同步
|
||||
|
||||
---
|
||||
|
||||
## 一、核心原则
|
||||
|
||||
1. **单一真相源(SSOT)**:每类信息只在一个主文档展开,别同一内容抄多处。
|
||||
2. **不复制,只引用**:他处需要时加 `[详情](链接)`,不抄正文。
|
||||
3. **决策与流水分离**:决策记「为什么这么定」,流水记「做了啥」。别混。
|
||||
4. **先主后辅**:同一变更涉及多处 → 先写真相源,再在引用处加链接。
|
||||
5. **决策记录范围收紧 — 只记人定事实,不记大模型分析结论**(2026-06-14,📐 基准原则):
|
||||
- 决策记录**只记**与大模型能力无关的人定事实 — 业务需求规格 / 人定技术选型 / 人的设计取舍。
|
||||
- **不记**大模型分析/推断结论(性能瓶颈 / 根因 / 最优架构 / 排查结果) — 这些按需让当时的模型即时产出,不沉淀。
|
||||
- 原因:大模型分析结论受当前模型能力天花板约束,模型逐月变强,今天的「最优分析」明天会被更强模型超越 → 记录过时;沉淀 = 固化次优解,阻碍未来用更强模型即时得出更优解。即使埋点/实测「验证」了,也不改其受能力天花板约束、会随模型升级被超越的本质。
|
||||
|
||||
---
|
||||
|
||||
## 二、文档职责矩阵(真相源)
|
||||
|
||||
| 文档 | 唯一职责(记什么) | 不记什么 |
|
||||
|---|---|---|
|
||||
| `PROGRESS.md`(根级) | 工作流水:Sprint 做了啥 / 遗留 / 下一步 | 决策原因、实现细节、需求规格 |
|
||||
| `ARCHITECTURE.md` | 系统架构全貌:crate 结构 / 数据模型 / Phase 规划 | 功能点取舍、Sprint 流水 |
|
||||
| `02-架构设计/Phase1架构决策.md` | 架构级选型(ADR,系统级) | 功能实现层取舍 |
|
||||
| `02-架构设计/功能决策记录.md` | 功能**需求规格 + 设计决策规格**(为什么这么定 + 要做什么) | 流水、架构级选型、经验性内容 |
|
||||
| `02-架构设计/经验记录.md` | 经验性内容(踩坑/约定/技巧/bug 排查教训) | 决策、需求、流水 |
|
||||
| `02-架构设计/功能决策记录-归档.md` | 纯流水/老 Sprint/UX 微调/已被取代(归档只读) | (不再维护更新) |
|
||||
| `03-模块文档/*.md` | 各 crate 实现细节(单模块内) | 跨模块决策、流水 |
|
||||
| `04-功能迭代/DEVFLOW-N.*.md` | 功能开发过程记录(一次性,开发期) | 持续维护的决策 |
|
||||
| `05-代码审查/*.md` | 审查报告与发现 | (若成决策 → 转记功能决策记录) |
|
||||
| `06-前端开发/*.md` | 前端规范 / 迁移指南 | 后端实现 |
|
||||
| `07-项目管理/*.md` | Phase 计划 / 任务清单 | 实现流水(那是 PROGRESS) |
|
||||
| `08-用户指南/*.md` | 用户手册 / 配置 / FAQ | 内部实现细节 |
|
||||
| `01-技术文档/*.md` | 技术专题研究(CRUD 模式、IPC 模式) | 业务功能 |
|
||||
|
||||
---
|
||||
|
||||
## 三、内容路由表(写东西先查这个)
|
||||
|
||||
| 你要记的内容 | 主文档(优先写) | 按需交叉引用 |
|
||||
|---|---|---|
|
||||
| 本 Sprint 做了啥 / 遗留 | `PROGRESS.md` | `04-功能迭代/`(详过程) |
|
||||
| 为什么这么实现(选 A 不选 B) | `功能决策记录.md` | 模块文档、PROGRESS |
|
||||
| 踩坑 / 约定 / 技巧 / bug 排查教训 | `经验记录.md` | 功能决策记录 |
|
||||
| 架构级选型 | `Phase1架构决策.md` / `ARCHITECTURE.md` | — |
|
||||
| 新需求 / 待办 / 功能规格 | `功能决策记录.md`(需求维度) | `Phase2计划.md` |
|
||||
| 对话中需求澄清(原以为 X 实为 Y) | `功能决策记录.md`(需求澄清) | — |
|
||||
| 老 Sprint 决策 / UX 微调 / 已被取代 | `功能决策记录-归档.md` | (归档只读,不再维护) |
|
||||
| 单模块实现细节 | `03-模块文档/<对应>.md` | — |
|
||||
| 代码审查发现 | `05-代码审查/` | 转决策 → `功能决策记录.md` |
|
||||
| 前端规范变更 | `06-前端开发/` | — |
|
||||
| 用户操作说明 | `08-用户指南/` | — |
|
||||
|
||||
---
|
||||
|
||||
## 四、更新顺序(同一变更涉及多处)
|
||||
|
||||
1. **真相源先写完整**(按路由表的主文档)
|
||||
2. **PROGRESS 记一笔 + 链接**(流水 + 指向详情)
|
||||
3. **引用处加交叉链接**,不抄正文
|
||||
|
||||
**例**:做了「shouldKeepOpen 折叠」
|
||||
- 真相源:`功能决策记录.md` 写决策 / 原因 / 状态 ✅
|
||||
- 流水:`PROGRESS.md` 记「审查①已落地」+ 链接到功能决策记录
|
||||
- **不**在模块文档 / ARCHITECTURE 重复抄决策正文
|
||||
|
||||
---
|
||||
|
||||
## 五、唯一性记录与检测(防散乱)
|
||||
|
||||
**核心要求:每类信息一个主文档,不散乱、不重复。** 文档治理底线。
|
||||
|
||||
### 唯一真相源
|
||||
|
||||
见「二、文档职责矩阵」——每类信息的唯一主文档。
|
||||
|
||||
### 检测方法
|
||||
|
||||
**1. 记前查重(每次记录时)**
|
||||
- 记决策/需求前,先 grep 查该点是否已存在:
|
||||
```bash
|
||||
grep -rl "<关键词>" docs/ PROGRESS.md ARCHITECTURE.md
|
||||
```
|
||||
- 已存在 → 更新原条,不新增(见 decision-record skill「维护:查重」)
|
||||
|
||||
**2. 交叉引用单向(禁双向复制)**
|
||||
- 主文档(真相源)展开内容,引用方只放 `[详情](链接)`
|
||||
- ❌ A 写决策正文,B 又抄一遍 → ✅ B 只链接 A
|
||||
|
||||
**3. 配合交接文档(PROGRESS)**
|
||||
- PROGRESS 是**交接文档**,只记「做了啥 + 链接」,不展开决策/需求正文
|
||||
- 决策正文 → `功能决策记录.md`;需求 → `功能决策记录` 的「需求与待办」
|
||||
- 交接路径:读 PROGRESS 知进度 → 读 `功能决策记录` 知「为什么 + 要做什么」→ 读模块文档知「怎么实现」
|
||||
|
||||
**4. 定期唯一性扫描(防积累散乱)**
|
||||
- 时机:文档结构变化 / 新增文档 / 每个 Sprint 末
|
||||
- 方法:对关键决策点跨文档 grep,确认只在主文档展开
|
||||
```bash
|
||||
grep -rl "shouldKeepOpen\|connect_timeout" docs/ PROGRESS.md
|
||||
```
|
||||
- 发现散乱 → 合并到主文档,他处改链接
|
||||
|
||||
### 不散乱红线
|
||||
|
||||
- 同一决策**不**同时进 `功能决策记录` 和 `Phase1架构决策`(功能层 vs 架构层二选一)
|
||||
- 同一需求**不**同时在 `功能决策记录·需求与待办` 和 `Phase2计划` 展开(一处为主,一处链接)
|
||||
- PROGRESS**不**抄决策正文,只记「做了 + 链接」
|
||||
|
||||
---
|
||||
|
||||
## 六、与 decision-record skill 的关系
|
||||
|
||||
`decision-record` skill 触发时,按本规范路由:
|
||||
|
||||
- **决策 / 需求** → `功能决策记录.md`(主,真相源)
|
||||
- skill 执行后 → `PROGRESS.md` 加一笔流水 + 链接(可选,重大决策才加)
|
||||
|
||||
Stop hook 触发 skill 时同理,不另立记录位置。
|
||||
|
||||
---
|
||||
|
||||
## 七、治理体系实现决策(hook 设计)
|
||||
|
||||
本规范 + `decision-record` skill + 降频 Stop hook 构成文档治理体系。hook 设计取舍:
|
||||
|
||||
### 降频 Stop hook(替代 PreCompact / 每轮自检)
|
||||
- **决策**:用 `~/.claude/hooks/dr-check.sh`(settings.json 配 Stop hook)按阈值注入自检提示,触发 `decision-record`。
|
||||
- **原因**:PreCompact hook **只读输入、无法注入 prompt** 触发 skill;Stop hook 支持 `additionalContext` 注入。每轮 Stop 自检消耗大且打断;降频用纯脚本计数**无 API**,省 ~90% token。
|
||||
- **状态**:✅ 2026-06-12
|
||||
|
||||
### 触发阈值:≥20 轮 或 (≥2 轮 且 ≥20 分钟)
|
||||
- **决策**:累计 ≥20 轮,或 (>1 轮 且 距上次 ≥20 分钟) 才触发;首次运行静默初始化(计时,本轮不触发)。
|
||||
- **原因**:10 轮约一个功能点推进周期;10 分钟兜底防长对话漏记;兼顾及时与不打扰。
|
||||
- → 2026-06-14 阈值翻倍(10→20 轮 / 10→20 分钟)。原阈值触发过频,多数自检轮次无实质决策;翻倍减半打扰。✅ 落地(dr-check.sh)
|
||||
|
||||
### 防循环:stop_hook_active guard
|
||||
- **决策**:hook 检测输入 `stop_hook_active=true` → 直接 `exit 0` 放行。
|
||||
- **原因**:Stop hook 注入 additionalContext 会触发主 Claude 继续 → 再次 Stop → 无限循环;guard 放行第二轮(因 hook 继续的)。
|
||||
- **状态**:✅
|
||||
|
||||
### 状态按项目隔离
|
||||
- **决策**:轮次计数 + 上次触发时间戳存 `~/.claude/.dr-state/<项目key>.rounds|.last`(键由路径转义),不落项目目录。
|
||||
- **原因**:多项目独立计数不串;不污染 git 仓库。
|
||||
- **状态**:✅
|
||||
|
||||
### 决策记录执行子代理化(2026-06-14)
|
||||
- **决策**:stop hook 自检触发后,记录动作 spawn 后台子代理执行(项目 `decision-recorder` 子代理,位于 `devflow/.claude/agents/`),主代理不亲自 grep/写文档。
|
||||
- **原因**:避免记录动作(grep/读写文档)污染主对话上下文、打断主流程;记录规范固化进子代理 system prompt,主代理只传决策内容。
|
||||
- **状态**:✅ 落地(dr-check.sh 的 CTX 已改为指令 spawn 子代理)
|
||||
|
||||
---
|
||||
|
||||
## 八、待修(文档不一致)
|
||||
|
||||
- ~~`docs/INDEX.md` 在 `07-项目管理/` 树下登记了 `PROGRESS.md`,但实际 PROGRESS 只在根级,`07-项目管理/` 下无此文件~~ → ✅ 已修(2026-06-12):移除该行,PROGRESS 统一指向根级。
|
||||
|
||||
---
|
||||
|
||||
**相关文档**:
|
||||
- `docs/INDEX.md` — 文档导航
|
||||
- `docs/02-架构设计/功能决策记录.md` — 需求规格 + 设计决策规格(本规范的主要应用对象)
|
||||
- `docs/02-架构设计/经验记录.md` — 踩坑/约定/技巧/bug 排查教训
|
||||
- `docs/02-架构设计/功能决策记录-归档.md` — 归档只读(纯流水/老 Sprint/UX 微调)
|
||||
- `PROGRESS.md` — 工作流水
|
||||
140
docs/02-架构设计/经验记录.md
Normal file
140
docs/02-架构设计/经验记录.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 经验记录
|
||||
|
||||
> DevFlow 开发中沉淀的**经验性内容**——踩坑、约定、技巧、bug 排查教训。聚焦「这个坑怎么踩的 / 这个约定为什么这么定 / 这个 bug 怎么定位的」,区别于 [功能决策记录](./功能决策记录.md)(记需求规格 + 设计决策规格)。
|
||||
>
|
||||
> 创建:2026-06-14(从功能决策记录.md 分流出经验性条目) | 维护:随开发追加
|
||||
|
||||
## 约定
|
||||
|
||||
- 按类型分组:**踩坑**(隐性坑/反直觉)/ **约定**(代码实现约定 / 命名约定)/ **技巧**(具体技巧/配置)/ **bug 排查**(bug 定位过程与教训)。
|
||||
- 每条标题标 `[来源日期]` + `[Sprint]`(如有),便于回溯原上下文。
|
||||
- 三要素:**现象/决策** → **原因/根因** → **状态/教训**。
|
||||
- 与功能决策记录区分:这里记「怎么实现的细节坑」,不记「为什么这么设计」。
|
||||
|
||||
---
|
||||
|
||||
## 一、踩坑
|
||||
|
||||
### i18n 模块必须命名空间化导出(扁平导出会断 $t + 键覆盖)[2026-06-14]
|
||||
|
||||
- **现象**:左侧菜单显示 `'nav.tasks'`(原样键名);`dashboard` 整页显示 key 名;`AiChat` 的 `$t('ai.assistant')` 失效。
|
||||
- **决策**:每个 i18n 模块文件 `export default { 命名空间: {...} }`(如 `nav.ts` → `{ nav: {...} }`),**禁止扁平导出顶层词条**。模板查询走 `$t('命名空间.key')`。
|
||||
- **根因**:`index.ts` 聚合是 `Object.assign` 扁平合并各模块顶层 key(见「locale 拆分 + glob 聚合」决策)。扁平导出导致两个 bug:① `$t('nav.tasks')` 找 `messages.nav` 不存在,原样显示键名;② 扁平键(如 nav 的 `ideas`/`projects`/`tasks`/`knowledge`)与同名命名空间模块(ideas.ts/projects.ts/...)按文件名字母序互相覆盖。本次 nav.ts 扁平导出导致 4 个键被覆盖。
|
||||
- **状态/教训**:✅ 系统性修复,共 4 模块扁平已全部改嵌套(zh/en 8 文件):nav / common(8 文件 25 处 $t 引用)/ dashboard / ai。原本正确嵌套:ideas/projects/tasks/settings/knowledge/projectDetail/aiChat。**教训**:扁平导出是体系性 bug 非单点。排查「$t 显示原样键名」时应**优先怀疑模块导出结构(扁平 vs 嵌套)**,而非 SSR / locale 初始化。
|
||||
- **绕路纠错**:曾误判根因为 SSR(实际 Tauri 纯客户端无 SSR)→ nav 走 `getNavTranslations` 硬编码 map + displayText 绕路 → 清除绕路恢复标准 `$t`。
|
||||
|
||||
---
|
||||
|
||||
## 二、约定
|
||||
|
||||
### Anthropic 流式 output_tokens 当累计值直接覆盖 / 流式 token 落库走累加模式 [2026-06-13]
|
||||
|
||||
- **决策**:① `message_delta` 事件的 output_tokens 直接覆盖 completion_tokens,**不像 prompt 那样累加**;② `save_conversation` upsert 路径 token 读旧值叠加(非覆盖);`run_agentic_loop` 局部累加器每轮叠加、退出时一次性传 save。
|
||||
- **原因**:① Anthropic 协议在 `message_delta` 返回的是**累计** output_tokens(截至当前总量),非增量;当增量处理会重复计算。OpenAI 则是末 chunk 一次性给全量——两协议语义不同,各自处理。② 审批暂停→恢复 spawn 全新 `run_agentic_loop` 实例,新 loop 局部累加器从 0 起;若覆盖写会丢旧 loop 已落库的 token。累加保证跨 loop 实例的对话总用量正确。
|
||||
- **状态**:✅ 2026-06-13(两协议各自语义处理 + 跨 loop 累加保对话总量)
|
||||
|
||||
### db 字段加列须同步四处:migration + crud 白名单 + AI 工具层白名单 + 工具描述 [2026-06-14]
|
||||
|
||||
- **现象**:AI 对话让 AI 绑定目录,update_project(path) 报「不允许更新字段 'path'」,但 db schema 和 crud 白名单都已有 path。
|
||||
- **根因**:可更新字段有两套独立白名单——crud.rs::allowed_columns_for(DB 层)+ ai.rs 工具闭包硬编码 match(AI 工具层)。加 path/stack 时只同步 DB 层漏 AI 工具层,两层不一致。
|
||||
- **教训**:加 Record 可变字段同步四处(migration + crud 白名单 + ai.rs 工具白名单 + 工具描述)。排查「DB 有字段但工具报不允许」直查 ai.rs 硬编码。架构债:白名单双份去重。
|
||||
|
||||
### Tauri 命令文件拆子 module:命令函数必须 glob `pub use *`,不能逐个显式 [2026-06-14]
|
||||
|
||||
- **现象**:把含 `#[tauri::command]` 的单文件(如 ai.rs)拆成 `ai/` 子 module 时,mod.rs 用 `pub use self::commands::{ai_chat_send, ...}` 逐个显式重导出 17 个命令,`cargo check` 报 40 个 E0433:`cannot find __cmd__ai_chat_send in ai` / `cannot find __tauri_command_name_ai_chat_send in ai`。
|
||||
- **根因**:`#[tauri::command]` 宏不只生成命令函数本身,还用 `paste!` 宏拼接生成一组同模块定义的内部符号(`__cmd__xxx`、`__tauri_command_name_xxx`)。`generate_handler!` 解析 `commands::ai::ai_chat_send` 时会查找 `commands::ai::__cmd__ai_chat_send`。逐个 `pub use self::commands::{ai_chat_send}` 只拉函数本身,**拉不到这些 `__cmd__` 内部符号**(即使它们在原模块是 pub 的)。
|
||||
- **教训**:拆命令文件时,mod.rs 重导出命令必须用 `pub use self::commands::*;`(glob 把宏生成的全部符号一起拉到上层路径),不能用逐个显式。非命令 pub 项(如 `build_ai_tool_registry`/`restore_pending_approvals`)可逐个显式。后续若拆 idea.rs/project.rs/task.rs 等其他含命令的大文件,同此模式。
|
||||
- **状态**:✅ 2026-06-14 验证(ai.rs 拆 11 子 module,glob 重导出后 cargo check 0 error)
|
||||
|
||||
### 跨层模块拆分:`super::xxx` 路径失效需改全限定 [2026-06-14]
|
||||
|
||||
- **现象**:ai.rs(commands 直接子模块)拆到 `ai/xxx.rs`(commands 孙模块)后,6 个子文件 `use super::now_millis` 全报 E0425 unresolved import。
|
||||
- **根因**:`super` 指向当前模块的父——ai.rs 时 `super` = `commands`(`now_millis` 定义处);拆到 `ai/xxx.rs` 后 `super` = `commands::ai`,`now_millis` 在祖父模块 `commands`。
|
||||
- **教训**:拆层后所有 `super::xxx` 引用需重审。父模块的 helper(如 `now_millis`)改全限定 `crate::commands::now_millis` 最稳(不依赖层级)。或拆层前把 helper 下沉到子 mod.rs 内 `use` 一次,子文件用 `super::xxx`。
|
||||
- **状态**:✅ 2026-06-14 验证(批量改 `crate::commands::now_millis`,6 文件 20+ 处)
|
||||
|
||||
### 删文件后被 linter/工具重建为 0 字节触发 E0761 [2026-06-14]
|
||||
|
||||
- **现象**:`rm commands/ai.rs` 后某 linter/hook 又建了 0 字节的 ai.rs,触发 `E0761: file for module ai found at both ai.rs and ai/mod.rs`,且 Rust 优先选空文件导致后续 40 个 `cannot find __cmd__xxx`(与 glob 重导出坑叠加,表象一致根因不同)。
|
||||
- **教训**:拆分时删原文件后**立即 ls 验证不存在**再跑 cargo check,避免空文件 + 目录并存的 E0761 与命令宏符号坑混淆。E0761 出现先查是否有 0 字节残留文件。
|
||||
- **状态**:✅ 2026-06-14 验证(删空 ai.rs 后通过)
|
||||
|
||||
### ALLOWED_COLUMNS 从全局共享演进为按表隔离 [2026-06-13]
|
||||
|
||||
- **决策**:`crud.rs` 列名白名单从单一全局 `ALLOWED_COLUMNS` 改为 `allowed_columns_for(table)` 按表 match;`validate_column_name(field, table)` 接收表名;宏 `query`/`update_field` 传 `$table`。专用更新路径列(knowledges.embedding 走 set_embedding、projects.deleted_at 走 soft_delete/restore)排除出白名单。
|
||||
- **演进原因**:原原则(见功能决策记录需求澄清「代码审查甄别原则」)基于「全局白名单够防注入」。本轮多代理代码审查发现**真实 bug**:全局白名单**误含 ideas 表没有的 `reasoning` 列**(reasoning 属 knowledges/V10),`update_idea("reasoning")` 会 validate 通过但 SQLite 报 `no such column`——错误从「白名单拒绝」退化成「底层 SQL 错」且语义错。按表隔离既修此 bug(ideas 白名单不含 reasoning)又防未来跨表字段(update_task 误传 projects 的 `name` 在校验阶段拒绝,非靠 SQL 兜底)。
|
||||
- **代价/取舍**:12 表 × N 列的 match 冗长,但数据驱动、可读、一次写对。**规模判断不变**(仍不加分页/不拆 LIMIT),仅白名单从「全局防注入」升级为「按表防注入 + 防跨表字段」。
|
||||
- **状态**:✅ 2026-06-13 落地(cargo check + df-storage 32 test 全绿,含 `update_field_rejects_cross_table_column_tasks_name` 用例验证跨表字段被拒)
|
||||
|
||||
### 配置存储:SQLite/AppState Arc<Mutex>,非 Tauri app config [2026-06-13]
|
||||
|
||||
- **决策**:KnowledgeConfig(提取+注入共 5 项)**存 AppState 内存**(`knowledge_config: Arc<Mutex<KnowledgeConfig>>`),前后端通过 `knowledge_get_config`/`knowledge_save_config` IPC 读写;**不引入 tauri-plugin-store**。
|
||||
- **演进**:[2026-06-13 初版设计] 写「存 Tauri app config」 → [2026-06-13 审查修正] 代码实证项目 Cargo.toml 仅 opener+window_state 两插件,**从未用过 config/store 机制**;现有设置走两条路(SQLite 存 provider / localStorage 存 UI 偏好) → [2026-06-14] **`SettingsRepo` 兑现本条预言**:`app_settings` KV 表(V13 迁移)+ 手写 `SettingsRepo`(get/set/get_all/delete,不走 `impl_repo!` 宏因 KV 无固定 schema)。localStorage 11 key 迁移启动:敏感 `df-connections` + UI 偏好(theme/language/ai-width/ai-ui/token/concurrency) + `df-ai-active-conv`;例外 `df-ai-gen`/`df-ai-text`(流式临时快照,每个 delta 写一次,SQLite 高频写拖慢流式,留 localStorage)。
|
||||
- **原因**:AI Provider 配置已是 SQLite+Repo+IPC 模式,知识库行为配置(后端行为,非 UI 偏好)对齐同模式最一致。引入 tauri-plugin-store 是全新基础设施依赖,与既有 DB 路线割裂。AppState Arc<Mutex> 内存持有 + IPC 读写,启动时 `default()` 初始化(Tier 1 未持久化到 DB,进程重启回默认——够用,因这是行为偏好非数据)。未来要持久化时复用同一套 SettingsRepo 即可。
|
||||
- **状态**:✅ 已实施(Tier 1)
|
||||
|
||||
### 知识删除语义:knowledge_archive 软删除(命名统一)[2026-06-13]
|
||||
|
||||
- **决策**:知识删除 command 命名 `knowledge_archive`(执行 `UPDATE status='archived'`),**不叫 knowledge_delete**。匹配 `ai_conversation_archive` 先例;主列表 `knowledge_list(status=None)` 默认 `AND status!='archived'` 过滤。
|
||||
- **原因/取舍**:idea/task/project 的 `delete_xxx` 都是硬删(DELETE FROM),若 knowledge 也叫 delete 却做归档,API 语义混淆(调用方期望数据消失,实际还在 DB)。conversation 模块已有正确先例(archive 命名表示软删除)。软删除复用 archived 状态,数据保留可追溯,列表默认过滤保证用户感知「已删除」。状态机 published→archived 也走同一路径。
|
||||
- **状态**:✅ 已实施(Tier 1)
|
||||
|
||||
### Store 状态字段用 getter 替代引用快照 [Sprint 10]
|
||||
|
||||
- **决策**:`useProjectStore()` 返回对象的状态字段(projects/tasks/ideas/workflowExecutions/liveEvents/loading/error)改 getter 实时读 state,而非 `ideas: state.ideas` 引用快照。
|
||||
- **原因**:引用快照在 `loadIdeas()` 等重新赋值 state.ideas 后,返回对象的 ideas 属性不更新(刷新后视图空,需切菜单再切回才显示);getter 每次读 state,响应链成立。computed(stats/pendingApproval)在 reactive 内仍自动解包,各视图用法零改动。
|
||||
- **状态**:🚧 Sprint 10(编译/构建通过,未 tauri dev 实测,根因通杀 Projects/Tasks/Dashboard)
|
||||
|
||||
---
|
||||
|
||||
## 三、技巧
|
||||
|
||||
### migrate_v4:PRAGMA table_info 探测列存在性 [Sprint 10]
|
||||
|
||||
- **决策**:v4 加 `archived` 列时,用 `PRAGMA table_info` 幂等探测列是否已存在,而非仅依赖 `schema_version` 版本号 gate。
|
||||
- **原因**:历史坏库 `schema_version` 值混乱(早期迁移异常致版本号与实际 schema 不符),版本号不可靠;直接探列存在性最稳——已存在则跳过,不存在则补建,幂等可重入。
|
||||
- **状态**:✅ Sprint 10
|
||||
|
||||
### Vite 端口 `strictPort: true` 不自动迁移 [Sprint 1]
|
||||
|
||||
- **决策**:`vite.config.ts` 设 `port: 1420` + `strictPort: true`,端口被占时**直接报错退出**而非自动 +1 迁移;`tauri.conf.json` 的 `devUrl` 写死 `http://localhost:1420`。
|
||||
- **原因**:Tauri webview 启动时按 `devUrl` 加载前端,若 Vite 因冲突静默迁移到 1421 而 devUrl 仍是 1420 → 白屏/连不上,错误难定位(易误判为前端代码 bug)。`strictPort` 让端口冲突当场炸出,定位明确。代价:1420 被占需手动杀进程,但换取「devUrl 与实际端口必一致」的不变量。
|
||||
- **状态**:✅ Sprint 1(本次会话核对:1420 vs 2661 反复折腾后回退到 1420,即此耦合的直接体现)
|
||||
|
||||
---
|
||||
|
||||
## 四、bug 排查
|
||||
|
||||
### ai_tool_executions 审计回写失效(Med/High 审批后卡 pending)[#54 实测]
|
||||
|
||||
- **现象**:用户审批 Med/High 工具后执行成功(副作用落库,如 create_project→projects 有记录),但 `ai_tool_executions` 仍 `status=pending / decided_by=None / executed_at=None / result=None`,审计未闭环。Low 工具正常(`decided_by=auto` 完整)。
|
||||
- **根因(代码层定位)**:`crud.rs:103` 宏 `impl_repo!` 生成的通用 `query` 硬编码 `ORDER BY created_at DESC`,但 `ai_tool_executions` 表**无 `created_at` 列** → `audit_finalize` 的 `query("tool_call_id", x)` SQL 报 `no such column: created_at` → `.unwrap_or_default()` 吞错返回空 → `if let Some(rec)` 为 None → **永不回写**。Low 工具不走 query(`process_tool_calls` Low 分支直接 `audit_tool_call` insert 完整记录)故不受影响。
|
||||
- **架构隐患**:通用 `query` 的 `ORDER BY created_at` 假设所有表都有该列——`ai_tool_executions`(及潜在其他无 `created_at` 的表)任何 `query()` 调用都静默失败;`unwrap_or_default` 吞 SQL 错误放大隐患。
|
||||
- **修复**:✅ 已落地(2026-06-13)。采用方向①:`crud.rs` 给 `AiToolExecutionRepo` 加专用 `find_by_tool_call_id`(裸 SQL `ORDER BY requested_at DESC LIMIT 1`,绕过宏的 `created_at` 假设);`ai.rs audit_finalize` 改用之,查不到记录改 `tracing::warn`(不再 `unwrap_or_default` 静默吞错)。
|
||||
- → 未改宏(方向②影响 7+ 表)/ 未加列(方向③需迁移):隐患仅 `ai_tool_executions` 一处暴露,局部修最小影响。
|
||||
- → **架构隐患仍存(未根治)**:通用 `query`/`list_all` 宏对无 `created_at` 的表(`ai_tool_executions`/`node_executions`/`workflow_executions`)调用仍静默失败。当前仅 `ai_tool_executions` 有 `query` 调用且已绕开,余者暂无 `query` 调用点。未来新增调用时,要么该表登记 `created_at`,要么宏做容错。
|
||||
- **教训**:宏生成的通用方法对表 schema 的隐式假设(这里「所有表都有 created_at」)是隐蔽的系统性风险;`unwrap_or_default()` 吞错误让 bug 隐形——关键路径慎用。
|
||||
|
||||
### reasoning 字段回填(修 bug:prompt 要求但写库丢弃)[2026-06-13]
|
||||
|
||||
- **现象/决策**:`KnowledgeRecord` 加 `reasoning: Option<String>`(V10 ALTER),`extract_knowledge_from_conversation` 解析 LLM JSON 的 `reasoning` 字段写入主表;前端详情溯源区展示「🤖 AI 判断依据」。
|
||||
- **根因**:`EXTRACTION_SYSTEM_PROMPT` 早已要求 LLM 输出 `reasoning: "为何值得沉淀"`,但提炼循环(ai.rs 旧版)只取 kind/title/content/tags/confidence,**reasoning 被 LLM 产出却遭代码丢弃**——是信息链断裂的 bug,非缺功能。审核员光看 content 结论,缺 AI 判断依据(尤其 confidence=low 的弱信号更靠 reasoning 解释为何还提炼)。回填后溯源完整。
|
||||
- **状态**:✅ 已实施(reasoning 存主表 + extracted 事件 context.reasoning 双写,前端优先取主表降级取事件)
|
||||
- **教训**:LLM 输出字段与代码消费字段须对账——prompt 要求 LLM 产出的字段,代码侧漏消费是常见隐性 bug。
|
||||
|
||||
### prompt_tokens=0:深挖证伪非代码 bug(疑 GLM 订阅端点 message_start 缺 input_tokens)[#54 实测发现]
|
||||
|
||||
- **现象**:`ai_conversations.prompt_tokens=0`(completion=1496 正常)。GLM-订阅(anthropic 协议)1 对话 24 消息,所有 assistant 消息 `usage=None`。
|
||||
- **深挖结论(→ 修正初判)**:初判「anthropic_compat usage 解析漏 input_tokens,待修」**证伪**。逐段验证:
|
||||
1. `anthropic_compat` message_start 取 `input_tokens→prompt_tokens` **有单测**(input=42 过);
|
||||
2. `stream_llm`(857)`final_usage=chunk.usage.clone()` 累积对;
|
||||
3. `ai.rs:699` `tokens.add` 链路对。
|
||||
代码按标准 Anthropic 协议解析正确。`inp as u32`(Some→值,None→0):completion 有值说明 message_delta 的 output GLM 返回了,**prompt=0 = GLM 订阅端点 message_start 疑未返回 `usage.input_tokens`**(协议非标)。**勿改 anthropic_compat**(改了 = 误改正确实现)。
|
||||
- **状态**:📐 待修(误判)→ 🚫 非代码 bug。待抓 GLM 订阅 SSE 原文确认 input_tokens 在哪个事件/字段(临时打 message_start/message_delta 的 usage JSON 日志,测完删);若确认端点缺则属 provider 兼容性待办,非解析 bug。
|
||||
- **教训**:bug 定位优先用单测/逐段验证证伪代码层假设,不要急着改「看似正确」的实现。深挖证伪避免了一次误改。
|
||||
|
||||
---
|
||||
|
||||
**相关文档**:
|
||||
- [功能决策记录](./功能决策记录.md) — 需求规格 + 设计决策规格
|
||||
- [功能决策记录-归档](./功能决策记录-归档.md) — 纯流水 / 老 Sprint / UX 微调 / 已被取代
|
||||
359
docs/02-架构设计/规格契约自检机制.md
Normal file
359
docs/02-架构设计/规格契约自检机制.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# 规格契约自检机制
|
||||
|
||||
> 创建:2026-06-13 | 阶段:设计定稿,待落地
|
||||
> 性质:设计说明 + 可执行规格基准。后续 `spec-verifier` agent、`/spec-check` skill、自检 hook 均从本文档推导。
|
||||
|
||||
---
|
||||
|
||||
## 0. 背景与问题
|
||||
|
||||
全程 AI coding 下,开发节奏快(实测 ~4 Sprint/天),产生两个痛点:
|
||||
|
||||
1. **规格无锚点 → 漂移 → 不敢当契约用**:写下的 spec 没人验证,与代码逐渐脱节,最终失去参考价值。
|
||||
2. **done/todo/decision 散落 → 记不住**:做了什么、没做什么、做了哪些决策,事后查不清。
|
||||
|
||||
**错误方向**:新建独立的「需求规格」文档。静态 spec 必漂移;本项目无外部契约/验收需求,spec 的核心价值(沟通契约/验收基准)不成立;独立文档违反 SSOT,成为第三处真相源。
|
||||
|
||||
**正确方向**:**活契约(living contract)**——契约跟决策一起演进,由 AI 自检维持与代码一致。不新建文档,改造现有功能决策记录。
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心方向:活契约
|
||||
|
||||
契约不单独成文,而是挂在功能决策记录的每条决策上。一条 `✅ 已落地` 的决策,就是一条当前生效的契约。
|
||||
|
||||
```
|
||||
决策记录(活契约载体)
|
||||
├─ 决策三要素:决策 / 原因 / 状态
|
||||
├─ 代码锚点:让契约可被验证(机制 A)
|
||||
└─ 状态字段:聚合出完成度(机制 B)
|
||||
↓
|
||||
AI 自检维持契约与代码一致(机制 C/D/E)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 机制 A:代码锚点(防漂移)
|
||||
|
||||
给 `✅ 已落地` 的决策加一个**代码锚点**。锚点以**符号名 + grep 关键词为主锚**(跨修改稳定),**行号为辅锚**(最近定位,可漂,AI 自愈)。规格真相留在代码里,文档只留索引 + 意图。
|
||||
|
||||
### 形态
|
||||
|
||||
```markdown
|
||||
### connect_timeout 不设总 timeout [Sprint 6]
|
||||
- 决策:reqwest Client 加 connect_timeout(30s),不设总 timeout
|
||||
- 原因:连接阶段防无限 hang;总 timeout 会误砍流式长生成任务
|
||||
- 状态:✅ 已落地
|
||||
- 锚点:符号 `Client::new` | grep `connect_timeout` @ client.rs:142(行可漂,AI 自愈)
|
||||
- 自检:PostEdit 触发锚点一致性 | 上次:2026-06-13 ✅
|
||||
```
|
||||
|
||||
- **锚点**:符号 + grep 为主(真相),行号为辅(快照)。人补一次,AI 维护行号。
|
||||
- **自检行**:AI 验证后回写时间戳与结果,人扫一眼即知近期是否验过。
|
||||
|
||||
### 主辅分明:为什么行号不是主体
|
||||
|
||||
| 锚组成部分 | 稳定性 | 角色 |
|
||||
|------------|--------|------|
|
||||
| 符号名(函数/类/常量) | 高(重构才改) | 主锚 |
|
||||
| grep 关键词 | 中高 | 主锚(定位调用点) |
|
||||
| 行号 | 低(加删几行就漂) | 辅锚(最近定位,可漂) |
|
||||
|
||||
代码高频修改下,行号必然漂;行号作主体 = 锚点必然失效。符号 + grep 才是跨修改稳定的真相。
|
||||
|
||||
### 三层抗漂
|
||||
|
||||
1. **行号漂(最常见)**:grep 不受影响,重新定位。AI 自愈(行号漂但 grep 在附近 → 自动修);机制未跑时 grep 命中也能秒级定位。人无感。
|
||||
2. **符号/grep 漂(罕见,如重命名)**:必伴随决策变更 → grep 失效 = 正确的报警信号,触发决策同步(命中第 5 节"信息不足"上报条件)。
|
||||
3. **功能删除**:grep 全失效 → 报警 → 人确认废弃或误删。
|
||||
|
||||
维护靠 AI 不靠人:人只在新增决策时补一次锚点;之后行号漂由 AI 在自检环节自愈,人改代码时无需动锚点。
|
||||
|
||||
### 状态阀门:锚点只绑稳定态
|
||||
|
||||
锚点验证只对相对稳定的契约有效。剧烈重构期契约本身不稳,强行锚是噪音。
|
||||
|
||||
| 状态 | 是否锚 | 原因 |
|
||||
|------|--------|------|
|
||||
| `✅ 已落地` | 锚定 | 稳定,可验证 |
|
||||
| `🚧 待实测` | 不锚/暂锚 | 不稳定,重构中 |
|
||||
| `📐 设计未实施` | 不锚 | 未实现,无代码可锚 |
|
||||
|
||||
重构完成、代码稳了,`🚧→✅` 再锚定。状态字段是防锚点失效的阀门。
|
||||
|
||||
---
|
||||
|
||||
## 3. 机制 B:完成度聚合表
|
||||
|
||||
完成情况已编码在状态字段里(✅/🚧/📐)。缺的是按状态聚合的视图。在功能决策记录头部维护一张索引表:
|
||||
|
||||
```markdown
|
||||
## 完成度总览
|
||||
|
||||
| 状态 | 数量 | 代表条目 |
|
||||
|------|------|----------|
|
||||
| ✅ 已落地 | 38 | connect_timeout、provider 路由、知识库 Tier 分层 |
|
||||
| 🚧 待实测 | 5 | 审计回写、… |
|
||||
| 📐 设计未实施(TODO) | 12 | 向量检索、count_any 否定检测、IdeaPromoter 接线 |
|
||||
```
|
||||
|
||||
- `✅` = 做了什么;`📐` = 没做什么;决策条目 = 做了哪些决策。三问一表答完。
|
||||
- 增量维护:新增决策更新计数;`📐→✅` 迁移挪列。按状态聚合,不按时间,比 PROGRESS 流水好查。
|
||||
|
||||
---
|
||||
|
||||
## 4. AI 自检:三级验证
|
||||
|
||||
| 层级 | 验证内容 | 可靠性 | 谁验 |
|
||||
|------|----------|--------|------|
|
||||
| **L1 锚点存在性** | 文件:行 + grep 关键词命中 | ✅ 高(确定性) | 主代理 grep |
|
||||
| **L2 取值一致性** | 具体数值/标志是否如 spec 所述 | ⚠️ 中(范围窄,误读低) | 主代理读码 |
|
||||
| **L3 行为契约** | 代码逻辑是否遵守 spec 意图 | ❌ 低(主观) | 子代理最小上下文 |
|
||||
|
||||
**核心贡献**:AI 把脆弱的行号锚点变成自愈的语义锚点——
|
||||
- 行号漂但 grep 关键词在附近 N 行 → **AI 自动修正锚点行号**(可逆,自处理)。
|
||||
- 关键词消失 → **真报警**,语义变了,需人决策。
|
||||
|
||||
---
|
||||
|
||||
## 5. 分流规则:AI 能做 vs 人必须做
|
||||
|
||||
目标:让 AI 机械吞掉确定性/低风险/可逆的 80%,只把真正需要人脑的推到人面前。
|
||||
|
||||
### 5.1 两轴判定矩阵
|
||||
|
||||
| | 客观唯一(确定) | 主观/多解 |
|
||||
|---|---|---|
|
||||
| **只读/可逆** | ✅ AI 全权自处理 | ⚠️ AI 给候选 → 人定 |
|
||||
| **有后果/不可逆** | ⚠️ 报告 → 人定 | 🔴 必须人定 |
|
||||
|
||||
### 5.2 两条机械判定
|
||||
|
||||
**AI 自处理(不报人)的充要条件**:`确定性 = 客观 AND 动作 = 可逆`。
|
||||
(锚点行号自愈、数值核对一致、自检时间戳回写。)
|
||||
|
||||
**必须上报人的条件(任一命中即报)**:
|
||||
1. **主观**——验证答案不唯一(行为契约、意图符合性)。
|
||||
2. **有后果**——动作不可逆(改决策语义、标记契约被破坏、回滚代码)。
|
||||
3. **信息不足**——AI 无法判定(锚点关键词消失,但不知是否故意改的)。
|
||||
|
||||
### 5.3 兜底安全阀
|
||||
|
||||
规则未覆盖的场景,**默认上报人**,不擅自自处理。宁可多报,不可漏报关键。
|
||||
|
||||
### 5.4 高后果判定(决定是否触发子代理)
|
||||
|
||||
| 判为高后果(满足任一) | 例 |
|
||||
|------------------------|-----|
|
||||
| 数据完整性 | 写库、迁移、状态机流转 |
|
||||
| 并发安全 | 锁、共享状态、异步竞态 |
|
||||
| 安全 | 鉴权、注入、凭据处理 |
|
||||
| 外部契约 | API/协议、第三方对接 |
|
||||
| 不可逆操作 | 删除、覆盖、发布 |
|
||||
|
||||
高后果决策即使主代理自检报绿,仍触发子代理第二意见。
|
||||
|
||||
---
|
||||
|
||||
## 6. 反馈规格:五字段决策单元
|
||||
|
||||
上报给人的每条,必须是**可点的闭合决策**,不是要调查的谜题。AI 把上下文打包进去,人只回答 yes/no 或选 A/B。
|
||||
|
||||
```markdown
|
||||
🔴 [决策名] connect_timeout 不设总 timeout
|
||||
锚点:client.rs:142 | 实际:行号漂至 158,且 grep "timeout" 消失
|
||||
证据:预期 .connect_timeout(30s) 无 .timeout() | 实际代码已加 .timeout(60s)
|
||||
为何上报:命中「信息不足」——无法判定是故意改回总 timeout,还是误改
|
||||
候选:A. 故意改 → 更新决策记录(状态/原因)
|
||||
B. 误改 → 回滚代码(AI 推断 B 更可能:总 timeout 会误砍流式)
|
||||
需你定:A 还是 B?
|
||||
```
|
||||
|
||||
「为何上报」显式化分流规则,使判定可审计。
|
||||
|
||||
---
|
||||
|
||||
## 7. 子代理隔离验证(L3 / 高后果层)
|
||||
|
||||
### 7.1 为什么用 agent 不用 skill
|
||||
|
||||
| | Skill | Agent |
|
||||
|---|---|---|
|
||||
| 上下文 | 复用主对话,**不隔离** | 独立窗口,**隔离** |
|
||||
| 偏误 | 主代理带作者偏误执行(白搭) | 消除作者偏误 |
|
||||
|
||||
主代理验证有结构性确认偏误:决策是它记的、代码是它改的,倾向支持自己对。**隔离验证必须 agent。**
|
||||
|
||||
### 7.2 零上下文 → 最小必要上下文
|
||||
|
||||
完全零上下文是双刃剑:子代理无领域知识会误判(局外人偏误)。正确形态是**给事实,不给立场**——子代理是陪审员,只看证据下判断。
|
||||
|
||||
**卷宗格式**(由编排方构造,传入 agent):
|
||||
|
||||
```
|
||||
断言:此函数应"丢弃残缺响应,不入库"
|
||||
证据:<精确代码片段>
|
||||
任务:判断代码行为是否符合断言
|
||||
```
|
||||
|
||||
**不给**:决策原因字段、对话历史、是否刚改的、当初怎么定的。
|
||||
|
||||
### 7.3 分歧才报人
|
||||
|
||||
子代理不替代人,是在「上报人」前加第二意见:
|
||||
|
||||
```
|
||||
主代理自检(带上下文判一次)
|
||||
├─ L1/L2 确定性 → 自处理
|
||||
└─ L3/高后果 → 起 spec-verifier agent(最小上下文判一次)
|
||||
├─ 两代理一致(都绿/都红)→ 按结论走
|
||||
└─ 两代理分歧 → 🔴 报人(分歧暴露主观性,只有人能定)
|
||||
```
|
||||
|
||||
人的事件面从「所有主观项」压缩到「主观项中的分歧项」。
|
||||
|
||||
---
|
||||
|
||||
## 8. 触发环节:何时拉起 Agent
|
||||
|
||||
起 agent ⟺ 命中下列环节之一 AND 验证项是主观层(L3)或高后果。
|
||||
|
||||
| 环节 | 时机 | 起 agent? | 验证范围 | 频率控制 |
|
||||
|------|------|-----------|----------|----------|
|
||||
| **A 改代码** | PostEdit hook,改到挂锚文件 | 改到高后果锚点才起;L1/L2 主代理自验 | 仅被改那条契约 | 每次相关编辑,单条 |
|
||||
| **B 回合结束** | Stop hook(降频) | 本回合涉及的高后果/主观项 | 本回合动过的 | 抽样,≥10 轮/≥10 min |
|
||||
| **C 主动审计** | 手动 `/spec-check` | 全部 L3/高后果 | 所有 ✅ 决策 | 人触发,全量并行 |
|
||||
| **D 记录决策** | decision-record 标 ✅/演进时 | 新记或 📐→✅ 的高后果项 | 该单条 | 每次 ✅ 迁移 |
|
||||
|
||||
- **A 最值钱**:在「可能制造漂移的时刻」拦截,单条,便宜。优先级最高。
|
||||
- **B 兜底**:catch A 漏的(一处改多处)。
|
||||
- **C 体检**:清历史漂移,最贵,人触发。
|
||||
- **D 防脱节**:决策记了但代码没跟上,✅ 迁移时必验。
|
||||
|
||||
全程 AI coding 下,A/B 自动跑零摩擦,C 人按需,D 跟 decision-record 自然触发。无需人记「该验证了」。
|
||||
|
||||
---
|
||||
|
||||
## 9. 三层架构与组件骨架
|
||||
|
||||
```
|
||||
hook(时机)→ /spec-check skill(编排)→ spec-verifier agent(隔离验证)
|
||||
PostEdit/Stop 读记录+分流+封装卷宗 最小上下文判定
|
||||
L1/L2 自处理 L3/高后果
|
||||
收集+分歧上报 返回:判定+置信+分歧点
|
||||
```
|
||||
|
||||
### spec-verifier agent(`.claude/agents/spec-verifier.md`)
|
||||
|
||||
```
|
||||
你是独立契约验证者。只依据调用方给你的【断言+证据】判断。
|
||||
不假设意图,不参考对话历史,不信任任何"应该是什么"的预设。
|
||||
输出:判定(符合/违反/无法判定)+ 置信度 + 关键分歧点(一句话)。
|
||||
无法判定时必须明说,禁止凑结论。
|
||||
```
|
||||
|
||||
### /spec-check skill(`.claude/skills/spec-check/`)
|
||||
|
||||
```
|
||||
1. 读功能决策记录,提取所有 ✅ 条目(锚点+断言)
|
||||
2. 分流:L1/L2(确定性)→ 自己 grep 验,自处理
|
||||
L3/高后果(主观)→ 调 spec-verifier agent(传断言+代码片段,不传决策原因)
|
||||
3. 主代理自己也判一次 L3(带上下文)
|
||||
4. 比对:分歧项 → 按五字段格式化上报;一致项 → 按结论走
|
||||
5. 锚点行号漂移 → 自愈(可逆,自处理)
|
||||
```
|
||||
|
||||
基建复用:devflow 已在用 hook(Stop 降频)+ skill(/review、decision-record)。三层机制全是同构基建,不引入新依赖。
|
||||
|
||||
---
|
||||
|
||||
## 10. 落地顺序
|
||||
|
||||
1. **本文档定稿**(当前)——后续所有实现的规格基准。
|
||||
2. **功能决策记录瘦身 + 补锚点**——删微决策膨胀(730→~300),给 ✅ 条目补代码锚点。
|
||||
3. **主代理自检 hook**——PostEdit(环节 A)+ Stop 降频(环节 B),覆盖 L1/L2 确定性层。
|
||||
4. **spec-verifier agent + /spec-check skill**——覆盖 L3/高后果,四环节(A/B/C/D)主观层。
|
||||
5. **分歧上报机制**——五字段决策单元,接入 Stop hook 通知。
|
||||
|
||||
---
|
||||
|
||||
## 11. 用户操作指南
|
||||
|
||||
> 本节是人视角的操作手册。机制细节见 2-9 节,这里只讲「你做什么」。
|
||||
|
||||
### 心智模型:3 按钮 + 1 屏
|
||||
|
||||
整个机制里,人只做三件事,看一块屏。其余全是 AI 自动。
|
||||
|
||||
| | 人的动作 | 时机 |
|
||||
|---|----------|------|
|
||||
| 🔘 记决策 | 做取舍时,让 AI 用 decision-record 记下(决策/原因/状态) | 每次开发有取舍 |
|
||||
| 🔘 裁决分歧 | AI 上报时,在候选里选 A 或 B | 子代理与主代理打架时(偶发) |
|
||||
| 🔘 跑体检 | 执行 `/spec-check` 全量扫描 | 大版本前 / 重构后 |
|
||||
| 🖥 完成度表 | 翻功能决策记录头部「完成度总览」表 | 想看进度时 |
|
||||
|
||||
### 人 vs AI 分工
|
||||
|
||||
| 动作 | 归属 | 频率 |
|
||||
|------|------|------|
|
||||
| 做开发取舍(选 A 不选 B) | 人 | 每次开发 |
|
||||
| 记决策 + 补锚点 | AI 做,人确认 | 决策落地时 |
|
||||
| 维护锚点行号(自愈) | AI | 自动 |
|
||||
| 验证代码符合契约 | AI | 自动 |
|
||||
| 起 hook / 子代理自检 | AI | 自动 |
|
||||
| 裁决 AI 分歧 | 人 | 上报时 |
|
||||
| 查进度 | 人(看表) | 随时 |
|
||||
|
||||
人只做两件:**记决策 + 裁决分歧**。验证、维护、检查全归 AI。
|
||||
|
||||
### 看的入口与时机
|
||||
|
||||
| 想知道 | 看哪里 | 时机 |
|
||||
|--------|--------|------|
|
||||
| 做了/没做/做了哪些决策 | 功能决策记录头部「完成度总览」表 | 随时 |
|
||||
| AI 发现的契约冲突 | 上报条目(五字段:锚点/证据/为何上报/候选/需你定) | 被动收(偶发) |
|
||||
| 全量漂移体检 | `/spec-check` 红项报告 | 主动(大版本前) |
|
||||
|
||||
### 做的节奏
|
||||
|
||||
- **记决策**:开发中一有取舍,当场记。齿轮转起来的起点,零额外成本。
|
||||
- **裁决**:收到上报 → 选 A/B → AI 执行。
|
||||
- **体检**:每 Sprint 末或重构后跑一次 `/spec-check`,清历史漂移。
|
||||
- **迭代机制**:规则不准(误报/漏报)→ 改本文档第 5 节分流规则。机制文档是活的。
|
||||
|
||||
### 一天的工作流
|
||||
|
||||
```
|
||||
开发中做取舍 ──→ 🔘记决策(AI 补锚点,人不管)
|
||||
│
|
||||
│ AI 后台:hook 自检 / 子代理验证 / 行号自愈
|
||||
│
|
||||
AI 打架?─是─→ 🔘裁决(选 A/B)
|
||||
│否
|
||||
▼
|
||||
想看进度 ────→ 🖥翻完成度表
|
||||
│
|
||||
大版本前 ────→ 🔘跑 /spec-check 体检
|
||||
```
|
||||
|
||||
### 当前态:能做什么
|
||||
|
||||
机制尚未落地(落地链 ②-⑤)。当前能力边界:
|
||||
|
||||
| 能力 | 现在 | 建成后 |
|
||||
|------|------|--------|
|
||||
| 🔘 记决策 | ✅ 已有(decision-record) | ✅ |
|
||||
| 🖥 完成度表 | ❌ 需先补锚点 + 建表(②) | ✅ |
|
||||
| 🔘 裁决上报 | ❌ 需 ③④⑤ | ✅ |
|
||||
| 🔘 /spec-check | ❌ 需 ④ | ✅ |
|
||||
|
||||
解锁其余能力的起点是落地链 ②:补锚点 + 建完成度表。
|
||||
|
||||
---
|
||||
|
||||
## 12. 诚实边界
|
||||
|
||||
- **AI 自检降低漂移,不消除。** 行为级契约仍需人盯。别因「AI 验过」就放心改语义。
|
||||
- **同一 AI 的盲区贯穿写与验。** 全程 AI coding 下,当初记录漏掉的约束,验证时 AI 也想不到查。关键决策的 spec,人过一眼。
|
||||
- **一致 ≠ 正确。** 两代理一致时仍可能共享同一盲区(spec 本身写错,两代理按错的理解一致)。一致只代表「无分歧可上报」。
|
||||
- **selective 用子代理。** 全用 = 成本爆炸。只 L3 + 高后果。
|
||||
@@ -1,298 +1,234 @@
|
||||
# df-ai - AI 集成模块
|
||||
# df-ai — AI 集成模块
|
||||
|
||||
> Provider 抽象层与流式响应处理
|
||||
> 创建: 2026-06-10 | 最后更新: 2026-06-13
|
||||
|
||||
## 📋 模块概览
|
||||
---
|
||||
|
||||
`df-ai` 负责 AI 功能的核心集成,支持多个 AI Provider,提供统一的接口和流式响应处理。
|
||||
## 概述
|
||||
|
||||
### 主要特性
|
||||
- 多 Provider 支持(OpenAI、Anthropic、DeepSeek)
|
||||
- 流式响应处理
|
||||
- 工具调用支持
|
||||
- 错误处理和重试机制
|
||||
`df-ai` 是 DevFlow 的 AI 核心层,提供 LLM Provider 抽象、双协议实现(OpenAI 兼容 + Anthropic)、流式 SSE 解析、工具调用、上下文窗口管理和 embedding 支持。
|
||||
|
||||
## 🏗️ 架构设计
|
||||
---
|
||||
|
||||
### 核心组件
|
||||
```rust
|
||||
// Provider trait 定义
|
||||
pub trait AIProvider: Send + Sync {
|
||||
async fn chat_completion(&self, request: ChatRequest) -> Result<ChatResponse>;
|
||||
async fn create_embedding(&self, text: &str) -> Result<Vec<f32>>;
|
||||
}
|
||||
## 当前状态
|
||||
|
||||
// 流式响应处理
|
||||
pub struct StreamProcessor {
|
||||
event_sender: mpsc::UnboundedSender<StreamEvent>,
|
||||
}
|
||||
| 能力 | 状态 |
|
||||
|------|------|
|
||||
| LlmProvider trait | ✅ |
|
||||
| OpenAI 兼容 Provider(流式/非流式/embed)| ✅ |
|
||||
| Anthropic Provider(流式)| ✅ Sprint 8 |
|
||||
| ContextManager(分组滑窗)| ✅ Sprint 11 |
|
||||
| embed() 向量生成 | ✅ Sprint 15 |
|
||||
| AiToolRegistry(基础设施)| ✅ Sprint 5(12 工具注册在 commands/ai.rs)|
|
||||
| coordinator | ⬜ 空壳(B 路线待填)|
|
||||
|
||||
// 统一的 AI 服务
|
||||
pub struct AIService {
|
||||
providers: HashMap<String, Box<dyn AIProvider>>,
|
||||
default_provider: String,
|
||||
}
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
crates/df-ai/src/
|
||||
├── lib.rs — 公共导出
|
||||
├── provider.rs — LlmProvider trait(含 embed 默认实现)
|
||||
├── openai_compat.rs — OpenAI 兼容实现(chat + embed)
|
||||
├── anthropic_compat.rs — Anthropic Messages API 实现(Sprint 8)
|
||||
├── context.rs — ContextManager 分组滑动窗口(Sprint 11)
|
||||
├── ai_tools.rs — AiToolRegistry + 12 工具定义
|
||||
├── coordinator.rs — AgentCoordinator 空壳(B 路线)
|
||||
├── router.rs — ModelRouter 模型路由空壳(route() 按 TaskType 选模型,当前全返回 default_model)
|
||||
└── stream.rs — StreamCollector 流式辅助(累积 chunk.delta 文本 + 跟踪 finished 标志)
|
||||
```
|
||||
|
||||
### Provider 实现
|
||||
- **OpenAIProvider**: OpenAI GPT 系列模型
|
||||
- **AnthropicProvider**: Claude 系列模型
|
||||
- **DeepSeekProvider**: DeepSeek 模型
|
||||
|
||||
## 🔧 使用方法
|
||||
---
|
||||
|
||||
## LlmProvider Trait
|
||||
|
||||
### 基础聊天
|
||||
```rust
|
||||
use df_ai::AIService;
|
||||
pub type StreamResult = Pin<Box<dyn Stream<Item = anyhow::Result<StreamChunk>> + Send>>;
|
||||
|
||||
let ai_service = AIService::new(config);
|
||||
let request = ChatRequest {
|
||||
model: "gpt-4".to_string(),
|
||||
messages: vec![Message {
|
||||
role: "user".to_string(),
|
||||
content: "Hello, world!".to_string(),
|
||||
}],
|
||||
};
|
||||
#[async_trait]
|
||||
pub trait LlmProvider: Send + Sync {
|
||||
// 非流式完整响应(用于标题生成/知识提炼)
|
||||
async fn complete(&self, request: CompletionRequest)
|
||||
-> anyhow::Result<CompletionResponse>;
|
||||
|
||||
let response = ai_service.chat_completion(request).await?;
|
||||
```
|
||||
// 流式 SSE(主对话)
|
||||
async fn stream(&self, request: CompletionRequest)
|
||||
-> anyhow::Result<StreamResult>;
|
||||
|
||||
### 流式响应
|
||||
```rust
|
||||
use df_ai::stream_chat;
|
||||
|
||||
let (mut receiver, mut stream) = stream_chat(&ai_service, request).await?;
|
||||
|
||||
while let Some(event) = receiver.recv().await {
|
||||
match event {
|
||||
StreamEvent::Content(chunk) => {
|
||||
print!("{}", chunk);
|
||||
}
|
||||
StreamEvent::Done => {
|
||||
println!("\n完成");
|
||||
}
|
||||
StreamEvent::Error(e) => {
|
||||
eprintln!("错误: {}", e);
|
||||
}
|
||||
// 向量生成(默认 bail,openai_compat 覆盖实现)
|
||||
async fn embed(&self, _model: &str, _texts: Vec<String>)
|
||||
-> anyhow::Result<Vec<Vec<f32>>> {
|
||||
anyhow::bail!("该 Provider 不支持 embedding({})", self.name())
|
||||
}
|
||||
|
||||
// Provider 名称(必填)
|
||||
fn name(&self) -> &str;
|
||||
|
||||
// 支持的特性(必填:streaming / function_calling / vision)
|
||||
fn supported_features(&self) -> ProviderFeatures;
|
||||
}
|
||||
```
|
||||
|
||||
### 工具调用
|
||||
```rust
|
||||
let request = ChatRequest {
|
||||
model: "gpt-4".to_string(),
|
||||
messages: vec![Message {
|
||||
role: "user".to_string(),
|
||||
content: "创建一个文件".to_string(),
|
||||
}],
|
||||
tools: vec![Tool {
|
||||
r#type: "function".to_string(),
|
||||
function: FunctionDef {
|
||||
name: "create_file".to_string(),
|
||||
description: "创建文件".to_string(),
|
||||
parameters: Parameters {
|
||||
r#type: "object".to_string(),
|
||||
properties: serde_json::json!({
|
||||
"path": {"type": "string"},
|
||||
"content": {"type": "string"}
|
||||
}),
|
||||
required: vec!["path".to_string()],
|
||||
},
|
||||
},
|
||||
}],
|
||||
tool_choice: "auto".to_string(),
|
||||
};
|
||||
```
|
||||
|
||||
## ⚙️ 配置
|
||||
|
||||
### Provider 配置
|
||||
```yaml
|
||||
# config/ai.yaml
|
||||
providers:
|
||||
openai:
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
base_url: "https://api.openai.com/v1"
|
||||
model: "gpt-4"
|
||||
max_tokens: 4000
|
||||
temperature: 0.7
|
||||
|
||||
anthropic:
|
||||
api_key: ${ANTHROPIC_API_KEY}
|
||||
model: "claude-3-sonnet-20240229"
|
||||
max_tokens: 4000
|
||||
temperature: 0.7
|
||||
|
||||
deepseek:
|
||||
api_key: ${DEEPSEEK_API_KEY}
|
||||
model: "deepseek-chat"
|
||||
max_tokens: 4000
|
||||
temperature: 0.7
|
||||
|
||||
default_provider: "openai"
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
```bash
|
||||
export OPENAI_API_KEY="sk-your-key"
|
||||
export ANTHROPIC_API_KEY="sk-ant-key"
|
||||
export DEEPSEEK_API_KEY="your-key"
|
||||
```
|
||||
|
||||
## 🔄 错误处理
|
||||
|
||||
### 错误类型
|
||||
```rust
|
||||
pub enum AIError {
|
||||
APIError(String), // API 调用失败
|
||||
Timeout, // 请求超时
|
||||
RateLimit, // 达到速率限制
|
||||
InvalidResponse, // 响应格式错误
|
||||
ProviderNotFound, // Provider 不存在
|
||||
ConfigurationError, // 配置错误
|
||||
}
|
||||
```
|
||||
|
||||
### 重试机制
|
||||
```rust
|
||||
let config = RetryConfig {
|
||||
max_attempts: 3,
|
||||
backoff: ExponentialBackoff::from_millis(1000),
|
||||
retryable_errors: vec![
|
||||
AIError::Timeout,
|
||||
AIError::RateLimit,
|
||||
],
|
||||
};
|
||||
|
||||
let response = ai_service.chat_with_retry(request, &config).await?;
|
||||
```
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 缓存机制
|
||||
```rust
|
||||
pub struct CachedAIService {
|
||||
inner: AIService,
|
||||
cache: Arc<Mutex<HashMap<String, ChatResponse>>>,
|
||||
}
|
||||
|
||||
// 缓存键生成
|
||||
fn cache_key(request: &ChatRequest) -> String {
|
||||
format!("{:?}-{:?}", request.model, request.messages)
|
||||
}
|
||||
```
|
||||
|
||||
### 连接池
|
||||
```rust
|
||||
pub struct ConnectionPool {
|
||||
connections: HashMap<String, Vec<Client>>,
|
||||
max_connections: usize,
|
||||
}
|
||||
|
||||
pub async fn get_client(&self, provider: &str) -> Result<Client> {
|
||||
// 从连接池获取或创建新连接
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 监控与日志
|
||||
|
||||
### 请求追踪
|
||||
```rust
|
||||
pub struct RequestTracer {
|
||||
request_id: String,
|
||||
start_time: Instant,
|
||||
metrics: RequestMetrics,
|
||||
}
|
||||
|
||||
impl RequestTracer {
|
||||
pub fn log_request(&self, provider: &str, duration: Duration) {
|
||||
metrics.record_request(provider, duration);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 指标收集
|
||||
```rust
|
||||
pub struct RequestMetrics {
|
||||
total_requests: AtomicU64,
|
||||
successful_requests: AtomicU64,
|
||||
failed_requests: AtomicU64,
|
||||
average_duration: AtomicDuration,
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
### 单元测试
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_completion() {
|
||||
let ai_service = AIService::new(test_config());
|
||||
let request = test_request();
|
||||
let response = ai_service.chat_completion(request).await;
|
||||
assert!(response.is_ok());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_multiple_providers() {
|
||||
let providers = vec!["openai", "anthropic"];
|
||||
for provider in providers {
|
||||
let ai_service = AIService::new(config_for_provider(provider));
|
||||
let response = test_chat(&ai_service).await;
|
||||
assert!(response.is_ok(), "Provider {} failed", provider);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚨 最佳实践
|
||||
|
||||
### 1. 错误处理
|
||||
```rust
|
||||
// ✅ 正确
|
||||
match ai_service.chat_completion(request).await {
|
||||
Ok(response) => handle_response(response),
|
||||
Err(AIError::RateLimit) => wait_and_retry(),
|
||||
Err(e) => log_error_and_notify(e),
|
||||
}
|
||||
|
||||
// ❌ 错误 - 忽略错误
|
||||
let _ = ai_service.chat_completion(request).await;
|
||||
```
|
||||
|
||||
### 2. 资源管理
|
||||
```rust
|
||||
// ✅ 正确 - 使用连接池
|
||||
let client = connection_pool.get_client("openai").await?;
|
||||
|
||||
// ❌ 错误 - 每次创建新连接
|
||||
let client = Client::new(config);
|
||||
```
|
||||
|
||||
### 3. 并发控制
|
||||
```rust
|
||||
// ✅ 正确 - 使用信号量
|
||||
let semaphore = Arc::new(Semaphore::new(10));
|
||||
let permit = semaphore.acquire().await?;
|
||||
let response = ai_service.chat_completion(request).await;
|
||||
|
||||
// ❌ 错误 - 无限制并发
|
||||
let handles: Vec<_> = requests.into_iter().map(|req| {
|
||||
tokio::spawn(ai_service.chat_completion(req))
|
||||
}).collect();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**相关文档**:
|
||||
- [df-storage - 存储层](./df-storage-存储层.md)
|
||||
- [df-workflow - 工作流引擎](./df-workflow-工作流引擎.md)
|
||||
- [df-nodes - 节点集合](./df-nodes-节点集合.md)
|
||||
## OpenAI 兼容 Provider(openai_compat.rs)
|
||||
|
||||
支持 OpenAI / DeepSeek / GLM / 本地 Ollama 等任意 OpenAI 兼容端点。
|
||||
|
||||
| 特性 | 实现 |
|
||||
|------|------|
|
||||
| base_url 智能拼接 | `chat_url()` 三分支:已含 `/chat/completions` 直用;以 `/v<数字>` 结尾(如 `/api/paas/v4`,由 `ends_with_version` 判定)补 `/chat/completions`;仅域名(如 `api.openai.com`)补 `/v1/chat/completions` |
|
||||
| 流式 | `stream: true` + SSE 逐 chunk 解析(`apply_openai_sse` 纯函数)|
|
||||
| 工具调用 | tool_calls 按 index 排序(消 HashMap 迭代乱序)|
|
||||
| usage 解析 | `stream_options: {include_usage: true}`,末 chunk 读累计值 |
|
||||
| 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(避免误砍流式长任务)|
|
||||
|
||||
> 注:idle timeout(120s)与断连丢弃(finished_received)属上层 `stream_llm`(src-tauri/commands/ai.rs)的职责,不在本 Provider 层。
|
||||
|
||||
SSE 解析抽成 `pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>) -> StreamChunk` 纯函数,与 HTTP/eventsource 解耦,便于单测(喂构造 data 字符串验证 `[DONE]`/usage 覆盖/tool_calls 等分支)。
|
||||
|
||||
---
|
||||
|
||||
## Anthropic Provider(anthropic_compat.rs,Sprint 8)
|
||||
|
||||
| 特性 | 实现 |
|
||||
|------|------|
|
||||
| 认证 | `x-api-key` + `anthropic-version: 2023-06-01` |
|
||||
| 消息格式 | 顶层 `system` 字段,`max_tokens` 必填(请求缺省时兜底 `DEFAULT_MAX_TOKENS = 4096`)|
|
||||
| 流式 SSE | event 类型解析(全集见下)|
|
||||
| 工具调用 | `content_block` type=tool_use |
|
||||
| usage | `message_start` 初始化累加器(input_tokens)、`message_delta` 覆盖 completion(output_tokens 为累计值,非增量)、`message_stop` 经 `take()` 带出累积 usage |
|
||||
| embed | 不支持(Anthropic 无 embed API),继承 trait 默认 Err |
|
||||
|
||||
SSE 事件全集(按 `type` 字段分发):
|
||||
|
||||
| event type | 处理 |
|
||||
|------------|------|
|
||||
| `message_start` | 用 `message.usage.input_tokens` 初始化累加器(output 置 0)|
|
||||
| `message_delta` | `usage.output_tokens` 是累计值(非增量),直接覆盖 completion 并重算 total |
|
||||
| `content_block_delta`(text_delta) | 文本增量 |
|
||||
| `content_block_delta`(input_json_delta) | 工具入参增量(带 index)|
|
||||
| `content_block_start`(tool_use) | 工具块开始,带 id + name |
|
||||
| `message_stop` | 返回 `finished=true` 终态 chunk,usage 经 `take()` 带出 |
|
||||
| `error` | 返回 `finished=true` 终态空 chunk(不清空累加器)|
|
||||
| 其它(content_block_stop / ping)| 空 chunk |
|
||||
|
||||
SSE 解析抽成 `pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUsage>) -> StreamChunk` 纯函数,与 HTTP/eventsource 解耦,便于单测(喂构造 data 字符串验证事件分支与 usage 累积)。
|
||||
|
||||
支持 GLM 订阅端点(`https://open.bigmodel.cn/api/anthropic`)。
|
||||
|
||||
---
|
||||
|
||||
## ContextManager 分组滑动窗口(context.rs,Sprint 11)
|
||||
|
||||
解决长对话无限增长导致 `context_length_exceeded` 死锁。
|
||||
|
||||
### 核心设计
|
||||
|
||||
- **TokenEstimator**:字符粗估,零依赖,保守 ±15%。单条公式 = `content(chars×0.35 ceil)` + `per_message 4` + 每个 `tool_call(per_tool_call 30 + name/arguments 各按 chars×0.35 ceil)` + 有 `tool_call_id` 时 `+3`
|
||||
- **ContextConfig**:max_tokens 128k / output_reserve 8192 / safety 0.85;预算公式 `(max_tokens − output_reserve) × safety_ratio`,由 `ContextConfig::budget_limit()` 提供(`ContextManager::budget_limit()` 仅转发它)
|
||||
- **分组淘汰单元**:工具调用三元组(ToolCallHead + ToolResultTail* + 紧随文本 Assistant)原子性同进同出,防上下文语义断裂
|
||||
- **保护区**:`PROTECT_COUNT = 6`(编译期常量,≈ 最近 2 个完整用户轮次),最后 6 条消息永不裁
|
||||
- **裁剪范围**:仅影响发送视图(`build_for_request`),全量历史(`all_messages_clone`)用于持久化
|
||||
|
||||
### 关键方法
|
||||
|
||||
| 方法 | 用途 |
|
||||
|------|------|
|
||||
| `push(message: ChatMessage)` | 追加消息并计 token、更新 `history_tokens` 缓存(push 不裁剪,裁剪统一在 build_for_request)|
|
||||
| `clear()` | 清空消息历史并重置 token 计数 |
|
||||
| `len() -> usize` | 历史消息条数 |
|
||||
| `is_empty() -> bool` | 历史是否为空 |
|
||||
| `history_tokens() -> u32` | 当前历史累计 token(不含 system prompt)|
|
||||
| `budget_limit() -> u32` | 上下文预算上限,转发 `config.budget_limit()` = (max_tokens − output_reserve) × safety_ratio |
|
||||
| `iter() -> impl Iterator<Item = &ChatMessage>` | 只读迭代历史消息(如标题生成 filter)|
|
||||
| `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` | 原子更新工具结果内容(审批通过/拒绝时回填),返回是否找到并替换 |
|
||||
|
||||
---
|
||||
|
||||
## AI 工具注册
|
||||
|
||||
> 归属:`ai_tools.rs` 仅提供基础设施(`RiskLevel` / `AiTool` / `AiToolRegistry`);12 个工具的具体定义与注册在 `src-tauri/src/commands/ai.rs::build_ai_tool_registry`,handler 即唯一执行路径(schema+risk+实现同源)。
|
||||
|
||||
12 个内置工具,按风险分级:
|
||||
|
||||
| 风险 | 工具 |
|
||||
|------|------|
|
||||
| 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 |
|
||||
|
||||
工具执行结果写 `ai_tool_executions` 表(审计日志)。
|
||||
|
||||
---
|
||||
|
||||
## 知识库集成(Sprint 15,逻辑在 src-tauri/commands/ai.rs)
|
||||
|
||||
### 知识注入
|
||||
|
||||
`build_knowledge_context(state, query, config) -> String`:
|
||||
- `config.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(`tauri::async_runtime::spawn`),提炼失败仅 warn 不阻断
|
||||
- 取最后 6 条 user/assistant 消息 → LLM JSON 输出(强制 JSON schema)
|
||||
- parse 失败整批丢弃;成功则逐条写 candidate
|
||||
|
||||
`maybe_spawn_extraction(...)`:agentic loop 两处正常退出路径统一调用。
|
||||
|
||||
### 向量 embedding(Phase 5.5)
|
||||
|
||||
`generate_embedding(state, text, config) -> Option<Vec<f32>>`:
|
||||
- 截断 8000 字 → 找 `embedding_provider_id` 对应 provider → `embed()`
|
||||
- 失败返回 None(降级 LIKE)
|
||||
|
||||
`hybrid_search(state, query, limit, config)`:三层降级链:
|
||||
1. `vector_enabled == false` → 纯 LIKE
|
||||
2. embed 调用失败 → 纯 LIKE
|
||||
3. 正常 → 双信号合并排序(同时 LIKE+向量 cos≥0.3 > 仅 LIKE > 仅向量 cos≥0.3)
|
||||
|
||||
---
|
||||
|
||||
## LLM 并发控制(state.rs,Sprint 11 Part C)
|
||||
|
||||
`LlmConcurrency`:双层 Semaphore,运行时可调。
|
||||
|
||||
| Semaphore | 默认 permits | 限流对象 |
|
||||
|-----------|------------|---------|
|
||||
| global | 3 | 全部 LLM 调用(stream_llm / 标题 / 提炼)|
|
||||
| per_conv | 2 | 单对话并发 |
|
||||
|
||||
本地工具执行(`tools.execute`)不限流,无外部成本。
|
||||
|
||||
---
|
||||
|
||||
## 决策能力缺口(B 路线,待立项)
|
||||
|
||||
| 能力 | 现状 | 需补 |
|
||||
|------|------|------|
|
||||
| Planning(任务规划)| 缺失 | 意图 → LLM 规划子任务 DAG |
|
||||
| Coordinator(多 agent)| `coordinator.rs::run()` 全 TODO | 实现多 agent 协作拆 DAG |
|
||||
| Conditions(条件分支)| 缺失(df-ai 无实现,条件求值属 df-workflow crate)| JSON Path / 比较 / and-or-not |
|
||||
| Reflection(自纠)| 缺失 | 执行后自检 / 重试 |
|
||||
|
||||
详见 [Phase 2 计划 - 决策能力升级](../07-项目管理/Phase2计划.md)。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [df-storage 存储层](./df-storage-存储层.md)
|
||||
- [df-workflow 工作流引擎](./df-workflow-工作流引擎.md)
|
||||
- [df-nodes 节点集合](./df-nodes-节点集合.md)
|
||||
|
||||
135
docs/03-模块文档/df-knowledge-知识库.md
Normal file
135
docs/03-模块文档/df-knowledge-知识库.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 知识库模块
|
||||
|
||||
> 创建: 2026-06-13 | 阶段: Tier 1 已实现
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
知识库是 DevFlow 的"共享记忆层",被动积累 AI 对话中产生的可复用经验,供后续对话注入使用。外部工具(Claude Code / CodeX / Cursor)可通过相同 IPC 接口读写,内外零差异。
|
||||
|
||||
---
|
||||
|
||||
## 实现状态
|
||||
|
||||
| 功能 | 状态 |
|
||||
|------|------|
|
||||
| candidate→published 状态机 | ✅ Tier 1 |
|
||||
| 手动录入(Knowledge.vue)| ✅ Tier 1 |
|
||||
| AI 自动提炼(对话完成后)| ✅ Tier 1 |
|
||||
| LIKE 关键词检索 + 注入 system prompt | ✅ Tier 1 |
|
||||
| 审核收件箱(人工门控)| ✅ Tier 1 |
|
||||
| 向量 embedding + 混合检索 | ✅ Phase 5.5(开关控制,默认关)|
|
||||
| ai_node prompt 注入 | ⬜ Tier 2 |
|
||||
| MCP 对外 API | ⬜ Tier 2 |
|
||||
|
||||
---
|
||||
|
||||
## 状态机
|
||||
|
||||
```
|
||||
candidate ──→ pending_review ──→ published ──→ archived
|
||||
│ │ │
|
||||
└────────────────┴───────────────┘
|
||||
(可直接到 archived)
|
||||
```
|
||||
|
||||
- AI 提炼只产 **candidate**,绝不自动 published(人工门控)
|
||||
- `knowledge_archive`:软删除(status=archived),不物理删除
|
||||
|
||||
---
|
||||
|
||||
## 知识类型(KnowledgeKind)
|
||||
|
||||
7 种:`pitfall`(踩坑)/ `review_rule`(审查规则)/ `prompt_template`(Prompt 模板)/ `architecture_pattern`(架构模式)/ `diagnosis`(诊断知识)/ `deployment_note`(部署经验)/ `workflow_optimization`(工作流优化)
|
||||
|
||||
---
|
||||
|
||||
## IPC 命令(11 个)
|
||||
|
||||
| Command | 说明 |
|
||||
|---------|------|
|
||||
| `knowledge_list(status?)` | 全量列表,默认排除 archived |
|
||||
| `knowledge_get(id)` | 单条查询 |
|
||||
| `knowledge_search(query, kind?, limit?)` | LIKE 检索,top-N≤3 |
|
||||
| `knowledge_create(input)` | 创建(status=candidate)|
|
||||
| `knowledge_update_status(id, status)` | 状态转换(含合法矩阵校验)|
|
||||
| `knowledge_record_reuse(id)` | reuse_count +1 |
|
||||
| `knowledge_list_candidates()` | 审核收件箱(按 confidence 排序)|
|
||||
| `knowledge_archive(id)` | 软删除 |
|
||||
| `knowledge_get_config()` | 读取 KnowledgeConfig |
|
||||
| `knowledge_save_config(config)` | 保存 KnowledgeConfig |
|
||||
| `knowledge_extract_now()` | 手动触发提炼(ManualOnly 模式)|
|
||||
|
||||
---
|
||||
|
||||
## KnowledgeConfig
|
||||
|
||||
```rust
|
||||
pub struct KnowledgeConfig {
|
||||
pub auto_extract: bool, // 提炼总开关,默认 true
|
||||
pub trigger_mode: ExtractTrigger, // on_complete | on_idle | manual_only
|
||||
pub min_messages: u32, // 守卫:最少消息数,默认 4
|
||||
pub idle_timeout_ms: u64, // 闲置触发超时,默认 30000
|
||||
pub auto_inject: bool, // 聊天注入开关,默认 true
|
||||
pub vector_enabled: bool, // 向量检索开关,默认 false
|
||||
pub embedding_provider_id: Option<String>, // 仅 openai_compat 类型
|
||||
pub embedding_model: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
存储:`AppState.knowledge_config: Arc<Mutex<KnowledgeConfig>>`(内存,重启恢复默认值)。
|
||||
|
||||
---
|
||||
|
||||
## 检索与注入
|
||||
|
||||
### LIKE 检索(默认)
|
||||
|
||||
`search(query, kind, limit=3)` → `WHERE title LIKE ? OR content LIKE ?` → `ORDER BY reuse_count DESC`
|
||||
|
||||
### 混合检索(vector_enabled=true)
|
||||
|
||||
三层降级链:
|
||||
1. 开关关 → 纯 LIKE
|
||||
2. embed 调用失败 → 纯 LIKE
|
||||
3. 正常 → 双信号排序(同时命中 LIKE+向量 cos≥0.3 > 仅 LIKE > 仅向量 cos≥0.3)
|
||||
|
||||
嵌入时机:知识**发布时**(不在 candidate 阶段浪费 embed 调用),`spawn_embedding_for_knowledge` fire-and-forget。
|
||||
|
||||
### 注入位置
|
||||
|
||||
system prompt 头部([知识库上下文] --- [技能指令] --- [原始 system prompt]),仅 auto_inject=true 时生效。
|
||||
|
||||
---
|
||||
|
||||
## AI 自动提炼流程
|
||||
|
||||
1. `run_agentic_loop` 正常退出 → `maybe_spawn_extraction()` 守卫检查
|
||||
2. 守卫:`auto_extract=true` + `messages.len() >= min_messages`
|
||||
3. `tauri::async_runtime::spawn` 后台执行,不 await(不阻断聊天)
|
||||
4. 取最后 6 条 user/assistant 消息 → 构造 JSON schema prompt → LLM `complete()`
|
||||
5. `serde_json::from_str<Vec<ExtractedItem>>` 解析,失败整批丢弃(warn 不报错)
|
||||
6. 逐条写 `knowledges`(status=candidate,source_ref="conv:{id}")
|
||||
|
||||
---
|
||||
|
||||
## 矛盾知识处理
|
||||
|
||||
不做结构层消歧,在内容和 tags 中自述限制范围。检索时两条知识都可能返回,由 LLM 上下文理解取舍。
|
||||
|
||||
---
|
||||
|
||||
## 外部工具访问(规划)
|
||||
|
||||
Tier 1+ 目标:MCP Shell 封装(`mcp-server` 转发 IPC),外部工具使用逻辑与内部零差异:
|
||||
- 外部写入:走 candidate → 人工审核流程
|
||||
- 外部读取:`knowledge_search` / `knowledge_list`(published)
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [df-storage 存储层](./df-storage-存储层.md) — knowledges 表结构 + 向量工具函数
|
||||
- [df-ai AI 集成模块](./df-ai-AI集成模块.md) — hybrid_search / generate_embedding / extract
|
||||
- [功能决策记录](../02-架构设计/功能决策记录.md) — 检索方案演进决策
|
||||
@@ -1,52 +1,140 @@
|
||||
# df-storage 存储层
|
||||
|
||||
> 创建: 2026-06-10 | 状态: 初稿
|
||||
> 创建: 2026-06-10 | 最后更新: 2026-06-13
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
df-storage 是 DevFlow 的数据持久化层,基于 SQLite (rusqlite),负责连接管理、Schema 迁移和 CRUD 操作。
|
||||
df-storage 是 DevFlow 的数据持久化层,基于 SQLite (rusqlite),负责连接管理、Schema 迁移(V1-V8)和 CRUD 操作。全部 Repo 由 `impl_repo!` 宏自动生成。
|
||||
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
|
||||
| 功能 | 状态 |
|
||||
|------|------|
|
||||
| SQLite 连接管理 | ✅ 已实现 |
|
||||
| Schema 迁移 (6 张表) | ✅ 已实现 |
|
||||
| CRUD 操作 | ⬜ 待实施 |
|
||||
| 事务支持 | ⬜ 待实施 |
|
||||
| SQLite 连接管理 | ✅ |
|
||||
| Schema 迁移 V1-V8 | ✅ |
|
||||
| impl_repo! 宏 CRUD | ✅ |
|
||||
| KnowledgeRepo(含向量)| ✅ Sprint 15 |
|
||||
| 事务支持 | ⬜ 按需 |
|
||||
|
||||
## 数据表
|
||||
---
|
||||
|
||||
Phase 1 已创建的 6 张核心表:
|
||||
## 数据表(V1-V8 迁移历史)
|
||||
|
||||
1. **ideas** — 想法池
|
||||
2. **projects** — 项目
|
||||
3. **tasks** — 任务
|
||||
4. **workflow_defs** — 工作流定义
|
||||
5. **workflow_runs** — 工作流执行
|
||||
6. **artifacts** — 产出物
|
||||
| 版本 | 新增/变更 |
|
||||
|------|-----------|
|
||||
| V1 | ideas / projects / tasks / releases / workflow_executions / node_executions(6 张基础表 + 4 索引)|
|
||||
| V2 | ideas 加 promoted_to/ai_analysis/scores;tasks 加 workflow_def_id/base_branch;workflow_executions 加 project_id/task_id;新建 branches 表(含 2 索引)|
|
||||
| V3 | ai_providers / ai_conversations / ai_tool_executions(AI 功能 3 张表)|
|
||||
| V4 | 幂等补列:ai_conversations.archived(PRAGMA 探测,兼容坏库)|
|
||||
| V5 | 幂等补列:ai_conversations.prompt_tokens / completion_tokens / model / models(Token 用量)|
|
||||
| V6 | 幂等补列:ai_conversations.skill(技能注入)|
|
||||
| V7 | 新建 **knowledges** 表(Sprint 15)|
|
||||
| V8 | 幂等补列:knowledges.embedding BLOB(Phase 5.5 向量检索)|
|
||||
|
||||
完整表结构见 `ARCHITECTURE.md` 数据模型章节。
|
||||
|
||||
## 依赖关系
|
||||
### knowledges 表(V7)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS knowledges (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL DEFAULT 'pitfall',
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
tags TEXT, -- JSON array string
|
||||
status TEXT NOT NULL DEFAULT 'candidate',
|
||||
confidence TEXT, -- 'high'|'medium'|'low'
|
||||
reuse_count INTEGER NOT NULL DEFAULT 0,
|
||||
verified INTEGER NOT NULL DEFAULT 0, -- 0/1
|
||||
source_project TEXT,
|
||||
source_ref TEXT,
|
||||
created_at TEXT NOT NULL, -- 毫秒字符串
|
||||
updated_at TEXT NOT NULL,
|
||||
embedding BLOB -- V8 补列,f32 little-endian
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledges_status ON knowledges(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledges_kind ON knowledges(kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledges_reuse_count ON knowledges(reuse_count DESC);
|
||||
```
|
||||
df-core (错误类型、ID 生成)
|
||||
← df-storage
|
||||
|
||||
---
|
||||
|
||||
## impl_repo! 宏
|
||||
|
||||
自动生成以下方法:`insert` / `get_by_id` / `list_all` / `query` / `update_field` / `update_full` / `delete`
|
||||
|
||||
列名白名单(ALLOWED_COLUMNS)防 SQL 注入,各 Repo 声明各自允许的列。
|
||||
|
||||
### 已注册 Repo 列表
|
||||
|
||||
| Repo | 表 |
|
||||
|------|----|
|
||||
| IdeaRepo | ideas |
|
||||
| ProjectRepo | projects |
|
||||
| TaskRepo | tasks |
|
||||
| ReleaseRepo | releases |
|
||||
| WorkflowRepo | workflow_executions |
|
||||
| NodeExecutionRepo | node_executions |
|
||||
| BranchRepo | branches |
|
||||
| AiProviderRepo | ai_providers |
|
||||
| AiConversationRepo | ai_conversations |
|
||||
| AiToolExecutionRepo | ai_tool_executions |
|
||||
| **KnowledgeRepo** | **knowledges** |
|
||||
|
||||
---
|
||||
|
||||
## KnowledgeRepo 自定义方法(Sprint 15)
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `search(query, kind?, limit)` | LIKE 双分支(有/无 kind 过滤),均含 `WHERE status='published'`,`ORDER BY reuse_count DESC LIMIT ?`,top-N≤3 用于注入 |
|
||||
| `list_by_status(status)` | CASE WHEN confidence 语义排序(High→Medium→Low),用于审核收件箱 |
|
||||
| `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 BLOB(f32 little-endian)|
|
||||
| `search_vector(query_vec, limit)` | SELECT published + embedding IS NOT NULL → 纯 Rust 余弦批量比较,skip 维度不匹配 |
|
||||
| `list_non_archived()` | `WHERE status != 'archived'` 全量,CASE confidence 语义排序(high>medium>low),次 created_at DESC → `Vec<KnowledgeRecord>` |
|
||||
|
||||
### 向量工具函数
|
||||
|
||||
```rust
|
||||
fn f32s_to_blob(v: &[f32]) -> Vec<u8> // f32 → little-endian bytes
|
||||
fn blob_to_f32s(b: &[u8]) -> Vec<f32> // bytes → f32(chunks_exact(4),尾部非 4 倍数残字节截断丢弃)
|
||||
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 // 点积 / (‖a‖‖b‖ + 1e-8 防零除),零向量返回有限值
|
||||
```
|
||||
|
||||
> **不引入 sqlite-vec**:规避 Windows MSVC 下编译 C 扩展的风险。纯 Rust 实现,零外部 C 依赖。
|
||||
|
||||
---
|
||||
|
||||
## 迁移幂等设计
|
||||
|
||||
迁移机制:`schema_version` 表记录当前版本,`MIGRATION_VERSION` 常量为目标版本,`run()` 按 `current_version < N` 顺序应用各版本。
|
||||
|
||||
### v4 解法:PRAGMA 探测列存在性
|
||||
|
||||
关键列补建不依赖版本号,用 `PRAGMA table_info(<table>)` 探测实际 schema,缺列才 `ALTER TABLE ADD COLUMN`。
|
||||
|
||||
- 对新库(列已由建表带入)、老库(DDL 正常生效)、坏库(版本号已写入但 DDL 漏生效)三种情况都安全幂等。
|
||||
- V5/V6/V8 均复用此模式。
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
crates/df-storage/src/
|
||||
├── lib.rs — 模块入口,导出公共 API
|
||||
├── connection.rs — SQLite 连接管理
|
||||
├── schema.rs — Schema 定义与迁移
|
||||
└── crud.rs — CRUD 操作 (待创建)
|
||||
├── lib.rs — 模块入口,导出公共 API
|
||||
├── db.rs — SQLite 连接管理(Database struct)
|
||||
├── migrations.rs — V1-V8 迁移逻辑,MIGRATION_VERSION=8
|
||||
├── models.rs — 全部 *Record struct(含 KnowledgeRecord)
|
||||
└── crud.rs — impl_repo! 宏 + 全部 Repo(含 KnowledgeRepo)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [SQLite CRUD 模式](../01-技术文档/SQLite-CRUD模式.md)
|
||||
|
||||
@@ -15,43 +15,171 @@ df-workflow 是 DevFlow 的核心引擎,负责 DAG 定义、拓扑排序、节
|
||||
| DAG 数据结构 | ✅ 已实现 |
|
||||
| 拓扑排序 | ✅ 已实现 |
|
||||
| DagExecutor (顺序执行) | ✅ 已实现 |
|
||||
| 同层节点并行执行 | ⬜ 有 TODO 注释 |
|
||||
| 同层节点并行执行 | ✅ 已实现 |
|
||||
| Node trait 定义 | ✅ 已实现 |
|
||||
| 状态机 (WorkflowRunStatus) | ✅ 已实现 |
|
||||
| EventBus (broadcast) | ✅ 已实现 |
|
||||
| 条件表达式引擎 | ⚡ 仅支持 true/false |
|
||||
| 断点续跑 | ⬜ 待实施 |
|
||||
| 断点续跑 | ⬜ 待实施(引擎整体无暂停/恢复/快照机制,缺乏底层基础设施支撑) |
|
||||
|
||||
## 核心设计
|
||||
|
||||
### Node trait
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait Node: Send + Sync {
|
||||
fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput>;
|
||||
async fn execute(&self, ctx: NodeContext) -> NodeResult;
|
||||
fn schema(&self) -> NodeSchema;
|
||||
fn is_blocking(&self) -> bool { true }
|
||||
fn is_blocking(&self) -> bool { false }
|
||||
fn node_type(&self) -> &str;
|
||||
}
|
||||
|
||||
// NodeResult = anyhow::Result<NodeOutput> 的别名
|
||||
// ctx 按值传递(非引用),由 executor 在每层构建
|
||||
```
|
||||
|
||||
### NodeContext / NodeOutput(node.rs)
|
||||
|
||||
执行上下文与输出结构,由 executor 在每层为每个节点构建。
|
||||
|
||||
**NodeContext 字段**:
|
||||
|
||||
| 字段 | 类型 | 职责 |
|
||||
|------|------|------|
|
||||
| `node_id` | `NodeId` | 当前节点 ID |
|
||||
| `inputs` | `HashMap<String, NodeOutput>` | 上游节点输出,key 为上游节点 ID |
|
||||
| `config` | `serde_json::Value` | 节点配置参数 |
|
||||
| `execution_id` | `String` | 工作流执行 ID |
|
||||
| `event_bus` | `EventBus` | 事件总线(可 Clone,广播状态变更) |
|
||||
| `node_status` | `StateMachine` | 节点状态机(用于检查取消状态) |
|
||||
|
||||
**NodeOutput 字段与构造器**:
|
||||
|
||||
| 成员 | 签名 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | `serde_json::Value` | 输出数据 |
|
||||
| `metadata` | `HashMap<String, String>` | 输出元数据 |
|
||||
| `empty()` | `() -> Self` | 空输出(`data = Null`,空 metadata) |
|
||||
| `from_value(data)` | `(Value) -> Self` | 从 JSON 值构造(空 metadata) |
|
||||
|
||||
> `NodeOutput` derive `Debug/Clone/Serialize/Deserialize`;`NodeContext` 仅 `Debug/Clone`(`StateMachine` 非 Serialize)。
|
||||
|
||||
### 节点注册机制(NodeRegistry)
|
||||
|
||||
节点工厂注册表,据 `DagDef.node_type` 字符串创建 `Box<dyn Node>` 实例,桥接可序列化定义与运行时 trait object。
|
||||
|
||||
| 方法 | 签名 | 职责 |
|
||||
|------|------|------|
|
||||
| `new` | `() -> Self` | 创建空注册表 |
|
||||
| `register` | `(&mut self, type_name: &str, factory: F)` | 注册一个节点工厂(`F: Fn(&Value) -> Box<dyn Node>`) |
|
||||
| `create` | `(&self, type_name: &str, config: &Value) -> Result<Box<dyn Node>>` | 按类型名 + 配置创建节点实例,未注册则报错 |
|
||||
| `build_dag` | `(&self, def: &DagDef) -> Result<Dag>` | 从 `DagDef` 构建完整运行时 `Dag`(建节点 + 加边,自动分发条件边) |
|
||||
| `is_registered` | `(&self, type_name: &str) -> bool` | 检查类型是否已注册 |
|
||||
| `registered_types` | `(&self) -> Vec<&str>` | 列出所有已注册类型名 |
|
||||
|
||||
> `Default` 实现仅注册占位 `script` 工厂(`unimplemented!`),实际 `ScriptNode` 由 `df-nodes` crate 注册。
|
||||
|
||||
### DAG 执行流程
|
||||
|
||||
```
|
||||
1. 接收 WorkflowDef (DAG 定义)
|
||||
2. 拓扑排序 → 得到执行层 (layers)
|
||||
3. 逐层执行:
|
||||
- 同层节点并行 (TODO)
|
||||
- 阻塞节点等待人工操作
|
||||
- 非阻塞节点异步完成
|
||||
- 同层节点并行(`futures::future::join_all`,已实现)
|
||||
- 阻塞/非阻塞节点当前同等异步执行(`is_blocking` 未被 executor 消费,人工等待逻辑规划中、当前未实现)
|
||||
4. 状态变更通过 EventBus 广播
|
||||
5. 每个节点完成后持久化快照
|
||||
5. 节点完成后仅更新内存态(`StateMachine` + `outputs` HashMap),无持久化快照(规划中)
|
||||
```
|
||||
|
||||
### DAG 定义序列化(dag_def.rs)
|
||||
|
||||
区分两层表示:运行时 `Dag` 持 `Box<dyn Node>`(trait object,不可序列化);可持久化的 `DagDef` / `NodeDef` / `EdgeDef` 均 `#[derive(Serialize, Deserialize)]`,用于模板与存盘。`NodeRegistry::build_dag` 负责从 `DagDef` 还原运行时 `Dag`。
|
||||
|
||||
| 方法 | 签名 | 职责 |
|
||||
|------|------|------|
|
||||
| `DagDef::new` | `() -> Self` | 空定义 |
|
||||
| `DagDef::add_node` | `(&mut self, id, node_type, config: Value)` | 加节点定义(label 默认 None) |
|
||||
| `DagDef::add_edge` | `(&mut self, source, target)` | 加普通边(condition=None) |
|
||||
| `add_edge_with_condition` | `(&mut self, source, target, condition)` | 加带条件表达式的边 |
|
||||
| `from_dag_edges` | `(&dag: &Dag) -> Self` | 从运行时 `Dag` 反推定义;**注意**只能还原边的 condition 与节点的 `node_type`,`config` 一律填 `Value::Null`(无法从 trait object 反推) |
|
||||
|
||||
### 事件类型
|
||||
|
||||
- `WorkflowStarted` / `WorkflowCompleted` / `WorkflowFailed`
|
||||
事件枚举定义在 `df-core::events::WorkflowEvent`,`DagExecutor` 通过 `EventBus::send` 广播。
|
||||
|
||||
**执行器实际广播的(executor.rs)**:
|
||||
|
||||
- `NodeStarted` / `NodeCompleted` / `NodeFailed`
|
||||
- `WorkflowPaused` / `WorkflowResumed`
|
||||
- `WorkflowCompleted`
|
||||
|
||||
**枚举已定义但执行器当前未触发**(`df-core::events` 中存在,DagExecutor 不发):
|
||||
|
||||
- `NodeProgress`(节点进度)
|
||||
- `NodeOutput`(节点输出流)
|
||||
- `WorkflowPaused`(暂停等待外部输入)
|
||||
- `WorkflowFailed`(工作流失败,带 failed_node)
|
||||
- `HumanApprovalRequest` / `HumanApprovalResponse`(人工审批)
|
||||
|
||||
> 注:枚举中无 `WorkflowStarted` / `WorkflowResumed`,旧文档所述为误。
|
||||
|
||||
### EventBus(eventbus.rs)
|
||||
|
||||
基于 `tokio::sync::broadcast` 的发布/订阅,内部持 `broadcast::Sender<WorkflowEvent>`。
|
||||
|
||||
| 方法/成员 | 签名 | 职责 |
|
||||
|------|------|------|
|
||||
| `DEFAULT_CAPACITY` | `const usize = 256` | 默认通道容量 |
|
||||
| `new` | `() -> Self` | 用默认容量建总线 |
|
||||
| `with_capacity` | `(usize) -> Self` | 指定容量建总线 |
|
||||
| `send` | `(&self, WorkflowEvent) -> ()` | 广播事件(忽略接收者已关闭错误,异步) |
|
||||
| `subscribe` | `(&self) -> broadcast::Receiver<WorkflowEvent>` | 订阅事件流 |
|
||||
| `emit_human_approval_request` | `(&self, WorkflowEvent) -> Result<usize, SendError>` | 发送人工审批请求(返回接收者计数) |
|
||||
| `try_recv_human_approval` | `(&self, execution_id, node_id) -> Option<HumanApprovalResponse>` | **TODO 占位**:当前恒返回 `None`,审批响应存储/检索未实现,需配合前端 |
|
||||
| `Default` / `Clone` | — | `Default` 走 `new`;`Clone` 复刻 `sender`(broadcast sender 可 clone,共享通道) |
|
||||
|
||||
> `emit_human_approval_request` 与 `try_recv_human_approval` 为人工审批占位接口,后者**未实现**,执行器当前不消费审批响应(对应 `is_blocking` 等待逻辑亦未落地)。
|
||||
|
||||
### 状态机(state.rs)
|
||||
|
||||
`StateMachine` 维护 `HashMap<NodeId, NodeStatus>`,校验节点状态转换,非法转换返回错误。
|
||||
|
||||
**合法转换链**(`is_legal`):`Pending → Running`,`Running → Completed`,`Running → Failed`。其余均拒绝。
|
||||
|
||||
| 方法 | 签名 | 职责 |
|
||||
|------|------|------|
|
||||
| `new` / `get` | `... -> Self` / `(&NodeId) -> NodeStatus` | 创建;取状态(缺失默认 `Pending`) |
|
||||
| `set_running` | `(&mut self, NodeId) -> Result<()>` | `Pending → Running` |
|
||||
| `set_completed` | `(&mut self, NodeId) -> Result<()>` | `Running → Completed` |
|
||||
| `set_failed` | `(&mut self, NodeId) -> Result<()>` | `Running → Failed` |
|
||||
| `set_waiting` | `(&mut self, NodeId)` | 设为 `Waiting`,**绕过转换校验**(直接 set) |
|
||||
| `set_skipped` | `(&mut self, NodeId)` | 设为 `Skipped`,**绕过转换校验** |
|
||||
| `is_cancelled` | `(&NodeId) -> bool` | 是否为 `Cancelled`(同样无对应 setter,外部直接 set) |
|
||||
| `snapshot` | `() -> &HashMap<NodeId, NodeStatus>` | 全量状态快照引用 |
|
||||
|
||||
> `Waiting` / `Skipped` / `Cancelled` 三态暂未纳入 `is_legal` 校验链,对应的 `set_*` 直接 `insert`,可从任意态跳转。
|
||||
|
||||
### Dag 对外 API(dag.rs)
|
||||
|
||||
| 方法 | 签名 | 职责 |
|
||||
|------|------|------|
|
||||
| `new` | `() -> Self` | 空 DAG |
|
||||
| `add_node` | `(&mut self, id: NodeId, node: Box<dyn Node>)` | 加节点 |
|
||||
| `add_edge` | `(&mut self, source, target)` | 加普通边(condition=None) |
|
||||
| `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 中存在环") |
|
||||
|
||||
`Edge { source, target, condition: Option<String> }` — condition 为可选条件表达式,由 `conditions.rs` 求值。
|
||||
|
||||
### 条件表达式引擎(conditions.rs)
|
||||
|
||||
`ConditionEngine::evaluate(expr: &str, context: &Value) -> Result<bool>` — 仅支持 `"true"` / `"false"` 字面量(区分大小写,先 `trim()` 去空白)。
|
||||
|
||||
**默认放行(安全风险)**:空串、`"True"`/`"FALSE"` 等大小写不匹配字面量、以及任意非 `true`/`false` 字面量(如 `"yes"`、`"1"`、`"$.status == 'completed'"`)均回退为 `Ok(true)` 并 `tracing::warn!`。即**条件不匹配时放行而非阻断**,DAG 边全通 —— 无法据上游输出做条件分支。
|
||||
|
||||
> 现状:JSON Path、比较运算、`contains`、逻辑组合均未实现(`context` 参数当前未被使用,仅占位对齐签名)。详见 [B 路线决策接入需求](#🔮-决策能力接入需求b-路线)。
|
||||
|
||||
## 依赖关系
|
||||
|
||||
@@ -66,15 +194,33 @@ df-core (类型、事件、错误)
|
||||
|
||||
```
|
||||
crates/df-workflow/src/
|
||||
├── lib.rs — 模块入口
|
||||
├── dag.rs — DAG 数据结构与拓扑排序
|
||||
├── executor.rs — DagExecutor
|
||||
├── node.rs — Node trait + NodeContext/Output
|
||||
├── state.rs — 状态机
|
||||
├── event.rs — EventBus
|
||||
└── condition.rs — 条件表达式引擎
|
||||
├── lib.rs — 模块入口
|
||||
├── dag.rs — DAG 数据结构与拓扑排序
|
||||
├── dag_def.rs — 可序列化 DAG 定义(DagDef/NodeDef/EdgeDef)
|
||||
├── executor.rs — DagExecutor
|
||||
├── node.rs — Node trait + NodeContext/Output
|
||||
├── state.rs — 状态机
|
||||
├── eventbus.rs — EventBus(broadcast)
|
||||
├── registry.rs — NodeRegistry(节点类型注册表)
|
||||
└── conditions.rs — 条件表达式引擎
|
||||
```
|
||||
|
||||
## 🔮 决策能力接入需求(B 路线)
|
||||
|
||||
> 2026-06-12 记录。executor 分层并行已具骨架,conditions 为最大空壳。
|
||||
|
||||
AI Chat(B 路线)从单链 ReAct 升级为规划式协作时,本引擎需补两处:
|
||||
|
||||
1. **`condition.rs::ConditionEngine::evaluate()`** —— 当前只认 `"true"` / `"false"` 字面量,默认 `Ok(true)`,DAG 边全通,无法据 AI 输出做条件分支。需补:
|
||||
- JSON Path 取值(从上游节点输出读字段)
|
||||
- 比较运算(`==` `!=` `>` `<` `>=` `<=`)
|
||||
- `contains` / 字符串匹配
|
||||
- `and` / `or` / `not` 组合
|
||||
|
||||
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)。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [df-nodes 节点集合](./df-nodes-节点集合.md)
|
||||
|
||||
@@ -57,6 +57,69 @@
|
||||
2. **数据导出**:备份和分享
|
||||
3. **插件系统**:扩展功能
|
||||
|
||||
## 🗂️ 知识库(Tier 1 已完成,Sprint 15)
|
||||
|
||||
> 2026-06-13 完成。
|
||||
|
||||
**已落地**:
|
||||
- candidate→published→archived 状态机 + 人工门控(AI 只产 candidate)
|
||||
- AI 自动提炼(对话完成后 spawn,min_messages≥4 守卫)
|
||||
- LIKE 关键词检索 + system prompt 注入(auto_inject 开关)
|
||||
- 手动录入(Knowledge.vue)+ 审核收件箱(按 confidence 排序)
|
||||
- Phase 5.5:向量 embedding + 混合检索(Settings 开关,默认关,openai_compat embed)
|
||||
- 11 个 IPC Command,设计对齐 MCP 语义
|
||||
|
||||
**Tier 2 待做**:
|
||||
- ai_node prompt 注入(节点级知识上下文)
|
||||
- 工作流 NodeFailed→pitfall 自动沉淀
|
||||
- 决策记录→architecture_pattern(前置 traceability 持久化)
|
||||
- MCP Shell 封装(外部工具 Claude Code/CodeX/Cursor 访问)
|
||||
|
||||
详见 [知识库模块文档](../03-模块文档/df-knowledge-知识库.md)。
|
||||
|
||||
---
|
||||
|
||||
## 🧠 决策能力升级(B 路线)
|
||||
|
||||
> 2026-06-12 记录。单独立项,A 路线(UX 快赢)完成并验证后启动。
|
||||
|
||||
**现状**:AI Chat 为单链 ReAct —— `run_agentic_loop`(`src-tauri/src/commands/ai.rs`)最多 10 轮,串行执行工具调用,无规划、无协作、无条件分支、无自纠。仅风险门控(Low 自动 / Medium+High 审批)做得较完整。
|
||||
|
||||
**目标链路**:用户意图 → LLM 规划子任务 → coordinator 拆 DAG → executor 分层并行执行 → conditions 按 AI 输出做条件路由 → reflection 自纠。
|
||||
|
||||
**需求点**:
|
||||
1. **Planning(任务规划)**:意图 → LLM 先规划子任务 DAG,再执行
|
||||
2. **Coordinator(多 agent 协作)**:填 `crates/df-ai/src/coordinator.rs::AgentCoordinator::run()`(当前全 TODO,返回硬编码 `"TODO: Agent 协作结果"`)
|
||||
3. **Conditions(条件分支)**:填 `crates/df-workflow/src/conditions.rs::ConditionEngine::evaluate()`(当前只认 `true`/`false` 字面量,缺 JSON Path / 比较 / contains / and-or-not,默认 `Ok(true)` 致 DAG 边全通)
|
||||
4. **分层并行接入**:`executor.rs` 已有 `topological_layers` + `join_all`(测试 `test_same_layer_runs_in_parallel`),但仅喂静态 DAG,agentic loop 未接入
|
||||
5. **Reflection(自纠)**:缺失,需执行后自检 / 重试机制
|
||||
|
||||
**启动前置**:重读 coordinator.rs / conditions.rs 确认仍为空壳(期间可能有变动)。
|
||||
|
||||
## 🧩 aichat 后续需求点(路线总览)
|
||||
|
||||
> 2026-06-12 记录。汇总 aichat「决策能力 / 技能联想」两类后续需求,与上方 B 路线互补,便于排期。
|
||||
|
||||
### A/B 路线拆分
|
||||
|
||||
- **A 线 — UX 快赢(进行中/部分已完成)**:对话管理(时间分组 + 归档折叠 + 重启恢复对话/窗口)、技能指令注入等体验改进,低成本快速落地。
|
||||
- **B 线 — 决策能力补强(单独立项)**:当前 AI 会话是单链 ReAct,coordinator / conditions 等为空壳,无规划式智能。需补多步规划、条件分支、任务分解等决策能力(详见上方「🧠 决策能力升级(B 路线)」)。
|
||||
|
||||
### 决策能力现状(待 B 线解决)
|
||||
|
||||
- **单链 ReAct**:LLM 流式生成 → 工具调用 → 结果回传 → 循环(最多 `MAX_AGENT_ITERATIONS=10` 轮)。
|
||||
- **缺**:无前置规划、无条件编排、无多路径裁决。能力天花板受限于单轮 tool-use 循环。
|
||||
- **佐证**:`run_agentic_loop`(`src-tauri/src/commands/ai.rs`)串行执行工具,`coordinator.rs::run()` 返回硬编码 TODO,`conditions.rs::evaluate()` 默认 `Ok(true)` 致 DAG 边全通。
|
||||
|
||||
### 技能 / 联想需求
|
||||
|
||||
- **目标**:输入 `/` 时联想本机 Claude 技能(skills / commands / plugins 三类)并选用。
|
||||
- **现状**:
|
||||
- 后端:扫描三类来源(`~/.claude/skills/*/SKILL.md` + `~/.claude/commands/*.md` + `~/.claude/plugins/marketplaces/**/skills/*/SKILL.md`)+ SKILL.md 全文注入 system prompt 已实现(`ai_list_skills` / `read_skill_content`)。
|
||||
- 前端:`/` 联想浮层 UI 已初步,待完善匹配排序与参数提示。
|
||||
- **定位**:与 B 路线(决策能力)独立,属技能注入体系;可作为独立小需求排期,工作量中(后端扫文件 + IPC、前端联想浮层)。
|
||||
- **后续可扩展**:Codex(`~/.codex/vendor_imports/skills`)frontmatter 与 Claude 一致,可统一解析纳入;openclaw 属 agent 选择层、不纳入。
|
||||
|
||||
## 📊 关键指标
|
||||
|
||||
### 使用指标
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# DevFlow 文档索引
|
||||
|
||||
> 创建: 2026-06-10 | 当前阶段: Phase 2 本地优先开发流程验证
|
||||
> 更新: 2026-06-11 | 清理 60% 功能,聚焦核心链路
|
||||
> 更新: 2026-06-13 | 新增规格契约自检机制(活契约 + AI 自检)
|
||||
|
||||
---
|
||||
|
||||
@@ -21,13 +21,20 @@ docs/
|
||||
│ ├── 业务系统设计.md # 业务系统设计
|
||||
│ ├── 前后端类型对齐.md # Rust/TS 类型对齐规范
|
||||
│ ├── 对抗论证裁决报告.md # 60% 功能清理决策
|
||||
│ └── 产品定位调整.md # 新定位:本地优先个人开发流程驾驶舱
|
||||
│ ├── 产品定位调整.md # 新定位:本地优先个人开发流程驾驶舱
|
||||
│ ├── 功能决策记录.md # 需求规格 + 设计决策规格(为什么这么定 + 要做什么)
|
||||
│ ├── 经验记录.md # 踩坑/约定/技巧/bug 排查教训
|
||||
│ ├── 功能决策记录-归档.md # 归档只读(纯流水/老 Sprint/UX 微调/已被取代)
|
||||
│ ├── 文档记录规范.md # 写文档路由:记哪/优先级/去重(SSOT)
|
||||
│ ├── 规格契约自检机制.md # 活契约 + AI 自检 + 子代理验证(agent/skill/hook 基准)
|
||||
│ └── B-03-人工审批响应机制.md # HumanNode 审批响应:subscribe→send→select! 广播过滤等待
|
||||
├── 03-模块文档/ # 各功能模块实现文档
|
||||
│ ├── df-storage-存储层.md # 存储层概览
|
||||
│ ├── df-workflow-工作流引擎.md # 工作流引擎概览
|
||||
│ ├── df-nodes-节点集合.md # 8 种节点概览
|
||||
│ ├── df-ai-AI集成模块.md # AI Provider 集成
|
||||
│ └── 想法探索-对抗式评估.md # 想法池对抗评估设计
|
||||
│ ├── df-ai-AI集成模块.md # AI Provider 集成(OpenAI/Anthropic/embed/ContextManager)
|
||||
│ ├── 想法探索-对抗式评估.md # 想法池对抗评估设计
|
||||
│ └── df-knowledge-知识库.md # 知识库 Tier 1(候选→发布状态机 + 检索注入 + 向量)
|
||||
├── 04-功能迭代/ # 功能开发过程记录
|
||||
│ ├── DEVFLOW-1.CRUD层实施.md # CRUD 层实施记录
|
||||
│ ├── DEVFLOW-2.IPC桥接实施.md # IPC 桥接实施记录
|
||||
@@ -42,8 +49,7 @@ docs/
|
||||
│ └── 组件设计规范.md # Vue 3 组件设计规范
|
||||
├── 07-项目管理/ # 项目状态、功能清单、版本管理
|
||||
│ ├── Phase1任务清单.md # Phase 1 任务清单
|
||||
│ ├── Phase2计划.md # Phase 2 开发计划
|
||||
│ └── PROGRESS.md # 项目进展与交接记录
|
||||
│ └── Phase2计划.md # Phase 2 开发计划
|
||||
└── 08-用户指南/ # 用户手册、配置指南
|
||||
├── 快速上手.md # 5分钟快速上手
|
||||
├── 配置指南.md # AI 配置、Git 集成
|
||||
|
||||
94
docs/todo.md
Normal file
94
docs/todo.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# DevFlow 工作看板
|
||||
|
||||
> 来源:`docs/02-架构设计/功能决策记录.md`「需求与待办」+ `PROGRESS.md` 各 Sprint 遗留,2026-06-14 汇总去重 + 代码核对修正。
|
||||
> 互操作:执行走 mission-control,回写 mission_id;审查走 cr;发布走 publish-*。
|
||||
> 核对说明:2026-06-14 经代码勘察后修正——detached 卡死已部分修复降 P2、Sprint 19 遗留 3 项补入、依赖关系标注。
|
||||
|
||||
---
|
||||
|
||||
## 交接状态(2026-06-14)
|
||||
|
||||
**代码健康度**:`cargo test --workspace` 全过、`npx vue-tsc --noEmit` 0 error(主代理独立验证,非 mission 自报)。
|
||||
|
||||
**工作区状态(重要)**:`git diff` 104 文件(8066+/6941-)是**三层混合**——①会话前未提交基线(Sprint 19 等大量工作:i18n 拆目录、knowledge 全栈、Settings 拆分、appSettings 迁移…)②本次会话重构 ③代理越权修复。**接手前务必 `git diff` 通览区分**,勿整体当作单一改动提交。
|
||||
|
||||
**本次会话完成**:
|
||||
- 重构(用户授权):删 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 诊断日志清理
|
||||
|
||||
**待设计交其他会话(核心)**: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 的前置。
|
||||
|
||||
**失控代理教训**:本次会话派的后台拆分代理在 stop hook 循环里失控,越权改代码/文档(先斩后奏)。接手方若再派 agent,注意约束其不碰决策记录(用户已要求手动触发)+ 限定单任务不自主续推。
|
||||
|
||||
---
|
||||
|
||||
## 待办
|
||||
|
||||
### 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-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)
|
||||
|
||||
### P0 — 阻断性 bug(df-workflow 审批闭环,依赖链:B-06/B-07 → B-03)
|
||||
|
||||
- [ ] B-260614-06 — **[P0→前置]** df-workflow DagExecutor execution_id 硬编码 "dummy-execution-id" — `executor.rs:76` 所有执行 ID 相同,追踪/审计失效。改为从外部传入(run_workflow IPC 已生成真 execution_id 但未下沉到 executor) — source:代码审查 (06-14)
|
||||
- [ ] B-260614-07 — **[P0→前置]** df-workflow executor 每节点拿全新空 StateMachine — `executor.rs:78` 每节点 `StateMachine::new()`,self.state_machine 从不传入 NodeContext,HumanNode is_cancelled 恒 false。改为共享 self.state_machine — source:多代理探索 (06-14)
|
||||
- [ ] B-260614-03 — **[P0→依赖 B-06/B-07]** df-workflow HumanNode 假实现 — `human_node.rs:55` 注释"等待审批"但首次迭代直接 return "同意",与 ai.rs 严谨审批链路矛盾。**📐 设计完成 [B-03-人工审批响应机制.md](./02-架构设计/B-03-人工审批响应机制.md)**(subscribe→send→select! 广播过滤;execution_id+node_id 双键;拆 B-03a 响应等待/超时 + B-03b 取消机制;通道选型 = **工作流独立审批通道**复用 EventBus / HumanApprovalResponse 事件 / approve_human_approval IPC,非 ai.rs AiApprovalRequired)。核对修正原注:B-06 = 并发隔离前置(非单流功能前置),B-07 = 取消必要非充分,**B-03a 不依赖 B-07** — source:多代理探索 + 设计 (06-14)
|
||||
|
||||
### P1 — 重要缺陷
|
||||
|
||||
- [x] B-260614-08 — ~~promote_idea 两步写非事务~~ ✅ mission:T-260614-05 已修复(idea.rs 第二步 update_full 失败时补偿删除已建 project;Repository 不支持跨 repo 共享事务对象,选补偿删除非真事务,改动最小;附 logging)(06-14)
|
||||
- [ ] T-260614-01 — **[P1]** Sprint 9/10/14/15/16/18 多项未 tauri dev 实测 — 评分 IPC 缩放 / update_full / promote_idea / Store getter / token 落库 / 知识库 Tier 1 全栈 / LLM 并发 Semaphore / 知识生命线(#54 跟踪)— source:Sprint 9-18 (06-14)
|
||||
- [ ] T-260614-02 — **[P1]** 切对话不中断路由:部分场景运行时实测(A 路线场景 2/3) — source:Sprint 8 (06-14)
|
||||
|
||||
### P1 — 设计完成待实施
|
||||
|
||||
- [ ] F-260614-01 — **[P1]** 模型能力系统 Phase 1 — ModelCapability 数据模型 + ModelRouter 重写 + 7 调用点接入 + Settings 模型池编辑 UI + AiChat 模型下拉。按任务需求(模态/功能/成本)自动匹配合适模型,不再所有场景共用 default_model — source:📐 设计完成 (06-14)
|
||||
|
||||
### P1 — Sprint 19 遗留
|
||||
|
||||
- [x] T-260614-05 — ~~工具结果入库前截断 50KB~~ ✅ mission:T-260614-04 已修复(conversation.rs 加 `truncate_for_persist` 纯函数,50KB 阈值 + 头尾各 20KB + 中段标注省略字符数;仅作用于持久化视图不污染内存真相源;3 单测 pass)(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)
|
||||
|
||||
### 待澄清 / A-B 待定
|
||||
|
||||
- [ ] S-260614-01 — 「显示多开」需求待澄清 — 用户报"设置勾选显示多开但 AiChat 未显示",全 src grep 零命中,疑似旧版本/指分离窗口/想新增开关,待用户截图确认 (06-14)
|
||||
- [ ] S-260614-02 — 审批可见性 A/B 待定 — B-01 修复后 pending_approvals 内存态不再被 switch 清空,前端 `ai_pending_tool_calls` 查询有数据,但 `state.pendingApprovals` 在 AiChat.vue 是否有兜底渲染仍需实测确认。A. 加兜底渲染 / B. 实测 tc 卡片是否渲染 (06-14)
|
||||
|
||||
### P2 — 不阻断缺陷 / 增强
|
||||
|
||||
- [ ] B-260614-05 — **[P2→降级]** 分离窗口(detached)跨窗口状态 — **核对修正**:reattachPanel 已接线(不再死代码)+ `tauri://destroyed` 监听已复位状态,"detached 永真卡死"已修复;剩余 localStorage `df-ai-gen`/`df-ai-text` 是 Sprint 19 **有意保留**(流式临时快照高频写),非 bug。仅在出现新场景失效时再评估改全局 emit/listen — source:代码审查 + Sprint 19 (06-14)
|
||||
- [ ] T-260614-03 — 工具层白名单与 crud 白名单双份去重(架构债) — source:经验记录 (06-14)
|
||||
- [ ] F-260614-02 — 技能联想「使用」 — 首批 3 类联想已做,联想后实际触发/执行技能未实现 (06-14)
|
||||
- [ ] F-260614-03 — **[需设计]** 灵感对抗评估接 LLM — 核对 adversarial.rs:`evaluate()` 纯启发式,接 LLM 需改签名注入 provider。3 种注入方式待定:A.trait 解耦(df-ideas 定义 IdeaAnalyzer trait,推荐)/ B.df-ideas 直接依赖 df-ai / C.closure。属架构决策非直接做 (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-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)
|
||||
|
||||
## 已完成
|
||||
|
||||
### 2026-06-14
|
||||
|
||||
- [x] R-260614-01 ai.rs 拆 11 子 module(commands/ai/)+ glob 重导出保路径 + models bug 修复 — cargo check 0 error / 19 test passed
|
||||
- [x] B-260614-01 待审批持久化根治 — mission:T-260614-01
|
||||
- [x] B-260614-02 df-workflow ConditionEngine 默认 true→false — mission:T-260614-02
|
||||
- [x] B-260614-04 NodeRegistry unimplemented!→删 Default impl — mission:T-260614-03
|
||||
- [x] T-260614-05 工具结果入库前截断 50KB(含 3 单测)— mission:T-260614-04
|
||||
- [x] B-260614-08 promote_idea 补偿删除保最终一致性 — mission:T-260614-05
|
||||
- [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 前置**
|
||||
|
||||
## Bug
|
||||
|
||||
(P0/P1 bug 见上方「待办」分类,此处不重复)
|
||||
|
||||
## 长期 / 待需求驱动(不进看板主线)
|
||||
|
||||
- 裁剪/压缩消息按需召回(Query Function + 分层存储)
|
||||
- 停止生成 idle 即时优化(`tokio::sync::Notify` 替代 120s 轮询)
|
||||
- 模型能力系统 Phase 3(Agent 内智能路由 + 成本预算 + 模型级联)
|
||||
- `node_executions` 全表 list 命令(当前只写不读)
|
||||
- `do_promote` crate 层 TODO(promotion.rs,现走前端闭环)
|
||||
Reference in New Issue
Block a user