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

6.3 KiB
Raw Blame History

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。

七、回滚

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。