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 一律写「远程服务器」。
6.3 KiB
6.3 KiB
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 重放 |
缺口:用户已经打开着会话页面、实例随后被回收/关闭时 ——
- 页面上的 XHR/
fetch打到实例 → 代理返回404 {error:'not_running'}(proxy.ts非导航分支),而注入脚本hit()只判 401 → 静默失败; - SSE 流静默断掉,页面看起来只是"卡住/不动";
- 没有任何"用户回到页面"的探测点 → 只能用户自己按 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。
五、边界(有意不做)
- 用户一直盯着页面、且页面不发任何请求 → 没有探测点。但实例被回收时 SSE 会断,dsh 前端自身的重连请求会成为触发源(被代理判成
not_running→ 被hit()捕获);且visibilitychange覆盖了"用户回来"这个主场景。未加周期性轮询(每用户每分钟一次 status 的负载换不来相应的收益)。 - 不做"自动刷新后恢复用户输入" —— 实例被回收的前提是长时间不活跃,此刻输入丢失风险低;且抓取 dsh DOM 输入态的做法会随官方前端升级而碎(脆弱性 > 收益)。
- 本档只解决"回到页面自动恢复";实例启动耗时的体感由档案 59 的加载动画承担。
六、红线遵守
- R8:重启
dshs→ 断当时 1 个在线实例(guest100002)约 4 秒;执行前已与用户确认节奏("现在可以改了吗"的授权模式)。 - R2:纯注入脚本,不改官方 dsh 主程序与缓存;不落盘。
- R7:只改 1 个源文件(+ 1 个
.bak-t06-*备份作回滚点)。 - 未 push。
七、回滚
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。