# AI助手链路架构 + 「回答超时」根治（流式输出）

2026-09-08 实测。复杂问题报「AI 回答超时或服务不可用」的完整诊断与修复。

## 链路全貌

前端(index.html/app.html) → 8765 `销售工作台服务.py` → `ai_chat.py` handle()/handle_stream() → 8642 网关 API(`/v1/chat/completions`, model='hermes-agent') → DeepSeek V4 Pro

- 8765 = `ai.hermes.workbench`（launchd 托管，开机自启）
- 8642 = `ai.hermes.gateway`（Hermes 网关 OpenAI 兼容端点）
- 鉴权：`Authorization: Bearer $API_SERVER_KEY`，key 在 `~/.hermes/.env`（`API_SERVER_KEY=`）；ai_chat.py 硬编码 fallback `nas-file-share-token-2026`（与 .env 一致，对得上）

## 超时根因（三个叠加）

1. **思考模式**：网关用 `active_provider: deepseek`（config.yaml 顶层键，**不是** `model.provider: custom`）→ providers.deepseek 有 `extra_body.thinking.enabled + reasoning_effort: medium`。思考模型先吐隐藏推理再出答案，复杂题首 token 前静默 ~90s。
   - 关键区分：顶层 `model.provider: custom` 是主对话（本会话）用的，**无 thinking**；`active_provider: deepseek` 是 8642 API server 用的，**有 thinking**。改 thinking 只影响 API server，不影响主对话。
2. **串行检索**：handle() 依次做 `_search_background`（MCP 飞书活动，单次 15-30s）+ `_search_knowledge_base`（IMA 8s）+ `_load_customer360`（本地+IMA 竞品）。
3. **超时上限**：后端 `_call_hermes` timeout=180s（ai_chat.py），前端 `CHAT_TIMEOUT_MS=240000`。

## 关键诊断信号

- **BrokenPipe([Errno 32]) = 前端超时切断，不是通路断**。traceback `do_POST → _send_json → wfile.write → BrokenPipeError` 表示「服务端写好答案回写时客户端已断开」。若是通路断应是 `Connection refused` / `gateway_auth_failed` / 模型 500。日志 `grep -an 'BrokenPipe\|POST /api/chat' ~/.hermes/logs/workbench.error.log`。
- 服务是否活：`curl http://127.0.0.1:8765/`（200）、`curl http://127.0.0.1:8642/v1/models`（带 Bearer key）。
- 复杂题实测：status t=0 → 首 token ~89s（思考）→ 完成 ~98s。
- 前端是否跑到新代码：看界面是否出现「正在连接…」（流式版初始占位，旧版无此字样）。

## 流式输出方案（保留思考模式，不动网关配置）

1. **8642 支持 `stream:true`**：返回 `text/event-stream`，`data:{...delta.content}` 逐块 + `data:[DONE]`，最后一块带 `usage`。
2. **ai_chat.py**：加 `_call_hermes_stream(sys_prompt, messages, usage_out)`（读 SSE 逐段 yield content）+ `handle_stream(payload)`（generator，先 yield `status` 再 `delta`/`done`/`error`）。不动原 `handle()`，保证非流式路径不受影响。
3. **销售工作台服务.py**：加 `_stream_sse(generator)`（send_response + `text/event-stream` + `Access-Control-Allow-Origin:*` + `X-Accel-Buffering:no`，逐事件写 `data: {json}\n\n`），do_POST 里 `payload.get('stream')` 分流。
4. **前端**：payload 加 `stream:true`；tryNext 改 `r.body.getReader()` 逐块读，`status`→显示状态、`delta`→累加 `mdRender(fullText)`；超时从硬 240s 改为**空闲 240s**（有数据就 `resetTimer`）。
5. **思考静默期**：思考阶段（首 token 前）只有 `reasoning_content`、无 `delta.content`，故空闲超时必须 ≥240s；如需思考进度可加节流心跳（每 ~15s 发一条 heartbeat status）。

