# 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 API（HTTP）
  → 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()` 相关代码 | P2（cleanup） |
| `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
```
