# 第 19 棒交付件 · 基础插件「默认开启+不可关闭」机制 + 回滚缺失侧修复 > **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`|**线**:插件投放与分库线(入口 `接续入口_插件投放与分库线_20260922.md`) > **轮次**:第 19 棒 · 执行棒(2026-09-25 10:2x–10:5x)|**代码仓**:`D:\github\dsh_shenxian`(生产口径 `dshs`) > **结论一句话**:第 1 件**完成并上线**;第 2 件**机制代码面全落并上线**,真机判据 **2 ✅ / 1 ✗ / 1 未取证** ⇒ **⛔ 不宣称验收通过**;同时把 pnpm `exit 255` 的根因从"原因不明"推进到**有具体错误码指向**(未修,交第 20 棒)。 --- ## 一、第 1 件 · 回滚"缺失侧"(P0)✅ 完成 **问题**(第 18 棒真机实证):`off` 撞 pnpm 偶发失败 ⇒ `package.json` 里声明还在、`node_modules/` 软链已被删 ⇒ 实例启动 `Error: dsh: cannot resolve profile bundle "dsh-plugin-mcn-suite"` ×5 → `crash-loop-circuit-open`(冷却 600 s)。旧 `restoreProfile()` 只做"删多余",**不重建缺失**。 **修法**(`src/supervisor/plugin-assembly.ts`):新增 `linkRefsOf(dir)` / `rebuildMissingDepLinks(dir)`,接入 `restoreProfile()`(在 `pruneOrphanDepLinks` 之后)+ `applyProfileChanges()` 收尾,并把重建结果作为 `repaired[]` 回给调用方。 **四条不变量**(判据,单测钉住): | # | 不变量 | 实现要点 | |---|---|---| | 1 | **只补 `link:` 声明** | 遍历 `dependencies` 里 `link:<绝对路径>` 的条目;`file:` / registry 依赖一律不动 | | 2 | **只补"缺"** | `lstatSync` 命中即 `continue`(已正确 ⇒ kept,不重写) | | 3 | **⛔ 不建悬空链** | 目标 `statSync` 不是目录 ⇒ 跳过(宁可缺,不留指向空气的软链) | | 4 | **不碰真目录、不跑 pnpm** | 纯 `symlinkSync`;子路径 `@scope/x` 先 `mkdirSync(recursive)`;`node_modules` 不存在 ⇒ 整体跳过;属主 `chownSync` 尽力而为(非 root 静默跳过)、⛔ 回滚路径上不许再抛 | **单测**:`test/plugin-assembly.test.mjs` 第 ⑨ 组 2 例(缺链重建 / 不造半安装态)+ 第 ⑩ 组 2 例(落盘拒绝 / 拒绝码同名);`S3 请求体形状` 补 `essential: false`。`npm test` = **577 过 / 0 败 / 2 跳过**(第 18 棒基线 575 ⇒ +2)。 --- ## 二、第 2 件 · 基础插件「默认开启 + 用户不可关闭」(用户 09:3x 拍板) ### 2.1 第 0 步:真机确证"dsh 自带插件清单"(⛔ 不许跳的那一步) | 查什么 | 47 上的实测结果 | |---|---| | `@deepseek-ai/dsh` 的 `package.json` | **不声明任何插件清单** —— 无 `dsh.plugins` / `bundled` 字段,只有 `configTrees` | | `node_modules/@deepseek-ai/*` | 是**库依赖**(`dsh-base` / `dsh-web-app` 等运行时 / UI 包),不是"自带业务插件" | | `@softspark` 在 dsh 包内 | **零命中** | | **可比对的平台语义(唯一)** | 门户 `/opt/dshs/web/admin.html`「运行环境」页用 `i.removable ? pa.removable : pa.required` 渲染,文案 = **「平台必备」/「可移除」** 二分,并有"「平台必备」项请勿删" | ⇒ 判定:**"基础插件"不是 dsh 的既有概念,无"自带清单"可枚举**;机制**必须数据驱动**(台账身份标记),⛔ 不硬编码包名清单。 ### 2.2 落地形态(上线件) - **身份存台账**:`src/db/schema.ts` **v17** —— `business_plugins.essential`(SQLite `INTEGER NOT NULL DEFAULT 0` / PG `BOOLEAN NOT NULL DEFAULT FALSE`);`types.ts` / `repo.ts` / `pg.ts` / `sqlite.ts` / `adapter.ts` 五件加列读出口与 `setBusinessPluginEssential`。 - **门禁两处(双保险)**:① 路由 `POST /api/plugins/mine/apply`:对基础插件的 `enabled:false` **直接拒** ⇒ HTTP **409** `{error:'essential_plugin', pluginId, message}` + 写 `disable_denied_essential` 审计;② 装配层 `applyProfileChanges()`:结构性跳过并回 `refused: [{id, reason:'essential_plugin'}]`(⇒ 即便别的调用方绕过路由,也关不掉)。 - **播种三条路径**:审批钩子 `POST /api/admin/users/:id/approve`(fire-and-forget)/`POST /api/plugins/business/essential/sweep`(admin,`{userIds?}` 省略 = 全部公开用户)/`/mine/apply` 顺带。 - **跨机**:`remote-spawner.ts` 请求体带 `essential` ⇒ `worker/agent.ts` 严格解析(只认布尔 `true`)。 - **门户 UI**:`poc/business-plugins/lib/client.js` —— 勾选框 `disabled`(基础插件不给操作入口)、状态徽章改渲染 **「平台基础插件 · 不可关闭」**(身份标注优先于启停徽章)+ `title`;版本 `0.3.25 → 0.3.26`。 ### 2.3 真机判据(47,临时 admin 会话,用完即删) | 判据 | 读数 | 结论 | |---|---|---| | ④ 关闭请求 = 具名拒绝且状态不变 | `http=409` + `{"error":"essential_plugin", …}`;拒绝前后 `/mine` **完全一致** | ✅ **通过** | | ② 台账标记生效 | admin 列表 `@softspark/dsh-file-preview essential=True version=2.0.0` | ✅ **通过** | | ① 默认播种 ⇒ 该实例默认 `enabled` | sweep 回 `{"ok":false,"seeded":1,"failed":1, error:"pnpm_failed"}`;`/mine` 仍 `enabled=False` | 🔴 **未通过**(被 pnpm 挡住) | | ③ 门户 UI 无关闭入口 | 0.3.26 tgz **已 pack、未投放** ⇒ 无读数 | 🔴 **未取证**(⛔ 无读数不宣称通过) | --- ## 三、附录 A · pnpm `exit 255` 根因新证据(⛔ 未修 · 交第 20 棒) **取证命令(第三方可复现)**: ```bash ssh bt-server "journalctl -u dshs-worker --since '45 min ago' --no-pager | grep plugin-assembly" ``` **读数**(Sep 25 10:52:09–10:52:21,**12 秒内连续 4 次** ⇒ 在该 profile 上**稳定复现,不是"偶发"**): ``` [plugin-assembly] pnpm 失败 code=255 uid=114801 cwd=…/users//home/profiles/web args=install --reporter silent stderr=- stdout=- ``` **已排除的两块**:① **权限** —— 共享层链 `755`(`/var/lib/dshs` 711 → `bundled-plugins` 755 → 包目录 755),uid `114801` 对新旧包目录 `package.json` **A/B 双读均通过**;② **残留** —— apply 失败后 `dependencies` **已回滚干净**(无 `@softspark/dsh-file-preview` 残留)、`node_modules` 无半安装态。 **新证据(指向具体错误码)**: | 事实 | 读数 | |---|---| | profile 记的 store | `node_modules/.modules.yaml` ⇒ `storeDir=/var/lib/dshs/users//ws/.local/share/pnpm/store/v3`(**HOME=ws 时代**产物,Sep 11) | | 平台跑 pnpm 时的 HOME | `src/supervisor/plugin-assembly.ts:499` ⇒ `env HOME=${homeDir}`(= `/home`) | | 该 HOME 下的默认 store | `/var/lib/dshs/users//home/.local/share/pnpm/store/v3` ⇒ **No such file or directory** | | 同仓已有的解法 | `scripts/ensure-biz-plugins.cjs:existingStoreDir()` —— 注释**明写** `ERR_PNPM_UNEXPECTED_STORE`(档案 57 实测):pd 记录 store 与解析到的 store 不一致时,pnpm **直接拒绝** | ⇒ **高度指向 `ERR_PNPM_UNEXPECTED_STORE`**(立即拒绝特征 ≈2 s 与读数吻合);**正解 = 平台路径复刻脚本里的 store 口径**(读 `.modules.yaml` ⇒ 传 `--store-dir`),而非只补 reporter。**为什么一直"看起来像偶发"**:只有 **HOME 换代(ws → home)前建的存量 profile** 会命中。 --- ## 四、未取证 / 未修清单(如实) | # | 项 | 状态 | |---|---|---| | 1 | 基础插件判据 ①(默认播种 ⇒ `enabled`) | 🔴 未通过(pnpm) | | 2 | 基础插件判据 ③(门户 UI 无关闭入口) | 🔴 未投放、未取证 | | 3 | pnpm `exit 255` | 🔴 未修(根因已成形,交第 20 棒) | | 4 | `npm run verify` 尾段 `verify-platform-admin-section.mjs` | ⚠️ **既有缺陷**(同文件 `require()` + top-level `await` ⇒ `ERR_AMBIGUOUS_MODULE_SYNTAX`;单独跑同样失败,与本棒无关) | | 5 | `@softspark/dsh-file-preview` 已进共享层 ⇒ **所有登录用户可见**(可自助启用);若"先启用后标 essential"则用户**无法自行关闭** | ⚠️ 副作用,已上报(撤回口 = `…/:id/essential {essential:false}`) | | 6 | `@dsh-local/business-plugins` 0.3.26 的 UI 改动 | ⚠️ **未投放**(需 pnpm 修通后铺装) | --- ## 五、部署与卫生读数 - 47 部署 **11 件** / 106 **2 件**,md5 与本地**逐字一致**;备份 `/opt/dsh/backups/lib-p19/47-pre-p19-20260925-104848.tgz`、`106-pre-p19-20260925-105012.tgz`。 - 47 `systemctl restart dshs` ⇒ **active · NRestarts=0 · 门户 200**;**v17 迁移已落**(`max(version)=17`),池内 `essential` **全 `f`**(存量行为零变化)。 - ⚠️ 部署途中曾把 `lib/` 内容误落 `/opt/dshs` 根(散进 `supervisor/ web/ db/ worker/`)⇒ 已逐一 `rm` + `rmdir` 复原(四目录均系本次误建)后按 `-C /opt/dshs/lib` 重投;期间 47 **未用新代码重启**。 - 清理:临时 admin 会话**残留 0**;⛔ 未 commit / push;⛔ `src/web/routes/im.ts` 零改动;⛔ 未手改用户 `home` 的 profile / `.pnpm`。 --- ## 六、补充(2026-09-25 11:0x · 用户口径定案后,本文件第二节结论已更新) 用户原话:「**dsh 自带的就是,admin 添加的就不是**」⇒ 判定 = **按来源**(`@deepseek-ai/dsh` 自身声明的依赖 = 自带),⛔ 不是按"平台必备"。据此: - ✅ **基础插件名单 = 空**:业务插件台账全量 **5 项**(`@dsh-local/im-conversation-tabs`、`@dsh-local/storyforge`、`@softspark/dsh-file-preview`、`dsh-plugin-mcn-suite`、`dsh-univer-office`)**全属 admin/平台添加**;`@softspark` 在**两机**的 dsh 安装树 + 全局 npm 根**零命中**(dsh 包内提到它的文件 0 个)。 - ✅ **`@softspark/dsh-file-preview` 的 `essential` 标记已撤销**(真机 `changed:true`;台账复读 essentials **空**;用户面仍可见、可自助启停)⇒ 第二节的"判据 ①③"**已无主体**;门户 `0.3.26`("平台基础插件"徽章)**暂不投放**,产物留在 `poc/business-plugins/dsh-local-business-plugins-0.3.26.tgz`。 - ✅ **新核实(免掉一次改造)**:dsh 自带的包**本来就关不掉** —— 装配入口对清单里每一项都查台账,不在候选池 ⇒ **404 `not_in_pool`**(`src/web/routes/business-plugins.ts:2299-2302`)⇒ **绕过 UI 自造请求也关不掉**;门户清单又不渲染它们 ⇒ **UI 层与接口层同时成立,无需新增代码**。 - ⛔ 仍待办(未变):pnpm `exit 255` 可见化 + 根因修复 —— 已交第 20 棒(automation `421a8bad-…`)。 - 取证脚本:`tmp/p19b-quota.sh`(台账全量 → 撤销标记 → 复核 → 用户面清单 → 清理临时会话)。