Files
mnote/.codex/reasonix-tasks/results/batch-j-worker-a-http-proxy-fallback.md
T

133 lines
6.9 KiB
Markdown
Raw Normal View History

2026-05-21 23:53:39 +08:00
# Batch J Worker A:旧 HTTP proxy / page AI fallback 调用链审查
> 创建时间:2026-05-21
>
> 只读审查,不修改代码。
## 1. 读取过的关键文件
| 文件 | 行数 | 内容 |
|------|------|------|
| `rust/crates/mnote-web/src/routes/hermes_client.rs` | 8202 | 首页面 AI Hermes 路由:session/run/tool 的 proxy 和 ACP 双路径 |
| `rust/crates/mnote-web/src/page_ai_workflow.rs` | — | **不存在**(不在根目录,在 `routes/` 下) |
| `rust/crates/mnote-web/src/routes/page_ai_workflow.rs` | 500+ | 页面 AI block-edit fast path |
| `rust/crates/mnote-web/src/acp_runtime.rs` | 500+ | ACP runtime manager |
| `rust/crates/mnote-web/src/acp_session_manager.rs` | 700+ | ACP session lifecycle |
| `rust/crates/mnote-web/src/routes/mod.rs` | 200+ | 全路线由注册 |
| `rust/crates/mnote-web/src/routes/compat.rs` | 200+ | 旧 `/api/ai-agent/run` 退役 guard |
## 2. 调用链分析
### 2.1 旧 HTTP proxy 路径
**判断函数:** `hermes_client.rs:2844` `hermes_http_proxy_enabled()`
```rust
fn hermes_http_proxy_enabled() -> bool {
["MNOTE_WEB_ENABLE_HERMES_HTTP_PROXY", "MNOTE_ENABLE_HERMES_HTTP_PROXY"]
.into_iter().find_map(env_or_dotenv)
.map(|v| matches!(v.trim().to_ascii_lowercase(), "1" | "true" | "yes"))
.unwrap_or(false)
}
```
**默认关闭**`unwrap_or(false)`)。HTTP proxy 仅在显式设 `MNOTE_WEB_ENABLE_HERMES_HTTP_PROXY=true` 时启用。
**上游 URL 来源:** `configured_upstream()``hermes_client.rs:2813`
- 检查 `MNOTE_WEB_HERMES_UPSTREAM_URL``MNOTE_HERMES_UPSTREAM_URL``MNOTE_HERMES_API_BASE_URL`
- 均不存在时返回 `None`
**`is_acp_profile()` 路由逻辑**`hermes_client.rs:2871`):
```rust
fn is_acp_profile(profile: &str) -> bool {
if !hermes_http_proxy_enabled() { return true; } // 默认全走 ACP
if profile == "reasonix" || profile == "hermes" { return true; }
// 还可通过 MNOTE_WEB_{PROFILE}_RUNTIME_TYPE=acp 环境变量控制
...
}
```
### 2.2 各路由入口分类
| Route | Handler | HTTP proxy 条件 | 默认走 | 分类 |
|-------|---------|-----------------|--------|------|
| `GET /client/sessions` | `list_sessions` | `!is_acp_profile(profile) && configured_upstream_for_profile(profile).is_some()` | **ACP** | `compat` |
| `GET /client/sessions/search` | `search_sessions` | 同上 | **ACP** | `compat` |
| `GET /client/gateway/health` | `gateway_health` | `!is_acp_profile(&profile)` → HTTP proxy | **ACP**(对于非 ACP profile 走 HTTP proxy | `compat` |
| `POST /client/runs` | `create_run` | `configured_upstream_for_profile(&registration.profile).is_some()` | **ACP** | `compat` |
| `GET /client/events/{run_id}` | `stream_events` | 先查 `acp_runtime_for_run()`,再 fallback `configured_upstream_for_profile()` | **ACP** | `compat` |
| `POST /client/runs/{run_id}/abort` | `abort_run` | 同上 | **ACP** | `compat` |
| `GET /client/models` | `list_models` | 直接 `configured_upstream()` | HTTP proxy(与 ACP 无关的 model 列表) | `compat` |
| 队列自动处理 | `auto_start_next_queued_run`line 5377 | `configured_upstream_for_profile(queued.profile)` | **ACP** | `compat` |
| `POST /api/ai-agent/run` | `compat::next_ai_agent_run` | — | **始终返回 410 GONE** | `retired` |
| `POST /api/page-ai/block-edit-workflow` | `page_ai_workflow::block_edit_workflow` | 绕过 ACP,直接调模型 API + `execute_mnote_tool_call` | debug-only fast path | `debug-only` |
### 2.3 page_ai_workflow 是否绕过 ACP / tool executor
**调用链:**
```
POST /api/page-ai/block-edit-workflow
→ looks_like_block_edit(message) ← 中文关键词匹配
→ call_block_edit_model() ← 直调 DeepSeek/etc APIHTTP
→ extract_markdown_plan_from_model_text()
→ 构造 ToolCallInput { tool_name: "mnote.doc.markdown_edit", capability_scope: ["block.write","page.write"], ... }
→ hermes_tools::execute_mnote_tool_call() ← 共享 tool executor
→ is_mnote_tool_disabled() & ensure_write_authorized() & 实际执行
```
**结论:**
1. **不经过旧 HTTP proxy** ✓ — 没有调用 `configured_upstream_for_profile`
2. **不经过 ACP session manager** — 直接调用 `execute_mnote_tool_call`,没有 `session/new` + `session/prompt` 生命周期
3. **但经过共享 tool executor** — 受 `is_mnote_tool_disabled()``ensure_write_authorized()``is_shared_read_scope()` 等守卫保护
4. **设计意图**`7-15` 设计文档说明 "local-first 普通正文编辑默认不经过它",该路径是给合模型快速做 search/replace 块编辑的 fast path,不是常规 AI 执行路径
### 2.4 `compat::next_ai_agent_run` 状态
`routes/compat.rs:10``next_ai_agent_run()` 始终返回 `410 GONE` + 消息"旧 /api/ai-agent/run 页面 AI 主链已退场"。**已确认退役**,无 P0 风险。
## 3. 各入口分类汇总
| 分类 | 条目 | 说明 |
|------|------|------|
| `default` | 无 | HTTP proxy 默认关闭;ACP 是默认 runtime |
| `compat` | `list_sessions`, `search_sessions`, `gateway_health`, `create_run`, `stream_events`, `abort_run`, `list_models`, 队列处理 | 仅当 `MNOTE_WEB_ENABLE_HERMES_HTTP_PROXY=true` 时激活;不影响正常用户 |
| `debug-only` | `page_ai_workflow::block_edit_workflow` | bypass ACP session,但受工具 executor 守卫保护 |
| `retired` | `compat::next_ai_agent_run` | 已返回 410,无降级路径 |
| `未被调用` | `configured_runtime_for_profile()`line 3368 | 标记 `#[allow(dead_code)]` |
## 4. P0 缺口评估
**当前没有 P0 必须修复的缺口。**
理由:
1. HTTP proxy 默认关闭(`hermes_http_proxy_enabled() = false`
2. `compat::next_ai_agent_run` 已正确返回 410
3. `page_ai_workflow` 经过共享 tool executor 守卫,不绕过权限检查
4. 所有路由的第一分支都是 ACP
## 5. 建议的后续修改范围
| 项 | 建议 | 紧急程度 |
|----|------|---------|
| HTTP proxy 完整退役 | 删除 `configured_upstream()` / `configured_upstream_for_profile()` / `proxy_json()` 相关代码 | P2cleanup |
| `list_models` 从 HTTP proxy 迁移 | 改为 ACP 的 `session/initialize` 返回 model 信息 | P2(依赖 ACP 协议增强) |
| `page_ai_workflow` 检查驱动 | 确保测试覆盖其 bypass ACP session 的行为的非期望路径(`task-page-block-ai-smoke.js` → 已存在) | 已有 |
| 旧环境变量清理 | 文档标记 `MNOTE_WEB_HERMES_UPSTREAM_URL` 等为退役 | P2 |
## 6. 建议测试
```
# 确认 HTTP proxy 默认关闭(infra 测试)
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web hermes_http_proxy -- --test-threads=1
# 确认 compat 路由返回 410(已存在)
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web direct_ai_agent_run_returns_legacy_retired_guard -- --test-threads=1
# 确认 page_ai_workflow 受守卫保护(已存在)
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web block_edit_workflow_respects_disabled_markdown_edit_tool -- --test-threads=1
```