# 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/` → `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/失败原因**(单条失败不中断整批)| - **缓存**:`/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 `。代价:安装时以 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 的前提清单立项。