# dsh-plugin-diraud

[English](README.md) | 中文


基于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)（DSH）的插件管理增强：按「来源」分组插件列表，一眼区分**官方自带插件**与**自装插件**。

## 特性

- **`/plugin-audit` 命令**：按「官方 / 自装」分组列出当前已加载插件，支持 `user` / `official` / 关键词过滤；
- **自装插件开关（两处入口）**：
  - 左侧菜单栏底部「插件目录」入口（图标按钮，点击弹出面板）：按来源分组展示，每张自装插件卡片带**启用 / 停用按钮**（点一下就切换）；
  - 聊天命令：`/plugin-audit disable|enable <关键词>`；
  - 均持久化到 profile 的 `cordis.patch.yml`（重启保留、可逆），并调用 `ctx.loader.update` 即时生效（不依赖 HMR）；官方/内置插件锁定不可操作；
- **「插件目录」面板（v0.5 起）**：
  - 卡片化分组展示，带来源徽标、**已安装版本号**、entry 标识与功能描述；搜索框按模块名 / entry 过滤，每个分组实时显示数量；加载失败时提供「重试」按钮；
  - **双语描述（v0.6）**：内置「模块名 → 中/英」翻译字典优先取词（随系统语言变化），查不到再回退到包自身 `description`；
  - **GitHub 链接（v0.8）**：若插件 package.json 的 `repository` / `homepage` 指向 GitHub，卡片会显示 **GitHub 按钮**，可直接打开详细功能介绍（独立于官方「插件列表」页）；
