Files
dsh_ai1net_server/交付物/基础插件身份与回滚缺失侧-20260925.md
T
admin 318430c9e9 chore(工作区): 插件投放与分库线收口入库(第 35 棒 + 22:44 拍板落盘)
范围 = 本线(插件投放与分库线)产物 + 记忆类,共 27 件:
- 接续入口_插件投放与分库线(1 件):§0 新增 22:44 拍板行;§2 第 36 棒范围改 5 步、条件步转无条件
- 交付物(22 件):MCN 数据面接入阶段一/阶段二系列(含 B 案落地与 shadow 读数)、
  P0 修通与移动端真机验收、pnpm-EPERM、两机 lib 差异、共享层台账语义、
  基础插件身份与回滚、插件接入验收、插件数据面取数口、移动端迁包与字号扩面、跨机错误消息
- 记忆(4 件):2026-09-24 / 2026-09-25 日志、MEMORY.md、本棒 automation 执行记录

⛔ 未含他线在途改动(只报告、不代提交):机制层 CODEBUDDY.md / state.py /
.codebuddy/rules/server-ops.md、接续入口_IM线、接续入口_StoryForge验收线、
其余 29 个 automation 目录、docs/规则与载体/、归档/、接续包_*。
2026-09-25 23:06:29 +08:00

120 lines
11 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.
# 第 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/<pkg>` 软链已被删 ⇒ 实例启动 `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/<uid>/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/<uid>/ws/.local/share/pnpm/store/v3`(**HOME=ws 时代**产物,Sep 11) |
| 平台跑 pnpm 时的 HOME | `src/supervisor/plugin-assembly.ts:499` ⇒ `env HOME=${homeDir}`(= `<user>/home`) |
| 该 HOME 下的默认 store | `/var/lib/dshs/users/<uid>/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`(台账全量 → 撤销标记 → 复核 → 用户面清单 → 清理临时会话)。