Files
dsh_shenxian/dsh-server-docs/04-调整方案/77-回到页面自检与就地恢复-恢复过程可见化.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

20 KiB
Raw Blame History

77 · 回到页面自检 + 就地恢复:把「恢复过程」做成看得见的

  • 日期:2026-09-13
  • 状态:✅ 已落地并验证(服务器 src/supervisor/proxy.ts + lib/ 已构建,dshs 已重启)
  • 触发:用户「不自然的地方是我回到页面根本没看到过拉起实例的提示」(承接档案 72 的「回到页面没反应」)
  • 结论一句话:能检测,但判据选错了 —— 探活只问「进程在不在跑」,而平台把 starting 也算在跑、崩溃后新实例 1 秒内就起来 ⇒ 用户切回页面时几乎永远命中「没问题」,既不提示也不恢复。本次把判据换成「页面还能不能连上实例」,并把恢复过程做成可见的,同时不再跳到门户域。
  • 关联:档案 72(回到页面自动唤醒)/49(导航过渡页)/50(注入脚本)/51(401 透明重放)/59(慢请求提示)/24(导航 401 恢复)

TL;DR|结论:提示"看不到"不是没做,是判据失效 + 提示只活 600ms。 关键:① 判据改为「实例侧探针」(getting starting 也必然超时 → 能被识别)② 顶部轻提示条 + 不自动消失的覆盖层 ③ 恢复就地,不再跳门户过渡页。 状态:✅ 已部署(配置层面全绿;浏览器观感待用户实测)。


一、根因(三条,都从代码与生产日志取证)

# 根因 证据
主 注入脚本 probe() 只在 j.running === false 时恢复;而 running 来自 src/web/routes/dsh.ts 的 alive(status) = status !== 'crashed' && !== 'stopped' && !== 'failed' ⇒ starting(正在启动)也算"活着" 源码;且实测每次崩溃后新 scope 在同一秒或 1 秒内起来(23:37:46→23:37:46、23:41:06→23:41:07、23:45:30、23:46:46、23:48:46、23:50:51)⇒ 用户切回标签页时实例几乎总是 starting/running ⇒ 判据永远不动手
次 即便触发,提示只活 600 ms 就 location.replace(wake.html);实例已在跑时过渡页的 enter 立即返回 ⇒ 过渡页一闪而过,肉眼看不到 旧 recover();dsh.ts /api/dsh/enter 的"复用已运行实例"分支
次 旧路径 跳门户域,且 recover() 跳的 wake.html 不带 next ⇒ 恢复后落回根地址、地址栏换域名、页面状态全丢 档案 72 §三

二、方案取舍

方案 做法 判定
A 现状(只判进程) 只在 running:false 恢复 ❌ 判据与"页面能不能用"不等价 ⇒ 主场景不触发(本次要修的)
B 周期性轮询(如每 30 秒探活一次) 定时器主动探 ❌ 每个在线用户每分钟多次请求,且用户在场却不操作时毫无收益;档案 72 §五已否
C 探针 + 可见反馈 + 就地恢复(采用) 回到页面才探;门户口判进程 + 实例侧探针判连通;提示条→覆盖层;恢复就地 ✅ 只在"用户真的回到页面"这一个时机付出成本;失败即可见;不换域名、保留路径
D 保活(页面可见即上报活跃,防被 LRU 挤掉) 定时上报 ⏸ 本次不做:会改变常驻实例的抢占语义(cap=4 下会挤掉别人),需单独评估容量,另开档案

三、实现(唯一改动文件 src/supervisor/proxy.ts,改的都是注入脚本 SESSION_RECOVERY_JS)

改动 内容
新增探针 poke() 同源 GET /,cache:'no-store'、redirect:'manual'、2.5 秒超时(AbortController)。200 → ok;404/405 → unknown(官方若改了入口形态就退回旧判据,绝不误报);其余 → bad
判据升级 门户口 status 说 running:false → 直接恢复;否则再跑 poke(),bad 才恢复
提示可见 新增 ensureBar/showBar/hideBar → 顶部轻提示条 #__dshConnBar(不遮罩);至少显示 900 ms 再撤(否则一闪而过等于没提示)
触发细分 只有"离开 ≥20 秒又回来"(visibilitychange + leftAt)才亮提示条;瞬时切换静默探活。focus 静默、pageshow(persisted) 带提示
就地恢复 recover() 不再跳 wake.html,改为跨域 POST /api/dsh/enter(带 cookie)拿带新 token 的地址 → withPath() 保留 pathname/hash → location.replace();取不到则回落 location.reload()
文案去归因 「实例已休眠(空闲回收)」→「工作区已休眠,正在唤醒…」;「会话已过期(实例被回收重建)」→「正在重新连接你的工作区…」

