Files
mnote/design/07-ai/done/7-44-chatonly-doubao-session-binding-v1.md
T

13 KiB
Raw Blame History

7-44 ChatOnly Doubao session binding v1

创建时间:2026-05-31

状态:done

OwnerPage 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 层有 acpSessionIdMNote 会通过 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 发送消息使用:

POST /samantha/chat/completion

请求体关键字段:

{
  "completion_option": {
    "need_create_conversation": false,
    "is_delete": false
  },
  "conversation_id": "38428454119180290"
}

conversation_id 为空或 "0" 时,豆包创建新 conversation。响应 SSE 中会出现 conversation_idOpenClaw 已能解析并打印:

[Doubao Web Browser] Captured conversation_id: ...

3.2 删除会话

从豆包当前 Web UI 已加载脚本和 CDP 请求监听确认,删除会话优先走 IM cmd 链路:

POST /im/conversation/batch_del_user_conv

请求头关键字段:

content-type: application/json; encoding=utf-8
accept: application/json, text/plain, */*
agw-js-conv: str

请求体结构:

{
  "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

POST /samantha/im/conversation/batch_delete

但当前删除弹窗路径使用 /im/conversation/batch_del_user_conv,实现优先采用该路径。

4. 数据合同

新增 SQLite 控制面表:

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 runstatus=session.created
  3. 不立即创建豆包远端 conversation。
  4. 绑定表暂不写,或写入一行 remote_conversation_id=NULL 的 pending 记录。

5.2 第一次发送

  1. mnote-web 创建 runpayload 携带 sessionIdagentId=chat_onlyprofile=openclaw-doubao-chat
  2. mnote-web 查询绑定表;若无 remote_conversation_id,不传远端 ID。
  3. OpenClaw 发送给豆包,豆包创建新 conversation。
  4. OpenClaw 从 SSE 捕获 conversation_id
  5. OpenClaw 需要把 conversation_id 作为结构化事件回传给 MNote。建议事件:
{
  "event": "provider.conversation.bound",
  "data": {
    "provider": "doubao-web",
    "remoteConversationId": "38428454119180290",
    "remoteUrl": "https://www.doubao.com/chat/38428454119180290"
  }
}
  1. 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 中加入:
{
  "providerConversation": {
    "provider": "doubao-web",
    "remoteConversationId": "38428454119180290"
  }
}
  1. OpenClaw Doubao provider 使用该 ID 调 /samantha/chat/completion,设置 need_create_conversation=false
  2. 若豆包返回会话不存在或被删除,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 执行远端删除:
POST /im/conversation/batch_del_user_conv
  1. 成功后标记 remote_deleted
  2. 失败时保持本地删除已完成,绑定标记 remote_delete_failedmetadata_json 记录错误码、响应摘要和时间。
  3. UI 提示:MNote 会话已删除,豆包远端删除失败,可稍后重试

6. API 边界

6.1 mnote-web 内部 API

建议新增 provider conversation helper,不把豆包细节散落在 hermes_client.rs

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-planebinding upsert / lookup / local delete / remote delete status transition。
  • mnote-webChatOnly run payload 能注入已有 remoteConversationId
  • mnote-webprovider.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_deletedremote_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. 实施清单

  • control-plane 增加 ai_external_conversation_bindings schema、store trait 和 SQLite 实现。
  • mnote-web 增加 provider conversation helper,避免继续扩大 hermes_client.rs
  • 在 ACP run 创建时为 ChatOnly / 豆包注入已有 remoteConversationId
  • 在 ACP event 持久化时处理 provider.conversation.bound
  • 在 session delete 路径中加入远端删除 best-effort 状态机。
  • 在 OpenClaw Doubao provider 中加入结构化 conversation bound 事件。
  • 在 OpenClaw Doubao provider 中加入 deleteConversation,走 /im/conversation/batch_del_user_conv
  • 补真实浏览器 smokenode scripts/task512-chatonly-doubao-sync-smoke.js 覆盖远端 conversation id 绑定、豆包同会话 marker、session 删除、provider delete 和 SQLite binding remote_deleted
  • 补跨 provider 真实浏览器 smokenode scripts/task513-chatonly-provider-sync-smoke.js deepseeknode scripts/task513-chatonly-provider-sync-smoke.js gemini
  • 补 MNote Rust 单测:control-plane binding、mnote-web provider conversation 注入、bound event 持久化和 session delete provider 状态。
  • OpenClaw provider 源码不在本仓库,provider 单测缺口已归档到 bugs/07-ai/done/7-49-chatonly-openclaw-provider-unit-test-gap-v1.mdDoubao 有真实远端删除 helperDeepSeek / Gemini 在缺少已验证远端删除 API 时明确返回 remote_delete_failed,不阻塞 MNote 侧设计归档。