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.
409 lines
14 KiB
Markdown
409 lines
14 KiB
Markdown
# [recycle] 7-45 ChatOnly API provider runtime v1
|
||
|
||
> 创建时间:2026-06-02
|
||
>
|
||
> 状态:`done`
|
||
>
|
||
> Owner:Page AI ChatOnly / mnote-web / SQLite control-plane / OmniRoute API provider
|
||
>
|
||
> 上位依据:
|
||
> - `design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||
> - `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-39-page-ai-agent-selector-context-authorization-settings-v1.md`
|
||
> - `design/07-ai/done/7-44-chatonly-doubao-session-binding-v1.md`
|
||
|
||
## 1. 背景
|
||
|
||
当前 Page AI `Chat-only` 已接入豆包、DeepSeek、Gemini 等网页 provider。网页 provider 的价值是复用网页账号、网页历史和远端会话删除,但它也带来浏览器、登录态、验证码、前台窗口、OpenClaw provider prompt 和 provider-specific 会话绑定复杂度。
|
||
|
||
现在 OmniRoute 已能提供 OpenAI-compatible API endpoint:
|
||
|
||
```text
|
||
http://127.0.0.1:20128/v1/chat/completions
|
||
```
|
||
|
||
并已验证以下模型可用:
|
||
|
||
- `aisz-chat/grok-4.3`
|
||
- `aisz-chat/gemini-3.1-pro`
|
||
- `aisz-chat/gpt-5.5-extra-high-fast`
|
||
- `aisz-chat/kimi-k2.5`
|
||
- DeepSeek pro / flash 组合模型
|
||
|
||
这些模型不需要浏览器,也不需要 OpenClaw。对于 MNote 当前需求,它们只是最简 ChatOnly provider:用户选一个 provider,发送消息,MNote 本地保存历史,删除 MNote 会话时删除本地历史。
|
||
|
||
## 2. 第一结论
|
||
|
||
API ChatOnly 不走 OpenClaw。
|
||
|
||
原因:
|
||
|
||
- OpenClaw 是 agent runtime,包含 agent prompt、skills、workspace、compaction、浏览器 profile 等能力;最简聊天窗口不需要这些能力。
|
||
- API provider 没有网页远端历史对齐语义,不应复用 `CHATONLY_PROVIDER_CONFIGS` 的网页 conversation binding / remote delete 链路。
|
||
- MNote 已有 ChatOnly UI、SSE 消费、SQLite runtime session、删除入口和 `reqwest` streaming 依赖;新增一个薄的 OpenAI-compatible adapter 比引入完整第三方 chat app 更可控。
|
||
|
||
最终分层:
|
||
|
||
| Provider 类别 | 默认状态 | 运行方式 | 历史删除语义 |
|
||
|---|---|---|---|
|
||
| 豆包网页 | 保留默认可用 | OpenClaw `doubao-web` | MNote 本地删除 + 豆包远端 best effort 删除 |
|
||
| DeepSeek 网页 | fallback | OpenClaw `deepseek-web` | 网页远端 best effort 删除 |
|
||
| Gemini 网页 | fallback | OpenClaw `gemini-web` | 网页远端 best effort 删除 |
|
||
| Grok 网页 | fallback | OpenClaw `grok-web` | 暂不作为默认 |
|
||
| OmniRoute API Chat | 新默认候选 | mnote-web 直连 `/v1/chat/completions` | 仅 MNote 本地删除 |
|
||
|
||
## 3. 目标
|
||
|
||
- 新增 `api-chat` provider kind,直接调用 OpenAI-compatible `/v1/chat/completions`。
|
||
- 支持 DeepSeek、Gemini、Grok、GPT、Kimi API ChatOnly profiles。
|
||
- 复用现有 Page AI ChatOnly UI、session list、session detail、rename、delete、search 和 SSE 消费模型。
|
||
- API provider 不启动 ACP runtime,不启动 OpenClaw,不注入网页 provider metadata prompt。
|
||
- API provider 的会话和消息以 SQLite/control-plane 为准,按用户隔离。
|
||
- 网页 DeepSeek/Gemini/Grok 保留为 fallback,后续稳定后再决定是否从默认 UI 中隐藏。
|
||
|
||
非目标:
|
||
|
||
- 不实现完整 ChatGPT / Open WebUI / LibreChat 类产品。
|
||
- 不导入第三方 chat app 的数据库、用户系统或前端壳。
|
||
- 不同步 Gemini/Grok/DeepSeek 网页历史。
|
||
- 不把 API ChatOnly 暴露为可写文件 agent。
|
||
- 不支持 tool calling、function calling、vision、文件上传、多模态附件。
|
||
|
||
## 4. Provider 配置合同
|
||
|
||
新增一个小型静态 registry,建议先放在 mnote-web 后端,前端只消费 profile 列表:
|
||
|
||
```text
|
||
api_chat_profiles
|
||
- profile_id
|
||
- label
|
||
- provider_kind = api-chat
|
||
- base_url
|
||
- model
|
||
- api_key_env
|
||
- fallback_profile_id
|
||
- status = active | fallback | hidden
|
||
```
|
||
|
||
第一批 profiles:
|
||
|
||
| profile_id | label | model | 默认状态 |
|
||
|---|---|---|---|
|
||
| `shared_api_deepseek_flash_chat` | DeepSeek Flash | DeepSeek flash 组合模型 | active |
|
||
| `shared_api_deepseek_pro_chat` | DeepSeek Pro | DeepSeek pro 组合模型 | active |
|
||
| `shared_api_gpt_chat` | GPT | `aisz-chat/gpt-5.5-extra-high-fast` | active |
|
||
| `shared_api_kimi_chat` | Kimi | `aisz-chat/kimi-k2.5` | active |
|
||
| `shared_api_gemini_chat` | Gemini API | `aisz-chat/gemini-3.1-pro` | active |
|
||
| `shared_api_grok_chat` | Grok API | `aisz-chat/grok-4.3` | active 或 hidden,按限额策略决定 |
|
||
|
||
默认 base URL:
|
||
|
||
```text
|
||
http://127.0.0.1:20128/v1
|
||
```
|
||
|
||
API key 优先级:
|
||
|
||
1. `MNOTE_API_CHAT_<PROFILE>_API_KEY`
|
||
2. `MNOTE_API_CHAT_API_KEY`
|
||
3. `OPENAI_API_KEY`
|
||
|
||
不建议直接读取 `/home/lix/.codex/auth.json` 作为长期方案。开发期可以从环境注入,避免 MNote 代码绑定 Codex 配置文件。
|
||
|
||
## 5. 数据模型
|
||
|
||
继续复用现有 `ai_runtime_runs` / `ai_runtime_events` 作为会话索引和事件审计。API ChatOnly 的 run payload 增加:
|
||
|
||
```json
|
||
{
|
||
"agentId": "chat_only",
|
||
"providerKind": "api-chat",
|
||
"profileId": "shared_api_gpt_chat",
|
||
"model": "aisz-chat/gpt-5.5-extra-high-fast",
|
||
"sessionId": "mnote_...",
|
||
"message": "..."
|
||
}
|
||
```
|
||
|
||
消息恢复来源:
|
||
|
||
- 用户消息:从 run payload 的 `message` 恢复。
|
||
- 助手消息:从 `ai_runtime_events` 中的 `message.delta` 聚合,或从 `run.completed` 的 final text 恢复。
|
||
- 错误:写入 `run.failed`,用于 session detail 展示。
|
||
|
||
API ChatOnly 不写 `ai_external_conversation_bindings`。该表只属于网页 provider 远端会话绑定。
|
||
|
||
## 6. 后端数据流
|
||
|
||
### 6.1 创建会话
|
||
|
||
沿用现有:
|
||
|
||
```text
|
||
POST /api/hermes/client/sessions
|
||
```
|
||
|
||
当 `agentId=chat_only` 且 profile 是 `api-chat` 时:
|
||
|
||
1. 写入本地 session index。
|
||
2. `persistence=local_ai_session_jsonl` 或当前 SQLite runtime persistence 口径。
|
||
3. 不创建远端会话。
|
||
4. 不创建 ACP session。
|
||
|
||
### 6.2 发送消息
|
||
|
||
沿用现有:
|
||
|
||
```text
|
||
POST /api/hermes/client/runs
|
||
```
|
||
|
||
分流规则:
|
||
|
||
```text
|
||
agentId == chat_only
|
||
且 profile/profileId 命中 api-chat registry
|
||
-> api_chat_runtime
|
||
否则
|
||
-> 当前 ACP/OpenClaw/Web provider runtime
|
||
```
|
||
|
||
`api_chat_runtime` 执行:
|
||
|
||
1. 按当前 `user_id + workspace_id + session_id + profile_id` 读取最近消息。
|
||
2. 构造 OpenAI-compatible messages:
|
||
|
||
```json
|
||
[
|
||
{ "role": "system", "content": "你是 MNote 的简洁聊天助手。不要声称能编辑文件。" },
|
||
{ "role": "user", "content": "..." },
|
||
{ "role": "assistant", "content": "..." },
|
||
{ "role": "user", "content": "当前问题" }
|
||
]
|
||
```
|
||
|
||
3. 调 OmniRoute:
|
||
|
||
```text
|
||
POST {base_url}/chat/completions
|
||
Authorization: Bearer ...
|
||
Content-Type: application/json
|
||
```
|
||
|
||
请求体最小字段:
|
||
|
||
```json
|
||
{
|
||
"model": "aisz-chat/kimi-k2.5",
|
||
"messages": [],
|
||
"stream": true
|
||
}
|
||
```
|
||
|
||
4. 解析 upstream SSE:
|
||
|
||
```text
|
||
data: {"choices":[{"delta":{"content":"..."}}]}
|
||
data: [DONE]
|
||
```
|
||
|
||
5. 转成 MNote 现有 SSE event:
|
||
|
||
```text
|
||
event: message.delta
|
||
data: {"delta":"..."}
|
||
|
||
event: run.completed
|
||
data: {"output":"...","model":"..."}
|
||
```
|
||
|
||
6. 同步写入 `ai_runtime_events`,用于历史恢复和审计。
|
||
|
||
### 6.3 删除会话
|
||
|
||
沿用现有 session delete endpoint。
|
||
|
||
当 session 属于 `api-chat`:
|
||
|
||
1. 软删除本地 runs/events 或标记 session deleted。
|
||
2. 不调用 provider conversation delete。
|
||
3. 返回:
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"remoteDelete": {
|
||
"attempted": false,
|
||
"reason": "api_chat_has_no_remote_conversation"
|
||
}
|
||
}
|
||
```
|
||
|
||
UI 文案必须避免暗示已删除网页历史。
|
||
|
||
## 7. 代码边界
|
||
|
||
建议新增模块:
|
||
|
||
```text
|
||
rust/crates/mnote-web/src/api_chat.rs
|
||
```
|
||
|
||
职责:
|
||
|
||
- profile registry。
|
||
- provider config resolution。
|
||
- OpenAI-compatible request construction。
|
||
- streaming parser。
|
||
- upstream error normalization。
|
||
- 将 upstream chunk 映射为 `message.delta` / `run.completed` / `run.failed`。
|
||
|
||
`hermes_client.rs` 只做分流和现有 session/run 持久化衔接,不继续塞大量 provider 细节。
|
||
|
||
前端改动控制在:
|
||
|
||
```text
|
||
rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
|
||
rust/crates/mnote-web/browser/sidebar-page-ai-session-runtime.js
|
||
```
|
||
|
||
前端只需要:
|
||
|
||
- 展示新增 profiles。
|
||
- 对 API ChatOnly 不显示“网页问答”描述,改为“API 聊天”。
|
||
- 删除成功提示区分本地删除和网页同步删除。
|
||
|
||
SQLite/control-plane 改动:
|
||
|
||
```text
|
||
rust/crates/control-plane/src/sqlite.rs
|
||
```
|
||
|
||
新增 shared profiles provisioning。第一版可以不新增表,只复用 profile metadata;如后续要 UI 动态配置模型,再引入 `ai_chat_provider_profiles` 表。
|
||
|
||
## 8. 第三方参考取舍
|
||
|
||
已考虑的参考:
|
||
|
||
- `async-openai`:适合参考 OpenAI-compatible 配置、SSE streaming、请求类型;如果后续字段变多,可引入依赖。
|
||
- HuggingFace `chat-ui`、Open WebUI、LibreChat:功能完整,但带独立前端、用户、会话和数据库模型,不适合嵌入 MNote 当前 Page AI sidebar。
|
||
- Vercel AI SDK:前端/Next 生态友好,但 MNote 当前主链是 Rust SSR + Rust Axum,不应为最小 adapter 引入 JS server 侧依赖。
|
||
|
||
第一版采用 `reqwest + serde_json` 的最小 adapter。理由:
|
||
|
||
- 当前仓库已有 `reqwest` streaming。
|
||
- 需要支持的协议子集很小。
|
||
- 不引入大型依赖和类型迁移成本。
|
||
- bug 面集中在一个小模块,可用 mock upstream 和真实 OmniRoute smoke 覆盖。
|
||
|
||
## 9. 错误处理
|
||
|
||
必须明确区分:
|
||
|
||
- `api_chat_profile_unknown`:profile 未注册。
|
||
- `api_chat_base_url_missing`:base URL 缺失。
|
||
- `api_chat_api_key_missing`:API key 缺失。
|
||
- `api_chat_upstream_unavailable`:OmniRoute 不可达。
|
||
- `api_chat_upstream_error`:OmniRoute 返回非 2xx。
|
||
- `api_chat_stream_parse_error`:SSE chunk 解析失败。
|
||
- `api_chat_empty_response`:流结束但没有助手文本。
|
||
|
||
错误事件写入:
|
||
|
||
```text
|
||
event: run.failed
|
||
data: {"code":"api_chat_upstream_error","message":"..."}
|
||
```
|
||
|
||
前端展示为普通 ChatOnly 失败消息,不进入文件权限或 tool call UI。
|
||
|
||
## 10. 验收
|
||
|
||
### 10.1 单元测试
|
||
|
||
- profile registry 能解析 DeepSeek/Gemini/Grok/GPT/Kimi。
|
||
- API key 优先级正确。
|
||
- OpenAI-compatible SSE chunk 能解析 `delta.content`。
|
||
- `[DONE]` 正确结束。
|
||
- 非 2xx upstream 映射为 `run.failed`。
|
||
- API ChatOnly 不命中 `CHATONLY_PROVIDER_CONFIGS`。
|
||
- API ChatOnly 删除不调用网页 provider delete。
|
||
|
||
### 10.2 集成测试
|
||
|
||
Mock upstream:
|
||
|
||
- `/v1/chat/completions` 返回 SSE delta。
|
||
- MNote `/runs` 返回 runId。
|
||
- MNote `/events/{runId}` 输出 `message.delta` 和 `run.completed`。
|
||
- session detail 可恢复 user/assistant messages。
|
||
- 删除 session 后列表不再出现。
|
||
|
||
真实 OmniRoute smoke:
|
||
|
||
- `shared_api_deepseek_flash_chat` 发一条短消息成功。
|
||
- `shared_api_gpt_chat` 发一条短消息成功。
|
||
- 确认没有启动或调用 OpenClaw gateway/proxy。
|
||
|
||
浏览器 smoke:
|
||
|
||
- 打开 Page AI ChatOnly。
|
||
- 选择 GPT API,发消息,只出现一条助手回复。
|
||
- 新建会话、切换会话、刷新页面后历史仍存在。
|
||
- 删除会话后本地列表消失。
|
||
- 豆包网页会话链路不回归,仍能远端删除。
|
||
|
||
## 11. 分阶段实施
|
||
|
||
### Phase 1:最小 API runtime
|
||
|
||
- 新增 `api_chat.rs`。
|
||
- 接入 `shared_api_deepseek_flash_chat`、`shared_api_gpt_chat`。
|
||
- 复用现有 `/runs` 和 `/events`。
|
||
- 跑 mock upstream 和真实 OmniRoute smoke。
|
||
|
||
### Phase 2:扩展 provider 列表
|
||
|
||
- 增加 `deepseek_pro`、`gemini_api`、`kimi`、`grok_api`。
|
||
- Grok 默认可设为 hidden 或 fallback,避免限额被普通默认选择消耗。
|
||
- 前端区分 `API` 与 `网页 fallback` 标签。
|
||
|
||
### Phase 3:收口网页 fallback
|
||
|
||
- 观察 API provider 稳定性。
|
||
- DeepSeek/Gemini/Grok 网页降级为 fallback 或隐藏。
|
||
- 保留豆包网页主链,直到豆包也有可替代的稳定 API provider 且不再要求网页历史对齐。
|
||
|
||
## 12. 风险与约束
|
||
|
||
- OmniRoute 模型名可能变动,profile registry 要允许环境覆盖 model。
|
||
- 某些 upstream 可能不完全遵守 OpenAI streaming chunk 格式,parser 要容忍 `content` / `text` / `delta` 变体,但不要吞掉错误。
|
||
- API ChatOnly 没有网页远端删除,UI 和返回值必须说清楚。
|
||
- 不要让 API ChatOnly 继承 Page AI 文件写权限;它只能读已显式加入 prompt 的上下文。
|
||
- 不要为第一版引入 tool calling,否则会重新变成 agent runtime。
|
||
|
||
## 13. 退出条件
|
||
|
||
本设计可归档到 `done/` 的条件:
|
||
|
||
- API ChatOnly 至少两个 provider 完成真实浏览器 smoke。
|
||
- 本地历史创建、恢复、删除通过。
|
||
- API ChatOnly 确认不启动 OpenClaw。
|
||
- 豆包网页删除同步回归通过。
|
||
- DeepSeek/Gemini/Grok 网页 fallback 没有被误删或误改。
|
||
|
||
## 14. 完成验证
|
||
|
||
完成时间:2026-06-02
|
||
|
||
已完成:
|
||
|
||
- `api-chat` runtime 已接入 MNote ChatOnly,直接调用 OmniRoute OpenAI-compatible `/v1/chat/completions`,不启动 ACP/OpenClaw。
|
||
- SQLite profile provisioning 已包含 DeepSeek Flash / DeepSeek Pro / GPT / Kimi / Gemini API / Grok API 六个 shared API ChatOnly profiles。
|
||
- 后端 SSE 已将 OpenAI-compatible streaming 映射为 `message.delta` / `run.completed` / `run.failed`,并写入 `ai_runtime_events` 用于历史恢复。
|
||
- API ChatOnly 删除只删除 MNote 本地会话,返回 `api_chat_has_no_remote_conversation`,不进入网页 provider 远端删除链路。
|
||
- 真实 OmniRoute smoke 已验证六个模型均返回 200。
|
||
- 真实浏览器 smoke 已验证 GPT API 与 DeepSeek Flash API:发送后只有一条助手回复,刷新后历史恢复,删除后本地会话消失,且没有写入 `ai_external_conversation_bindings`。
|
||
- 豆包网页主链回归已验证:豆包端 user/assistant marker 各一条,未重复发送助手回复,删除走侧栏三点菜单并返回 `remote_deleted`。
|