# dsh-web-plugin-manager

> ## ⚠️ 本仓库已停止维护（对应 DSH <= 0.1.5-rc.2）
>
> **DSH 0.1.6-alpha.2 起请改用 [dsh-plugin-manager-companion](https://github.com/LX2000WASD/dsh-plugin-manager-companion)。**
>
> 原因：官方 0.1.6-alpha.2 自带完整插件管理页（`@deepseek-ai/dsh-client-ui-plugin-manager` +
> `@deepseek-ai/dsh-plugin-manager` 宿主服务），本仓库原先"遮蔽官方只读页 + 自建 profile 写权"的
> 路线不再成立。而且官方在 `dsh-base` bundle 里新增了 `id: plugin-manager` 行，与本仓库
> `cordis.patch.yml` 的行 id 撞车，**在 0.1.6-alpha.2 上会让 profile 完全无法启动**
> （`TypeError: duplicate loader entry id: plugin-manager`），服务名也与官方同名冲突。
>
> 新仓库改为**伴生补强**路线：当前环境的写操作全部交还官方通道，只补官方明说不做的部分
> （多环境管理、agent 预设组合、普通插件模块的文件操作、版本/更新、安装前质量门、深度诊断）。
>
> 本仓库的 0.6.x 仍可用于 DSH <= 0.1.5-rc.2；**在 0.1.6-alpha.2 及以上请勿安装**。

[中文](./README.md) | [English](./README.en.md)

[![npm version](https://img.shields.io/npm/v/dsh-web-plugin-manager)](https://www.npmjs.com/package/dsh-web-plugin-manager)
[![License](https://img.shields.io/npm/l/dsh-web-plugin-manager)](LICENSE)

在 Web UI 中一键管理 DeepSeek Harness (DSH) 插件：查看、实时启停、安装/卸载、更新检测、健康检查（依赖/冲突/兼容性分析）、环境管理、插件市场。bundle 与非 bundle 插件全覆盖。

> 强烈建议先装管理器、再装其他插件：本管理器自带质量门与健康检查（安装时全链依赖扫描 + bundle patch 行校验 + 安装即回滚；安装后可做依赖图/冲突/循环/peer 兼容性分析）。先装管理器，后续每个插件的安装都会经过检测，能显著减少"装上就炸、重启起不来"的情况。
>
> 若已存在未经验证的插件导致 profile 无法启动：先用 `dsh --profile <name> --patch <empty.yml>` 等方式排查，或手动清理对应依赖/`cordis.patch.yml` 行后，再安装管理器接管。

## 安装

```sh
# 方式一（推荐）：从 npm 安装（务必带 @latest）
dsh plugin --profile <name> add dsh-web-plugin-manager@latest

# 方式二：从源码构建
cd /path/to/dsh-web-plugin-manager
pnpm install && pnpm run build
dsh plugin --profile <name> add .
```

重启 profile 后，Web UI 的"设置"会出现"插件管理"标签页、"技能与预设"与"市场"一级菜单。

## 更新

```sh
# 命令方式（推荐）：升级到最新版（重写 specifier，质量门 + 失败自动回滚到旧版本）
dshpm update dsh-web-plugin-manager --profile NAME
# 等价命令（pnpm 语义）
dsh plugin --profile NAME add dsh-web-plugin-manager@latest
```

UI 自更新：打开"设置 → 插件管理"，"检查更新"会列出全部已装插件（含管理器自身），有新版时点对应卡片上的"更新"即可——管理器可以一键更新自己，失败自动装回上一版本；更新后需重启 profile 生效。

注意：

- add 不带版本号不会升级：profile 里已声明旧版本（如 `^0.1.2`）时，`dsh plugin add`（即 `pnpm add`）保留原 specifier 不升；`dsh plugin update`（即 `pnpm update`）也只在已声明的范围内重解析。跨版本升级必须用 `add ...@latest` 或 `dshpm update`。
- @latest 解析到旧版时：先查 `pnpm config get registry` 是否为镜像源（npmmirror 等 dist-tag 同步滞后 + pnpm 元数据缓存），用 `pnpm add dsh-web-plugin-manager@latest --registry=https://registry.npmjs.org` 强制走官方源；显式指定版本号（如 `@0.3.8`）永远可靠。
- pnpm 11 用户：pnpm 11 默认 `minimumReleaseAge: 1440`（24 小时）——发布不足 24 小时的版本会被扣住，导致"当天发版当天点更新装不上"或解析到旧版。在 profile 的 `pnpm-workspace.yaml` 中加 `minimumReleaseAge: 0`（或 `minimumReleaseAgeExclude` 白名单）即可；dsh 不会覆盖该文件，改动会保留。

## CLI（dshpm）

用户让 AI 安装插件时，AI 默认会跑裸 `dsh plugin add` / `pnpm add`——这绕过了全部防护。本包提供 `dshpm` bin（随插件安装进入 profile 的 node_modules，也可 `node <profile>/node_modules/dsh-web-plugin-manager/dist/cli.js` 直接调用），所有变更走与 Web UI 完全相同的受保护链路（质量门 + 自动回滚 + 分析 + insert 行维护）：

```sh
dshpm install <source> [--env KEY=value ...] --profile <name>   # npm 名 / github:user/repo / git URL / tarball / 本地路径
dshpm remove <name>    --profile <name>   # insert 行 + 包依赖一并清理
dshpm update <name>    --profile <name>   # 升级到 @latest（重写 specifier，质量门+回滚）
dshpm mount <name>     --profile <name>   # 补挂载官方 CLI 手动安装的未挂载依赖
dshpm uninstall-kind <owner/repo> [--profile <name>]  # 卸载市场安装的 skill/预设/cordis 插件
dshpm list             --profile <name>   # bundle 层栈 / 已装包 / insert 行
dshpm analyze          --profile <name>   # 健康检查，有问题退出码 1
```

git 源插件需要安装期环境变量时，CLI 会打印缺失变量清单并给出 `--env KEY=value` 续装命令（非交互式）。

## 功能

| 能力 | 说明 |
|---|---|
| 查看 | 合并展示层栈/依赖/挂载行/运行条目；手动安装未挂载的依赖显示"未挂载"并可一键挂载；agent 预设构成展示（官方 0.1.3 inventory 对齐，0.1.2 自动隐藏） |
| 实时启停/卸载 | managed 块编辑 patch，经 loader 直接应用，实时生效、重启后持久；删除已装插件（含 bundle）同步实时卸载其运行条目与遗留 managed 块，客户端启动表立即清除（删除后刷新不再加载已删的 bundle 脚本）；禁用/卸载插件时按归属标记管理其自带 agent 预设（卸载清原版、用户改过的保留并报告；禁用零损失归档、重新启用自动恢复；归属标记 `.dsh-preset-owner.json` 中立标准 + 兼容 gamelike/dsh-agent-rp 既有标记，经宿主 agentPresets 服务删除） |
| 安装 | 官方 dsh plugin CLI + 质量门 + 自动回滚；非 bundle 自动写挂载行并实时加载；git 源自动 clone、已发布 npm 优先；多类型安装：skill（SKILL.md→`~/.dsh/skills`，热加载）与 agent 预设（agent.cordis.yml→`~/.dsh/.agent-presets`，官方发现机制直接可见）直装+记录；非插件/skill/预设仓库拒绝并加入市场屏蔽名单；git 源安装前扫描仓库所需环境变量（TOKEN/KEY/SECRET 形态），缺则暂停询问、行内填写后继续，answers 仅按扫描白名单注入（防 PATH/HOME 注入），宿主敏感键不传给第三方脚本 |
| 更新 | 检查更新（npm dist-tag / git HEAD / 安装 commit），更新带质量门与回滚；含管理器自身（自更新）——管理页可直接点更新升级，失败自动装回旧版本 |
| 健康检查 | 依赖图/缺失/循环/重复行 id/同名注册冲突（服务/工具/section/路由）/peer 版本/官方包重复；运行中追加 pending 与失败诊断及遮蔽自检；A 级问题一键修复、B 级建议确认后修复 |
| 环境管理 | 启停、复制/转移插件、创建/重命名/删除 profile（官方 profile 只读）、备份导出/导入恢复（差异对比 + 恢复前行内二次确认 + 切换目标自动重算差异；批量恢复与其他变更互斥） |
| 市场 | 静态索引（topic:dsh-plugin 全量 1.3 万+ 条，多源兜底 + gzip + 磁盘缓存 + 新鲜度门控）+ awesome 精选覆盖层 + dsh.so 独立验证/安全扫描徽标（L1-L5 + 风险等级叠加）；服务端已安装判定（包名/repository 双向/git 源/目录探测）；更新检测（索引版本对比）；同名包冲突消解；**分类筛选**（上游 CI 分类器 id 聚合，服务端 `categories` 计数 + 客户端兜底）；模糊搜索 + 相关性排序（`plgmgr` 命中 `plugin-manager`，官方 rankByName 算法长名适配版 + 字符掩码预筛）；24h 缓存、超时预算、代理支持、失败负缓存 |
| agent 工具 | plugin_status/search/install/uninstall/toggle + 安装守卫（拦截裸命令并引导）+ 提示词注入；plugin_search 自然语言检索市场（name/topics/描述加权），结果提示安装前先浏览仓库 |

功能与限制的详细说明见 [docs/feature-reference.md](docs/feature-reference.md)（随仓库与 npm 包发布）。

## 架构

- Host：`src/index.ts` —— `PluginManagerService`（`ctx.pluginManager`）+ `/api2/plugin-manager/*` REST 路由（带信任围栏：POST+JSON 强制、Host 回环/白名单校验、Origin 同源——防 CSRF/DNS-rebinding）；安装链路 installWithSource→installProtected（质量门+回滚）整体串行互斥；**七条**长操作（install/update/remove/backupRestore/uninstallKind/copyPlugins/checkUpdates）REST job 化——POST 即返 jobId、客户端经 `job` op 轮询取结果（HTTP 超时不再与服务端状态脱节，短任务用 250ms 起步的轮询），在途 job 数上限背压堆叠点击
- 基础层：`src/paths.ts` —— profile 路径/manifest/patch 读取、全局变更互斥队列、宿主 profile 识别（argv → 安装位置兜底）；`src/childproc.ts` —— 命令解析（运行中 node 目录 → PATH → $NVM_DIR 兜底）、PATH 注入子进程环境、官方 `dsh plugin` 运行器、异步 exec 工具（请求路径上零 execFileSync）
- 保护链路：`src/installFlow.ts` —— 安装/更新/删除保护流（源准备：git clone 缓存 + npm-first 探测；质量门 + 自动回滚；更新检查 npm dist-tag / git HEAD / lockfile commit；managed 行清理），全部经全局互斥串行
- 市场管道：`src/marketplaceMerge.ts` —— catalog/PLUGINS.md 抓取、registry 索引合并、星数富化、服务端已安装标记、dsh.so 徽标叠加、屏蔽名单过滤与同名包消解（index.ts 只保留缓存闭包与 REST 接线）
- profile 生命周期：`src/profiles.ts` —— 运行实例进程表扫描（3s TTL 缓存）、终端窗口启动、端口探测、新 profile 模板、in-box bundle 保护恢复
- 实时应用：`src/live.ts` —— 补丁变更先经 loader include 条目直接应用（`entry.update`，与平台 watchUserPatches 同通道）再写文件，绕开平台级死锁（watcher 刷新增量卸载 HMR 依赖的 timer 行造成循环等待）；补偿平台 `applyEntryPatches` 对 patch 对象的原地改写（深克隆 + 烘焙值归一化）；插件自有的 patch 文件 watcher 让手动编辑持续实时生效；删除包时按行名卸载其全部 loader 行（remove-by-name，容忍烘焙字段），防止残留客户端条目引用已删除的 bundle 脚本
- 分析引擎：`src/analyze.ts` —— 离线依赖图/冲突/兼容性分析（与质量门共享扫描器）；运行时诊断读取 `ctx.reflect` 活跃服务表
- Patch 编辑：`src/patch.ts` —— managed 标记块追加/移除（insert/disable 双类型识别）、行级操作、原子写入（tmp + rename）；处理 YAML 陷阱（`@` 包名引号、空数组文档、纯注释文件恢复模板）
- 环境变量扫描：`src/scan.ts` —— git 源安装期 env 需求扫描（2 层/40 文件/8 变量上限）+ 子进程 env 敏感键剔除；`src/installSession.ts` 会话状态机（15 分钟 TTL、answers 白名单校验）
- Agent 工具：`src/tools.ts` —— 依赖注入避免循环依赖；安装守卫 `src/guard.ts` 拒绝裸 `dsh plugin`/npm/yarn/bun/pnpm 变更命令（只读 verb 放行），拒绝原因直接指路 `plugin_*` 工具与 `dshpm`；`systemPrompt.section` 常驻提示同一条规则
- 模糊打分：`src/rank.ts` —— vendor 自官方 0.1.3-alpha.1 `rankByName` 的有序子序列对齐算法（长仓库名适配版），client/host 共用纯函数
- CLI：`src/cli.ts` —— 复用受保护链路（ctx 可空：无宿主进程时跳过 live 应用，文件级操作与 Web UI 完全一致）
- Client：`src/client/` —— 注册 `settings.plugins.tab`（遮蔽官方只读列表 + manager）+ `settings.section`（marketplace/kinds）；同源 fetch 调 REST；危险操作行内二次确认（无弹窗）
- 通信：官方 webServer 路由 + 同源 fetch（不走 Typert Remote）

## 已知限制

- 禁用被依赖的条目可能导致 profile 启动失败（官方 fail-loud 设计）；恢复：手动删除该 profile `cordis.patch.yml` 里的 managed 块
- 安装来自 git 的 bundle 需在终端放行 `pnpm allowBuilds`（命令输出会回显）
- 随机行（无显式 id 的挂载行）不可经此启停（id 每次挂载变化）
- git 子包安装：多包仓库用 `#路径:<dir>` 约定指定子目录（子目录必须位于克隆缓存内）
- 质量门可能误伤未声明运行时依赖的插件（保守策略，可加白名单）；Node 内置模块说明符（裸名 `crypto` 与 `node:crypto` 等价）已豁免——由运行时不依赖提供，不构成缺失依赖
- 官方包默认只能 peerDependencies（普通依赖会装出第二份拷贝并劫持官方 loader 行）；豁免白名单：`@deepseek-ai/schemastery`（官方 cookbook 允许的运行时 validator，模块身份不敏感，官方 dsh-llm-deepseek 即按 dependencies 声明）
- 安装守卫只拦 agent 工具调用，拦不住用户在终端手工执行裸 `dsh plugin`
- 更新检测边界：本地目录安装（非 git）报告"不可检测"；git URL 源需 manifest 记录安装 commit（gitHead）；用户本地 git 工作区只做只读比较（不 fetch），离线时报告"无法判定"而非"无更新"
- 健康检查为静态+尽力而为：同名注册冲突依赖源码正则扫描（动态拼接的名字检测不到）；语义冲突无同名可查，不在检测范围
- 手动安装的插件不会自动挂载：管理器显示"未挂载"并提供"挂载"/`dshpm mount`，不擅自改变 profile 行为
- 环境变量过滤只覆盖常见敏感键形态（TOKEN/KEY/SECRET/PASSWORD/PASS/CREDENTIAL）；宿主其它未匹配形态的变量仍会进入 git 源安装的 pnpm 解析进程（安装为 link 语义，不执行第三方脚本）
- 市场条目来源于 awesome 目录，个别仓库可能已删除/私有
- 市场代理：host 读 `HTTP_PROXY`/`HTTPS_PROXY`；系统代理/规则模式加速器对 Node 进程无效（undici 不读系统代理）——把代理写进环境变量或改 TUN/全局模式
- 市场索引项目（DSH-Plugins-Marketplace）为第三方维护：索引数据问题由精选覆盖层与安装质量门兜底；GitHub API 未认证限流 60/h 仅影响 catalog 独有条目的星数富化（量小，列表不受影响）
- REST 信任围栏仅按浏览器语义放行：非 HTTP 载体（第三方桌面壳/自定义协议分发 app:// 请求）不带 Host 头时，请求须无 Origin 且非 cross-site 才被接受（本地非浏览器发起者语义，issue #11）；这类载体若连请求体都不转发（无 stream 也无 body），操作会以字段校验错误返回而不是崩溃
- 删除 bundle 插件的实时卸载失败/超时不再静默：输出会提示「live unmount 未完成，请重启后完全生效」——半挂的 loader 树可能让 Web 服务已关但进程不退，此时自动重启链路可能卡死，需手动拉起（issue #10；根治在宿主侧 flush 超时兜底）
- nvm 用户注意：子进程命令按"运行中 node 目录 → PATH → $NVM_DIR"兜底并注入 PATH——宿主进程不在 nvm 激活的 shell 中启动也能工作；仅当 dsh 未安装时才需从 nvm 激活终端

## 开发

```sh
pnpm run build   # host: tsc（标准装饰器转译）; client: tsdown
pnpm test        # 纯函数单测（node --test 跑 dist 产物）
```

> 教训记录：host 必须用 tsc 构建（tsdown/rolldown 会保留原生装饰器语法，Node 不支持）；插件不能同时导出 default（类）与 named（apply）——Loader 会丢弃 apply。新增 host 源文件需加入 tsconfig.host.json 的 include。

### 客户端产物的 externals 必须对齐官方平台种子表

`tsdown.client.config.ts` 里的 `PLATFORM` 是**唯一允许 external 的清单**，须与官方 `packages/client/web/src/platform.ts` 的 `PLATFORM_MODULES` 逐项一致：

- external 了表外的包 → 浏览器抛 `require("x") missed the module table`，**整个插件页面启动中断**（所有插件的 UI 一起消失，不只是出错的那个）
- 内联了表内的包 → 模块身份分裂（同一个包两份实例，服务/上下文对不上）

这张表**随 DSH 版本变动**：0.1.2-alpha.1 删除了 `@deepseek-ai/dsh-client-runtime`、新增了 `@deepseek-ai/dsh-client-store`。每次跟进 DSH 版本都要比对该表并重新构建产物。

`tests/client-boot.test.mjs` 把这条契约做成了自动化检查：它用模拟的模块表真正启动 `dist/client.js`，越表 require 会当场抛 `missed the module table`；同时核对五个页签的 slot 注册面、zh/en 字典键位对齐与可渲染性。

## 相关

- 源码与 Issue：[github.com/LX2000WASD/dsh-web-plugin-manager](https://github.com/LX2000WASD/dsh-web-plugin-manager)
- 市场索引数据：[DSH-Plugins-Marketplace](https://github.com/bradeGithub/DSH-Plugins-Marketplace)（topic:dsh-plugin 全量索引，第三方维护）
- 精选数据：[awesome-dsh-plugins](https://github.com/AdamPlatin123/awesome-dsh-plugins)
- dsh.so 验证与安全数据：[dsh.so](https://www.dsh.so)（插件独立验证 L1–L5 与自动化安全扫描，第三方维护；仅叠加徽标，不作安装源）
- 许可证：MIT
