Files
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

51 lines
3.3 KiB
Markdown
Raw Permalink 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.
# 24 · 实例侧 401(launch token 过期)浏览器导航自动恢复
- 日期:2026-09-11
- 触发:用户刷新实例子域页面(`admin.alotbuy.com`)看到 **`dsh web authentication required; reopen the URL printed by dsh web.`**,**既不自动跳转也不自动重启**
- 结论一句话:**代理只处理了"门户侧 401"(会话失效),没处理"实例侧 401"(launch token 过期)** → 浏览器停在 dsh 的死端页。已修复:实例侧 401 + 浏览器导航 → 用当前实例的**最新 token 302 回同一地址**(拿不到则回门户)。
- 状态:**已实施并上线**(服务已重启,`lib/` 已验证)
---
## 一、现象与成因
| 环节 | 事实 |
|---|---|
| 触发场景 | 实例**重启后 token 轮换**(本次由我 00:14 为验证 picker 重启实例引起;此前 23:32 域名迁移 drain 亦同) |
| 浏览器侧 | 旧标签页 URL 形如 `https://admin.alotbuy.com/?token=<旧token>`,token 已失效 |
| dsh 侧 | 返回 `401` + 文案 `dsh web authentication required; reopen the URL printed by dsh web.`(HTML) |
| 平台侧 | `proxy.ts` 的 401 处理**只覆盖门户自身判定**(`resolveSubdomainAccess` 的 `unauthorized` → 302 `/login.html`);**上游(实例)返回的 401 被原样透传** → 用户看到死端页 |
| 为何没重启 | "自动重启"逻辑只在 `not_running`(404)分支触发;本例实例**正在运行**,只是 token 过期 |
## 二、修复(`src/supervisor/proxy.ts`)
1. `proxyHttp` 新增参数 `freshAuthUrl?: () => Promise<string | undefined>`;
2. 上游响应为 **401 且是浏览器导航**(`GET` + `accept: text/html`)时:
- `upRes.resume()` 释放上游连接;
- 调用 `freshAuthUrl()` 取恢复地址并 **302**;
3. 子域 hook 传入的 `freshAuthUrl`:`app.supervisor.status(userId).main?.launchToken` → 有 token 则 `https://<同一 Host>/?token=<新token>`(**原地恢复,无需重新登录**),否则回门户根;
4. 路径式路由 `/u/:slug/dsh/` 同样接入(复用 `prefix`);
5. `SubdomainAccess` 成功分支补充 `userId`(取 token 需要)。
## 三、验证
| 检查 | 结果 |
|---|---|
| `bash scripts/ci.sh`(typecheck + build + 单测) | ✅ 38 pass / 0 fail |
| 运行产物 | ✅ `lib/supervisor/proxy.js` 含 `freshAuthUrl` ×3;`lib/supervisor/orchestrator.js` 含 `symlink` ×3(档案 23 的沙箱修复同时生效) |
| 服务重启(drain) | ✅ `active` / 残留实例 0 / 门户 200 |
| 端到端 | ⏳ 待用户实测:实例重启后刷新旧标签页应自动恢复(302 带新 token) |
## 四、回滚
- 代码:`/opt/dsh/backups/proxy.ts.bak-<TS>`;或 `git revert d7ab5b4` → `npm run build` → `systemctl restart dshs`。
## 五、顺带说明(本次一并上线)
- **档案 23 的沙箱修复同时生效**:实例 bwrap 参数补齐 `/bin`、`/sbin`、`/lib` 符号链接 → 实例内 **bash 工具恢复可用**(此前被 dsh 沙箱后端整体拒绝)。
- 本次重启后所有实例已清空,用户下次进入会以新参数启动。
## 六、红线遵守
- 只改编排器自身代码(`src/supervisor/proxy.ts`),未触碰官方 dsh 主程序与缓存;修复走既有的"302 透明恢复"模式(与档案 13 的 not_running 自动重启同源)。