Files
dsh_shenxian/dsh-server-docs/04-调整方案/49-实例回收后首次访问的反馈与自愈.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

5.0 KiB
Raw Blame History

49-实例回收后首次访问的反馈与自愈(2026-09-11 落地)

TL;DR|结论:实例被空闲回收后首次访问"没反应":拉起逻辑早就有,缺的是反馈。 关键:导航(GET+HTML)→ 立刻 302 到平台过渡页(动画),由过渡页调 /api/dsh/enter 完成「拉起+等 token+跳回」;XHR 等服务就绪。 状态:✅ 已落地

症状

用户反馈:dsh 主界面一段时间不对话,后台实例被空闲回收;下次对话没有反应,必须手动刷新。 诉求:感知用户回到页面时自动启动 + 页面给出「进程启动中」的加载动画。

根因(比预期乐观:拉起逻辑早就有,缺的是反馈)

环节 事实
谁回收 orchestrator.ts:70-117 local-mode idle reap:定期扫 lastActive,超 instanceIdleTtlSeconds 即 stop(userId);proxy.ts:120 注释确认"真实用户流量会刷新活跃时间"
谁拉起 ✅ 早就有:proxy.ts 的 not_running 分支会 supervisor.launch(...);POST /api/dsh/enter 同样"不在则 launch + 返回带 token 直达 URL"
真实症状 不是没拉起,而是拉起要 5–20 秒,这期间浏览器零反馈(服务端阻塞等 launch token)→ 用户以为卡死 → 手动刷新时实例已就绪 → 看似"刷新才好"
关键架构 alotbuy.com.conf 的 server_name 含 *.alotbuy.com → 所有用户子域都先 proxy_pass 127.0.0.1:3080(门户),再由门户 proxy.ts 转发到实例 → nginx 的 error_page 用不到,正确位置是 proxy.ts

用户的关键质疑与修正

「为什么要跳转到过渡页,直接在当前页面播放加载动画不是更好?」

质疑成立 —— 原方案只覆盖次要场景。两个场景必须分开处理:

场景 表现 正解
① 页面已打开,实例被回收,用户继续对话(XHR/WS) 页面内静默失败 = 用户说的"没反应" 不是"显示动画",而是"别让请求失败":等实例就绪后继续转发 —— 请求最终成功,dsh 自身 loading 即反馈,用户无感
② 刷新 / 重开页面(整页导航) 导航被拦 立刻 302 到平台自有过渡页(含动画),由过渡页完成拉起并跳回

实现(src/supervisor/proxy.ts 的 not_running 分支)

  1. 导航请求(GET + Accept: text/html)→ 立刻 302 到 /wake.html?next=<原 URL>,不在此分支 launch(拉起交给过渡页,单点可重试)→ 消除"服务端阻塞 20 秒白屏"。
  2. 非导航请求(XHR/API)→ 等实例就绪后继续转发:launch()(并发时退避到 waitForLaunchTokenForUser)→ 重新 resolveSubdomainAccess → 走正常 proxyHttp 转发。真起不来才 503 + Retry-After: 3。
  3. 新增 web/wake.html(4591B,门户同源,cookie 天然可用、无需 CORS):加载动画 + 自动 POST /api/dsh/enter 拿直达 URL → location.replace;失败自动重试 3 次 + 重试按钮;next 只允许 *.alotbuy.com(防开放重定向)。零后端新增接口(复用既有 enter)。

验证记录(真实反代链路)

用例 结果
A) 导航(Accept: text/html)到 guest.alotbuy.com 41 ms 返回 302 → https://alotbuy.com/wake.html?next=… ✅ 且实例 scope 仍 0 个(导航分支未拉起)
A2) 过渡页可取 4591 字节,含 wk-spin(动画)与 /api/dsh/enter 调用 ✅
B) XHR(Accept: application/json) 5057 ms 返回 401(不是 503)→ 说明走了"等待 + 转发";期间实例 scope 变 1 个 ✅ 拉起成功
清理 测试实例 scope 已 stop;临时 guest 会话已删(user_agent=poc-wake-xhr);共享层 0 项

测试遵守 R4:DB 直插 guest 临时会话,不经登录接口、用完即删。

未做(可选增强)

  • 页面内覆盖层动画:场景 ① 已由"等待就绪"从根上解决(dsh 自身 loading 即反馈),故未做"注入官方 SPA 响应"方案 —— 收益有限、风险与维护成本高,且更接近 R2 边界。
  • WS(app.server.on upgrade):实例未运行时仍 socket.destroy();dsh 前端会自行重连,等实例就绪即可恢复。要连 WS 也"等就绪再建隧道",需在 upgrade 回调做同样等待(未做)。
  • 空闲 TTL 调优:instanceIdleTtlSeconds(属性能取舍,与 R5 无关)。

回滚

cp src/supervisor/proxy.ts.bak-<ts> src/supervisor/proxy.ts && npm run build && systemctl restart dshs

(wake.html 为惰性文件,回滚 proxy 后无入口,可留存)

事故 / 踩坑

⚠️ 本轮第 5 次撞上 ssh 单引号:档案正文里的 A') 提前闭合了外层 ssh '...' 的引号,整条命令直接语法错误、什么都没执行。 → 再次验证硬性要求:任何含引号/括号/反引号的文本(尤其文档正文)一律"本地 Write → scp → 远端执行",禁止 inline heredoc 塞进 ssh '...'。