# AI 助手多端点降级 + SSE 流式超时修复（2026-09-08 实战）

## 症状
发问题后界面连出 3 个「正在连接…」再弹「⚠️ AI 回答超时或服务不可用」，全程无回答、不报具体错误。

## 根因 A：`!r.ok` 静默卡死（前端多端点降级 bug）

多端点降级链里，fetch 返回非 200（如 CloudBase 域名上相对路径 `/api/chat` 返回 404）时，旧代码写：
```js
if (!r.ok) {
  return r.json().then(j => ({ _err: ... })).catch(() => ({ _err: ... }));
}
```
返回的 `{_err}` 对象没人消费——既不 `tryNext()` 也不报错，promise 链**静默结束** → 界面永远卡「正在连接…」。

**修法**：非 200 必须显式降级到下一端点：
```js
if (!r.ok) {
  if (timer) clearTimeout(timer);
  console.error('[AI助手] HTTP ' + r.status + ': ' + url);
  tryNext();
  return;
}
```

## 根因 B：前端 240s 硬超时 vs 服务端思考慢

原前端 `CHAT_TIMEOUT_MS = 240000` 硬超时主动 `abort()`，服务端已处理完、回写时客户端已断 → `BrokenPipeError: [Errno 32] Broken pipe`（日志里表现为 `POST /api/chat 500`）。

**修法**：SSE 流式输出（保留思考模式前提下唯一解法）：
- 后端 `ai_chat.py`：`handle_stream(payload)` 产 `{status, delta, done, error}` 事件；`_call_hermes_stream()` 流式读 8642 网关的 SSE。
- 后端 `销售工作台服务.py`：`_stream_sse(generator)` 先 `send_response(200)` + SSE 头，再逐事件写 `data: {json}\n\n` 并 flush。
- 前端：payload 加 `stream:true`；用 `r.body.getReader()` 读流，`decoder.decode(res.value, {stream:true})` 累积，按 `\n` 切行、`line.indexOf('data:')===0` 过滤；`status` 事件更新提示、`delta` 事件逐字渲染。
- 超时改**空闲超时**（240s 无数据才断，收到 token 就重置），替代硬超时。

**不能关思考模式**（用户硬约束：`thinking.type=enabled`、`reasoning_effort=medium` 不可动）。复杂题首 token 前有 ~87s 静默思考，流式让连接全程活跃、前端实时显示进度，是唯一两全办法。

## CHAT_APIS 顺序（3 端点降级）
```js
var CHAT_APIS = ['https://api.biorisales.site/api/chat','/api/chat','http://127.0.0.1:8765/api/chat'];
```
- 同源相对路径 `/api/chat` 排第二（消除跨域/混合内容），但 CloudBase 域名上**没有** `/api/chat` 会返回 404 —— 必须配 `!r.ok` 降级才不卡死。
- 首选 `https://api.biorisales.site/api/chat`（frp 隧道 + Caddy HTTPS 的国内出口，见 tailscale-http-server 技能）。

## 排障要点
- 3 个「正在连接…」= 3 个 CHAT_APIS 端点各失败一次；「正在连接…」未被 status 事件替换 = fetch 在拿到 SSE 前就失败（连接层问题，非超时）。
- 前后端都要改：只改后端（`stream:true` 分流）或只改前端（流式读取）都会失败。改完用 `python3 -c "import ast; ast.parse(open('ai_chat.py').read())"` 验后端语法，前端内联 JS 抽出来 `node --check` 验语法。
- 线上验证：`curl -s -X POST .../api/chat -d '{"stream":true,...}'` 应返回 `text/event-stream` + 逐段 `data:`。
