优化: 文档路径同步 + 审查登记 + 消息级溯源设计文档
- PROGRESS/URGENT 文档分类后子目录路径修正
- 待审查.md 登记 CR-260619-08 intent + CR-260619-09 PhaseB+C(均 ✅ PASS)
- 新增消息级溯源设计(消息拆分存储 + 知识/审计/灵感全链路消息级定位)
This commit is contained in:
308
docs/02-架构设计/专项设计/消息拆分存储设计-2026-06-19.md
Normal file
308
docs/02-架构设计/专项设计/消息拆分存储设计-2026-06-19.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# 消息拆分存储设计
|
||||
|
||||
> 创建:2026-06-19 | 编号:F-260619-03(消息拆分) | 状态:📐 设计待实施
|
||||
> 关联任务:`消息拆分存储:ai_messages 表 + 全量迁移 + 三阶段渐进切换`(DevFlow 项目)
|
||||
> 关联灵感:b2e61a21 消息拆分存储 + 消息级溯源 + 知识脉络网络
|
||||
> 关联设计:[消息级溯源设计-2026-06-19.md](./消息级溯源设计-2026-06-19.md)(本设计的前置依赖 + 消费方)
|
||||
> 上级索引:[../INDEX.md](../INDEX.md)
|
||||
|
||||
---
|
||||
|
||||
## 一、目标
|
||||
|
||||
将 `ai_conversations.messages`(整个对话 JSON 数组存于单 TEXT 列)拆分为独立的 `ai_messages` 表(每条消息一行),实现:
|
||||
|
||||
- **增量写入**:长对话(50+ 轮)save 不再全量序列化覆盖写
|
||||
- **O(1) 删除/压缩/单条编辑**:clear/compress/replace_tool_result_content 改为 SQL 单行操作
|
||||
- **游标分页加载**:超长对话按 seq 范围拉取(未来)
|
||||
- **持久化消息 ID**:为[消息级溯源](./消息级溯源设计-2026-06-19.md)提供 O(1) 查询基础
|
||||
|
||||
---
|
||||
|
||||
## 二、现状
|
||||
|
||||
### 2.1 存储模型
|
||||
|
||||
```
|
||||
ai_conversations.messages TEXT -- 存整个 Vec<ChatMessage> 序列化 JSON
|
||||
```
|
||||
|
||||
- `save_conversation()`(`conversation.rs:135`)全量序列化覆盖写
|
||||
- 被 `chat.rs` / `agentic.rs` / 审批 / 压缩 / 编辑重生成等 **10+ 处**调用
|
||||
- `ContextManager`(`crates/df-ai/src/context.rs`)的 push/pop/replace_tool_result_content/compress_old_messages/insert_at/restore_from_messages 全部基于"内存 Vec"假设
|
||||
|
||||
### 2.2 ChatMessage 结构(当前 9 字段)
|
||||
|
||||
定义在 `crates/df-ai-core/src/types.rs:72`:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `role` | `MessageRole` | system/user/assistant/tool |
|
||||
| `content` | `String` | 文本内容(parts 的 Text 片与 content 一致) |
|
||||
| `parts` | `Option<Vec<ContentPart>>` | 多模态片(F-05 Phase 2a) |
|
||||
| `tool_call_id` | `Option<String>` | role=Tool 时必填 |
|
||||
| `tool_calls` | `Option<Vec<ToolCall>>` | AI 发起的工具调用 |
|
||||
| `model` | `Option<String>` | 生成该消息的 model(仅 assistant) |
|
||||
| `status` | `Option<String>` | None/"active" 正常;"truncated" 软删 |
|
||||
| `reasoning_content` | `Option<String>` | DeepSeek thinking 推理内容 |
|
||||
| `timestamp` | `Option<i64>` | Unix 毫秒(前端展示用) |
|
||||
|
||||
> **本设计新增**:`id: Option<String>`(ULID)——见[消息级溯源设计](./消息级溯源设计-2026-06-19.md) §三。
|
||||
|
||||
### 2.3 迁移基线
|
||||
|
||||
- 当前最新迁移版本:**V19**(`migrations.rs:45`)
|
||||
- V20 预留给 F-260619-01(任务关联灵感)
|
||||
- **本设计使用 V21**(建 ai_messages 表 + 全量迁移)
|
||||
- V22 预留给 Phase 3 删旧列
|
||||
|
||||
---
|
||||
|
||||
## 三、数据模型
|
||||
|
||||
### 3.1 新建 `ai_messages` 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS ai_messages (
|
||||
id TEXT PRIMARY KEY, -- ULID 全局唯一 + 时间有序
|
||||
conversation_id TEXT NOT NULL, -- 关联对话
|
||||
seq INTEGER NOT NULL, -- 对话内序号(排序用,从 0 起)
|
||||
role TEXT NOT NULL, -- system/user/assistant/tool
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
parts TEXT, -- 多模态 parts JSON(可空)
|
||||
tool_call_id TEXT,
|
||||
tool_calls TEXT, -- AI 发起的工具调用 JSON(可空)
|
||||
model TEXT, -- 生成该消息的 model(仅 assistant)
|
||||
status TEXT NOT NULL DEFAULT 'active', -- active/compressed/archived_segment/truncated
|
||||
reasoning_content TEXT,
|
||||
timestamp INTEGER, -- 消息创建时间(Unix 毫秒)
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE(conversation_id, seq)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ai_messages_conv ON ai_messages(conversation_id, seq);
|
||||
```
|
||||
|
||||
### 3.2 `ai_conversations` 表保留 messages 列
|
||||
|
||||
- Phase 2 双写期:旧列做回退保险
|
||||
- Phase 3 切读稳定后:V22 迁移删列
|
||||
|
||||
---
|
||||
|
||||
## 四、渐进路径(三阶段)
|
||||
|
||||
### Phase 1:脏标记增量写入(不改 schema)
|
||||
|
||||
**目标**:减少 save_conversation 的序列化开销。
|
||||
|
||||
- `ContextManager` 加 `dirty_min_seq: Option<usize>` / `dirty_max_seq: Option<usize>` 标记变化范围
|
||||
- `save_conversation` 只序列化变化范围内的消息(但仍写整列)
|
||||
- push/pop/replace/compress/insert_at 各操作设置 dirty 标记
|
||||
- save 后清空 dirty 标记
|
||||
|
||||
**风险**:低,纯内存优化,不涉及 schema 变更。
|
||||
|
||||
### Phase 2:建表 + 双写 + 全量迁移
|
||||
|
||||
**目标**:数据落入新表,读仍走旧列。
|
||||
|
||||
#### 4.2.1 迁移 V21
|
||||
|
||||
```rust
|
||||
fn migrate_v21(conn: &Connection) -> Result<()> {
|
||||
// 1. 建表(IF NOT EXISTS 幂等)
|
||||
conn.execute_batch(V21_SQL)?;
|
||||
|
||||
// 2. COUNT 探测:ai_messages 已有数据 → 跳过迁移只写版本号
|
||||
let existing: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM ai_messages", [], |row| row.get(0)
|
||||
)?;
|
||||
if existing > 0 {
|
||||
tracing::info!("v21: ai_messages 已有 {} 条,跳过迁移", existing);
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [21])?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 3. 遍历 ai_conversations
|
||||
let mut stmt = conn.prepare("SELECT id, messages, created_at FROM ai_conversations")?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?))
|
||||
})?;
|
||||
let all_rows: Vec<_> = rows.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// 4. 分批 commit(每 50 个对话一批,避免长事务持有写锁)
|
||||
const BATCH_SIZE: usize = 50;
|
||||
for (batch_idx, batch) in all_rows.chunks(BATCH_SIZE).enumerate() {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
for (conv_id, messages_json, conv_created_at) in batch {
|
||||
// 5. 逐对话反序列化 messages JSON → Vec<serde_json::Value>
|
||||
// (用裸 JSON 而非 ChatMessage,因 df-storage 不依赖 df-ai-core)
|
||||
let messages: Vec<serde_json::Value> = match serde_json::from_str(messages_json) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!("v21: 对话 {} messages JSON 解析失败,跳过: {}", conv_id, e);
|
||||
continue; // 坏数据跳过,不中断迁移
|
||||
}
|
||||
};
|
||||
|
||||
for (seq, msg) in messages.iter().enumerate() {
|
||||
// 6. 逐条消息提取字段 → INSERT INTO ai_messages
|
||||
// 字段名硬编码("role"/"content" 等)——ChatMessage 改名会漏数据!
|
||||
// 耦合点标注:见下方「迁移耦合点」
|
||||
let id = format!("msg_migrated_{}_{}", conv_id, seq); // 迁移期 ID,天然唯一
|
||||
let role = msg.get("role").and_then(|v| v.as_str()).unwrap_or("user");
|
||||
let content = msg.get("content").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let parts = msg.get("parts")
|
||||
.filter(|v| !v.is_null())
|
||||
.map(|v| v.to_string());
|
||||
let tool_call_id = msg.get("tool_call_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
let tool_calls = msg.get("tool_calls")
|
||||
.filter(|v| !v.is_null())
|
||||
.map(|v| v.to_string());
|
||||
let model = msg.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
// status 归一化:None/空 → "active"(列语义清晰,永不 NULL)
|
||||
let status = msg.get("status")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or("active");
|
||||
let reasoning_content = msg.get("reasoning_content")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
// created_at:有 timestamp 用消息自己的,没有 fallback 到对话创建时间
|
||||
let timestamp = msg.get("timestamp").and_then(|v| v.as_i64());
|
||||
let created_at = timestamp
|
||||
.map(|ts| ts.to_string())
|
||||
.unwrap_or_else(|| conv_created_at.clone());
|
||||
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO ai_messages
|
||||
(id, conversation_id, seq, role, content, parts, tool_call_id,
|
||||
tool_calls, model, status, reasoning_content, timestamp, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||
rusqlite::params![
|
||||
id, conv_id, seq as i64, role, content, parts,
|
||||
tool_call_id, tool_calls, model, status,
|
||||
reasoning_content, timestamp, created_at
|
||||
],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
tracing::info!("v21: 批次 {} 完成({} 对话)", batch_idx, batch.len());
|
||||
}
|
||||
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [21])?;
|
||||
tracing::info!("迁移 v21 完成");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2.2 迁移设计要点
|
||||
|
||||
| 要点 | 说明 |
|
||||
|---|---|
|
||||
| **幂等安全** | COUNT 探测 + INSERT OR IGNORE;中途崩溃重跑跳过已迁移数据 |
|
||||
| **分批 commit** | 每 50 对话一批,避免长事务持有 SQLite 写锁导致应用不可用 |
|
||||
| **迁移期 ID** | `msg_migrated_{conv_id}_{seq}` ——天然唯一(UNIQUE 是 conv_id+seq)、人类可读、零依赖(不调 new_id) |
|
||||
| **裸 JSON 提取** | 用 `serde_json::Value` 而非 `ChatMessage`,因 df-storage 不依赖 df-ai-core(分层约束) |
|
||||
| **坏数据跳过** | JSON 解析失败 → warn + continue,不中断迁移 |
|
||||
| **status 归一化** | None/空 → "active",列语义清晰永不 NULL |
|
||||
| **created_at 语义** | 有 timestamp 用消息自己的;没有 fallback 到对话 created_at |
|
||||
|
||||
#### 4.2.3 迁移耦合点(⚠️ ChatMessage 改名会漏数据)
|
||||
|
||||
迁移函数硬编码以下 JSON 字段名,与 `ChatMessage` 的 serde 序列化字段一一对应:
|
||||
|
||||
| 硬编码字段名 | ChatMessage 来源 | serde 属性 |
|
||||
|---|---|---|
|
||||
| `"role"` | `role: MessageRole` | `#[serde(rename_all = "lowercase")]`(enum) |
|
||||
| `"content"` | `content: String` | 默认 |
|
||||
| `"parts"` | `parts: Option<Vec<ContentPart>>` | `skip_serializing_if = "Option::is_none"` |
|
||||
| `"tool_call_id"` | `tool_call_id: Option<String>` | `skip_serializing_if = "Option::is_none"` |
|
||||
| `"tool_calls"` | `tool_calls: Option<Vec<ToolCall>>` | `skip_serializing_if = "Option::is_none"` |
|
||||
| `"model"` | `model: Option<String>` | `default, skip_serializing_if` |
|
||||
| `"status"` | `status: Option<String>` | `default, skip_serializing_if` |
|
||||
| `"reasoning_content"` | `reasoning_content: Option<String>` | `default, skip_serializing_if` |
|
||||
| `"timestamp"` | `timestamp: Option<i64>` | `default, skip_serializing_if` |
|
||||
|
||||
> **ChatMessage 如改字段名,必须同步更新迁移函数**。建议在 `types.rs` ChatMessage 定义处加注释标注此耦合点。
|
||||
|
||||
#### 4.2.4 双写改造
|
||||
|
||||
`save_conversation`(`conversation.rs:135`)改造:
|
||||
|
||||
```rust
|
||||
// 同一事务内:旧列全量写 + 新表增量写
|
||||
let tx = conn.transaction()?;
|
||||
// 旧列(回退保险)
|
||||
tx.execute("UPDATE ai_conversations SET messages = ? WHERE id = ?", params![json, conv_id])?;
|
||||
// 新表(增量:DELETE 旧 dirty 范围 + INSERT 新)
|
||||
if let Some((min_seq, max_seq)) = ctx_mgr.dirty_range() {
|
||||
tx.execute(
|
||||
"DELETE FROM ai_messages WHERE conversation_id = ? AND seq >= ? AND seq <= ?",
|
||||
params![conv_id, min_seq, max_seq]
|
||||
)?;
|
||||
for (seq, msg) in ctx_mgr.messages()[min_seq..=max_seq].iter().enumerate() {
|
||||
tx.execute("INSERT INTO ai_messages (...) VALUES (...)", params![...])?;
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
```
|
||||
|
||||
**风险**:中。双写增加事务开销,但保证一致性。
|
||||
|
||||
### Phase 3:切读 + 删旧列
|
||||
|
||||
**目标**:读走新表,删旧列,清理双写代码。
|
||||
|
||||
| 操作 | 旧实现 | 新实现 |
|
||||
|---|---|---|
|
||||
| `restore_from_messages` | 反序列化 messages JSON | `SELECT * FROM ai_messages WHERE conversation_id = ? ORDER BY seq` |
|
||||
| `clear_messages` | 写空 JSON `[]` | `DELETE FROM ai_messages WHERE conversation_id = ?` |
|
||||
| `compress_old_messages` | 遍历改 status + 重写 JSON | `UPDATE ai_messages SET status = 'compressed' WHERE conversation_id = ? AND seq < ?` |
|
||||
| `replace_tool_result_content` | 遍历找 tool_call_id + 重写 JSON | `UPDATE ai_messages SET content = ? WHERE conversation_id = ? AND tool_call_id = ?` |
|
||||
|
||||
V22 迁移删 `ai_conversations.messages` 列(SQLite 不支持 DROP COLUMN,需重建表)。
|
||||
|
||||
**风险**:中。需全量回归测试对话加载/清空/压缩/编辑。
|
||||
|
||||
---
|
||||
|
||||
## 五、涉及文件
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `crates/df-storage/src/migrations.rs` | V21 迁移 + V1 建表 SQL 同步补 ai_messages + steps 数组追加 (21, migrate_v21) |
|
||||
| `crates/df-storage/src/models.rs` | 新增 `AiMessageRecord` struct |
|
||||
| `crates/df-storage/src/crud/message_repo.rs` | **新建**,AiMessageRepo CRUD(insert_batch / list_by_conversation / delete_range / update_status / update_content_by_tool_call_id) |
|
||||
| `crates/df-storage/src/crud/mod.rs` | 注册 message_repo 子模块 |
|
||||
| `src-tauri/src/commands/ai/conversation.rs` | save_conversation 双写改造(Phase 2)/ 切读改造(Phase 3) |
|
||||
| `src-tauri/src/commands/ai/commands/conversation.rs` | ai_conversation_switch 切读改造(Phase 3) |
|
||||
| `src-tauri/src/commands/ai/commands/chat.rs` | clear_messages / compress / replace_tool_result_content 改造(Phase 3) |
|
||||
| `crates/df-ai/src/context.rs` | dirty 标记(Phase 1)+ 适配新存储(Phase 3) |
|
||||
|
||||
---
|
||||
|
||||
## 六、验收标准
|
||||
|
||||
1. **V21 迁移幂等安全**:新库(空表直接建)/老库(全量迁移)/坏数据(JSON 解析失败跳过)均不崩
|
||||
2. **Phase 2 双写期**:旧列和新表数据一致(可对比校验脚本)
|
||||
3. **Phase 3 切读后**:对话加载/清空/压缩/编辑行为零回归
|
||||
4. **长对话(50+ 轮)save 性能显著改善**(增量写入)
|
||||
5. `cargo check --workspace EXIT 0` + `cargo test` 相关测试通过
|
||||
|
||||
---
|
||||
|
||||
## 七、风险与对策
|
||||
|
||||
| 风险 | 等级 | 对策 |
|
||||
|---|---|---|
|
||||
| 迁移函数字段名与 ChatMessage 脱节 | 中 | 迁移耦合点注释 + types.rs 标注 |
|
||||
| SQLite 不支持 DROP COLUMN | 低 | V22 用重建表方式(CREATE new → INSERT → DROP old → RENAME) |
|
||||
| 双写事务开销 | 低 | Phase 2 过渡期可接受,Phase 3 清理 |
|
||||
| ContextManager dirty 标记遗漏 | 中 | 所有变更操作(push/pop/replace/compress/insert)统一设置 dirty |
|
||||
235
docs/02-架构设计/专项设计/消息级溯源设计-2026-06-19.md
Normal file
235
docs/02-架构设计/专项设计/消息级溯源设计-2026-06-19.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# 消息级溯源设计
|
||||
|
||||
> 创建:2026-06-19 | 编号:F-260619-04(消息级溯源) | 状态:📐 设计待实施
|
||||
> 关联任务:`消息级溯源:知识库/审计/灵感全链路从对话级升级到消息级`(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)
|
||||
Reference in New Issue
Block a user