未新增:后端端点、DB 字段、依赖;未改动:导航路径的 302 → wake.html(刷新/首次进入仍走过渡页,行为不变)、401 既有路径、wake.html 本体。

复用而非新建(U2):探针用实例自己的 GET /(平台无新端点);恢复复用既有 /api/dsh/enter。

四、验证

项 方式 结果
本地类型检查 npm run build(tsc) ✅ 退出码 0
注入脚本不变量 模板字符串内反引号 = 0 / ${ = 0 ✅(长度 6604 → 10634 字符)
注入脚本语法 从编译产物抽 SESSION_RECOVERY_JS/SESSION_ASSIST_JS → new Function() ✅ 双双通过
CORS 前提(本方案命门) OPTIONS https://alotbuy.com/api/dsh/enter,Origin: https://admin.alotbuy.com ✅ 204 + allow-origin: https://admin.alotbuy.com + allow-credentials: true + allow-headers: Content-Type;无 cookie 的 POST → 401 且带 CORS 头
双端一致性 md5sum 本机 vs 服务器 + 服务器 CR 行数 = 0 ✅ 8c21a885… 双向一致,LF 保持
服务 systemctl restart → is-active / 门户 / 残留 scope ✅ active / 200 / 0 个
注入落位 临时会话(mksess.cjs,用完即删)→ enter → 三件套 curl 取实例页 ✅ enter_http=200、实例 running、页面 200;__dshConnBar/function poke/withPath/rawFetch/leftAt/「正在检查工作区连接」/「工作区正在恢复」全部命中;旧文案「会话已过期(实例被回收重建)」= 0
清理 临时会话删除 ✅ deleted_sessions=2(含历史残留)

未做(L5):浏览器端观感 —— 「切回标签页 → 顶部出现『正在检查工作区连接…』」、「实例确实不可用时 → 覆盖层 → 原地回到原路径」。需用户实测(本机无 Chromium,装它成本不成比例)。

验证顺带纠正的一条误报

[inject-recovery] skip: content-encoding=gzip 全历史 11 次、最近两次(09-12 22:40 / 23:26)紧邻 [proxy-auth-replay],remoteAddress 一次是服务器本机 IP、一次是 CF 回源 ⇒ 是我们自己的验证 curl(默认 Accept: */*,不含 text/html ⇒ 代理不覆盖 accept-encoding: identity ⇒ 上游 gzip ⇒ 跳过注入)。真实浏览器导航必带 text/html(实测注入正常)⇒ 不是用户侧的问题,本次无需处理。

五、边界(有意不做)

  1. 恢复后不恢复页面内状态(输入草稿 / 滚动位置)——同档案 72 §五,抓 DOM 状态会随官方前端升级而碎。
  2. 不做周期性探活(只在"回到页面"付出成本)。
  3. 不做保活(见 §二 方案 D,需单独评估容量)。
  4. 探针 2.5 秒超时是取舍:把"实例在启动中"判成 bad 正是我们要的;代价是网络抖动 + 实例侧同时慢时可能多一次原地重载(status 走门户、poke 走实例,两条链路同时慢才可能命中)。

六、红线遵守

  • R8:重启 dshs → 当时 2 个在线实例被停(约数秒);执行前已获用户明确授权("现在改")。
  • R2:纯注入脚本,不改官方 dsh 主程序与缓存;不落盘。
  • R7:只改 1 个源文件;改动前用 md5/diff 确认服务器版与本地版完全一致(排除了把在途改动带上生产的风险)。
  • R9:抢锁失败时停手并报告;本次解锁是用户明确点头后执行(--release-exec + FORCE=1 release univer-fork-deploy),未在本会话内自行判定对方死活。
  • 未 commit / 未 push。

七、回滚

cp -a /opt/dsh/backups/proxy.ts.bak-20260913-0012 /opt/dshs/src/supervisor/proxy.ts
cd /opt/dshs && npm run build && systemctl restart dshs

(wake.html 未被改动,导航路径行为不变,回滚面仅注入脚本本身。)

八、遗留(另行报告)

  1. 崩溃熔断可无限重来:熔断后 mains.delete + resetCrashState(),而用户下次 launch()/enter 又 resetCrashState() ⇒ 计数清零、重新给足预算;且熔断只写 stderr、无告警。 → ✅ 2026-09-13 已由档案 78 处置(跨轮熔断态 + 指数冷却 + 双通道告警;代码完成、本地全绿,部署待 R8 窗口)。
  2. INDEX.md §二 的状态摘要("档案 72 份" + 各状态计数)与表格实际行数(76)已不一致(本次未改,属历史漂移)。
  3. 实例被反复重启的外部诱因已查明(并行会话迭代插件版本),与本档无关。

九、补充(2026-09-13 07:0x):浏览器端实测已完成(关闭 §四 的 L5)

§四 写的「未做(L5):浏览器端观感 —— 需用户实测」已补做:本机真 Chrome(无头,playwright-core 驱动)+ 临时会话(mksess.cjs 直插,用完即删)打开 https://admin.alotbuy.com/,用真实事件(visibilitychange / pageshow(persisted))+ 网络层拦截触发两条路径。未重启服务、未停实例、未改任何源文件。

# 场景 手段 结果
0 注入落位 直接加载实例页 ✅ __dshRecover=1;__dshConnBar / function poke / function withPath / LEFT_MS 全部命中;旧文案「空闲回收」= 0
A 离开 ≥20 s 又回来 覆写 document.visibilityState 后派发 visibilitychange(hidden),21 s 后派发 (visible) ✅ 顶部条出现「正在检查工作区连接…」,约 1 s 后自动撤除;全程未误亮覆盖层
B 页面与实例已脱节 拦截实例侧 fetch / 使其失败 → 派发 pageshow(persisted) ✅ 覆盖层出现「工作区正在恢复,请稍候…」→ POST /api/dsh/enter 200 → 覆盖层撤除 → 仍在 admin.alotbuy.com(未跳门户域)

证据截图:_patch77/shots/(A-顶部提示条.png、B-恢复覆盖层.png、B-恢复后.png、0-初始.png)。

第一次跑出现两个「❌」,复盘后确认都是测量脚本的错,不是产品的错:① 提示条等待器在 21 s 等待之前就启动,被自身 6 s 超时耗光 → 必然抓不到;② 网络过滤写成 hostname.endsWith('.alotbuy.com'),漏掉裸域 alotbuy.com(/api/dsh/enter 在门户裸域上)→ 误报「没调 enter」。修正后 10/10 全绿。

结论:档案 72/77 的「回到页面自检 + 就地恢复」链路在真实浏览器里行为符合设计(含「看得见」这一条);§四 的 L5 关闭。


追加(2026-09-13 08:1x · 浮层视觉升级):「转圈」→「AI 核心」

触发:用户「工作区已释放正在恢复那个浮层的转圈动画效果太简陋,看看能否更科技化 AI 化」。

旧实现:一个 border:3px solid rgba(255,255,255,.25); border-top-color:#fff 的经典转圈(34×34 / 0.8s linear)—— 能看,但确实"工地"。

新实现(纯 CSS + createElement,零外部资源、零依赖):

元素 作用
.__dsh-ov 覆盖层:双层径向光晕 + rgba(9,11,16,.9) 深底
.__dsh-card 半透明卡片 + 渐变描边 + backdrop-filter: blur(9px)
.__dsh-halo 呼吸光晕(2.6s 缩放/透明度)
.__dsh-arc conic 渐变扫描弧(蓝 → 紫),用 mask 挖空成环、1.5s 旋转 —— 视觉主角
.__dsh-arc2 反向虚线环(7s,与主弧反差速,产生"仪器感")
.__dsh-core 脉冲核心(1.6s 缩放 + 透明度)
.__dsh-dots 三颗轨道粒子(各自 0.35s 延迟 + 三色)
.__dsh-msg 文案:极弱流光扫过(可读性下限 0.94 不透明度,不会像灰掉)
.__dsh-label DSH · AUTO RECOVERY 等宽小字
.__dsh-track 不定进度条(scan)

无障碍:@media (prefers-reduced-motion: reduce) 关闭全部动画(已出 reduced-motion 截图确认不是"卡住")。动画只用 transform / opacity / background-position,不触发布局抖动。

硬约束遵守(注入脚本的三条红线):

  1. 反引号 0 / ${ 0(CSS 里字体名改用单引号 'Segoe UI',从而 JS 侧可用双引号字符串承载 CSS 各行);
  2. 不用 innerHTML(实例页可能启用 Trusted Types)→ 结构全部 createElement;
  3. 样式集中在 #__dshRecoverCss 一次性注入;DOM 仍只建一次(否则每次改文案会重建元素、打断动画 —— 这是档案 77 原有教训,本次沿用)。

验证:

项 结果
npm run build(tsc) ✅ rc=0
npm test ✅ 45 pass / 0 fail / 1 skipped
注入脚本不变量 ✅ 编译产物抽 SESSION_RECOVERY_JS/SESSION_ASSIST_JS → new Function() 双通过(反引号 0 / ${ 0)
端到端(临时会话 + 三件套 curl 取实例页) ✅ __dsh-ov / __dsh-orb / __dsh-arc2 / __dsh-core / __dsh-track / conic-gradient / prefers-reduced-motion / DSH · auto recovery 全部命中;旧标记 border-top-color = 0;临时会话已删
部署 ✅ 备份 proxy.ts.bak-overlay-20260913-0815 → 服务器 build rc=0 → 重启(PID 413817→415625,门户 200)

预览与证据:_patch77/overlay-preview.html(与注入样式同源 = _patch77/overlay.css,避免"预览和线上不一样")+ 截图 _patch77/shots/overlay-{new,closeup,reduced-motion}.png。

本次未动:顶部提示条 #__dshConnBar 的视觉(用户只点了浮层)—— 若要一起统一风格,另说。


追加(2026-09-13 08:2x · 现场故障 + 热修):注入脚本被我自己写崩了

用户报告:「admin 的 dsh 主页面断开连接了,但页面没显示『工作区已休眠正在唤醒』浮层,模型一直显示连接异常」。

根因(我引入的):08:15 那次浮层视觉升级里,我写了 st.textContent = [...].join('\n'); —— 这段代码位于 TS 模板字面量 SESSION_RECOVERY_JS 内部,\n 在模板求值时就被转成真换行, 注入到浏览器的那段 JS 变成 ].join(' + 裸换行 + '); ⇒ SyntaxError ⇒ 整个注入脚本不执行 ⇒ 浮层 / 自检 / 自愈全部静默失效,页面只剩 dsh 自己的「连接异常」。

为什么我的"校验"没拦住:当时只做「从 lib/ 抽原始文本 → new Function()」—— 跳过了模板求值这一步,所以是假绿。浏览器里 window.__dshRecover 为 false、注入 CSS 长度 0,才是真相。

顺带挖出的第二个(更早存在、更严重的)缺陷:同一文件里的 SESSION_ASSIST_JS(档案 56 的 「我的文件 / 能力」助手)一直是坏的 —— 里面 8 处 '\n' 同样是单反斜杠写法 ⇒ 模板求值后裸换行 ⇒ 该脚本从来没在浏览器里跑过。此前"验证"只 grep 了页面 HTML 里有没有 __dshAssist 字样, 没有验"脚本跑不跑得起来"(同一个假绿模式)。两个脚本今天一起修好。

修复:

  1. 我引入的那处 → .join(String.fromCharCode(10))(不依赖任何转义);
  2. 助手脚本的 8 处 → 按源文件补成 \\n(只补未转义的,已转义的保持原样);
  3. ⛔ 新增常设校验 scripts/verify-inject.cjs(已接入 npm test):先把模板字面量求值成运行时字符串,再 node --check, 并断言运行时串里不含反引号 / ${。⇒ 这类"假绿"再也不可能溜过 npm test。

同时补齐的触发面(这才是"页面没反应"的另一半):实测(见下)确认——用户一直盯着页面、连接悄悄断掉时,原设计一个事件都不会来(只有切标签/切回来、focus、pageshow 才探活),所以既不提示也不恢复。

  • ① 心跳:页面可见时每 25 秒静默探一次(不可见时零成本);
  • ② 流断:包装 EventSource / WebSocket 的 error / close,立即探一次(比心跳更早);
  • 两者都走新的 soft 模式:连续两次失败才恢复 —— 因为恢复 = 原地 location.replace,页面内未保存的输入会丢,一次网络抖动不该触发它。
  • ⚠️ 这推翻了本文 §五 方案 B「不做周期性探活」:当时的前提是"用户在场却不操作时毫无收益",现场证明该前提不成立。代价是每个可见页面每 25 秒 2 个轻请求(/api/dsh/status + 实例侧 GET /),量级可忽略。

验证(真 Chrome + 临时会话,全部只读):

项 结果
修复后基线 ✅ window.__dshRecover = true、window.__dshAssist = true、语法类错误 0(助手脚本首次真正运行)
事件路径回归 ✅ pageshow(persisted) → 浮层出现、class=__dsh-ov、新视觉节点(orb/track)齐
无操作自动恢复(本次核心验收) ✅ 只打挂实例侧探针、全程不派发任何事件 → 47.4 秒后浮层自动出现(= 两次心跳判定),文案「工作区正在恢复,请稍候…」,0 页面错误
npm test ✅ 45 pass / 0 fail / 1 skipped,且新增的注入脚本运行时校验双脚本通过
部署 ✅ 两轮热修(PID 415625→416910→417427),备份 proxy.ts.bak-before-hotfix-0825 / proxy.ts.bak-before-triggers-0835

教训(已写进 MEMORY.md 与技能):模板字面量里的转义会先被求值一次 —— 凡"把 JS 塞进模板字面量"的注入脚本,校验必须先求值再验,否则所有 grep/new Function(原文) 都是假绿。


追加(2026-09-13 10:0x · 视觉升级续):导航过渡页 wake.html 也统一成「AI 核心」

触发:用户报「admin(Edge)能自动弹浮层恢复,guest(Chrome)不行」→ 实测(真 Chrome 无头 + guest 临时会话)guest 侧脚本、心跳、浮层全部正常(46.2 s 自动出现)⇒ 差异不在浏览器,而在落在了哪个界面:

界面 有无注入脚本 加载态视觉
实例页 ✅ 有 「AI 核心」浮层(档案 77 本次升级)
过渡页 wake.html(平台侧;刷新/首次进入且实例不在时落这里) ❌ 无(__dshRecover 命中 0) 仍是旧的 border-top 转圈

⇒ guest 实例今早崩过 5 次并被回滚,刷新时多半落在过渡页,所以"看不到浮层"。这是结构使然,不是 bug,但两套视觉确实割裂。

改动:web/wake.html 的加载态换成与浮层同一套视觉语言(conic 扫描弧 + 反向虚线环 + 脉冲核心 + 三轨道粒子 + 渐变进度条 + 等宽小字标签),配色跟随平台主题(#534AB7 / #8B7CF6,不硬套暗色)。

三条纪律:

  1. JS 逻辑一行未动(diff 只差档案 78 加的那段 instance_circuit_open 分支);
  2. 行尾保持 LF(本机 CRLF=0,与文件一致);
  3. 静态页免重启 ⇒ scp 即生效(比浮层省一次重启窗口)。

验证:线上 https://alotbuy.com/wake.html 取回即为新版(wk-orb / conic-gradient / prefers-reduced-motion / dsh · auto recovery 命中,旧 .wk-spin{width:34px…} 已消失);截图 _patch77/shots/wake-aicore.png。回滚点 /opt/dsh/backups/wake.html.bak-aicore-20260913。

留在档案里的诊断口诀:客户报"看不到浮层/没反应"时先分清是哪个界面 —— 实例页(有脚本,出浮层)还是过渡页(平台侧,只有加载态);再看该标签页是不是修复前打开的旧页面(那种情况刷新一次即可)。