Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
293 lines
13 KiB
Markdown
293 lines
13 KiB
Markdown
# [recycle] 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/done/7-49-chatonly-openclaw-provider-unit-test-gap-v1.md`;Doubao 有真实远端删除 helper,DeepSeek / Gemini 在缺少已验证远端删除 API 时明确返回 `remote_delete_failed`,不阻塞 MNote 侧设计归档。
|