Files
dsh_ai1net_server/dsh-server-docs/04-调整方案/29-官方白名单插件来源.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

131 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.
# 29 · 官方白名单插件来源(方案 C · 方案 B 口径)
- 日期:2026-09-11(初版)/2026-09-11(修订:数据源与导入口径)
- 触发:用户问「dsh 官方白名单推荐插件能否作为插件来源接入门户插件管理」→ 选定方案 C
- 状态:**✅ 已实施并端到端验证**
> **TL;DR**|**结论**:dsh 官方白名单插件接入门户插件管理(方案 C):官方目录 **3408 款**,npm 预构建路径覆盖 **52.6%**(下载 TOP10 全覆盖)。
> **关键**:**源码安装(`github:`)评估结论 = 不做**:会让第三方构建脚本以 root 在平台机执行(供应链风险),与红线冲突;替代路径 = admin 本地构建后上传 tgz。
> **状态**:✅ 已实施并端到端验证(§七为关闭结论)
## 一、需求与背景
**需求**:在 admin 门户「插件管理」(`#/plugins`)增加一个**官方可信来源**——直接浏览/挑选 dsh 官方精选清单里的插件,选中后导入功能插件候选池。
**背景**:`dsh-market` 是 **dsh 实例插件**(UI 在实例设置页,一键装到当前实例)。若把它装给普通用户,等于**用户可自助装任意社区插件**,绕过「admin 投放候选池 → 用户按需启用」三层管控(档案 16),安全面失控。故**不装 dsh-market**,改为**把它的数据源接入门户**。
## 二、调研结论(修订依据)
初版按「git clone 仓库 + 解析 3431 个 `*.yml` + 取 `tarball` 字段」实现,端到端验证时发现**两个致命问题**,遂修订:
| 结论 | 证据 |
|---|---|
| ① 初版覆盖率仅 **6%** | 3431 个 yml 中只有 **211 条**声明 `tarball`(上游它是**可选**字段,仅 6.1%)→ 一键导入功能基本不可用 |
| ② 有**更好的规范数据源** | 官方把完整目录发布为 `https://awesome-dsh-plugin.com/plugins.json`(3.1 MB,**3408 条**,字段含 `name/owner/category/description{zh,en}/npm/version/stars/downloads/install/added`),并镜像为 npm 包 `dsh-plugin-catalog`。**一次 HTTP 拿全**,比 git clone + yaml 解析更快、字段更全 |
| ③ 安装形态分布 | `install` 字段 3408/3408 全覆盖:**npm 包名 1659** + `github:owner/repo` 源码 1616 + 多包 monorepo 等其他 133 |
| ④ **热门插件全是 npm 包** | 下载量 TOP10 全部有 `npm`(dshmarket 383k、dsh-better-sidebar 273k、@linxin666/dsh-remote-web-ui 247k…)|
| ⑤ npm 官方 tarball 可直接复用现有链路 | 实测把 registry tarball 喂给现有 `stageTgzArchive`:**通过**(`package/` 包装层已被 `findPackageJson` 兼容)→ **无需改 DB schema、无需跑构建脚本** |
| ⑥ 上游对无 `tarball` 条目的官方回退 | 上游 `scripts/probe-tarballs.mjs` 注释明确:「without the field a user falls back to the `github:owner/repo` install command」 |
**上游免责声明(记录在案)**:收录 ≠ 安全审计。上游 README 明确「Installing a plugin runs third-party code on your machine with your own permissions… Being on this list is not a security review」。→ 我们的安全兜底**完全依赖平台侧 `stageTgzArchive` 扫描**。
## 三、设计(修订后 = 方案 B)
**导入口径 = 仅预构建**(平台侧绝不执行第三方构建脚本):
| 分支 | 判定 | 取包方式 | 条数 |
|---|---|---|---|
| 1 | 有 `npm` 包名 | `registry.npmjs.org/<name>` → `dist-tags.latest` → `dist.tarball` | 1659 |
| 2 | 无 npm、有 `tarball` | 直接用 release 资产直链 | 133 |
| 3 | 两者皆无 | **不支持**,返回明确原因(含官方 `install` 命令),前端标「需源码构建」 | 1616 |
**可导入 = 1792/3408 = 52.6%**,官方热门插件全部覆盖。其余可由 admin 自行构建后走「上传 / 替换」投放(同一条校验链路)。
**后端**(`src/web/routes/whitelist.ts`,重写):
| 接口 | 作用 |
|---|---|
| `GET /api/plugins/whitelist?q=&category=&onlyImportable=1` | 返回 `{total, importable, count, stale, categories, plugins[≤300]}`;条目含 `id(=owner/name)`/`importKind`/`npm`/`version`/`stars`/`downloads`/`install` |
| `POST /api/plugins/whitelist/refresh` | 强制重拉(绕过 TTL) |
| `POST /api/plugins/whitelist/import {names[]}` | 逐个解析 tarball → 下载 → 复用 `stageTgzArchive` 校验 → 同名替换策略 → 入候选池;**逐条返回 ok/失败原因**(单条失败不中断整批)|
- **缓存**:`<dataRoot>/whitelist-cache/index.json`,TTL **6h**;带 **`version` 结构版本守卫**(改字段必须 +1,否则 TTL 内旧格式缓存会被复用 → 过滤时抛异常)。
- **降级**:官方站不可达但有旧缓存 → 用旧缓存并返回 `stale: true`(前端提示「官方站暂不可达,使用本地缓存」);无缓存才报错。
- **排序**:按 `downloads` 降序 → 未发 npm 的条目 downloads 为 0 自然沉底,**默认前 300 条即最热门**。
- **上限**:单次导入 ≤ 20 个;单包 ≤ 150 MB。
**前端**(`web/portal.html` → `#/plugins`):
- 搜索框(插件名/描述/npm 包名/owner)+ 分类下拉 + **「只看可导入」开关(默认开)** + 搜索 / 刷新清单;
- 表格 7 列:勾选 | 插件(名称 + npm 包名@版本副行)| 分类 | **导入方式徽章**(`npm 预构建` / `release 资产` / `需源码构建`)| 下载量 | ★ | 说明;
- 「需源码构建」行**置灰 + 勾选框禁用**;
- 统计行:`清单 N 条 | 可导入 M 条 | 匹配 K 条 | 显示 J 条`;
- 导入结果:失败项**在页面内展开列出原因**(而非只打 console),P0 阻断原因原样回显,交 admin 判断;
- 原有「上传 / 替换」卡片保留在下方。
## 四、改动文件
| 文件 | 改动 |
|---|---|
| `src/web/routes/whitelist.ts` | **重写**(数据源换 `plugins.json`;缓存版本守卫;仅预构建导入口径;`stale` 降级;逐条结果) |
| `web/portal.html` | `#/plugins` 区块重写(导入方式徽章、只看可导入、失败原因回显)— 31446 B → 33203 B |
| `src/web/routes/business-plugins.ts` | `stageTgzArchive` 改为 `export`(复用) |
| `src/web/server.ts` | import + `register(whitelistRoutes)` |
## 五、验证(2026-09-11,端到端)
**方法**:DB 直插临时 admin session(`user_agent=poc-curl`,用完即删),`curl -sk --resolve alotbuy.com:443:127.0.0.1` 走真实域名 + TLS。
| 用例 | 结果 |
|---|---|
| `GET whitelist?onlyImportable=1` | 200 | `total=3408` **`importable=1792`** `count=1792` `stale=false` 23 分类;前 300 条 **100% npm** |
| `GET whitelist?q=dsh-status-rotator` | 200,精确命中 `01Virex/dsh-status-rotator`(npm 名亦可搜) |
| `GET whitelist?q=dsh-quality-review` | 200,`importKind=source` + `install` 原样返回 |
| `POST import` 正常 npm | ✅ OK,`dsh-status-rotator @ 0.17.2`,落盘 115257 B |
| `POST import` P0 命中 | ✅ FAIL,原因回显:`安全检测未通过(P0 阻断):读取实例会话密钥(package/docs/cordis-patch-profile-web.example.yml)` |
| `POST import` 需源码构建 | ✅ FAIL,原因:`该插件需从源码构建安装(dsh plugin --profile web add github:CAI-MH/dsh-quality-review),暂不支持一键导入` |
| `POST import` 不存在 | ✅ FAIL,`不在官方清单:nobody/nothing` |
| DB / 审计 / 落盘 | `business_plugins` 1 行 + `tgz` 文件 1 个 + `audit_log.import_whitelist_plugin` 1 条;**stage 残留 0** |
| 前端语法 | `portal.html` 内联 JS `node --check` 通过;线上实拉 200 / 35894 B |
| 缓存守卫 | 部署前旧 v1 缓存被正确判为未命中 → 重拉并写入 `{"version":2,…}` |
| 清理 | 测试插件已删(候选池归零 / tgz 移除)+ 临时 session 已删 |
## 六、已知限制与后续
1. **方案 A(源码安装支持)→ 记入 P2 待办**:让 `github:owner/repo` 的 1616 条也能导入,需 `business_plugins` 加 `install_spec` 列(`tgz_path` 改可空)+ 启用路径分支 `pnpm add <spec>`。代价:安装时以 root 跑第三方构建脚本(慢、需工具链、易失败、供应链风险上升)。**当前决策:不做**(详见 03 路线图)。
2. **安全扫描疑似误报**:`FuRongJun-1999/dsh-memory`(6988 下载,热门插件)被判 P0 阻断,命中的是 `package/docs/cordis-patch-profile-web.example.yml` —— 从文件名看是**文档示例文件**而非真实读取行为,疑似规则过宽。已通过「失败原因页面回显」让 admin 可判断;规则本身是否收窄**另议**。
3. **前端未做浏览器人工验收**:因红线 R4 禁止用真实 admin 账号登录(last-wins 会顶掉用户会话),本轮验证走 API + HTML/JS 静态校验;**视觉与交互验收留给用户**。
## 七、红线遵守
1. 不碰官方 dsh 主程序(R2 ✅);2. 不升级 dsh(R1 ✅);
3. 未用真实账号做登录测试(R4 ✅,走临时 session + 用后即删);
4. 导入的第三方包**仍过平台安全扫描**,未因「官方白名单」而放行(安全兜底不外包给上游)。
---
## 七、方案 A(源码安装支持)评估结论:**不做**(2026-09-11 定稿)
档案 29 §六.1 把"让 `github:owner/repo` 的 1616 条也能导入"记为 P2。经评估,**结论为不做**,理由与替代路径如下。
### 7.1 为什么不做的代价不可接受
| 维度 | 问题 |
|---|---|
| **供应链** | `pnpm add github:owner/repo` 会执行仓库自带的构建/安装脚本(`prepare`/`postinstall`)→ **等于让第三方代码以 root 在我们的平台机上执行**(不是在用户沙箱里)。上游 README 已声明"收录 ≠ 安全审计",我们无法对 1616 个仓库做人工审计 |
| **可复现性** | 需要完整 Node 工具链、网络、各仓库自定义构建(有的还要 Rust/Go/Python)→ 失败率高、排障成本高 |
| **性能/资源** | 每次导入都要拉仓库 + 装依赖 + 构建(分钟级),且构建产物无法复用(每版本重来) |
| **管控口径** | 与"平台不执行第三方构建脚本"这一既定红线冲突(档案 29 §二⑤ 之所以选预构建 npm tarball 正因如此) |
### 7.2 替代路径(已可用,覆盖"源码类"插件的真实需求)
1. **admin 本地构建后上传 tgz**:现有「插件管理 → 上传 tgz」通道**完整可用**(走 `stageTgzArchive` 结构校验 + P0/P1 安全扫描,档案 19 §C6),源码类插件由 admin 在受控环境自行构建 → 上传即可进候选池。**这是推荐路径**。
2. **npm 预构建路径已覆盖 52.6%**(1792/3408,且**下载量 TOP10 全部在内**)→ 热门插件不受影响。
3. 若将来确实要批量支持源码类:**必须先满足下列全部前提**(作为升级条件记录):
- 构建发生在**一次性沙箱**内(bwrap + 无网络 + 只读源码挂载 + 无凭据 + 时限 + 内存/CPU 限额);
- `npm ci --ignore-scripts`(**禁止安装脚本**),只允许显式的 `npm run build` 白名单脚本;
- 产物仍走 `stageTgzArchive` 全量扫描后才入池;
- 仅 admin 触发 + 二次确认 + `audit_log` 审计 + 构建日志留存;
- 失败一律拒绝导入(不得降级为"直接装源码")。
**当前状态**:**关闭**(不做)。若用户将来要求开启,按 §7.2.3 的前提清单立项。