Files
dsh_ai1net_server/交付物/技能插件管理-S6落地与四轮回执-20260926.md
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

187 lines
15 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.
# S6「第二块:技能插件管理」· 落地与四轮回执(2026-09-26 · 第 44 棒)
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server` | **会话名**:`插件投放与分库线-44`
> **执行依据**:本线入口 §0 最新行 + §2「🎯 本轮动作」(第 43 棒块)|规格书 `05-交接单/交接单_包规格书_20260926.md` §〇/§五 A·C|方案 `交付物/功能打包到基础插件-分步实施方案-20260926.md` §3.10.8 + §4
> **本棒只做一件事**:S6(⛔ 不碰 S7、⛔ 不跨 S9 的 P2)
---
## 一、一句话结论
S6 四条判据 **全部落地并逐条取证**——包内那一半(上传/检测/建库/启用 + 「更新」四拆的 ①②)真发到平台面;**「搬包」与「自动处理的执行者」两拆明确留在包外**(分别为 S4 拉取器与引导层),并由**负控**证明「关掉包外执行者 ⇒ 自动更新行为确实停」。
过程中**抓到并修掉一处会让 M3 五条路由全线静默失效的真 bug**(包名形态校验不收带 scope 的冻结包名),并已把它钉成包内门禁的第 ⑩ 条断言。
读数:包内门禁 **60 ✅ / 0 ❌(rc=0)**|S6 取证编排 **58 ✅ / 0 ❌(rc=0)**|S6-1 四条面 **rc=0**|`v-neg all` **rc=0**。
---
## 二、四条判据逐条回执
| # | §3.10.8 判据原文 | 本棒落地 | 取证 | 结论 |
|---|---|---|---|---|
| ① | 上传/检测/建库/启用 进包 ⇒ `v-plugin.sh` 四面绿 | 七条包内路由(`lib/plugins-sdk.mjs` + `lib/index.js` 注册面) | 装配 端A/端B · 取数 · 回调 · 运行 **全 `rc=0`**;spy ctx 抓到九条平台请求逐条对得上 | ✅ |
| ② | 「更新」四拆:①②进包(只到"请求")/③搬包走 S4 拉取器/④执行者留包外 ⇒ 能逐条指出"谁在做",且执行者在包外 | `UPDATE_SPLIT` 数据表 + `describeOwnership()`(UI 与回报件共用**同一份**来源) | 四拆逐条具名(见 §三);机验「凡 `executor:true` ⇒ `inPackage:false`」;机验「包内零搬包实现」 | ✅ |
| ③ | `package.json` 指向 yaml;自增 id ⛔ 不照搬;UI ≤900px 独占 ⇒ 窄屏截图 | `dsh.data.schema = ./dsh.data.yaml`(S3 已落)|行键 = 自然键(包名/版本/`user_id`)|窄屏样式段独立成块 | 门禁 ⑨(断点逐字 900px + 四条硬判据 + 零 `100vh`)+ 门禁「零自增 id」两处(包内 + 声明件)**全绿**;⚠️ **真壳窄屏截图未取到**(见 §八) | 🟡 机械判据 ✅/截图未取到 |
| ④ | 负控:关掉包外执行者 ⇒ 自动更新行为**必须停** | 包内只有 `requestUpdate`(登记请求)与 `setAutoHandling`(写偏好) | 对比实验:执行者**在位** ⇒ `pulled=2` + 取包口被调 2 次;执行者**关掉** ⇒ 本地**零包目录** + 取包口 **0 次调用**,而包内台账仍记着"请求发生过" | ✅ |
---
## 三、S6-2「谁在做」——四拆逐条(判据的输出就是这个表)
| # | 拆 | 包内? | 执行者? | 归属 | 做到哪为止 |
|---|---|---|---|---|---|
| ① | 看见新版本 | ✅ 是 | ❌ 否 | `package` | 只报差异(`differs`/`same`/`unknown` 三态),⛔ 不代替用户下"该更新"的结论 |
| ② | 点「更新」 | ✅ 是 | ❌ 否 | `package` | **只到 `requested`**(回 `requestId` + `executor:'s4-puller'`),⛔ 不假装进度 |
| ③ | 搬包 | ⛔ **否** | ✅ 是 | **`s4-puller`** | S4 已落地的拉取器(平台/引导层),⛔ 包内一个字节都不搬 |
| ④ | 设置自动处理 | ⛔ **否** | ✅ 是 | **`bootstrap-layer`** | 包内只写一条偏好(`pullsTriggered:0`)——**执行者必须活在被自动化对象之外**(实例没起 = 插件不存在) |
机验(不靠自述):
- `UPDATE_SPLIT.filter(r => r.executor).every(r => r.inPackage === false)` ⇒ **执行者全在包外**。
- 归属只落两个具名外部角色,`['s4-puller','bootstrap-layer']`,**不含本包**。
- **包内代码行**(去注释)扫 `spawn` / `createGzip` / `syncSharedLayer` / `pullOnePack` / `extract` / `rename(` / `cpSync` ⇒ **零命中**。
- 包内零自增字眼(`autoIncrement` / `AUTO_INCREMENT` / `rowid` / `serial`)⇒ 行键确为自然键。
---
## 四、包内七条路由 → 平台端点(逐条真发,spy ctx 记账)
| 包内路由 | 平台端点(真实实现) | 方法 | 备注 |
|---|---|---|---|
| `GET /api/ai1net/plugins` | `/api/plugins/business` | GET | 候选池清单 |
| `POST /api/ai1net/plugins/upload` | `/api/plugins/business` | POST | body `{file, filename, trust?}`;**检测链安全/兼容两项在这条响应里** |
| `POST /api/ai1net/plugins/detect` | `/api/plugins/business/datastore` | POST | body `{id, dryRun:true}`;⚠️ 返回值**如实标** `covers:'dataplane-declaration'`,⛔ 不让前端以为"三项都重跑了" |
| `POST /api/ai1net/plugins/db`(`plan`) | `/api/plugins/business/datastore` | POST | `{id, dryRun:true}`,⛔ 不动库 |
| 同上(`create`) | 同上 | POST | `{id}`,✅ 真建库 |
| 同上(`migrate`) | `/api/plugins/business/datastore/migrate` | POST | `{id, confirm:true, expect}`;🔴 `expect` **必填**且**包内不生成**(生成 = 替用户确认改动) |
| `POST /api/ai1net/plugins/enable` | `/api/plugins/mine/apply` | POST | `{plugins:[{id, enabled}]}`;平台两道门禁(基础插件不可关/未建库不许开)**都在平台**,包内只转交 |
| `GET /api/ai1net/plugins/updates` | `/api/plugins/shared/status` | GET | 折成三态,**判据只看 `treeSha256`** |
| `POST /api/ai1net/plugins/update-request` | `/api/plugins/business` | POST | 只到请求;`role='auto'` 时**改走 `setAutoHandling`,零平台请求** |
「该拒就拒」五条负控(**一律零平台请求**,⛔ 不发注定失败的请求):
| 场景 | 结论 | 平台请求数 |
|---|---|---|
| `mode=migrate` 缺 `expect` | `bad-args` | 0 |
| `upload` 缺 `file` | `bad-args` | 0 |
| 包名形态不合法(`../../etc/passwd`) | `bad-args` | 0 |
| 出示**空**凭据(带了身份头却没带令牌) | `plugin-not-enabled` | 0 |
| 出示**别人的**令牌 | `plugin-not-enabled` | 0 |
🔴 三态凭据语义(继承第 43 棒):**没带身份头** ⇒ 视为本包自己(用本机装配注入)|**带了身份头却是空串** ⇒ **必拒**(⛔ 不许回退到本机 env —— 那正是"插件自称身份"的缝)。
---
## 五、S6-4 负控(本棒核心)
装置:**真执行者** = `tmp/p42/puller.mjs`(S4 已落地的拉取器,⛔ 不重造)+ `tmp/p42/mgr-mock.mjs`(照抄平台对账/产物口口径)。
| 局面 | 观测 | 结论 |
|---|---|---|
| 执行者**在位** | `ok=true`;`pulled=[_dsh-local_ai1net, _dsh-local_probe2]`;盘上真出现包目录;**取包口被真实调用 2 次** | 行为确实会发生 |
| 执行者**关掉**(清空本地后,只跑包内动作) | 本地**零包目录**;**取包口 0 次调用**;`setAutoHandling` 回 `pullsTriggered:0` | **行为确实停** |
⚠️ 这条负控**不是空绿**:同一轮里包内台账记着 `updateRequests=1 / autoHandlingWrites=1` ⇒ 证明"**请求确实发生过,只是没人执行**",而不是"脚本压根没跑"。
叠加三条既有判据(S4 口径,本棒复测):
1. **清单拿不到** ⇒ `ok=false` + `pulled` 空 + **本地零改动**(目录清单前后一致)。
2. **单包失败**(截断 gzip)⇒ `failed` 恰含该包、另一包照常落地、失败已回报平台。
3. **`extra`**(本地多余目录)⇒ 如实出现在 `extra` 里,**目录仍在**(⛔ 平台不据此删)。
---
## 六、🔴 真实 vs 建模(逐条标注 · ⛔ 不许把建模当"已通")
| 面 | 本棒是**真实**还是**建模** | 说明 |
|---|---|---|
| 包内七条路由的实现与注册 | **真实** | `lib/index.js` 真装配 + 真 `apply(ctx)`(spy ctx 抓到路由原文) |
| 包内 → 平台的出向请求 | **真实**(HTTP 层) | 三条头(包名 / 每插件令牌 / 实例令牌)+ `body` 真的 `JSON.stringify` |
| **平台侧收到的效果** | ⛔ **建模** | 平台端由 spy / mock 代替;**未对真机投放**(属投放线) |
| 「更新」③ 搬包 | **真实**(复用 S4 产物) | 真拉取器 + 真 mock Manager;⚠️ 平台的**权威共享层**是沙箱替身 |
| 「更新」④ 自动处理的执行者 | ⛔ **仅契约** | 包内只有"偏好"这一半;**引导层那一半尚未实现**(本棒只证明"包外不干活 ⇒ 行为停") |
| **每插件令牌的平台侧那一半** | ⛔ **建模 / 尚无** | 真实 `im.ts` 里**仍无**这道闸 ⇒ ⛔ **不能断言"平台侧令牌闸已上线"**(属投放线待落地) |
| 长轮询 / 会话面 / 桥传输 | 继承第 43 棒口径 | 会话面走 `imAuth`;桥端点才有令牌闸 |
| 窄屏 UI | **机械判据真实 / 视觉效果未取到** | 样式段与断点由门禁逐条断言;**真壳截图未取到**(§八) |
---
## 七、§4 避坑总表对表(本棒相关类目)
| 类目 | 本棒处置 |
|---|---|
| **B1** 桌面端无 webServer | 服务端一律走 E1(`connection.fetch.register` 精确路径),⛔ 不用 webServer 具名路由 |
| **B2** 只认精确路径 | 七条逐字冻结、⛔ 无前缀匹配;门禁把七条与规格书 §五 A3 冻结清单**逐字比对** |
| **B3** 平台不是实例 | 「写平台库」的动作(建实例/租约/配额)⛔ 一律不进包 |
| **C1** 指纹两侧不一致 | 判据只看 `treeSha256`,⛔ 不比版本号;`diffOfSharedStatus` **只聚合不重算**(避免第二真相源) |
| **C5** 算不出指纹就报算不出 | 三态强制:全 `current` ⇒ `same`/有 `stale` ⇒ `differs`/有 `unknown` ⇒ **`unknown`(⛔ 绝不折成 `same`)** |
| **D1** 一个包 = 一个库,表名分段 | 库 `dshs_pl_ai1net`/前缀 `p_ai1net_`/M3 段 `p_ai1net_plugin_`(S3 已落,`DECL_DIGEST` 未破) |
| **E1** 测试跑的是构建产物 | 改完**必** `node scripts/build.mjs`(本轮改 `build.mjs` 自身后先 `--check-only` 复验) |
| **E6** 禁 `exports.default` | 门禁覆盖;`lib/client.js` 仍只导出 `apply` + `inject` |
---
## 八、尺子回归(⛔ 未改尺子一行)/ 未取到(如实)
```
⓪ 包内门禁 node scripts/build.mjs --check-only | rc=0 (60 ✅ / 0 ❌)
M5_DIGEST=55cdc0d5fc8eb99d ← 与第 43 棒逐字相同 ⇒ M5 的绿没被弄红
M3_DIGEST=7045a778baa82cfe DECL_DIGEST=88c752a3e12597b3…(8 表 29 列未破)
S6-1 装配 · 端A 客户端形态(桌面 profile 真件副本 + 本包) | rc=0
S6-1 装配 · 端B 平台形态(profiles/web 键位建模 link: 依赖) | rc=0
S6-1 取数 · 真实 import 本产物 | rc=0
S6-1 回调 · 真实调用 apply(ctx)(spy ctx) | rc=0
运行 · 真启动日志的失败面(⚠️ 弱证据) | rc=0
v-neg.sh all | rc=0
S6 取证编排 node tmp/p44/run-s6.mjs | rc=0 (58 ✅ / 0 ❌)
```
### ⚠️ 未取到 / 不成立(如实,⛔ 不折算成"已知/通过")
| 项 | 读数 | 原因 |
|---|---|---|
| **窄屏截图(S6-3 第三条)** | **`v-ui.sh` rc=3** | 需**真壳在跑**;启动器属**客户端线**(`dsh-ai1net-desktop/.workbuddy/_devkit/launch-desktop-dev-017.mts`),本线 ⛔ 不代启 ⇒ 机械判据已绿,**视觉效果未取到** |
| M5 卡片 + S4-6 实例页能力栏 | 同上 | 同因(第 43/42 棒已欠,本棒未消) |
| `v-baseline.sh` 平台侧 | **rc=255**(客户端侧 ✅ 取到 5 个 bundle,`@dsh-local/*` 计 0 个) | `ssh 47.77.182.89` 本轮连不上(**瞬时**;该项按尺子自述「只作开工对照,⛔ 不进判据」) |
| `v-plugin.sh 运行` 面 | rc=0 但**空真** | 可用日志 mtime 早于本包 ⇒ **弱证据**,⛔ 不当"运行期已验" |
---
## 九、本棒唯一一处真 bug(如实记录)
🔴 **包名形态校验不收带 scope 的包名** —— `lib/plugins-sdk.mjs` 首版 `PKG_RE = /^[a-z0-9][a-z0-9._-]*$/i`,而规格书 §五 A4 **冻结**:包名 = **`@dsh-local/ai1net`**(带 scope),`pluginId` 只是它的末段。
- **后果**:`upload` 之外的五条走 `pkgOf()` 的路由(`detect`/`db`/`enable`/`update-request`)**全线 `bad-args` 就地拒**,**一个平台请求都不发**;且这是**静默失败**(看着像"平台不认",其实是包内先拒的)。
- **发现方式**:`tmp/p44/run-s6.mjs` B 段的真发取证(九条里 5 条报"零请求")。
- **修复**:`PKG_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i` —— ⛔ 仍拒 `'../'` 穿越(scope 后那段与无 scope 时用**同一把尺子**)。
- **防复发**:把「带 scope 的冻结包名必须过 + `../` 必须拒」钉成包内门禁 **①f-⑩**(读 `package.json` 的 `name` + 从源码取 `PKG_RE` 字面量真跑:4 个合法名全过 / 7 个非法名全拒),并把包名与正则字面量一起喂进 `M3_DIGEST`。
另一处**自家脚本的坑**(不是包的问题,如实记):编排脚本首版把「记账窗口」取在 promise 创建之后,而 `guarded()` 是**同步**走到 `callPlatform` 的 ⇒ 九条全报"零请求"(假红)。改成传 thunk 后即正确。
---
## 十、硬边界自查 / 遗留 / 回滚
- ⛔ **未 commit / 未 push**(未明确要求)。
- ⛔ **未触碰官方 dsh 主程序与缓存**(R2);未把包装进任何 profile(装配用 `tmp/p44/` **沙箱 profile 副本**)。
- ⛔ **未重造分发与更新机制**:「搬包」直接复用 S4 已落地的拉取器(`tmp/p42/puller.mjs`)。
- ⛔ **未代用户起实例**;⛔ **未动客户端工作区**(`dsh-ai1net-desktop` / `dsh-desktop-0.1.7rc2` 只读引用)。
- 包内**零直连平台库路径**(红线扫描绿)。
- **S9 的 P2 未跨过**;本棒**未发现新停点**。
- **回滚**:本棒包内改动集中在 `lib/plugins-sdk.mjs`(新建)/`lib/index.js`(M3 装配块 + `statusOfReason`)/`lib/client.js`(M3 卡片 + 窄屏段)/`scripts/build.mjs`(①f + ⑩)/`package.json`(`exports` 加一条)。回滚 = 删 `lib/plugins-sdk.mjs` + 撤 M3 装配块与卡片即可(M5 与 S2–S5 的绿不受影响,`M5_DIGEST` 可作前后一致的锚点)。
---
## 十一、出处
| 项 | 出处 |
|---|---|
| 七条包内路由冻结清单 | `D:/github/dsh_shenxian/dsh-server-docs/05-交接单/交接单_包规格书_20260926.md` §五 A3 |
| 包名/库名/表前缀冻结值 | 同上 §五 A4 / §五 A5 |
| 四拆判据原文 | 本线入口 §2 第 43 棒块 → 方案 §3.10.8 |
| 避坑类目 | `交付物/功能打包到基础插件-分步实施方案-20260926.md` §4 |
| 编排脚本 | `tmp/p44/run-s6.mjs`(58 ✅ / 0 ❌)|`tmp/p44/run-scales.sh` |
| 读数 | `tmp/p44/读数-S6-20260926.txt`|`tmp/p44/尺读数-S6-20260926.txt` |
| 真执行者 | `tmp/p42/puller.mjs` + `tmp/p42/mgr-mock.mjs`(S4 产物,本棒⛔ 未改一行) |