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

67 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 '...'`。**