fix: stabilize local folder AI document workflow
- scope local-folder PageTree revision and document sidebar rendering to fileTreeScope - preserve projected table/image attrs for local Markdown aggregate fallback - avoid FileTree restore forced layouts on cold design open - add API ChatOnly provider runtime and local OCR task handling regressions
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
# 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`。
|
||||
Reference in New Issue
Block a user