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 一律写「远程服务器」。
85 lines
6.3 KiB
Markdown
85 lines
6.3 KiB
Markdown
# 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`。
|