收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目 - 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线 - 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径 验证: - cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1 - cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1 - git diff --check - git diff --cached --check - codegraph index . --force && codegraph status . - codegraph sync . && codegraph status .
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
# 7-44 ChatOnly Doubao session binding v1
|
||||
|
||||
> 创建时间:2026-05-31
|
||||
>
|
||||
> 状态:`done`
|
||||
>
|
||||
> Owner:Page AI ChatOnly / Hermes ACP runtime / SQLite control-plane / OpenClaw Doubao Web provider
|
||||
>
|
||||
> 上位依据:
|
||||
> - `design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md`
|
||||
> - `design/07-ai/done/7-30-acp-session-load-resume-checklist-v1.md`
|
||||
> - `design/07-ai/done/7-33-acp-session-info-plan-ui-checklist-v1.md`
|
||||
> - `design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md`
|
||||
> - `design/07-ai/done/7-41-page-ai-hermes-reasonix-user-profile-isolation-v2.md`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
MNote Page AI 的 `Chat-only / 豆包` 当前已经能通过 Hermes ACP -> OpenClaw `doubao-web` provider 调用豆包网页,但 MNote 会话与豆包网页会话还没有稳定统一:
|
||||
|
||||
- MNote 的会话主记录在 SQLite `ai_runtime_runs` / `ai_runtime_events`,按 `user_id + workspace_id + session_id` 查询和软删除。
|
||||
- Hermes / ACP 层有 `acpSessionId`,MNote 会通过 `session.info.updated` 持久化它。
|
||||
- OpenClaw `doubao-web` provider 会从豆包 SSE 中捕获 `conversation_id`,并在进程内 `sessionMap` 中用 ACP `sessionId` 复用豆包会话。
|
||||
- 该 `sessionMap` 没有落 SQLite。MNote 重启、OpenClaw 重启、跨用户、删除会话时,都无法可靠知道某个 MNote ChatOnly session 对应哪条豆包网页 conversation。
|
||||
|
||||
2026-05-31 已验证 ChatOnly 豆包重复会话的直接根因是 Hermes `title_generation` 复用了豆包主模型;当前已在 `openclaw-doubao-chat` profile 禁用标题生成辅助调用。但这只解决“一条消息变两条豆包会话”的触发点,不解决会话生命周期统一。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
本阶段目标是让 MNote 成为 ChatOnly 豆包会话的本地控制面:
|
||||
|
||||
- MNote ChatOnly session 与豆包 `conversation_id` 建立持久绑定。
|
||||
- 后续同一个 MNote session 继续发送时,复用同一个豆包 conversation。
|
||||
- 删除 MNote session 时,对豆包远端 conversation 做最佳努力删除。
|
||||
- 多用户会话隔离以 SQLite `user_id` 为准,不能只依赖 OpenClaw 全局内存。
|
||||
- 删除失败不能阻塞 MNote 本地删除,但必须可审计、可重试。
|
||||
|
||||
非目标:
|
||||
|
||||
- 不把豆包网页作为 MNote 会话真源。
|
||||
- 不同步豆包网页中用户手工创建的所有历史会话。
|
||||
- 不承诺豆包接口稳定可用;远端删除属于 provider-specific best effort。
|
||||
- 不在 ChatOnly 下申请 MNote 文件写权限。
|
||||
|
||||
## 3. 豆包侧接口取证
|
||||
|
||||
### 3.1 发送 / 继续会话
|
||||
|
||||
当前 OpenClaw `DoubaoWebClientBrowser` 发送消息使用:
|
||||
|
||||
```text
|
||||
POST /samantha/chat/completion
|
||||
```
|
||||
|
||||
请求体关键字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"completion_option": {
|
||||
"need_create_conversation": false,
|
||||
"is_delete": false
|
||||
},
|
||||
"conversation_id": "38428454119180290"
|
||||
}
|
||||
```
|
||||
|
||||
当 `conversation_id` 为空或 `"0"` 时,豆包创建新 conversation。响应 SSE 中会出现 `conversation_id`,OpenClaw 已能解析并打印:
|
||||
|
||||
```text
|
||||
[Doubao Web Browser] Captured conversation_id: ...
|
||||
```
|
||||
|
||||
### 3.2 删除会话
|
||||
|
||||
从豆包当前 Web UI 已加载脚本和 CDP 请求监听确认,删除会话优先走 IM cmd 链路:
|
||||
|
||||
```text
|
||||
POST /im/conversation/batch_del_user_conv
|
||||
```
|
||||
|
||||
请求头关键字段:
|
||||
|
||||
```text
|
||||
content-type: application/json; encoding=utf-8
|
||||
accept: application/json, text/plain, */*
|
||||
agw-js-conv: str
|
||||
```
|
||||
|
||||
请求体结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"cmd": 4171,
|
||||
"uplink_body": {
|
||||
"batch_delete_user_conversation_uplink_body": {
|
||||
"conversation_id": ["38428454119180290"],
|
||||
"delete_all": false,
|
||||
"conversation_type": 3
|
||||
}
|
||||
},
|
||||
"sequence_id": "uuid",
|
||||
"channel": 2,
|
||||
"version": "1"
|
||||
}
|
||||
```
|
||||
|
||||
其中 `conversation_type: 3` 对应 `ONE_TO_BOT_CHAT`。
|
||||
|
||||
脚本中还存在旧 wrapper:
|
||||
|
||||
```text
|
||||
POST /samantha/im/conversation/batch_delete
|
||||
```
|
||||
|
||||
但当前删除弹窗路径使用 `/im/conversation/batch_del_user_conv`,实现优先采用该路径。
|
||||
|
||||
## 4. 数据合同
|
||||
|
||||
新增 SQLite 控制面表:
|
||||
|
||||
```text
|
||||
ai_external_conversation_bindings
|
||||
- id
|
||||
- user_id # 当前 MNote 用户,必填
|
||||
- workspace_id # 可空,但列表/删除必须按上下文过滤
|
||||
- mnote_session_id # MNote ChatOnly session_id
|
||||
- acp_session_id # Hermes/OpenClaw ACP sessionId,可空,收到后补齐
|
||||
- agent_id # chat_only
|
||||
- profile # openclaw-doubao-chat 等
|
||||
- provider # doubao-web
|
||||
- remote_conversation_id # 豆包 conversation_id
|
||||
- remote_url # https://www.doubao.com/chat/{remote_conversation_id}
|
||||
- status # active | local_deleted | remote_deleted | remote_delete_failed
|
||||
- metadata_json # 捕获来源、失败原因、最后响应摘要
|
||||
- created_at
|
||||
- updated_at
|
||||
- deleted_at
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- `user_id + provider + remote_conversation_id` 唯一,避免同一豆包会话绑定给多个 MNote 用户。
|
||||
- `user_id + mnote_session_id + provider` 唯一,避免一个 MNote session 绑定多条豆包远端会话。
|
||||
- 查询、恢复、删除都必须带当前 `user_id`;不能只按 `mnote_session_id` 查。
|
||||
|
||||
## 5. 数据流
|
||||
|
||||
### 5.1 创建 MNote ChatOnly session
|
||||
|
||||
1. 前端 `pageAiEnsureHermesSession(forceCreate=true)` 调 `/api/hermes/client/sessions`。
|
||||
2. mnote-web 写入 SQLite session index run,`status=session.created`。
|
||||
3. 不立即创建豆包远端 conversation。
|
||||
4. 绑定表暂不写,或写入一行 `remote_conversation_id=NULL` 的 pending 记录。
|
||||
|
||||
### 5.2 第一次发送
|
||||
|
||||
1. mnote-web 创建 run,payload 携带 `sessionId`、`agentId=chat_only`、`profile=openclaw-doubao-chat`。
|
||||
2. mnote-web 查询绑定表;若无 `remote_conversation_id`,不传远端 ID。
|
||||
3. OpenClaw 发送给豆包,豆包创建新 conversation。
|
||||
4. OpenClaw 从 SSE 捕获 `conversation_id`。
|
||||
5. OpenClaw 需要把 `conversation_id` 作为结构化事件回传给 MNote。建议事件:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "provider.conversation.bound",
|
||||
"data": {
|
||||
"provider": "doubao-web",
|
||||
"remoteConversationId": "38428454119180290",
|
||||
"remoteUrl": "https://www.doubao.com/chat/38428454119180290"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. mnote-web 在 `persist_acp_runtime_event` 中识别该事件,upsert `ai_external_conversation_bindings`。
|
||||
|
||||
### 5.3 继续发送
|
||||
|
||||
1. mnote-web 根据 `user_id + mnote_session_id + provider=doubao-web` 查询绑定。
|
||||
2. 若找到 active `remote_conversation_id`,在传给 ACP/OpenClaw 的 payload 中加入:
|
||||
|
||||
```json
|
||||
{
|
||||
"providerConversation": {
|
||||
"provider": "doubao-web",
|
||||
"remoteConversationId": "38428454119180290"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. OpenClaw Doubao provider 使用该 ID 调 `/samantha/chat/completion`,设置 `need_create_conversation=false`。
|
||||
4. 若豆包返回会话不存在或被删除,OpenClaw 发 `provider.conversation.missing`;MNote 标记绑定异常,由用户决定是否新建远端会话。
|
||||
|
||||
### 5.4 删除 MNote session
|
||||
|
||||
1. 用户在 MNote 删除 ChatOnly 会话。
|
||||
2. mnote-web 先对 `ai_runtime_runs` 做本地软删除。
|
||||
3. mnote-web 将绑定表标记为 `local_deleted`。
|
||||
4. 若存在 `remote_conversation_id`,调用 OpenClaw / provider adapter 执行远端删除:
|
||||
|
||||
```text
|
||||
POST /im/conversation/batch_del_user_conv
|
||||
```
|
||||
|
||||
5. 成功后标记 `remote_deleted`。
|
||||
6. 失败时保持本地删除已完成,绑定标记 `remote_delete_failed`,`metadata_json` 记录错误码、响应摘要和时间。
|
||||
7. UI 提示:`MNote 会话已删除,豆包远端删除失败,可稍后重试`。
|
||||
|
||||
## 6. API 边界
|
||||
|
||||
### 6.1 mnote-web 内部 API
|
||||
|
||||
建议新增 provider conversation helper,不把豆包细节散落在 `hermes_client.rs`:
|
||||
|
||||
```text
|
||||
rust/crates/mnote-web/src/provider_conversations.rs
|
||||
```
|
||||
|
||||
职责:
|
||||
|
||||
- 解析 run payload 中的 ChatOnly provider。
|
||||
- 读写 `ai_external_conversation_bindings`。
|
||||
- 将 binding 注入 ACP run payload。
|
||||
- 处理 `provider.conversation.bound/missing/deleted/delete_failed` 事件。
|
||||
|
||||
### 6.2 OpenClaw Doubao provider
|
||||
|
||||
需要在 OpenClaw `doubao-web` provider 增加三个能力:
|
||||
|
||||
- 从 ACP/context payload 读取 `providerConversation.remoteConversationId`。
|
||||
- 捕获新 `conversation_id` 后发结构化事件,不能只写 console log。
|
||||
- 暴露 `deleteConversation(remoteConversationId)`,内部走 `/im/conversation/batch_del_user_conv`。
|
||||
|
||||
第一阶段如果 ACP 不支持 provider 自定义 RPC,可先由 mnote-web 调一个 OpenClaw 本地 HTTP helper;但长期应收口到 provider adapter。
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
| 场景 | 行为 |
|
||||
| --- | --- |
|
||||
| 豆包创建成功但未捕获 `conversation_id` | run 仍完成;绑定缺失;下一轮可能新建远端会话;UI 标记未绑定 |
|
||||
| 绑定表有 ID,但豆包返回不存在 | 标记 `remote_missing`,提示用户重新绑定或新建 |
|
||||
| 删除 MNote 本地成功,豆包远端失败 | 不回滚本地删除;标记 `remote_delete_failed` |
|
||||
| 多用户尝试绑定同一远端 ID | 拒绝后写 audit,避免跨用户串会话 |
|
||||
| OpenClaw 重启 | SQLite 绑定仍在;下一轮从 MNote 注入远端 ID |
|
||||
| 豆包接口变更 | 本地会话不受影响;远端能力降级为不可用 |
|
||||
|
||||
## 8. 验收
|
||||
|
||||
### 8.1 单元 / 集成
|
||||
|
||||
- `control-plane`:binding upsert / lookup / local delete / remote delete status transition。
|
||||
- `mnote-web`:ChatOnly run payload 能注入已有 `remoteConversationId`。
|
||||
- `mnote-web`:`provider.conversation.bound` 事件能写入 SQLite。
|
||||
- `mnote-web`:删除 session 时先软删除本地,再 best-effort 调 provider delete。
|
||||
|
||||
### 8.2 真实浏览器 smoke
|
||||
|
||||
1. 使用测试账号登录 `http://localhost:3000`。
|
||||
2. 创建 ChatOnly / 豆包新会话,发送 marker A。
|
||||
3. 复查:
|
||||
- MNote SQLite 有一个 `mnote_session_id -> remote_conversation_id` 绑定。
|
||||
- 豆包日志 `Captured conversation_id` 一次。
|
||||
4. 在同一 MNote 会话发送 marker B。
|
||||
5. 复查:
|
||||
- 豆包日志第二次 `Conversation ID` 等于第一次捕获值。
|
||||
- 豆包网页同一 conversation 中出现 A 与 B。
|
||||
6. 删除 MNote 会话。
|
||||
7. 复查:
|
||||
- MNote 会话列表不再显示该 session。
|
||||
- SQLite binding status 为 `remote_deleted` 或 `remote_delete_failed`。
|
||||
- 若远端删除成功,豆包网页侧该 conversation 从列表移除或打开后显示已删除。
|
||||
|
||||
2026-06-01 验证记录:
|
||||
|
||||
- `node scripts/task512-chatonly-doubao-sync-smoke.js` 通过:豆包远端 conversation 绑定、同会话回复、MNote session 删除、provider delete 和 SQLite binding `remote_deleted` 均通过。
|
||||
- `node scripts/task513-chatonly-provider-sync-smoke.js deepseek` 通过:DeepSeek `remoteConversationId` 绑定、provider delete 和 SQLite binding `remote_deleted` 均通过。
|
||||
- `node scripts/task513-chatonly-provider-sync-smoke.js gemini` 通过:Gemini conversation URL 绑定、provider delete 和 SQLite binding `remote_deleted` 均通过。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p control-plane external_conversation -- --test-threads=1` 通过:binding user scope 与状态迁移。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib provider_conversation -- --test-threads=1` 通过:provider conversation 注入与 bound event 持久化。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib chatonly_doubao_session_delete_calls_provider_and_marks_remote_deleted -- --test-threads=1` 通过:删除 session 后 provider delete 与 `remote_deleted` 状态。
|
||||
|
||||
## 9. 实施清单
|
||||
|
||||
- [x] 在 `control-plane` 增加 `ai_external_conversation_bindings` schema、store trait 和 SQLite 实现。
|
||||
- [x] 在 `mnote-web` 增加 provider conversation helper,避免继续扩大 `hermes_client.rs`。
|
||||
- [x] 在 ACP run 创建时为 ChatOnly / 豆包注入已有 `remoteConversationId`。
|
||||
- [x] 在 ACP event 持久化时处理 `provider.conversation.bound`。
|
||||
- [x] 在 session delete 路径中加入远端删除 best-effort 状态机。
|
||||
- [x] 在 OpenClaw Doubao provider 中加入结构化 conversation bound 事件。
|
||||
- [x] 在 OpenClaw Doubao provider 中加入 `deleteConversation`,走 `/im/conversation/batch_del_user_conv`。
|
||||
- [x] 补真实浏览器 smoke:`node scripts/task512-chatonly-doubao-sync-smoke.js` 覆盖远端 conversation id 绑定、豆包同会话 marker、session 删除、provider delete 和 SQLite binding `remote_deleted`。
|
||||
- [x] 补跨 provider 真实浏览器 smoke:`node scripts/task513-chatonly-provider-sync-smoke.js deepseek` 与 `node scripts/task513-chatonly-provider-sync-smoke.js gemini`。
|
||||
- [x] 补 MNote Rust 单测:control-plane binding、mnote-web provider conversation 注入、bound event 持久化和 session delete provider 状态。
|
||||
- [x] OpenClaw provider 源码不在本仓库,provider 单测缺口已转入 `bugs/07-ai/process/7-49-chatonly-openclaw-provider-unit-test-gap-v1.md` 跟踪,不阻塞 MNote 侧设计归档。
|
||||
Reference in New Issue
Block a user