经代码核验发现 9 份设计文档的状态标注严重滞后(标'待实施'但 实际已完整落地),本次批量同步: 已落地(核验确认): - 局部编辑工具:三层防御+三模式完整,仅文本不支持二进制 - 密钥迁移健壮性:空 key 不覆盖+即时迁移补密钥+阻断保存 - AST 符号解析:符号读取工具已注册+基线测试守护 - 查询能力补全:任务/项目/灵感均多维动态查询 - 条件表达式引擎:手写求值器+JSON Path+执行器集成+前端入口 - 工作流脚本边界:命令白名单/黑名单+危险关键词告警 - 消息拆分存储:消息表+全量迁移+读写全部切换 - 消息级溯源:消息 ID+四场景溯源+切读全部完成 部分落地: - 全局事件总线:基建+20 余个发射点就位,消费者未接(空转) 归档不实施: - 规格契约自检:核心价值已被求助协议+自审闸门覆盖,过度设计
236 lines
8.5 KiB
Markdown
236 lines
8.5 KiB
Markdown
# 消息级溯源设计
|
|
|
|
> 创建:2026-06-19 | 编号:F-260619-04(消息级溯源) | 状态:✅ **已落地**(2026-06-28 核验:ChatMessage.id + source_ref/audit/idea 四场景全部完成 + P2 切读全部完成)
|
|
> 关联任务:`消息级溯源:知识库/审计/灵感全链路从对话级升级到消息级`(DevFlow 项目)
|
|
> 关联灵感:b2e61a21 消息拆分存储 + 消息级溯源 + 知识脉络网络
|
|
> 关联设计:[消息拆分存储设计-2026-06-19.md](./消息拆分存储设计-2026-06-19.md)(本设计的持久化基础)
|
|
> 上级索引:[../INDEX.md](../INDEX.md)
|
|
|
|
---
|
|
|
|
## 一、目标
|
|
|
|
将 DevFlow 所有溯源场景从「对话级」升级为「消息级」,实现四个场景的精确定位:
|
|
|
|
| 场景 | 升级后溯源字段 | 粒度 |
|
|
|---|---|---|
|
|
| 知识库提炼 | `source_ref = "conv_msg:{message_id}"` | 消息级 |
|
|
| 知识生命线·提炼/引用 | `source_ref = "conv_msg:{message_id}"` | 消息级 |
|
|
| 工具执行审计 | `message_id: Option<String>` 独立列 | 消息级 |
|
|
| 灵感来源 | `source = "conv_msg:{message_id}"` | 消息级 |
|
|
|
|
> **依赖关系**:本设计依赖[消息拆分存储](./消息拆分存储设计-2026-06-19.md)提供持久化消息 ID,但**中间态可不依赖拆表先上线**(见 §五)。
|
|
|
|
---
|
|
|
|
## 二、现状(4 个溯源场景全部只到对话级)
|
|
|
|
| 场景 | 当前溯源字段 | 粒度 | 位置 |
|
|
|---|---|---|---|
|
|
| 知识库提炼 | `source_ref = "conv:{conversation_id}"` | 对话级 | `knowledge_inject.rs:434` |
|
|
| 知识生命线·提炼/引用 | `source_ref = "conv:{conversation_id}"` | 对话级 | `knowledge_timeline.rs:87,99` |
|
|
| 工具执行审计 | `conversation_id`(无 message_id 列) | 对话级 | `audit/mod.rs`(14 处写入) |
|
|
| 灵感来源 | `source`(自由文本,无结构) | 无结构 | `idea.rs:27` |
|
|
|
|
### 痛点
|
|
|
|
- **知识溯源**:打开整个对话,不知道是哪一轮产出的
|
|
- **工具审计(最痛)**:`tool_call_id` 埋在整个对话 JSON 里,排查"AI 为什么删这个文件"需反序列化全量 JSON 逐条搜
|
|
- **知识引用追踪**:不知道知识在对话哪一轮被使用
|
|
|
|
---
|
|
|
|
## 三、前置:ChatMessage 加 id 字段
|
|
|
|
### 3.1 字段定义
|
|
|
|
`crates/df-ai-core/src/types.rs:72` 的 `ChatMessage` 加:
|
|
|
|
```rust
|
|
/// 消息全局唯一 ID(ULID)。用于消息级溯源(source_ref / audit message_id / idea source)。
|
|
/// 构造时生成;老 JSON 反序列化为 None(向前兼容)。
|
|
/// 消息拆分存储(F-260619-03)后,此 ID 即 ai_messages.id 列主键。
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub id: Option<String>,
|
|
```
|
|
|
|
### 3.2 ID 生成时机
|
|
|
|
| 构造路径 | ID 生成 |
|
|
|---|---|
|
|
| `ChatMessage::new_user()` | 构造时 `Some(new_id())` |
|
|
| `ChatMessage::new_assistant()` | 构造时 `Some(new_id())` |
|
|
| `ChatMessage::new_tool()` | 构造时 `Some(new_id())` |
|
|
| `ChatMessage::new_system()` | 构造时 `Some(new_id())` |
|
|
| 老消息反序列化 | `None`(serde default) |
|
|
|
|
> `new_id()` 来自 `df-types::types::new_id()`(ULID)。df-ai-core 已依赖 df-types。
|
|
|
|
### 3.3 向前兼容
|
|
|
|
- `#[serde(default)]`:老 JSON 无 id 字段 → 反序列化为 `None`
|
|
- `skip_serializing_if = "Option::is_none"`:id = None 时不序列化(不污染老格式)
|
|
- **round-trip 测试**:序列化 → 反序列化 → 字段一致
|
|
|
|
---
|
|
|
|
## 四、溯源字段升级
|
|
|
|
### 4.1 知识库提炼
|
|
|
|
**文件**:`src-tauri/src/commands/ai/knowledge_inject.rs:434`
|
|
|
|
```rust
|
|
// 旧
|
|
source_ref: Some(format!("conv:{}", conv_id)),
|
|
|
|
// 新
|
|
source_ref: Some(format!("conv_msg:{}", message_id)),
|
|
```
|
|
|
|
`message_id` 来源:提炼触发时,取当前 assistant 消息的 `id`。
|
|
|
|
### 4.2 知识生命线
|
|
|
|
**文件**:`src-tauri/src/commands/knowledge_timeline.rs:87,99`
|
|
|
|
```rust
|
|
// 旧(timeline 提炼事件 + 引用事件)
|
|
source_ref: Some(format!("conv:{}", conv_id)),
|
|
|
|
// 新
|
|
source_ref: Some(format!("conv_msg:{}", message_id)),
|
|
```
|
|
|
|
### 4.3 工具执行审计
|
|
|
|
**文件**:`crates/df-storage/src/models.rs` + `crud/conversation_repo.rs` + `src-tauri/src/commands/ai/audit/mod.rs`
|
|
|
|
#### 4.3.1 Model + 迁移
|
|
|
|
`AiToolExecutionRecord` 加 `message_id: Option<String>`。
|
|
|
|
V21 迁移(与消息拆分同一版本,或拆为 V21a/V21b):
|
|
|
|
```rust
|
|
// migrations.rs migrate_v21
|
|
if !column_exists(conn, "ai_tool_executions", "message_id") {
|
|
conn.execute(
|
|
"ALTER TABLE ai_tool_executions ADD COLUMN message_id TEXT",
|
|
[],
|
|
)?;
|
|
tracing::info!("v21: 补建 ai_tool_executions.message_id 列(消息级溯源)");
|
|
}
|
|
```
|
|
|
|
V9 建表 SQL 同步补 `message_id TEXT` 列(新库直接有)。
|
|
|
|
#### 4.3.2 CRUD 层
|
|
|
|
- `ai_tool_execution_from_row`:加 `message_id: row.get("message_id")?`
|
|
- insert/update SQL:加 message_id 列 + params 占位
|
|
|
|
#### 4.3.3 审计写入(14 处)
|
|
|
|
`audit/mod.rs` 中 14 处写入 `AiToolExecutionRecord` 的位置,在 `conversation_id` 旁加 `message_id`:
|
|
|
|
```rust
|
|
// audit 记录构造
|
|
let record = AiToolExecutionRecord {
|
|
// ... 既有字段 ...
|
|
conversation_id: Some(conv_id.clone()),
|
|
message_id: current_message_id.clone(), // 新增
|
|
// ...
|
|
};
|
|
```
|
|
|
|
`current_message_id` 来源:audit 写入时,从 ContextManager 取当前 assistant 消息的 `id`。
|
|
|
|
### 4.4 灵感来源
|
|
|
|
**文件**:`src-tauri/src/commands/idea.rs:27`
|
|
|
|
```rust
|
|
// 旧(AI 路径,自由文本)
|
|
source: Some(format!("AI 对话 {}", conv_id)),
|
|
|
|
// 新(AI 路径,结构化)
|
|
source: Some(format!("conv_msg:{}", message_id)),
|
|
```
|
|
|
|
人工创建的灵感 source 保持自由文本不变。
|
|
|
|
---
|
|
|
|
## 五、中间态(不依赖消息拆表)
|
|
|
|
本设计可**先于消息拆分存储上线**:
|
|
|
|
### 5.1 内存 ID 已足够写入溯源
|
|
|
|
- ChatMessage 加 id 字段后,所有新消息有 ULID
|
|
- 溯源写入时带上 message_id —— **信息已存下**
|
|
|
|
### 5.2 查询体验(中间态)
|
|
|
|
| 操作 | 中间态(消息未拆表) | 最终态(消息拆表后) |
|
|
|---|---|---|
|
|
| 知识溯源点击 | 反序列化对话 JSON 搜 id | `SELECT * FROM ai_messages WHERE id = ?`(O(1)) |
|
|
| 工具审计查消息 | 反序列化对话 JSON 搜 id | `SELECT * FROM ai_messages WHERE id = ?`(O(1)) |
|
|
|
|
中间态查询不快,但**溯源信息已完整存下**,消息拆表后自然升级为 O(1)。
|
|
|
|
### 5.3 source_ref 格式约定
|
|
|
|
| 格式 | 含义 | 示例 |
|
|
|---|---|---|
|
|
| `conv:{conversation_id}` | 对话级(旧格式,向前兼容) | `conv:abc123` |
|
|
| `conv_msg:{message_id}` | 消息级(新格式) | `conv_msg:01J...` |
|
|
|
|
解析逻辑:前缀匹配 `conv_msg:` → 消息级;`conv:` → 对话级(兼容)。
|
|
|
|
---
|
|
|
|
## 六、涉及文件
|
|
|
|
| 文件 | 改动 |
|
|
|---|---|
|
|
| `crates/df-ai-core/src/types.rs` | ChatMessage 加 `id: Option<String>` + 构造器生成 |
|
|
| `crates/df-storage/src/migrations.rs` | V21 迁移 ai_tool_executions 加 message_id 列 + V9 建表 SQL 同步 |
|
|
| `crates/df-storage/src/models.rs` | AiToolExecutionRecord 加 `message_id: Option<String>` |
|
|
| `crates/df-storage/src/crud/conversation_repo.rs` | ai_tool_execution_from_row + insert/update SQL 补 message_id |
|
|
| `src-tauri/src/commands/ai/knowledge_inject.rs` | source_ref 升级为 `conv_msg:{id}` |
|
|
| `src-tauri/src/commands/knowledge_timeline.rs` | source_ref 升级(2 处) |
|
|
| `src-tauri/src/commands/ai/audit/mod.rs` | 14 处 conversation_id 写入旁加 message_id |
|
|
| `src-tauri/src/commands/idea.rs` | source 字段结构化(AI 路径) |
|
|
|
|
---
|
|
|
|
## 七、验收标准
|
|
|
|
1. **ChatMessage 有稳定 id**:构造时生成 ULID,序列化/反序列化 round-trip 正确
|
|
2. **知识库提炼的 source_ref 为 `conv_msg:{id}` 格式**
|
|
3. **工具审计记录携带 message_id**(14 处全覆盖)
|
|
4. **老数据(无 id)无回归**:source_ref 解析兼容 `conv:` 旧格式;ChatMessage id = None 不报错
|
|
5. `cargo check --workspace EXIT 0`
|
|
|
|
---
|
|
|
|
## 八、与消息拆分存储的协作
|
|
|
|
```
|
|
消息级溯源(F-260619-04) 消息拆分存储(F-260619-03)
|
|
┌─────────────────────────┐ ┌─────────────────────────┐
|
|
│ ChatMessage.id 字段 │◄────────│ ai_messages 表 │
|
|
│ (内存 ULID,中间态) │ 提供 │ (持久化 id,最终态) │
|
|
│ │ │ │
|
|
│ source_ref = conv_msg:X │ 查询时 │ SELECT WHERE id = X │
|
|
│ audit.message_id = X │───────►│ (O(1) 查询) │
|
|
└─────────────────────────┘ └─────────────────────────┘
|
|
```
|
|
|
|
**实施顺序建议**:
|
|
|
|
1. **先上消息级溯源**(本设计):ChatMessage 加 id + 溯源字段升级 —— 中间态可上线
|
|
2. **再上消息拆分存储**:ai_messages 表 + 全量迁移 + 切读 —— 溯源查询升级为 O(1)
|