- 变更规模:新增 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/ 知识文件,按口径入库)
17 KiB
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(346CREDENTIAL_COLS)·src/db/schema.ts(454-466shared_model_grantedv11)·src/fs/user-fs.ts(69UserFs接口) - 上两棒口径继承:
交付物/技能插件管理-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