119 lines
11 KiB
Markdown
119 lines
11 KiB
Markdown
# 第 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`(台账全量 → 撤销标记 → 复核 → 用户面清单 → 清理临时会话)。
|