Files
dsh_ai1net_server/交付物/S9-多租户-平台侧-回执-20260927.md
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

100 lines
7.2 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.
# S9 多租户 · **平台侧那一半** — 落地回执(2026-09-27)
> 执行棒回执。代码仓 = `D:\github\dsh_shenxian`(**不是**本工作区;本工作区只放回执)。
> 用户 2026-09-27 拍板契约(逐字):**「数据库 做 数据隔离就行 不需要单独建表建库,数据和实例都需要隔离」**
> 方案出处:`E:\ProgramData\AIProject\aliyun-dsh-server\交付物\功能打包到基础插件-分步实施方案-20260926.md` §S9(307–318 行)
## ① 结果
**已交付(接口可跑、有测试、门禁全绿)**:平台进程内的多租户**管理面 API** 四条 ——
租户列表、单租户详情、**确保隔离实例**、**回收隔离实例**。
**口径三件(照拍板落)**:
1. **数据隔离 = 逻辑隔离**:租户 = 既有账号(`users.id`,网名 `u:<id>`,与 `tenantNetworkOf` 同口径)。
⛔ 未建库、⛔ 未建表、⛔ 未加列、⛔ 未写迁移。
2. **实例隔离 = 每租户一份独立实例**:直接复用平台**已有**的按需编排(`launchForUser` → `supervisor.launch`/`supervisor.stop`)。
⛔ 没有另造第二套实例生命周期;⛔ 路由层不 spawn 进程、不 kill 进程。
3. **「创建隔离实例」这个动作落平台进程**:动作面 = 平台 HTTP 路由;插件包将来只放「界面 + 请求」。
⛔ 本棒未碰插件仓。
**配额按宿主算**:读数一律取 `supervisor.quotaInfo()`(= spawn 时写进 cgroup 的
`MemoryMax` / `--max-old-space-size` 的**同一份计算**)。⛔ 路由里没有任何配额常量可写死
⇒ 宿主规格一变,租户侧看到的数跟着变(用例 S9-② 就是这条的证伪实验)。
**未接入就如实说**:账单 / 用量 / 配额面缺失 / 成员读数缺失 ⇒ 一律 `state: 'not_connected'`
+ 原因话术;⛔ 不用 `0` 冒充、⛔ 不编数(用例 S9-④ 断言"不许出现编造字段")。
## ② 改动清单(绝对路径)
| 文件 | 性质 | 说明 |
|---|---|---|
| `D:\github\dsh_shenxian\src\web\routes\tenants.ts` | **新增** | 四条端点 + 数据源接缝(`TenantDbExtras`) |
| `D:\github\dsh_shenxian\src\web\server.ts` | 修改(2 处:import + `register`) | 挂载 `tenantRoutes` |
| `D:\github\dsh_shenxian\test\tenants.test.mjs` | **新增** | 10 个用例,`fastify.inject` + 注入桩,⛔ 不连真库、⛔ 不起进程 |
端点:
- `GET /api/admin/tenants` —— 每项含 标识 / 显示名 / 网名 / 角色 / **实例状态** / **配额读数** / **成员读数** / 创建时间 / 账单
- `GET /api/admin/tenants/:id` —— 成员列表 + 配额 + 用量(未接入)+ 账单(未接入)
- `POST /api/admin/tenants/:id/instance/ensure` —— **确保隔离实例**(复用 `launchForUser`)
- `POST /api/admin/tenants/:id/instance/reclaim` —— **回收隔离实例**(`supervisor.stop`)
四条**全部** `requireAdmin`(含跨用户归属信息 ⇒ 属管理面,与 `overlay-nodes.ts` / `admin-user-ops.ts` 同级)。
## ③ 门禁读数(真跑,命令 → 读数 → rc)
| 命令(cwd = `D:\github\dsh_shenxian`) | 读数 | rc |
|---|---|---|
| `npm run build` | `tsc -p tsconfig.json`,无输出 | **0** |
| `npm run typecheck` | `tsc -p tsconfig.json --noEmit`,无输出 | **0** |
| `node scripts/check-layering.mjs` | 扫描 131 个 .ts;现存违规 5 条(基线内 5 条);**✅ 无新增违规** | **0** |
| `node --test test/tenants.test.mjs` | `tests 10 / pass 10 / fail 0` | **0** |
测试覆盖(对应本棒验收点):
- S9-① 匿名 **401**、普通用户 **403**(四条端点全覆盖);租户不存在 ⇒ 具名 **404**
- S9-② 列表形状(含配额字段);**换一组宿主读数 ⇒ 端点读数跟着变**(证伪写死常量);成员读数接通后计数正确
- S9-③「确保」与「回收」**走的是 supervisor**(桩断言 `supervisor.launch` / `.stop` 被调用);
已在跑 ⇒ **409**、不是文件夹 ⇒ **400**、已停的租户 ⇒ `reclaimed: false`(⛔ 不假装成功)
- S9-④ 无数据源 ⇒ 账单 / 用量 / 配额 / 成员**全回 `not_connected`**,`count: null`,⛔ 无编造字段
## ④ 未达成项(具名)
1. **成员读数的真库查询未接**:`GET .../:id` 的成员面走可选 db 接缝 `listTenantMembers()`;
本仓 `DbAdapter` 目前**没有**这个只读查询 ⇒ 实部署会回 `state: 'not_connected'`
(**接口形状与计数语义已定、已测**,缺的只是 `src/db/adapter.ts` + `src/db/pg.ts` 里那条 SELECT 与真库用例)。
⚠️ 未擅自加:那属于"跑 SQL 的那一层",且真库用例要 `--test-concurrency=1`。
2. **宿主台账同理**:`listDshHosts()` 接缝已留;未接线时列表**不带** `hosts` 段(⛔ 不编宿主规格)。
3. **用量 / 账单无数据源**(平台既无计费表也无用量计数器)⇒ 恒 `not_connected`。这是**如实回**,不是"待办"。
4. **未做**:插件包内的「租户界面 + 请求」(S9 的另一半,另排一棒);前端页面;真机部署与浏览器级复验。
5. ⛔ **不能**说"多租户可用"——只能说**平台侧接口已就绪且有测试**。
## ⑤ 回滚点
- **备份目录**(改前原样 + 新增件留痕):`E:\ProgramDSH\.dsh\temp\s9-20260927\`
- `src\web\server.ts`(77705 B,改前原样)
- `src\web\routes\tenants.ts`(新增件副本)
- `test\tenants.test.mjs`(新增件副本)
- **回滚步骤**:① `server.ts` 还原备份(或删掉那 2 处改动:import + `app.register(tenantRoutes)`);
② 删 `src\web\routes\tenants.ts`、`test\tenants.test.mjs`;③ `npm run build` 复跑 rc=0。
三条都是纯增量 ⇒ 回滚无残留、不牵动任何既有路由语义。
- 未 commit / 未 push(本轮无提交授权)。
## ⑥ 边界自证
- ✅ **未建库、未建表、未加列、未写迁移**:`git status -- src/db/schema.ts` 显示的 `M` 是**开工前就存在的脏**
(mtime `2026-09-26 23:22:48`,早于本棒开工 `07:20`);本棒对 `schema.ts` **零改动**。
- ✅ 未碰 `src/net/relay/identity.ts`(同为开工前脏,mtime `2026-09-26 22:49:50`)、
`content/crypto.ts`、`registry.ts`、`directory.ts`。
- ✅ 未改插件仓、未改官方 dsh、未 commit / push / scp / 部署 / ssh。
- ✅ 路由层不跑 SQL、不 spawn 进程:起停一律经 `dsh.ts` 的 `launchForUser` + `app.supervisor.*`。
- ✅ 本棒实际改动:仅 `server.ts`(修改)+ 两个新增文件(mtime 07:23–07:24,全在本棒窗口内)。
## ⑦ 需父会话接手
1. **接线成员读数**(要动 `DbAdapter`):在 `src/db/adapter.ts` + `src/db/pg.ts` 加
`listTenantMembers(tenantId)` 的只读查询(读**既有**成员模型,按租户 = 账号聚合;
⛔ 不建表、⛔ 不加列),并补真库用例(`--test-concurrency=1`)。
2. **宿主规格显示**:`listDshHosts()` 接缝已就位,接线后列表自动多出 `hosts` 段。
3. **S9 的另一半**:插件包内「界面 + 请求」(另排一棒);S9 验证点「停掉平台侧 ⇒ 界面必须明确报做不到」
需在插件侧那一棒做负控。
4. **部署与浏览器级复验**未做 ⇒ 若要"交付"口径,还差生效链路 + 用户可见面复验。