Files
dsh_ai1net_server/交付物/模型管理-S7落地与四轮回执-20260926.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

186 lines
17 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.
# S7「第三块:模型管理」· 落地与四轮回执(2026-09-26 · 第 45 棒)
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server` | **会话名**:`插件投放与分库线-45`
> **执行依据**:本线入口 §0 最新行 + §2「🎯 本轮动作」(第 44 棒块)|规格书 `05-交接单/交接单_包规格书_20260926.md` §〇/§五 A3·A4·C|方案 `交付物/功能打包到基础插件-分步实施方案-20260926.md` §3.10.9 + §4
> **本棒只做一件事**:S7(⛔ 不碰 S8、⛔ 不跨 S9 的 P2)
---
## 一、一句话结论
S7 四条判据 **全部落地并逐条取证**——包 **`@dsh-local/ai1net`** 现在**自带模型管理面**:三条精确路由 / 六种方法(`/api/ai1net/models`〔GET·PUT〕· `/models/credentials`〔GET·POST·DELETE〕· `/models/grant`〔POST〕),**明文密钥只走一个入口、读路径根本不存在密钥**,共享模型**默认不共享 + 管理员逐用户授权**,写 home **一律走平台 `UserFs`** 且「直写 fs 的静默空操作」成了**可断言的检出**。
本棒**未抓到产品侧真 bug**;抓到并修掉的是**五处仪器错**(其中三处不修会产出**假绿 / 假红**)——逐条见 §九。
读数:包内门禁 **85 ✅ / 0 ❌(rc=0,S6 时为 60)**|S7 取证编排 **73 ✅ / 0 ❌(rc=0)**|门禁负控 **8/8(7 处改坏全部被门禁抓到)**|四把尺子 **装配端A/端B · 取数 · 回调 · 运行 全 rc=0**|`v-neg all` rc=0。
---
## 二、四条判据逐条回执
| # | §3.10.9 判据原文 | 本棒落地 | 取证 | 结论 |
|---|---|---|---|---|
| S7-1 | 模型配置/凭据/共享授权/界面 进包 ⇒ `v-plugin.sh` 绿 | `lib/models-sdk.mjs`(**新建** 32,634 B)+ `lib/index.js` 三条注册面 + `lib/client.js` 第四张卡 | 装配 端A/端B · 取数 · 回调 · 运行 **全 `rc=0`**;spy ctx 抓到三条路由原文;stub fetch 抓到 **13 条**出向逐条对得上 | ✅ |
| S7-2 | 共享模型走**管理员逐用户授权**(默认未授权)⇒ **负控:未授权用户看不见** | `SHARED_MODEL_POLICY`(`default:'not-granted'` · `grantedBy:'admin-only'` · `scope:'per-user'`)+ `grantShared()` 就地闸 | 策略表三条机验;grant 缺 `asAdmin` / 缺 `userId` / 缺 `enabled` 三条负控;**撤权负控**:管理员撤掉授权 ⇒ 用户自己开关虽开,视图里 `granted=false` | ✅ |
| S7-3 | 写 home 一律走 `UserFs` ⇒ **负控:直写 `fs` ⇒ 能检出"静默空操作"** | `HOME_WRITE_POLICY` + `createHomeWriter()` + 纯函数 `classifyHomeWrite()`;内核**零 `fs` 用法**(门禁扫代码行) | 支撑两支真跑:走 UserFs ⇒ `delivered=true`;**直写 ⇒ `silentNoopDetected=true`**(写成功、回读为空);无 UserFs ⇒ **拒**(⛔ 不退化成本机 fs) | ✅ |
| S7-4 | 验「平台级凭据 ⛔ 不下发客户端」(R5 红线)⇒ 客户端侧**零**平台凭据 | `redactModelConfig()` **白名单净化器**(全包唯一)+ 落台账前的**明文自检**(fail-closed) | 平台**故意**把明文塞进回体 ⇒ 出包体零明文(读配置 / 观测面 / 新增回包三条);明文在包内**只出现在写入那一条出向**上;`stats()` / 台账 / 日志三处零明文 | ✅ |
**完成判据**(§3.10.9 原文):模型配置**在实例内**可见(三条路由挂在本实例,经 `connection.fetch.register` 精确路径)+ **平台级凭据不下发客户端**(读路径结构化无密钥字段)。
---
## 三、三条路由 / 六种方法 → 平台真端点(逐条真发 · stub fetch 记账)
| 包内路由 | 方法 | 平台真端点 | 依据(真实实现) |
|---|---|---|---|
| `/api/ai1net/models` | GET | `GET /api/me/keys` | `src/web/routes/auth.ts:484` |
| `/api/ai1net/models` | PUT〔`role:'shared'`〕 | `POST /api/me/models/shared` | `auth.ts:617`(只动**自己**的偏好) |
| `/api/ai1net/models` | PUT〔`role:'entry-toggle'`〕 | `POST /api/me/keys/<id>/toggle` | `auth.ts:598` |
| `/api/ai1net/models/credentials` | GET | `GET /api/me/keys` | 同上(**只回"是否已配置"**) |
| `/api/ai1net/models/credentials` | POST | `POST /api/me/keys` | `auth.ts:526`(🔴 明文**唯一**一次转交) |
| `/api/ai1net/models/credentials` | DELETE | `DELETE /api/me/keys/<id>` | `auth.ts:664` |
| `/api/ai1net/models/grant` | POST | `POST /api/admin/users/<id>/models/shared` | `admin.ts:107`(`preHandler: requireAdmin`) |
🔴 **三条路径 → 六种方法**:`role` 分流复用同一条路径(与 M5 长轮询、M3「设置自动处理」同一手法)⇒ ⛔ **没有出现第四条路径**(门禁 ①g 第 ③/④ 条断言)。
🔴 **为什么包内读不出密钥**(真实依据,⛔ 不是设计稿):平台 `CREDENTIAL_COLS = 'id, key_name, enabled, updated_at, route, base_url, api, models'`(`src/db/repo.ts:346`)—— **本来就没有密钥列**。包内再过一遍净化,是防"平台日后把 ref 带出来"。
### 该拒就拒:十二条,一律**零平台请求**
| 类别 | 条数 | 例 |
|---|---|---|
| 入参形态闸 | 9 | 新增缺 `name` / 缺 `apiKey`|删除缺 `id` / `id` 穿越(`../`)|`PUT` 的 `role` 非法|授权缺管理员声明 / `asAdmin:false` / 缺 `userId` / `userId` 穿越 |
| 凭据闸(复用 `createHostKit`) | 3 | 出示空凭据(带身份头不带令牌)|借别人的令牌|身份头与用户名不符 |
> 复核命令:`node tmp/p45/run-s7.mjs` → B 段 12 行;每行都同时断言 `reason` 与 `平台请求=0`。
---
## 四、🔴 凭据不出包(S7-4「明文只出现一次」是机验结论)
| 观测面 | 断言 | 结论 |
|---|---|---|
| 读配置出包体 | 平台回体**故意**带 `apiKey`/`secret`/`apiKeyRef` ⇒ 出包体零明文 + `redactions>0` | ✅ |
| 凭据观测面 | 只回 `{id,name,enabled,configured:true,route,models}` —— **连 `apiKey` 字段都没有** | ✅ |
| 新增的回包 | 平台回体**回显**密钥 ⇒ 出包体零明文(包内只挑元数据重造) | ✅ |
| 出向请求 | 含明文的出向**全都是** `POST /api/me/keys`(那唯一一条写入) | ✅ |
| `stats()` / 包内台账 / 包内日志 | 三处序列化后**零明文**;台账只记 `hasKey:true` | ✅ |
| 落台账前自检 | `plaintextBlocked=0`(⛔ 没有任何明文走到过台账;命中即拦下并计数) | ✅ |
---
## 五、🔴 S7-2/S7-3 两条负控(本棒核心)
**S7-2 负控(撤掉授权 ⇒ 立刻拿不到)**:同一份代码、只差授权位——
`sharedModelEnabled:true`(用户自己开着)+ `sharedModelGranted:false`(管理员撤了)⇒ 视图里 `granted=false`;把授权位改回 `true` ⇒ `granted=true`。⇒ **"能不能用"确实由授权决定**,而不是由界面或偏好决定。
**S7-3 负控(直写 fs ⇒ 检出静默空操作)**:
```
走 UserFs :{"via":"userfs","ok":true,"delivered":true,"silentNoop":false}
直写 fs :{"via":"local-fs","silentNoopDetected":true,"ok":true,"delivered":false,"silentNoop":true,"reason":"readback-empty"}
无 UserFs :{"ok":false,"reason":"userfs-unavailable"} ← ⛔ 不退化成本机 fs
```
判据不是"写没报错",是**回读**(且回读走权威路径 `UserFs`)—— §4-D4 的症状正是"写入成功返回、内容根本没到"。
---
## 六、🔴 真实 vs 建模(逐条标注 · ⛔ 不许把建模当"已通")
| 面 | 本棒是**真实**还是**建模** | 说明 |
|---|---|---|
| 包内三条路由的实现与注册 | **真实** | `lib/index.js` 真装配 + 真 `apply(ctx)`(spy ctx 抓到三条路由原文与 methods) |
| 包内 → 平台的出向请求 | **真实**(HTTP 层) | 三条头(包名 / 每插件令牌 / 实例令牌)+ `body` 真的 `JSON.stringify`;stub fetch 逐条记账 |
| **平台侧收到的效果** | ⛔ **建模** | 平台端由 stub fetch / spy 代替;**未对真机投放**(属投放线) |
| 平台端点**字面量与形状** | **真实(照抄)** | 逐条取自 `auth.ts` / `admin.ts` 的实际行(见 §三表最后一列) |
| `/models/grant` 的**管理员权威闸** | ⛔ **平台侧未在本棒真跑** | 真实判据 = 平台 `requireAdmin`(`admin.ts:109`)。包内那道 `asAdmin` 只是**误调用闸**,⛔ **不许当成安全边界**、⛔ 不许写"平台侧管理员闸已上线" |
| 写 home 走 `UserFs` | ⛔ **建模**(注入式假 `UserFs`) | 真实平台侧 `app.userFs`(`auth.ts:646-652` 同款口径)未在本棒真跑;本棒证的是**机制与检出**成立 |
| 客户端第四张卡片 | **机械判据真实 / 视觉效果未取到** | 门禁覆盖封包契约(lazy-CJS · 零 `exports.default` · `type=password` · 提交后清空);**真壳截图未取到**(§八) |
| 「共享模型两列两个主体」 | **真实(照抄口径)** | `shared_model_granted`(管理员授权)⇄ `shared_model_enabled`(用户偏好),两者是**与**关系(`auth.ts:608-624` + `admin.ts:92-105` 原文口径) |
⇒ ⛔ **不能断言**「真机投放已通 / 平台侧管理员闸已上线 / 客户端卡片在真壳里长这样」。
---
## 七、§4 避坑总表对表(本棒相关类目)
| 类目 | 本棒处置 |
|---|---|
| **B2** 桌面端只认精确路径 | 三条逐字冻结、⛔ 无前缀匹配;门禁把三条 + **六种方法**与规格书 §五 A3 逐字比对 |
| **B3** 平台不是实例 | 「写平台库」的动作⛔ 不进包:授权只发请求,**落库权威在平台**(回体标 `executorInPackage:false`) |
| **D3** 列级 `maxBytes` 唯一把关人 = 插件侧 | 本棒**未改** `dsh.data.yaml` ⇒ `DECL_DIGEST=88c752a3e12597b3…` **逐字未变**(8 表 29 列未破) |
| **D4** 写 home 静默空操作 | `HOME_WRITE_POLICY.onlyPath='userfs'` + 内核**零 fs 用法**(门禁扫代码行)+ 直写支**检出** |
| **E1** 测试跑的是构建产物 | 改完**必** `node scripts/build.mjs --check-only`(本棒改 `build.mjs` 自身后复验两次) |
| **E6** 禁 `exports.default` | `lib/client.js` 仍只交 `apply` + `inject`(门禁 ①b 绿) |
| **G5** ⛔ 不下发平台共享密钥 | 明文**只在写入那一条出向**上;读路径无密钥字段(S7-4) |
| **G8** 改 / 重启要写进回执 | 见 §八:⛔ 未重启任何控制面、⛔ 未对真机投放 —— 如实标注"机械判据绿 / 真机未验" |
---
## 八、尺子回归(⛔ 未改尺子一行)/ 未取到(如实)
```
⓪ 包内门禁 node scripts/build.mjs --check-only | rc=0 (85 ✅ / 0 ❌,S6 时 60 ✅)
M5_DIGEST=55cdc0d5fc8eb99d ← 与第 43/44 棒逐字相同 ⇒ M5 的绿没被弄红
M3_DIGEST=7045a778baa82cfe ← 与第 44 棒逐字相同
DECL_DIGEST=88c752a3e12597b3…(8 表 29 列未破)
M2_DIGEST=11d7b1be8aea3996 ← 本棒新增
S7-1 装配 · 端A 客户端形态(桌面 profile 真件副本 + 本包) | rc=0
S7-1 装配 · 端B 平台形态(profiles/web 键位建模 link: 依赖) | rc=0
S7-1 取数 · 真实 import 本产物 | rc=0
S7-1 回调 · 真实调用 apply(ctx)(spy ctx) | rc=0
运行 · 真启动日志的失败面(⚠️ 弱证据) | rc=0
v-neg.sh all | rc=0 (装配/运行/取数/回调 四项红绿双态均成立)
S7 取证编排 node tmp/p45/run-s7.mjs | rc=0 (73 ✅ / 0 ❌)
门禁负控 node tmp/p45/neg-gate.mjs | rc=0 (8/8:7 处改坏**全部**被 ①g 抓到)
v-baseline.sh(开工对照,尺子自述⛔ 不进判据) | rc=0 (平台侧 4 个包 / 客户端 5 个 bundle;`@dsh-local/*` 计 0)
```
### ⚠️ 未取到 / 不成立(如实,⛔ 不折算成"已知/通过")
| 项 | 读数 | 原因 |
|---|---|---|
| **浏览器级(第四张卡片的真壳)** | **`v-ui.sh` rc=3**(`UNAVAILABLE / no page target`) | 需**真壳在跑**;启动器属**客户端线**(`dsh-ai1net-desktop/.workbuddy/_devkit/launch-desktop-dev-017.mts`),本线 ⛔ 不代启 ⇒ 机械判据已绿、**视觉效果未取到** |
| M5 卡片 + S4-6 实例页能力栏 | 同上 | 同因(第 42/43 棒已欠,本棒未消) |
| `v-plugin.sh 运行` 面 | rc=0 但**空真** | 可用日志 mtime 早于本包 ⇒ **弱证据**,⛔ 不当"运行期已验" |
| 平台侧真跑(真库 / 真 `requireAdmin`) | 未取到 | 属**投放线**,本棒⛔ 不越界 |
---
## 九、本棒的 5 处「仪器错」(如实记录 · ⛔ 其中 3 处不修就是假绿/假红)
| # | 错在哪 | 症状 | 修法 |
|---|---|---|---|
| ① | 门禁 ①g 的负控**夹具**用裸字符串当每插件令牌 | 三条授权断言全报"平台零调用" —— 看着像**产品没打到平台**,其实是**门禁自己的夹具不合格**(`resolveEnableProof` 会验签) | 用 `signPluginToken()` 真签一枚(判据:夹具也要合规格) |
| ② | 门禁 ①g 第 ⑩ 条的 fs 扫描认"文件里出现过 `fs` 这个词" | 把 `HOME_WRITE_POLICY.forbidden` 那一行**自己的字面量**当成引用 ⇒ **门禁自己假红** | 改成认**用法**(`from 'node:fs'` / `fs.xxx(` / `writeFileSync(`),⛔ 不认字符串里的名字 |
| ③ | `redactModelConfig()` 首版**先判白名单、再判敏感字段名** | 敏感字段被白名单**静默丢掉** ⇒ `redactions` 恒 0:输出确实干净,但**没有证据**("看着干净"不是"证明干净") | 调换顺序:先判敏感字段名、再判白名单;门禁加一条"喂明文必须计到净化数" |
| ④ | `run-s7.mjs` 首版令牌 `exp` 用固定时刻 `1774000000000`(= 2026-03-20) | host 半内部用**默认时钟**(无注入点)⇒ 令牌**已过期** ⇒ 全部正常路径被闸拒成 `plugin-not-enabled`,73 项里 **24 项假红**。识破它的是那一行 `出向请求总数 = 0` | 改用**真时刻** `Date.now()`;并把"出向总数"打进读数摘要(假红时一眼可见) |
| ⑤ | `run-s7.mjs` 三条断言写过头 | 分别错在:身份头里其实是**调用方出示的 pluginId**(不是包名)/`effective` 本就在白名单内、**应当保留**/明文写入被调了两次 | 逐条改成与实现口径一致(⛔ 不为了"变绿"放松,而是把**正确的口径**写清) |
**产品侧真 bug:本棒 0 处**(与第 44 棒不同 —— 那棒抓到 `PKG_RE` 不收带 scope 的包名)。如实记录,⛔ 不凑数。
---
## 十、硬边界自查 / 遗留 / 回滚
- ⛔ **未 commit / 未 push**(未明确要求)。
- ⛔ **未触碰官方 dsh 主程序与缓存**(R2);⛔ 未把包装进任何 profile(装配用 `tmp/p45/` **profile 副本**)。
- ⛔ **未重造分发与更新机制**;⛔ **未代用户起实例**;⛔ **未动客户端工作区**(`dsh-ai1net-desktop` / `dsh-desktop-0.1.7rc2` 只读引用)。
- ⛔ **未重启控制面 / 未中断任何在线用户 / 未批量改文件**(红线门禁未触发);包内**零直连平台库路径**(门禁扫代码行)。
- **S9 的 P2 未跨过**;**本棒发现一处新停点**(见下一行)⇒ 依纪律**未登记下一棒**,入口写「⏸ 等拍板」并列出候选。
- 🔴 **新停点(本棒发现)= S8 的硬门未满足**:方案 §3.10.10 原文「⛔ **本步的硬门**:**F1.5(G1 序号 + G6 撤根)必须先有结论**」,而现状 **G1 未修、G6 无任何机制**(依据 `交付物/AI1NET-三类节点结构调整可行性-20260926.md` §P0「G1 未修|G6 无机制 ⇒ 规模越大越撤不回去」与 `交付物/全功能承载判定-底层插件能否承-20260926.md` §七「F1.5 = 一切前置」)。⇒ 依用户 2026-09-17 明令「要用户拍板的,等拍了再新建接续会话」,**本棒 ⛔ 未登记下一棒**(候选 A/B 与倾向见入口 §2「⏳ 等拍板」块)。
- **并发**:本棒按惯例用 guard 锚点算法自定域键(8 个);`preflight-lock.sh` 仍把**工作区外仓库目录 + 工作区根文件**恒判【D】未归类(7 个)—— 与第 26–44 棒同一处**已知误报**,⛔ 未改那个跨库脚本,如实记此条。
- **回滚**:删 `lib/models-sdk.mjs` + 撤 `lib/index.js` 的 M2 装配块与常量 + 撤 `lib/client.js` 的 M2 字典/卡片/样式段与槽位注册 + 撤 `scripts/build.mjs` 的 ①g + 撤 `package.json` 的 `exports["./models-sdk"]` 一条。⚠️ 回滚后 `M5_DIGEST` / `M3_DIGEST` / `DECL_DIGEST` 因**本就未变**,仍可作前后一致锚点。
---
## 十一、出处
- 规格书:`D:/github/dsh_shenxian/dsh-server-docs/05-交接单/交接单_包规格书_20260926.md`(§〇 · §五 A1/A2/A3/A4 · §五 C)
- 方案:`交付物/功能打包到基础插件-分步实施方案-20260926.md`(§3.10.9 = S7 逐步照做唯一权威;§4-B/D/E/G 类)
- 平台真实实现(只读引用):`D:/github/dsh_shenxian/src/web/routes/auth.ts`(484 / 526 / 598 / 617 / 664)· `src/web/routes/admin.ts`(107-133)· `src/db/repo.ts`(346 `CREDENTIAL_COLS`)· `src/db/schema.ts`(454-466 `shared_model_granted` v11)· `src/fs/user-fs.ts`(69 `UserFs` 接口)
- 上两棒口径继承:`交付物/技能插件管理-S6落地与四轮回执-20260926.md`(§六 §九)· `交付物/IM插件侧-S5落地与四轮回执-20260926.md`(§六)
- 本棒读数(⛔ 不入库):`tmp/p45/读数-S7-20260926.txt`(73 ✅ / 0 ❌)· `tmp/p45/尺读数-S7-20260926.txt` · `tmp/p45/门禁负控-20260926.txt`(8/8)
- 编排与负控脚本:`tmp/p45/run-s7.mjs` · `tmp/p45/neg-gate.mjs` · `tmp/p45/run-scales.sh`