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 一律写「远程服务器」。
5.0 KiB
5.0 KiB
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 分支)
- 导航请求(GET +
Accept: text/html)→ 立刻 302 到/wake.html?next=<原 URL>,不在此分支 launch(拉起交给过渡页,单点可重试)→ 消除"服务端阻塞 20 秒白屏"。 - 非导航请求(XHR/API)→ 等实例就绪后继续转发:
launch()(并发时退避到waitForLaunchTokenForUser)→ 重新resolveSubdomainAccess→ 走正常proxyHttp转发。真起不来才503 + Retry-After: 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 '...'。