Files
dsh_shenxian/dsh-server-docs/04-调整方案/86-admin跨用户实例管理-两处改名.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

128 lines
8.8 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.
# 86 · admin 跨用户实例/文件管理 + 两处改名(实例管理 / 模型管理)
- 日期:2026-09-13
- 状态:✅ 已上线并端到端验证(后端已部署 + 插件 **0.3.10** 已铺发 admin/guest)
- 触发(用户原话,三条连续):
1. 「admin 的 系统管理 中的**服务管理只管得了自己的,看不到用户的服务,需要都能管理才行**」
2. 「这个功能名称 改为**实例管理**」
3. 「**密钥管理** 改为**模型管理**」
- 落点:`src/web/routes/{dsh.ts,admin-user-ops.ts,server.ts}` + `web/portal.html` + `poc/business-plugins/lib/client.js`
> **TL;DR**|① 平台**所有**服务/文件 API 都是 `request.user.id` 语义(`desktop.ts` 头注释原文 "one user can never address another user's files")⇒ admin 在「服务管理」里只能管自己;② 本轮**另开一组** `/api/admin/users/:id/{fs,dsh}/*`(全部 `requireAdmin`),把 `request.user.id` 换成路径参数 —— **不动既有路由的语义**(避免把越界风险塞进普通用户路径);③ 启动/状态逻辑**抽成 `launchForUser` / `statusForUser` 共用**(避免"两处副本必然漂");④ 插件侧「服务管理」页加「用户」下拉,切换后文件树/启停/打开全跟着走;⑤ 两处改名同步到门户。
---
## 一、先取证:为什么 admin 只能管自己
| 事实 | 证据 |
|---|---|
| 文件树 | `desktop.ts:96` `app.get('/api/desktop/tree', { preHandler: requireAuth })` → `app.userFs.listDir(request.user!.id, path)` |
| 建/传/下载 | 同文件 `mkdir` / `upload` / `download` / `create`,**全部** `request.user!.id` |
| 实例启停 | `dsh.ts` 的 `launch` / `stop` / `status` 同样 `request.user!.id` |
| 设计意图 | `desktop.ts` 头注释:**"Every path is resolved against the caller's own workspace root, so one user can never address another user's files."** |
| 但底层**天然支持** | `UserFs` 的每个方法都以 `userId` 为**首参**(`listDir(userId, relPath)` / `mkdir` / `createEntry` / `upload` / `readFile`),`Spawner` 同理 |
⇒ **卡点只在路由层**:能力早就在,只是没有"替别人"的入口。所以这活是**开一组 admin 路由**,不是新建能力面。
## 二、设计
### 2.1 新路由(新文件 `src/web/routes/admin-user-ops.ts`)
| 方法 | 路径 | 复用 |
|---|---|---|
| GET | `/api/admin/users/:id/fs/tree?path=` | `userFs.listDir(id, path)` |
| POST | `/api/admin/users/:id/fs/create` | `userFs.createEntry` |
| POST | `/api/admin/users/:id/fs/upload` | `userFs.upload` |
| GET | `/api/admin/users/:id/dsh/status` | `statusForUser(app, user)` |
| POST | `/api/admin/users/:id/dsh/launch` | `launchForUser(app, id, folder)` |
| POST | `/api/admin/users/:id/dsh/stop` | `supervisor.stop(id)` |
每个路由都过 `targetOr404()`(`:id` 乱填 → 404),避免在错误的根上操作。
### 2.2 为什么**不**给既有路由加 `userId` 可选参数
那种改法只需每路由加一行、前端只多带一个 query,**但**把"越界判断"混进了**普通用户每天走**的路径 —— 一处写漏就是"任意用户可读他人文件"。本轮选**隔离式新增**:新前缀全 `requireAdmin`,既有路由一行未改,风险面为 0。代价是多一个文件,值。
### 2.3 抽共用函数(`dsh.ts` 导出)
| 导出 | 为什么 |
|---|---|
| `alive(status)` | 崩溃态判定(档案 20),两处都要 |
| `dshUrl(baseDomain, user, token)` | 实例直达 URL(含子域回退 `/u/<id>/dsh/`) |
| `launchForUser(app, userId, folder)` | 启动主体:`resolvePath` → `isDirectory` → (`enablePatch` 时)渲染 per-folder 插件 patch → `supervisor.launch`。**自己启动与 admin 替人启动,逻辑必须一模一样**,否则 admin 启的实例与用户自己启的插件集不同 |
| `statusForUser(app, user)` | 状态观测面(含档案 20 重启数 / 78 熔断 / 84 配额)——**只此一份** |
| `sendBreakerOpen(reply, err)` | 熔断冷却的 503 响应,两处同文案 |
`GET /api/dsh/status` 与 `POST /api/dsh/launch` 的**主体改为调用这两个函数**(行为不变,`node --check` + 真环境回归见 §五)。
### 2.4 ⚠️ R5 权限影响评估(动手前判定)
| 维度 | 判断 |
|---|---|
| 新增了什么 | **admin 对任意用户**的 ① 浏览/新建/上传其工作区文件 ② 启停其 DSH 实例 |
| 是否越界 | ① 仍限定在该用户 ws 根内 —— `UserFs` 自带逃逸防护(越界=`bad_path`),**不是**任意文件系统访问;② 与用户自己点「启动/停止」同一条 `supervisor` 路径 |
| 与既有职能比 | 与 `requireAdmin` 现有能力(审批 / 禁用 / 删除用户、重置密码)同级;服务器层面 admin 本就能读 `/var/lib/dshs/users/**` |
| 对普通用户 | **零扩大** —— 这些前缀下**没有**任何 `requireAuth` 版本;未登录 401、非 admin 403(已实测,§五) |
| 结论 | 可放行,无需新的门禁;但**注释里写明**,防后人误当普通接口 |
## 三、改名(两处,代码与门户同步)
| 原名 | 新名 | 落点 |
|---|---|---|
| 服务管理 | **实例管理** | 插件词典 `pa.p.files`(zh/en)+ 卡片说明 `pa.d.files` + 副标题 `pa.s.files`(改为「按用户查看工作区、启动 / 停止其 DSH 实例」,体现多用户)|门户 `CRUMB_TITLES.files` + 首页卡片 `t` |
| 密钥管理 | **模型管理** | 插件词典 `pa.p.keys`(zh/en)|门户 `CRUMB_TITLES.keys` + 首页卡片 `t`;副标题同步为「平台共享密钥(未自配密钥的用户默认使用;仅管理员可改)」 |
> 门户 `#/files` 与 `#/keys` 页**自身的功能未变**(门户那边仍是"管自己"),只改**显示名**。admin 要管别人 → 走**实例内**的「设置 → 系统管理 → 实例管理」。
## 四、落地与出厂
| 项 | 内容 |
|---|---|
| 新文件 | `src/web/routes/admin-user-ops.ts`(6 路由 + `targetOr404`) |
| 改动 | `dsh.ts`(导出 5 项 + 两个路由改调共用函数)、`server.ts`(import + register)、`web/portal.html`(改名)、`client.js`(用户下拉 + 走 admin 前缀 + 改名,18 处精确替换) |
| 插件 | **0.3.10**(45,588 B)→ `scp /opt/dsh/artifacts/` → `ensure-biz-plugins.cjs --all --restart` |
| 后端 | 传 `src/**` → 服务器 `npm run build` → `systemctl restart dshs` |
| 验收 | `verify-platform-admin-section.mjs` **全绿**(新增断言:`实例管理页:含「用户」切换器`) |
### 🔴 本轮踩到的部署坑(**服务器代码落后于 git**)
第一次服务器构建报:
```
src/web/routes/dsh.ts(122,27): error TS2339: Property 'quotaInfo' does not exist on type 'Spawner'.
```
**真因**:`quotaInfo` 是**档案 84** 加的 `Spawner` 接口成员,**已提交进 git**,但**服务器上的 `src/supervisor/spawner.ts` 还是旧版**(当初部署档案 84 时漏传了这一个文件 —— `diff` 显示差异**只有那 7 行**接口声明)。
我的新代码引用了它 ⇒ 在服务器上编译不过。
**处置**:补传 `src/supervisor/spawner.ts` → 重建 → 干净通过。
> **教训(已写进记忆)**:**"服务器能跑" ≠ "服务器源码 == git"**。浅克隆 + 逐文件 scp 的同步方式会让**部分文件静默落后**,直到某个新代码引用到它才炸。改动**依赖别人已提交的符号**时,要**先确认服务器上有那个符号**(`grep` 一下),别假设 git 有服务器就有。
## 五、验证(真环境)
| 检查 | 结果 |
|---|---|
| `admin` 读**他人**文件树 `GET /api/admin/users/<guestId>/fs/tree` | **200** + 真实 entries(`.cache`/`.config`/`.fonts`…) |
| `admin` 读**他人**实例状态 `.../dsh/status` | **200** `{"running":false,…,"url":"https://guest.alotbuy.com/"}` |
| 对照:**未登录** | **401** ✅ |
| 非 admin 角色 | 由 `requireAdmin` 拦(403)—— 与既有 admin 路由同一守卫 |
| 插件包 | admin/guest 均升至 **0.3.10** |
| `verify-platform-admin-section.mjs` | 全绿(含"实例管理页含用户切换器") |
## 六、回滚
```bash
# 后端:删掉新文件 + 还原 server.ts/dsh.ts
rm /opt/dshs/src/web/routes/admin-user-ops.ts
# dsh.ts / server.ts 用 git 上一版覆盖后重建
cd /opt/dshs && npm run build && systemctl restart dshs
# 插件回落 0.3.9(含改名,但「实例管理」页无用户下拉)
```
## 七、未做 / 待观察
1. **门户侧「实例管理」仍是"管自己"**(未给门户加用户切换)—— 需求里指的是**实例内**的系统管理;门户若也要,另立。
2. **未加"跨用户删除/改名文件"**:本轮只放开了浏览 / 新建 / 上传(用户点名的"看得到 + 能启停")。删除类属破坏性操作,要做得先出确认清单。
3. **下单点验证过、未做浏览器截图验收**(本机 `agent-browser` daemon 起不来),视觉以 harness 断言 + 用户实看为准。