# @dsh-plugins/dsh-llm-hub 中文文档

规范地址：https://npmcn.com/package/%40dsh-plugins/dsh-llm-hub/

版本：1\.10\.1
npm 元数据更新时间：2026-11-10T13:19:45.875Z
中文译文更新时间：2026-11-12T23:00:32.249Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40dsh-plugins/dsh-llm-hub
原文对照：https://npmcn.com/package/%40dsh-plugins/dsh-llm-hub/?lang=original
项目主页：https://github.com/webkubor/dsh-llm-hub#readme
源码仓库：https://github.com/webkubor/dsh-llm-hub

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：465 kB，25 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- 运行依赖：0 个
- 首次发布：2026-09-17T12:11:05.149Z
- 最新版本发布：2026-11-10T13:19:45.337Z
- 近 30 天下载量：3433（2026-10-13 至 2026-11-11，npm 官方下载统计）
- 已发布版本数：32（近 12 个月 32 个）

## 中文简介

DSH 模型管理层：网关可达性探测、模型目录拉取、DeepSeek + MiniMax/智谱/Moonshot/StepFun 余额与配额、本月用量统计、余额预警、Provider 健康看板，再把本机装好的 codex / claude / agy 注册成子代理（装了才出现）。零运行时依赖，不改动 DSH 安装。

## 安装

```sh
npm install @dsh-plugins/dsh-llm-hub
```

也可以使用：`pnpm add @dsh-plugins/dsh-llm-hub`、`yarn add @dsh-plugins/dsh-llm-hub`、`bun add @dsh-plugins/dsh-llm-hub`

## README