- **自装插件更新（v0.6）**：
  - 打开面板自动检查 npm registry；顶部「更新」区显示可更新总数（带「重新检查」与「全部更新」按钮），每个 registry 插件卡片带独立「更新」按钮与进行中进度条（执行 `pnpm add <pkg>@latest --config.minimumReleaseAge=0`，corepack / npx 兜底，实时输出）；
  - **GitHub 插件更新（v0.9）**：`github:` / `git+` / `git://` / `git@` 装的插件（没有 npm 包也能更新）——分支 / 默认分支引用用 `git ls-remote` 对比本地锁定 commit 与远端 HEAD，tag 引用自动列出全部 tag 找更新的稳定版 tag；执行时 `pnpm add <原始 spec>` 重解析分支，或换成新 tag spec；「钉死具体 commit」的引用标记为不可自动更新；
  - **重命名检测（v0.9.1）**：若更新后发现旧包名仍锁旧版本、但已出现指向同一仓库的新包名，会提示「仓库已重命名（旧 → 新）」并给出移除旧包的命令，而不是笼统报「实际版本仍未更新」；
  - **构建脚本白名单（v0.9.2）**：`pnpm add` 因「需要执行构建脚本却未进 `allowBuilds`」失败时，面板会读取 pnpm 给出的建议条目、自动追加到 `pnpm-workspace.yaml` 的 `allowBuilds` 后重试（git-hosted 插件追加 `name@git-url#commit` 全量形式——pnpm 11 不再按裸包名匹配 git 依赖，见 [pnpm#12367](https://github.com/pnpm/pnpm/issues/12367)）；
  - 更新任务在模块级 store 里排队：运行期间的点击合并进下一批，**绝不并发启动两个 pnpm**；关闭面板后任务继续、重开可恢复进度；
  - **来源感知标注**：Desktop 托管的 `link:` 插件显示「随桌面版更新」，本地 `file:` / `workspace:` 插件显示「本地插件」，避免把 pnpm 退出码 0 误报成实际更新成功；官方/内置插件与本插件自身（`link:` 安装、保护开发链路）锁定不可更新；
- **自装插件卸载（v0.6）**：每张自装卡片带红色「卸载」按钮（最右）——点击先确认，执行 `pnpm remove`，执行中按钮置灰，完成后刷新列表；非自装插件与本插件自身会拒绝卸载；
- **判定零依赖、可单测**：包名作用域 + 用户显式安装集双重规则（`@deepseek-ai/` 判官方、其余判自装）；
- **完全可逆**：只读展示，不改任何官方 bundle，一条命令卸载。

## 界面截图

左侧菜单栏底部「插件目录」入口（v0.5 起），点击弹出按来源分组的插件面板，每张卡片带启用/停用开关：

顶部为更新区（「可更新: 1」「重新检查」「全部更新」），下方是搜索框与分组卡片；每张卡片带启用/停用、版本号、GitHub 按钮、更新状态与卸载按钮：

![插件目录](docs/plugin-catalog-updates.png)

## 快速开始

前置：`pnpm`、`dsh` CLI。

```sh
# 安装（推荐：从 npm）
dsh plugin --profile web add dsh-plugin-diraud

# 或从 GitHub 主分支
dsh plugin --profile web add https://github.com/tttwh/dsh-plugin-diraud/archive/refs/heads/main.tar.gz
```

装完**重启 `dsh web`**，然后：

- 聊天框输入 `/plugin-audit`，或
- 点击左侧菜单栏底部的「插件目录」入口。

## 命令一览

| 命令 | 用途 |
|---|---|
| `/plugin-audit` | 概览：自装逐行 + 官方计数 |
| `/plugin-audit user` | 只看自装 |
| `/plugin-audit official` | 只看官方（逐行） |
| `/plugin-audit <关键词>` | 按包名 / entry 过滤 |
| `/plugin-audit disable <关键词>` | 停用匹配到的**自装**插件（持久化） |
| `/plugin-audit enable <关键词>` | 启用匹配到的**自装**插件（持久化） |
| `dsh plugin --profile web remove dsh-plugin-diraud` | 卸载 |

> **开关如何持久化**：`disable/enable` 会把 `- id: <配置行原始 id>` + `disabled: true` 覆盖行写入 profile 的 `cordis.patch.yml`（用户配置层）。该文件被 dsh 的 `watchUserPatches` 监听（web profile 启动时会自动拉起监听），写入后即时生效；重启后保留；删除对应行即可恢复默认。
>
> **为什么是「原始 id」而不是 Loader 条目 id（v0.4 修复）**：Loader 树内条目的完整 id 带路径前缀（如 `include:ssh`），但 patch 层按配置行自身的 `id` 字段匹配，只认原始 id（`ssh`）。旧版本把带前缀的完整 id 写进 patch 文件，启动时被当作「找不到条目」跳过（每次 boot 打 warning），导致停用重启即失效——v0.4 起统一写原始 id，并自动清理历史遗留的 `include:<id>` 死行。

## 配置

来源判定默认按包名作用域；极端情况（如装了 `@deepseek-ai/` 前缀的第三方包）用 `extraUserPackages` 覆盖。在 profile 的 `cordis.patch.yml` 里覆盖本插件行：

```yaml
- id: plugin-audit
  config:
    extraUserPackages:
      - '@deepseek-ai/dsh-my-fork'   # 强制判为「自装」
```

## 目录结构

```text
dsh-plugin-diraud/
  src/classify.ts        来源判定纯函数（单一事实源，host / client 共用）
  src/patch.ts           cordis.patch.yml 读写：disable/enable 持久化（行级 YAML，零依赖）
  src/toggle.ts          开关共享核心（持久化 + ctx.loader.update 即时生效）
  src/updates.ts         更新核心：semver 比较、registry/git 探测、pnpm 执行（可单测）
  src/gitspec.ts         git-hosted 依赖解析与 lockfile/ls-remote 辅助（v0.9，纯函数可单测）
  src/metadata.ts        GitHub 仓库链接提取（repository/homepage 归一化，v0.8）
  src/translations.ts    双语描述字典（模块名 → {zh, en}，v0.6）
  src/render.ts          /plugin-audit 命令输出渲染（分组视图）
  src/contract.ts        typert wire 契约（pluginAudit/toggle，host/client 共用）
  src/typert.ts          host 侧 manifest 注册
  src/runtime.ts         PluginAuditRuntime（@Remote toggle，executeToggle 可单测）
  src/index.ts           host：/plugin-audit 命令 + 注册 remote
  src/client/            侧边栏「插件目录」入口（React）+ 开关按钮 + client remote
  src/client/updateStore.ts  更新任务模块级 store（排队 + 关闭面板后恢复）
  build.mjs              esbuild 构建（host ESM + client bundle）
  cordis.patch.yml       profile bundle patch（插入本插件）
  demo/                  演示与验证脚本（真实 profile 分组输出）
```

## 文档

- [DESIGN.md](DESIGN.md) —— 设计背景与查证依据（为何新增 tab 而非改官方 tab、来源判定规则与边界）

## 相关项目

- [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) —— DeepSeek Harness 官方仓库（DSH 本体）
- [omdsh-dev/community](https://github.com/omdsh-dev/community) —— 社区插件收录与协作入口

## 许可

[MIT](LICENSE) · Copyright (c) 2025 tttwh
