Files
dsh_shenxian/dsh-server-docs/04-调整方案/72-实例回收后回到页面自动唤醒重建连接.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

85 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 72 · 实例被回收/关闭后,回到会话页面自动唤醒并重建连接
- 日期:2026-09-12
- 触发:用户「用户回到 dsh 会话页面,假如连接已经断开、进程被回收或关闭,可以自动恢复进程建立连接,**现在还是需要手动刷新才行**,能否实现」
- 结论一句话:**能,已上线** —— 根因是注入的自愈脚本**只认 401**、不认实例回收时代理返回的 `404 not_running`,且**没有「用户回到页面」这个时机**的探测入口。补上识别与探测后,用户切回标签页 / 点回窗口即自动唤醒,并跳回**带新 token** 的会话地址。
- 状态:✅ **已完成并部署**(2026-09-12 16:55,服务器 commit **`b23e386`**)
- 关联:档案 49(过渡页)/50(注入脚本)/51(401 透明重放)/59(启动动画)/24 + 8(回收与 last-wins)/05(跨域 CORS 白名单)
---
## 一、现状:为什么"必须手动刷新"
链路里其实**已有**一套恢复机制,但只在**特定时机**生效:
| 已有机制 | 触发时机 | 覆盖 |
|---|---|---|
| 代理 302 → `wake.html`(档案 49) | **导航请求**(GET + `Accept: text/html`)撞到 `not_running` | ✅ 手动刷新/点链接 = 能自动拉起 |
| `wake.html` 内部调 `/api/dsh/enter` | 过渡页加载后 | ✅ 自动 launch + 跳回带 token 地址 |
| 注入脚本 `SESSION_RECOVERY_JS`(档案 50) | **401**(实例被回收重建、launch token 轮换) | ✅ `expire()` → `location.reload()` |
| 代理 401 透明重放(档案 51) | 服务端侧 | ✅ XHR/SSE 重放 |
**缺口**:用户**已经打开着**会话页面、实例随后被回收/关闭时 ——
1. 页面上的 XHR/`fetch` 打到实例 → 代理返回 **`404 {error:'not_running'}`**(`proxy.ts` 非导航分支),而注入脚本 `hit()` **只判 401** → 静默失败;
2. SSE 流静默断掉,页面看起来只是"卡住/不动";
3. 没有任何"用户回到页面"的探测点 → **只能用户自己按 F5**(F5 是导航请求,才走得到 `wake.html`)。
## 二、方案(三个探测点 + 一条恢复路径,零新增后端端点)
**探测(什么时候判断"实例没了")**
| 时机 | 手段 |
|---|---|
| ① 回到页面 | `visibilitychange`(变可见) / `window.focus` / `pageshow`(`persisted`) → 主动探活 |
| ② 请求失败 | `hit()` 扩展:`404` + 响应体含 `not_running`(与既有 401 判定并列) |
**探活**:`GET <门户域>/api/dsh/status` → 平台返回 `running: alive(main.status)`;**`running === false` 即判定不可用**。跨域调用依据:`server.ts` 的 CORS 白名单允许 baseDomain 及其子域 + `Allow-Credentials`(档案 05 PoC-2 ② 已验证过的既有能力)。
**恢复**:跳门户 `/wake.html` → 它内部调 `POST /api/dsh/enter`(会 launch 实例)→ 跳回**带新 token** 的实例地址。
> **为什么不是 `location.reload()`**:实例被回收重建后 **launch token 已轮换**,`reload()` 会带着**旧 token** 再撞一次 401 → 再走 `expire()` → 再 reload。用户看到的就是"**要刷新两次**"。走 `enter` 一步到位。这也是原有 401 路径的现实短板(本次**未改动**它,避免影响已验证的路径)。
## 三、实现(全部在 `src/supervisor/proxy.ts` 的注入脚本内)
| 改动 | 内容 |
|---|---|
| 新增 `portalOrigin()` | 由子域算出门户域(`a.b.com` → `b.com`;无子域时退回 `location.origin`,兼容本机/IP 直连) |
| 新增 `probe()` | 调 `/api/dsh/status`(`credentials: 'include'`);**15 秒节流**;跨域/网络异常**一律静默**,绝不误判 |
| 新增 `recover(reason)` | 显示覆盖层(复用既有 pending 浮层样式)→ 600ms 后 `location.replace(portalOrigin() + '/wake.html')`;带 `recovering` 闸门防重入 |
| `hit(u, s)` → `hit(u, s, readBody)` | 除 401 外,识别 `404` + 体含 `not_running`;`fetch` 用 `r.clone().text()` 读体(**不消费原响应**),`XHR` 读 `responseText` |
| 事件绑定 | `document.visibilitychange`(visible)/`window.focus`/`window.pageshow`(`persisted`) |
**未新增**:后端端点(复用 `/api/dsh/status` 与 `/api/dsh/enter`)、DB 字段、依赖;**未改动**:401 既有路径、导航 302 路径、`wake.html`。
## 四、验证
| 项 | 方式 | 结果 |
|---|---|---|
| 编译 | `npm run typecheck` / `npm run build` | ✅ 通过 |
| **注入脚本语法** | 从编译产物提取 `SESSION_RECOVERY_JS` / `SESSION_ASSIST_JS` → `new Function(code)` | ✅ 语法正确(6604 / 8823 字符) |
| **新能力落位** | 6 项关键字检查(portalOrigin / recover→wake.html / status 探活 / not_running 识别 / visibilitychange / pageshow) | ✅ 全部命中 |
| 服务 | `systemctl is-active` + 门户 `curl` | ✅ `active` / HTTP **200** |
**端到端(浏览器)需用户验一次**:把实例闲置到被回收(或手动 `systemctl stop dsh-*`),切回该标签页 —— 期望出现「实例已休眠,正在自动唤醒…」覆盖层并自动跳回会话,**不需要按 F5**。
## 五、边界(有意不做)
1. **用户一直盯着页面、且页面不发任何请求** → 没有探测点。但实例被回收时 SSE 会断,dsh 前端自身的重连请求会成为触发源(被代理判成 `not_running` → 被 `hit()` 捕获);且 `visibilitychange` 覆盖了"用户回来"这个主场景。**未加周期性轮询**(每用户每分钟一次 status 的负载换不来相应的收益)。
2. **不做"自动刷新后恢复用户输入"** —— 实例被回收的前提是**长时间不活跃**,此刻输入丢失风险低;且抓取 dsh DOM 输入态的做法会随官方前端升级而碎(脆弱性 > 收益)。
3. 本档只解决"回到页面自动恢复";**实例启动耗时的体感**由档案 59 的加载动画承担。
## 六、红线遵守
- **R8**:重启 `dshs` → 断当时 **1 个**在线实例(guest `100002`)约 4 秒;执行前已与用户确认节奏("现在可以改了吗"的授权模式)。
- **R2**:纯注入脚本,**不改官方 dsh 主程序与缓存**;不落盘。
- **R7**:只改 1 个源文件(+ 1 个 `.bak-t06-*` 备份作回滚点)。
- 未 push。
## 七、回滚
```bash
cd /opt/dshs && cp -a src/supervisor/proxy.ts.bak-t06-<HHMM> src/supervisor/proxy.ts && npm run build && systemctl restart dshs
```
或 `git revert b23e386`。