![dsh-llm-hub](https://img.webkubor.online/oss/dsh-llm-hub/banner.png)

# 🔌 dsh-llm-hub

**给 DSH 模型页补上官方适配器缺的那半。**  
Gateway health, model catalog, balance — the half the built-in adapter leaves out.

[![npm](https://img.shields.io/npm/v/%40dsh-plugins%2Fdsh-llm-hub?style=for-the-badge&color=4C7EF3&logo=npm&label=npm)](https://www.npmjs.com/package/@dsh-plugins/dsh-llm-hub) [![downloads](https://img.shields.io/npm/dm/%40dsh-plugins%2Fdsh-llm-hub?style=for-the-badge&color=6d7f9c&label=downloads)](https://www.npmjs.com/package/@dsh-plugins/dsh-llm-hub) ![DSH](https://img.shields.io/badge/DSH-%E2%89%A50.1.1--rc.2-4d6bfe?style=for-the-badge) ![zero deps](https://img.shields.io/badge/runtime_deps-0-5A9E6F?style=for-the-badge) ![MIT](https://img.shields.io/badge/license-MIT-777?style=for-the-badge)

[![DSH Plugin](https://img.shields.io/badge/DeepSeek_Harness-Plugin-4d6bfe?style=for-the-badge)](https://github.com/deepseek-ai/deepseek-harness) [![dsh-plugin](https://img.shields.io/badge/topic-dsh--plugin-4d6bfe?style=for-the-badge)](https://github.com/topics/dsh-plugin) [![Awesome DSH Plugin 社区收录](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com/) [![llm-gateway](https://img.shields.io/badge/topic-llm--gateway-4C7EF3?style=for-the-badge)](https://github.com/topics/llm-gateway)  ·  [English](https://npmcn.com/package/%40dsh-plugins/dsh-llm-hub/README.en.md) · [更新日志](https://npmcn.com/package/%40dsh-plugins/dsh-llm-hub/CHANGELOG.md)

DSH 的模型页上，官方适配器有一半事情没做。这个插件把它补上：

|  | 官方适配器 | dsh-llm-hub |
| --- | --- | --- |
| DeepSeek 有哪些模型 | 看不到 | **一键拉取在售列表** |
| 账户还剩多少钱 | 看不到 | **卡片下常驻余额 + 阈值预警** |
| 网关通不通、多快 | 按钮点了没反应 | **实测延迟与状态 + 健康看板** |
| 网关上有多少模型 | 看不到 | **实测 71 个**（手填只有 11） |
| 为什么探测不了 | 无提示 | **写明「没配 baseURL」** |
| 本月花了多少 token | 没有 | **用量统计 + 前 5 个最常用模型 + CSV 导出** |
| 不可用的 provider | 仍在下拉里 | **下拉里静默摘掉，卡片上标红 + 原因** |
| 外部 agent CLI（codex/claude/agy） | 装不装无感 | **装了就出现子代理工具** |

![装上之后的模型页：每张 provider 卡片下方多出一行 —— 协议、接入地址、已配模型数、余额，右侧是探测与拉取目录](https://img.webkubor.online/oss/dsh-llm-hub/v060/models-piai-cards.png)  
装上之后的模型页：每张 provider 卡片下方多出一行 —— 协议、接入地址、已配模型数、余额，右侧是探测与拉取目录

## 装

> 前提：装好 Node.js 即可，**无需全局安装 dsh** —— 官方入口就是 `npx @deepseek-ai/dsh`。已全局安装的可把它换成 `dsh`。

```sh
npx @deepseek-ai/dsh plugin --profile <你的 profile> add @dsh-plugins/dsh-llm-hub
```

再把 `@dsh-plugins/dsh-llm-hub` 加进该 profile 的 `package.json` 的 `dsh.profile.bundles` 数组，然后重启 DSH。打开**设置 → 模型**，provider 卡片下方会多出一行。

boot graph 变了必须重启，热载不生效；`cordis.patch.yml` 由 bundle 机制自动 insert。

* * *

## 最新发布 · v1.5.1 (2026-09-26)

> **一句话**：套餐 vs 按量付费**智能识别 + 手动切换**；LLM 下拉套餐模型**自动置顶**； 修了一个永远只显示「LLM Hub 已激活」的家族面板 bug。

📦 [v1.5.1 on npm](https://www.npmjs.com/package/@dsh-plugins/dsh-llm-hub/v/1.5.1) · 🚀 [GitHub Release notes](https://github.com/webkubor/dsh-llm-hub/releases/tag/v1.5.1) · 📋 [完整更新日志](https://npmcn.com/package/%40dsh-plugins/dsh-llm-hub/CHANGELOG.md)

三件用户能看到的事：

1.  **套餐 vs 按量付费**：每个 provider 卡片新增 billing capsule，左边"按量付费"右边"套餐"， 自动按 5 层 fallback 识别云端账单类型（settings → `/status` → `/token_plan/remains` → `/usage/quota/limit` → `/user/balance`），手动点另一边切模式即写回 settings。 ![](https://cdn.jsdelivr.net/gh/webkubor/free-imgbed@main/dsh-llm-hub/02-billing-capsule.svg)
2.  **LLM 下拉套餐优先**：composer 模型选择器按当前 provider 的 billingMode 把套餐模型 置顶（绿色「套餐」徽章段），按量模型跟在后面。**不**自动改路由、不隐藏按量。 ![](https://cdn.jsdelivr.net/gh/webkubor/free-imgbed@main/dsh-llm-hub/03-model-dropdown-priorities.svg)
3.  **家族面板真实激活探测（修 bug）**：设置 → 模型 → 底部「Webkubor DSH 扩展家族」， 之前的版本不管装几个插件永远只亮自己。改读 `window.__DSH_BOOT__.entries` 做真实激活探测，读不到时显示「判定不可用」而非谎报「没装」。 ![](https://cdn.jsdelivr.net/gh/webkubor/free-imgbed@main/dsh-llm-hub/01-family-dock.svg)

升级：

```sh
npx @deepseek-ai/dsh plugin --profile <你的 profile> update @dsh-plugins/dsh-llm-hub@^1.5.1
# 重启 DSH
```

**没有破坏性变更**。旧版（≤ 1.3.x）的 `keyPool[provider]` 配置在 1.4.0 已删除，所有 key 走 `apiKeyEnv` 数组。

* * *

## 它补的是什么

DSH 自己已经具备全部机制，缺的只是"官方适配器没去用它们"：

| 能力 | 官方机制 | 官方直连的现状 |
| --- | --- | --- |
| 模型发现 | `llm` 服务的 `registerModelDiscovery(ns, discover)` + Models 页「获取可用模型」 | `@deepseek-ai/dsh-llm-deepseek` **从未注册**（`0.1.2-rc.1` 与 `0.1.5-rc.2` 两版实测 `discover` 均零命中） |
| provider 卡片扩展 | `settings.models.provider-card`（按 `settingsNs` 做 key 分发） | 无注册者 → 该区域不渲染 |
| 账户余额 | DeepSeek `GET /user/balance` | 适配器不暴露 |

**发现注册表每个 settings 命名空间只允许一个注册**（第二次抛 `DUPLICATE_DISCOVERY`）， 而 `llm-deepseek` 这个槽是空的 —— 本插件占上即可。官方 `slot-contract.d.ts` 也明确： 那两个扩展位就是给**本仓库之外分发的插件**用的。

## 用法

**模型发现**：设置 → 模型 → **DeepSeek（官方直连）** → **「获取可用模型」**。 点下去会实时 `GET https://api.deepseek.com/models`，列出官方在售模型供勾选加入。

**余额**：同一张 DeepSeek 卡片下方会出现余额行（挂载即查，可手动刷新）。

**pi-ai 旁路卡**：设置 → 模型 → 任一 pi-ai provider（modelgo / minimax / zai-coding-cn …）卡片下方：

-   常驻行：`pi-ai · 显示名 · 已配 N 个模型 · Key ✓/✗`
-   **探测网关**：实时 GET 网关目录端点（`/v1/models` 与 `/models` 按 baseURL 形态自动回退），报告可达性、延迟与在售数量
-   **modelgo 专属**：**拉取目录**列出网关在售模型（实测 71 个，手填仅 11 个），**复制全部 id** 后可直接粘贴整理
-   zai-coding-cn 这类没写 baseURL 的 provider 显示"无法探测"提示，模型仍走手填

![DeepSeek 卡片上的余额行](https://img.webkubor.online/oss/dsh-llm-hub/v051/models-deepseek-balance.png)

## 行为细节

### 连接事实

baseURL 与 apiKey 的解析顺序与适配器自身一致，且**每次调用惰性重读** `llm-deepseek` 设置段 —— 插件 apply 时该段可能尚未注册（启动竞态），而适配器本身也按请求重解析：

|  | 解析顺序 |
| --- | --- |
| baseURL | `request.baseURL` → 设置段 `baseURL` → `$DEEPSEEK_BASE_URL` → `https://api.deepseek.com` |
| apiKey | `request.apiKey`（表单里现填的一次性 key）→ 设置段 `apiKeyEnv` 指定的凭据 → 该环境变量 |

### 余额路由

`GET /api/dsh-llm-hub/balance` → `{ ok, isAvailable, balances: [{ currency, total, granted, toppedUp }] }`

金额**原样保留 DeepSeek 返回的字符串**（上游是字符串，避免浮点误差）。 只接受 `GET`/`HEAD`（否则 405），并拒绝跨站读取（`Sec-Fetch-Site` 非 same-origin/none 时 403） —— 余额属账户信息，即使服务绑在 loopback 也不该被跨站页面读走。

### pi-ai 旁路路由

官方 `@deepseek-ai/dsh-llm-pi-ai` **自己占用了** `llm-pi-ai` 的 discovery 坑（抢注会 `DUPLICATE_DISCOVERY`），且其 `LISTABLE_PROTOCOLS = {openai-completions, openai-responses}`：

| provider | api | baseURL | 官方发现 | 本插件 |
| --- | --- | --- | --- | --- |
| minimax | openai-completions | ✓ | **已可用**（无需本插件） | 探测卡 |
| modelgo | anthropic-messages | ✓ | 天然失效（协议不可列） | 探测卡 + 目录拉取/复制 |
| zai-coding-cn | — | ✗ | 不可用（无端点） | 提示手填 |

三条只读路由，全部 `GET`/`HEAD` 限定 + 同源校验（与余额路由同一套纪律）， provider profile 每次调用惰性重读 `llm-pi-ai` 段：

-   `GET /api/dsh-llm-hub/pi-ai/status?provider=<id>` → `{ ok, displayName, api, baseURL, apiKeyEnv, keyConfigured, modelCount }`
-   `GET /api/dsh-llm-hub/pi-ai/probe?provider=<id>` → `{ ok, reachable, latencyMs, remoteCount?, sample?, code?, error? }`
-   `GET /api/dsh-llm-hub/pi-ai/catalog?provider=<id>` → `{ ok, latencyMs, models: [{ id, name?, contextWindow?, maxTokens? }] }`

密钥解析与官方一致：凭据服务（`apiKeyEnv` 引用）→ 进程环境变量。

### 前端挂载点

`settings.models.provider-card`，`key = 'llm-deepseek'`。owner props 的 `keyConfigured` 决定是否发起查询：未配置密钥时显示提示而不请求。

## 模型下拉可用性（只留能用的）

composer 的模型下拉默认列出**所有已配置 provider**，与它们能不能调通无关 —— key 过期、余额耗尽、网关 401，都会照常出现在那里，点了才报错。本插件把「确凿不可用」 的 provider 从下拉里摘掉：

**判定只认确凿证据**（fail-open，拿不准一律保留 —— 误藏一个能用的，比多显示一个不能用的更糟）：

| 信号 | 触发隐藏的例子 |
| --- | --- |
| 凭据 | `apiKeyEnv` 指向的变量解析不到（本机 / 凭据库里都没有） |
| 探测 | 网关对目录端点回 `401`/`403`/`402` |
| 余额 | DeepSeek `/user/balance` 报 `is_available=false` 或余额为 0；MiniMax/智谱的配额确凿用尽 |
| 运行期 | 真实请求因 `INVALID_CREDENTIAL` / `QUOTA_EXCEEDED` 失败（监听 `agent/request-error`） |

**恢复**是自动的：改好 key / 充值之后，`settings/document-updated` 会让缓存立刻作废， 下一次读取即重探；真实请求成功也会立即撤销运行期标记。设置页右下角的 \*\*「重新探测全部」\*\*用于立即强制重来一遍。

**被隐藏的 provider 不会消失**：设置 → 模型 的卡片照常在，只是那张卡的动作条上会多一枚 红色状态片「已从下拉隐藏」（原因在悬停提示里）；页脚则给出全局的「已隐藏 N」和 **「重新探测全部」**。不再另设一块逐条重述的面板 —— 卡片上本来就有状态， 再来一份只是重复（2026-09-16 owner：「这不是很多余吗，上面不都是显示了吗」）。

![ModelGo 卡上的「已从下拉隐藏」状态片，以及页脚的「已隐藏 1 · 重新探测全部」](https://img.webkubor.online/oss/dsh-llm-hub/v060/availability.png)

### 实现方式与取舍

过滤落在 **host 半的 `ctx.llm.listProviders()`** 上：这是唯一能一次覆盖所有消费方的缝 （composer 下拉、`/model` 弹窗、子代理选择器、ACP 都读它），而且**只做减法、可随插件卸载还原**。 设置页读的是 configurable-provider 目录（`listConfigurableProviders`），所以不受影响。

也试过在 client 半包 `ctx.modelDirectories`，**不行**：那是 cordis 的 inject 追踪代理， 读出来的方法被包成 traceable proxy，插件 fiber 缺 `remote.session` 注入， 一调用就 `cannot get property "remote.session" without inject` —— 2026-09-16 实测会把官方模型座位整个打崩（座位从 composer 里消失）。 契约里 `conversation.input.model` 是 single slot、`replaceRisk: shadows-shipped-ui`， 顶掉它意味着自己复刻整套菜单并长期跟版，收益不值。

另一个反直觉的坑：**判定目标必须取自设置段，不能取自 `listProviders()`** —— 后者正是过滤器的输出，拿它当目标，被隐藏的 provider 就再也不会进入下一轮探测， 「隐藏即永久」。同理，cordis 服务的方法**不能**用 `!==` 校验是否替换成功 （traceable 代理每次访问都是新对象），要看属性描述符。

## 本月用量（1.0.0 起）

`llm/stream` 的 `usage` chunk 在 finish 之前到达；本插件观察流时把每次 in / out / cacheRead / cacheWrite tokens 累加，finish 时一次性写入 storageDomain 的 per-record table。**不算钱** —— 开源 DSH 的 llm 服务只暴露 `imageRequestPricing`，没有 chat 文本定价接口；给每个 provider 写硬编码价目表 既过时又快塌。把 token 用量留给用户自己对照官方价目表定价。

设置 → 模型 → 页脚 order 80 处渲染成一张「本月用量」卡： 输入 / 输出 / 缓存 / 合计 tokens + 调用次数 + 前 5 个最常用模型。

-   **导出 CSV**：按钮把全部记录写到剪贴板（id / provider / model / at / 各 token / finished），文件命名 `dsh-llm-hub-usage-YYYY-MM-DD.csv`。
-   **清空记录**：POST + `window.confirm` 两段式，避免被预取或前进后退误删。

storageDomain 用**局部注入** —— 缺席时本插件其余功能（余额 / 可用性 / harness）照常工作，没有用量统计只是少一张卡，不会让整个 boot 失败。

## 余额预警（1.0.0 起）

阈值在 `settings.dsh-llm-hub.warning`：

```yaml
dsh-llm-hub:
  warning:
    cashCNY: 10       # DeepSeek / moonshot / stepfun 等 cash 类，CNY 余额下限，默认 10 元
    planPercent: 10    # minimax / zhipu 等 plan 类，「余量」下限，默认 10%
```

新加 `/api/dsh-llm-hub/warning/check`：客户端把当前余额信封 POST 进来，host 半 按阈值判定，返回 `{ level: 'low'|null, text }`。**阈值数字不进响应** —— 这是服务端策略，不让前端能关掉；也不暴露 USD/USDT 之外的汇率换算细节。

余额数字低于阈值时变红 + 一枚 ⚠️ chip（鼠标悬停看具体提示）： 「DeepSeek 余额 ¥8.50 低于 10，充值一下」或「minimax 5h 余 8% 低于 10%」。

## Provider 健康看板（1.0.0 起）

设置 → 模型 → 页脚 order 70 处渲染成一张表，每行一个 provider：

| 列 | 含义 |
| --- | --- |
| 名称 | provider id + displayName |
| 状态 chip | `available`（绿）/ `unavailable`（红）/ `unknown`（灰） |
| 延迟 | 本次探测 `latencyMs`，探测没成则显示 `—` |
| HTTP | 上游状态码（401 / 402 / 5xx 一眼可分） |
| 原因 | 不可用/未知时给一句人话提示（截断 + title 出全文） |
| 探测时间 | 本地时间 `HH:MM:SS`，方便判断缓存新鲜度 |

数据来源 `/api/dsh-llm-hub/health`：复用 availability 的 verdict，附加 `latencyMs` 与 `status` 字段。`ensureAvailabilityFresh()` 触发后台重探 （5 分钟 TTL 过期），不阻塞响应。

「重新探测」按钮复用 `availability.recheck`，与下拉重探同一条底层刷新 路径 —— 不会双探。

## 智能路由建议（1.1.0 起；原 P1-5 改方案）

DSH 的 `conversation.input.model` 是 **single + user-controlled** slot （`replaceRisk: shadows-shipped-ui`，README 已钉死），没有供插件改值的 setter。 **所以本插件不做自动切换**，只给建议 —— 让你在 Settings 页一眼看到 「如果现在要切，应该选哪个」。

设置 → 模型 → 页脚 order 75 处的「当前路由」卡：显示当前激活的 (provider, model)

-   状态 chip、配置里的 primary 与 fallbacks\[\] 各自的可用状态。当 current 不可用时 卡片底部高亮「建议切换到 X」；全不可用时给一句人话 reason。

配置（settings.dsh-llm-hub.routing）：

```yaml
dsh-llm-hub:
  warning:
    cashCNY: 10
    planPercent: 10
  routing:
    primary: minimax/abab5.5-chat
    fallbacks:
      - deepseek-official/deepseek-chat
      - modelgo/gpt-4o
```

请求体（POST `/api/dsh-llm-hub/routing/resolve`）：

```json
{ "active": "modelgo/gpt-4o" }
```

返回：

```json
{
  "ok": true,
  "active": { "provider": "modelgo", "model": "gpt-4o", "state": "available" },
  "candidates": [
    { "provider": "minimax", "model": "abab5.5-chat", "state": "available" },
    { "provider": "deepseek-official", "model": "deepseek-chat", "state": "unavailable" },
    { "provider": "modelgo", "model": "gpt-4o", "state": "available" }
  ],
  "recommendation": { "provider": "minimax", "model": "abab5.5-chat" },
  "reason": null
}
```

判定语义：按 primary → fallbacks\[\] 顺序找第一个 `state === 'available'` 的 作为 recommendation；全不可用返 null + reason；未配置 routing 返 null + 「尚未在 settings.dsh-llm-hub.routing 配置主力模型 / fallbacks」。

为什么不做自动切换：DSH 的 composer 下拉是 session-scope 的 single slot，宿主 没暴露 setter；强行模拟键盘事件去点下拉既脆又破可访问性。「提示」+「一键跳」 的姿势比「替你点」更尊重用户当前的下一步动作。

## 模型别名（1.2.0 起）

把长 model id 翻成短显示名 —— `deepseek-reasoner` 渲染成 `推理 (deepseek-reasoner)`、 `claude-3.5-sonnet` 渲染成 `Sonnet (claude-3.5-sonnet)`。完整 id 永远在 title 里 （复制 id 粘到 settings.yaml 时不能是「推理」），但日常切模型眼睛扫得快。

配置（`settings.dsh-llm-hub.aliases`）：

```yaml
dsh-llm-hub:
  aliases:
    'deepseek-official/deepseek-reasoner': '推理'
    'modelgo/claude-3.5-sonnet': 'Sonnet'
    'modelgo/gpt-4o': 'GPT-4o'
```

key 形态：`provider/model`。provider 严格（要去 settings 段查），model 允许 `.`/`+`（DSH 真实 id 有 `claude-3.5-sonnet`、`deepseek-reasoner` 这种）。

值是空字符串 / 纯空白 / 非字符串 全部过滤；不会让「短名 = 」这种占位塞进 UI。

未配置 aliases 段时 UI 退回只显示 `model.id`，与没装本插件时一致。

## 多账号 key 轮换（1.4.0 改设计）

### 设计动机

**之前（1.3.0）**：`settings.dsh-llm-hub.keyPool[provider] = [{name, env}, ...]` 独立配置区域。 **1.4.0 改**：用户反馈「何必要单独做一个区域」——`apiKeyEnv` 字段直接支持 string\[\]， DSH 在同一个 provider 字段里多填几把 env 名就完事，**不需要去任何新区域**。 keyPool 概念 / 独立 slot / 独立 UI 卡 —— 全部删除。

### 配置形态

```yaml
llm-deepseek:
  apiKeyEnv: DEEPSEEK_API_KEY          # 老写法：1 把 key（向后兼容）
# 或
llm-deepseek:
  apiKeyEnv:                            # 新写法：多把 key 轮换
    - DEEPSEEK_API_KEY_1
    - DEEPSEEK_API_KEY_2
    - DEEPSEEK_API_KEY_3
```

**用户操作**：在 DSH 设置 → 模型 → DeepSeek 行的「API Key Env」字段，**多填几个 env 名**， DSH 自动 round-robin。其它流程（连接、探针、余额、统计）都不变。

### 行为

-   **单 key**（string）：行为与 1.3.0 完全一致 —— `apiKeyEnv: 'DEEPSEEK_API_KEY'`。
-   **多 key**（string\[\]）：host 半**按 provider 维护 round-robin 索引**，每次解析连接取下一个。
-   **失败 key 不自动跳过** —— 下次请求自然轮换过去。失败信息进 `runtimeMarks`， 下次探针会标出来。**不**藏、不假装能用、不静默吞请求。
-   **向后兼容**：所有 1.3.x 之前的配置（单 key）继续工作。

### 为什么删独立的"多账号 key 轮换"UI 卡

1.4.0 之前有一个「多账号 key 轮换」独立 card（在 footer order 73）。**反模式**：

-   用户故事是「我已经在 DeepSeek 那一行填过 env 名，再加几把就行」，不需要跳到任何新区域。
-   独立区域让用户看不到**和自己已配 provider 的关联**，新人更难看懂。
-   UI 跟 source-of-truth（settings.yaml 的 `apiKeyEnv` 字段）分离 —— 两份真相。

删了之后：

-   用户在 ModelsSection 同一个 DeepSeek 行加 env 名 → 多 key 自动轮换
-   debug 信息统一进「健康看板」卡（跟 availability / latency 一起看）
-   没有「我不知道去哪配置」这种认知成本

## 外部 harness 子代理（装了才出现）

把**本机已经装好**的外部 agent CLI 注册成 DSH 的子代理提供方，会话里就能把一段独立 任务甩给它们，各自烧各自的订阅额度：

| 工具 | 需要本机装 | 实际执行 |
| --- | --- | --- |
| `subagent_codex` | `codex` | `codex exec --skip-git-repo-check <任务>` |
| `subagent_claude_code` | `claude` | `claude -p <任务>` |
| `subagent_antigravity` | `agy` | `agy -p <任务> --dangerously-skip-permissions` |

**没装的不会出现。** provider 只在对应可执行文件真的存在时才注册，而 `dsh-tool-subagent` 对缺失的 provider 只打一条 info、把工具行延迟到 provider 出现才 注册。所以在没装 codex 的机器上 `subagent_codex` 根本不进工具目录，宿主照常启动 —— 不需要任何开关或配置。

探测走 `ctx.subprocess.resolveExecutable`（内核自己的解析器，与子进程执行时同一套 PATH 视图），**不自己扫 PATH**：登录 shell 的 alias 会骗过 `command -v`（`agy` 常被 alias 成带 `--dangerously-skip-permissions` 的形式），而 launchd 起的宿主进程压根读 不到 `.zshrc` 里的 PATH。

### 几个钉死的边界

-   **`agy` 必须带 `--dangerously-skip-permissions`**：headless print 模式下它会自动 拒绝 `command` 权限，于是任何碰文件或命令的任务都 **exit 0 且零输出**。少这个 flag 不报错，只让子代理「成功」地什么也没干。
-   **exit 0 + 空输出一律报错**，不折成 `completed`：否则父 agent 拿着空答案往下走。
-   **`codex` 带 `--skip-git-repo-check`**：父会话 cwd 不一定是 git 仓库，缺了直接拒跑。
-   **stderr 只留 8 KiB 尾巴**：agy 的 glog 在日志目录不可写时能喷 300+ 行，不能全进父日志。
-   子进程**不继承父上下文**，也不声明任何 start 能力（persona / 工具过滤 / 深度上限 / 结构化输出在另一个运行时里都管不到），如实声明让内核提前拒掉要这些能力的请求。

### 与官方 bundle、与 preset 手工行共存

-   官方的 `@deepseek-ai/dsh-subagent-codex` / `-claude-code` 注册的是同名 provider （`codex` / `claude-code`）。**先到的赢**：本插件遇到重名只记一行 info 跳过，剩下的 继续注册。想让本插件统一提供，把那两个官方 bundle 从 profile 里移除。
-   ⚠️ 如果你在 `~/.dsh/.agent-presets/*/agent.cordis.yml` 里手工加过 `tool-subagent-codex` 这类行，**删掉它们** —— 同一个 `toolName` 不能注册两次。

### 为什么内核符号是动态 import

`@deepseek-ai/dsh-subagent` / `-session` 由宿主提供（profile 里经 `.dsh-module-fallback` 解析），**仓库目录里解析不到**。顶部静态 import 会两头挨打： 仓库内跑测试直接 `MODULE_NOT_FOUND`，线上一旦某个宿主版本少了其中一个导出，整个插件 加载失败 —— 连余额和模型发现一起没了。惰性载入把风险关在这一段里。探测与注册这条 启动路径完全不碰内核 import（空能力声明就地内联），只有真正 spawn 子进程时才载入。

## 已知限制

**发现候选承载不了 `inputModalities`。** llm 服务只保留 `id`/`name`/`contextWindow`/`maxTokens` 四个字段。所以通过按钮加入的 `deepseek-flash` 会落成**纯文本**条目，而它实际支持图像输入。加入后请手动补：

```yaml
llm-deepseek:
  models:
    - id: deepseek-flash
      inputModalities: [ text, image ]
```

这是 harness 发现契约本身的限制（官方 pi-ai 那条路同样如此），插件层无法修正。

## 开发

```sh
npm run check     # 两半语法
npm test          # 回归测试（node:test，零依赖）
npm run deploy    # 同步到 web profile
```

测试在 `test/`，零依赖（只用 `node:test` + `node:assert`），CI 里跑。每条用例都钉住一个 **真实踩过的坑** —— 判定链很长（凭据 → 探测 → 余额 → 运行期），任何一环退化都不会有编译 错误，只会静默地把能用的模型藏起来，或者把不能用的留在下拉里：

### 发布

推 `v*` tag 即触发 `.github/workflows/publish.yml`：先在发布前**再跑一遍**语法 + 回归测试 + 发布产物校验（不依赖「之前某次 CI 应该跑过了」），再 `npm publish`，最后在 Release 缺失时 按 CHANGELOG 对应小节建。需要在仓库 secret 里配 `NPM_TOKEN`。

漏发或失败可以手动补，不用重推 tag：

```sh
gh workflow run publish.yml -f tag=v0.7.0
```

幂等由两道判断保证 —— tag 必须与 `package.json` 的 version 一致；npm 上已有该版本就跳过 publish。Release 的存在性单独探测，所以「npm 发成功但 Release 没建起来」也能靠重跑补上。

-   **host 半** `lib/index.js`：ESM（cordis loader 按 ESM 读）。
-   **client 半** `lib/client.js`：**源码即产物**，classic script（无顶层 import/export）， 经 `window.__ModuleLoader__.load({ id, factory })` 注册。`id` **必须与 package.json 的 `name` 完全一致**，否则 DSH 拒绝注册。React 由 `factory(require)` 提供，不打包进产物。 当前体量无需构建步骤；若将来拆多文件，再加 esbuild（`format: 'iife'`，React 等标 external）。

## 🧩 DSH 插件全家桶

![DSH 插件全家桶：主题美化 / 模型管理 / 用户记忆 / 电脑环境](https://img.webkubor.online/projects/dsh-plugins/dsh-plugins-family.png)

[🎨 主题美化](https://github.com/webkubor/dsh-bloom-theme) · [⚡ 模型管理](https://github.com/webkubor/dsh-llm-hub) · [🪞 用户记忆](https://github.com/webkubor/dsh-mirror) · [🖥️ 电脑环境](https://github.com/webkubor/dsh-env-inspector)

一行装齐（只需 Node.js），装完重启 DSH 即可。**把 `PROFILE` 换成你自己的 profile 名**：

```bash
PROFILE=<你的 profile>
npx -y @deepseek-ai/dsh plugin --profile "$PROFILE" add dsh-bloom-theme @dsh-plugins/dsh-llm-hub && PROFILE="$PROFILE" node -e 'const f=(process.env.DSH_HOME||require("os").homedir()+"/.dsh")+"/profiles/"+process.env.PROFILE+"/package.json",p=require(f),b=p.dsh.profile.bundles;for(const n of Object.keys(p.dependencies))if(/^(dsh-bloom-theme|@dsh-plugins\/)/.test(n)&&!b.includes(n))b.push(n);require("fs").writeFileSync(f,JSON.stringify(p,null,2)+"\n")'
```

> **桌面端用户**：官方 Electron 端独占 `desktop` profile，命令行对它做插件操作会被拒 （`profile "desktop" is managed exclusively by the Electron application`）—— 请在应用内的**插件管理**里安装与升级。下面的 CLI 写法只适用于自建 profile。

* * *

## License

MIT

## 常见问题

### @dsh-plugins/dsh-llm-hub 是什么？

DSH 模型管理层：网关可达性探测、模型目录拉取、DeepSeek \+ MiniMax/智谱/Moonshot/StepFun 余额与配额、本月用量统计、余额预警、Provider 健康看板，再把本机装好的 codex / claude / agy 注册成子代理（装了才出现）。零运行时依赖，不改动 DSH 安装。

### 如何安装 @dsh-plugins/dsh-llm-hub？

在项目目录运行 npm install @dsh-plugins/dsh-llm-hub，或使用 pnpm add / yarn add / bun add。

### @dsh-plugins/dsh-llm-hub 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @dsh-plugins/dsh-llm-hub 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @dsh-plugins/dsh-llm-hub 可以商用吗？

@dsh-plugins/dsh-llm-hub 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @dsh-plugins/dsh-llm-hub 的下载量有多少？

根据 npm 官方下载统计，@dsh-plugins/dsh-llm-hub 在 2026/10/13 – 2026/11/11 这 30 天内被下载 3,433 次，日均约 114 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @dsh-plugins/dsh-llm-hub 还在维护吗？

最新版本 1\.10\.1 发布于 2026/11/10（3 天前）；至今共发布 32 个版本，其中过去 12 个月发布了 32 个；首次发布于 2026/09/17。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