## CORS / 混合内容坑（前端 fetch 静默失败的元凶）

- CHAT_APIS 必须 **`'/api/chat'`（相对路径）排第一**，再跟 Funnel 绝对 URL + localhost。否则 HTTPS 页面（CloudBase/Funnel）请求 `http://127.0.0.1:8765` 会被混合内容静默拦截；跨域还需 OPTIONS preflight。
- 后端 do_OPTIONS 已返回 CORS 头（Allow-Origin:* / Methods: GET,POST,OPTIONS / Headers: Content-Type），`_stream_sse` 也要带 `Access-Control-Allow-Origin:*`。
- 症状识别：界面停在「正在连接…」从未变「正在检索业务背景」= fetch 在拿到 SSE 前就失败（连接层），不是超时。加 `console.error('[AI助手] 端点失败: '+url, e.name, e.message)` 到 catch 便于 F12 定位。

## 改配置 / 部署铁律

- **直接 patch config.yaml 会被安全守卫拒绝**。用 `hermes config set providers.deepseek.extra_body.thinking.type disabled`（dotted path），`hermes config get <path>` 验证。
- 前端改完部署：`tcb hosting deploy 工具/index.html /workbench/index.html -e bier-sales-d0gatbvlx288724e9`（app.html 走 8765 本地，不部署）。
- 8765 重启：`launchctl kickstart -k gui/$(id -u)/ai.hermes.workbench`。
- 流式验证：`curl -sSN -X POST <url>/api/chat -d '{...stream:true}'` 看 `data:` 逐块返回；Tailscale Funnel(443) 支持 SSE 不缓冲，`curl -sSN` 直接验证。
- 用户约束：**不能停思考模式**（深度分析是卖点），只能走流式+超时放宽，不能关 thinking 提速。

## 致命坑：`if (!r.ok)` 静默丢弃（2026-09-08 最终根因）

前端 fetch 的 `!r.ok` 分支若 `return r.json().then(...)` 返回 `{_err}`，但后面没有下一个 `.then` 消费，该 promise 落在链尾无人接住 → **既不 `tryNext()` 降级、也不 `settle()` 报错**，界面永久卡「正在连接…」（无报错、无重试）。只在端点返回 404/500（非抛异常）时触发——CloudBase 域名上同源 `/api/chat` 返回 404 即中招（相对路径优先后必现）。

修复：非 200 直接降级，别 return 错误对象：

```js
if (!r.ok) {
  if (timer) clearTimeout(timer);
  console.error('[AI助手] HTTP ' + r.status + ': ' + url);
  tryNext();
  return;
}
```

通用铁律：**promise 链里 `if (!r.ok)` 分支必须显式降级或报错，不能只 return 一个无人消费的对象。** 抛异常（网络错/abort）走 `.catch` 会降级，但 404/500 走 `.then` 的 `!r.ok` 分支，不显式处理就静默卡死。

## 旁支：vision_analyze 挂时用 macOS OCR 读截图

Anthropic 图片识别 500（日志 `anthropic.InternalServerError`）时，用 macOS Vision Swift 脚本 OCR 读用户截图（中文+英文混排识别好）：

```swift
import Foundation, Vision, AppKit
let img = NSImage(contentsOfFile: "<图路径>")!
let cg = img.cgImage(forProposedRect: nil, context: nil, hints: nil)!
let req = VNRecognizeTextRequest { r, e in
  (r.results as? [VNRecognizedTextObservation])?.forEach { o in
    if let t = o.topCandidates(1).first { print(t.string) } } }
req.recognitionLevel = .accurate
req.recognitionLanguages = ["zh-Hans", "en-US"]
try? VNImageRequestHandler(cgImage: cg, options: [:]).perform([req])
```

`swift /tmp/ocr.swift` 运行，是 vision_analyze 500 时的可靠兜底。
