# 50-会话过期自愈:实例 HTML 注入「重连」脚本(2026-09-11 落地)
## 背景
档案 49 修好了「实例被空闲回收后,请求失败在传输层」的问题(反代自动拉起 + 等待就绪),
但用户实测仍报:
```
client api: session/prompt failed: transport failure for /api/session/prompt: HTTP 401 (gateway/internal)
```
**根因(代码级确认)**:`proxy.ts` 里那段 401 处理**只对"浏览器导航"生效**:
```ts
if (upRes.statusCode === 401 && request.raw.method === 'GET'
&& String(request.headers.accept ?? '').includes('text/html') && freshAuthUrl !== undefined) {
...302 到当前实例的新 token...
}
reply.raw.writeHead(upRes.statusCode ?? 502, headers); upRes.pipe(reply.raw) // ← XHR 一律原样透传
```
完整因果链:
1. 实例空闲被回收 → 2. 用户回到**已打开的页面**继续对话,前端发 XHR,**带着旧实例的 `dsh-auth-*` cookie**
→ 3. 档案 49 的修复把实例**拉起**了(这一步生效)→ 4. 新实例发现 token 是**旧 authority/端口的** → **401**
→ 5. 401 处理不管 XHR → 透传给前端 → dsh 显示 `transport failure` → 6. 用户手动刷新(=导航)才命中 401 处理、302 到新 token。
**→ 档案 49 消灭了"传输失败",但没消灭"认证失败"。**
## 方案取舍(用户选 C 先看效果)
| 方案 | 做法 | 评价 |
|---|---|---|
| **B** | 反代透明重放:XHR 401 时用 `freshAuthUrl()` 取新 token → 换新 cookie → **重放该请求** | 体验最好(前端无感),但要写 cookie 重放(中等复杂度)→ **留待后续** |
| **C(本次采用)** | 在反代返回的 **`text/html`** 里注入平台脚本 → 检测 `/api/*` 返回 **401** → 显示覆盖层动画 + `location.reload()`;reload 会走**已经工作**的"导航 401 → 302 新 token"链路自动恢复 | 简单、复用既有机制,且满足用户要的"当前页面给反馈" |
## 实现(`src/supervisor/proxy.ts`)
1. 新增模块级常量 **`SESSION_RECOVERY_JS`**(1480 字节,ES5 安全、全程 try/catch、幂等 `window.__dshRecover`):
- hook `window.fetch` 与 `XMLHttpRequest.prototype.open/send`(`loadend` 回调)
- 命中条件:**状态 401 且 URL 含 `/api/`** → 弹覆盖层(转圈 + 「会话已过期(实例被回收重建),正在重新连接…」)→ **900ms 后 `location.reload()`**
2. `injectRecovery(html)`:把 `` 插到 `