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 一律写「远程服务器」。
20 KiB
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(实测注入正常)⇒ 不是用户侧的问题,本次无需处理。
五、边界(有意不做)
- 恢复后不恢复页面内状态(输入草稿 / 滚动位置)——同档案 72 §五,抓 DOM 状态会随官方前端升级而碎。
- 不做周期性探活(只在"回到页面"付出成本)。
- 不做保活(见 §二 方案 D,需单独评估容量)。
- 探针 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 未被改动,导航路径行为不变,回滚面仅注入脚本本身。)
八、遗留(另行报告)
崩溃熔断可无限重来:熔断后→ ✅ 2026-09-13 已由档案 78 处置(跨轮熔断态 + 指数冷却 + 双通道告警;代码完成、本地全绿,部署待 R8 窗口)。mains.delete+resetCrashState(),而用户下次launch()/enter又resetCrashState()⇒ 计数清零、重新给足预算;且熔断只写 stderr、无告警。INDEX.md §二的状态摘要("档案 72 份" + 各状态计数)与表格实际行数(76)已不一致(本次未改,属历史漂移)。- 实例被反复重启的外部诱因已查明(并行会话迭代插件版本),与本档无关。
九、补充(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,不触发布局抖动。
硬约束遵守(注入脚本的三条红线):
- 反引号 0 /
${0(CSS 里字体名改用单引号'Segoe UI',从而 JS 侧可用双引号字符串承载 CSS 各行); - 不用
innerHTML(实例页可能启用 Trusted Types)→ 结构全部createElement; - 样式集中在
#__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 字样,
没有验"脚本跑不跑得起来"(同一个假绿模式)。两个脚本今天一起修好。
修复:
- 我引入的那处 →
.join(String.fromCharCode(10))(不依赖任何转义); - 助手脚本的 8 处 → 按源文件补成
\\n(只补未转义的,已转义的保持原样); - ⛔ 新增常设校验
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,不硬套暗色)。
三条纪律:
- JS 逻辑一行未动(diff 只差档案 78 加的那段
instance_circuit_open分支); - 行尾保持 LF(本机 CRLF=0,与文件一致);
- 静态页免重启 ⇒ 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。
留在档案里的诊断口诀:客户报"看不到浮层/没反应"时先分清是哪个界面 —— 实例页(有脚本,出浮层)还是过渡页(平台侧,只有加载态);再看该标签页是不是修复前打开的旧页面(那种情况刷新一次即可)。