# @fhxgs/dsh-model-hub **为 DeepSeek Harness 提供统一的模型提供方登录、目录管理与会话路由能力。** [English](README.md) | [简体中文](README.zh.md) [![npm version](https://img.shields.io/npm/v/%40fhxgs%2Fdsh-model-hub?color=cb3837&logo=npm&logoColor=white)](https://www.npmjs.com/package/@fhxgs/dsh-model-hub) [![license MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org) [![pnpm](https://img.shields.io/badge/pnpm-required-F69220?logo=pnpm&logoColor=white)](https://pnpm.io) [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org) [![DSH plugin](https://img.shields.io/badge/DSH-plugin-6E56CF)](#兼容性)
--- `@fhxgs/dsh-model-hub` 是 DeepSeek Harness (DSH) 的一体化模型管理插件。它在单个 npm 包中集成了 Node.js Host 服务端与按需加载的浏览器 Client 端。凡是本插件自己承担的业务——提供方生命周期、登录会话、模型目录、会话选择——都走它自有的、仅限本机访问的 `/model-hub` Loopback RPC 通道;凡是 Harness 本就承担的能力则继续走官方 `/api`:settings 的读写、凭据的描述与写入、端点模型发现,以及全局默认模型背后的宿主模型列表。插件是接入 Harness,而不是绕开它自己另开一套通道。 安装本插件后,它将完整接管原本分散的模型配置体验:提供统一的设置页面、输入框模型选择气泡以及 `/model` 快捷指令。 ## 核心特性 | 功能模块 | 说明 | | --- | --- | | **提供方登录 (Sign-in)** | 在浏览器中直接驱动 OAuth / 设备码授权流程。支持实时状态轮询、交互问答面板以及 30 分钟 Host 硬超时。凭据直达底层安全存储,绝不进入快照、日志或监控指标。 | | **生命周期管理** | 提供明确的写操作入口(`activate` 激活、`deactivate` 停用、`logout` 登出、`useRecord` 改用凭据),每次操作均附带明确的影响提示与二次确认,杜绝黑盒覆盖。 | | **提供方参数编辑** | 支持安全写入 API Key(通过 `credentials.set`,绝不混入 settings 配置)、自定义 Base URL、通信协议、显示名称及模型映射,支持端点模型自动发现与自定义 Provider 声明。 | | **增强型模型目录** | 原生 `llm.models` 的严格超集,补齐了原生裁剪掉的 `inputModalities`(输入模态)、`contextWindow`(上下文窗口)与 `defaultMaxTokens`。具备版本感知缓存、8 并发请求限流与单模型故障隔离。 | | **模型策展策略 (Curation)** | 支持全量展示 (`all`) 或白名单筛选 (`include`)。设置页勾选与输入框选择器实时联动,确保两者看到的数据完全一致。 | | **会话选择与路由** | 内存级会话模型绑定,配合 `{ prepend: true }` 确保路由准确注入。子代理支持 3 级路由策略(显式配置 > 静态默认 > 继承父会话)。前端主动拦截不可路由的消息发送。 | | **思考强度调节 (Effort)** | 离散滑块档位根据当前模型实际支持的 `reasoning.efforts` 动态派生(如 `gpt-5.6-sol` 显示 `xhigh`/`max`,非推理模型自动隐藏)。 | | **极速模式 (Fast Mode)** | 元数据驱动的开关,仅对声明了加速服务梯队(如 `service_tier: priority`)的模型展示。 | | **内置自建提供方** | 自带独立适配器与 OAuth 实现,内置支持 `qwen-code`(千问代码:基于 `chat.qwen.ai` 的 RFC 8628 设备码授权)与 `codex`(OpenAI Codex:授权码 + PKCE 回调,支持 7 款 GPT-5.x 模型)。 | | **原生双语界面** | 深度集成 Harness 语言服务,界面完整支持简体中文与英文,随系统语言自动切换。 | | **安全隔离** | 所有 `/model-hub` 接口均强制限制为 `authority: 'loopback'`(仅限本机),采用严格的 Zod Schema 校验,对外输出脱敏的统一错误结构。 | ## 安装说明 > [!NOTE] > `dsh plugin` 底层通过 pnpm 进行包管理 (`spawnSync('pnpm')`),**请确保当前环境变量 `PATH` 中包含 pnpm**。若未全局安装,可通过 corepack 启用: > ```sh > corepack enable pnpm > ``` 将插件添加至目标 Profile: ```sh dsh plugin --profile web add @fhxgs/dsh-model-hub ``` **安装后请重启该 Profile**(组合变更不支持热重载)。 插件会自动应用自带的补丁配置 (`cordis.patch.yml`),启用 `@deepseek-ai/dsh-authorization` 并自动禁用官方默认的 `ui-settings-models` 与 `ui-model-selection` 组件,**无需手动修改配置**。 卸载插件: ```sh dsh plugin --profile web remove @fhxgs/dsh-model-hub ``` 卸载后会自动恢复官方原生的 Models 设置页与选择器。 ### 关于 Peer 依赖告警 `dsh plugin add` 底层调用 pnpm,而 pnpm 只按 Profile 目录本身解析 peer 依赖。Harness 并不会把自己的包装进那个目录:Host 侧的 `@deepseek-ai/*` 导入与浏览器侧的平台模块,都由运行中的宿主在运行期通过模块表提供。因此这 18 个 peer 已声明为 `optional`——它们的版本范围表达的是「本次构建针对哪一版 Harness 编写」,而不是「请包管理器去下载」——冷装隔离 Profile 时不再报告它们缺失。 保留为必需的只有两个,因为它们是真的可能缺失、告警值得保留信号的两个: ``` Issues with peer dependencies found ✕ missing peer @earendil-works/pi-ai ✕ missing peer react ``` 冷装时出现这段告警属于预期,插件照常加载——`@earendil-works/pi-ai` 由 Host 运行闭包提供,`react` 由 Web 宿主在任何插件工厂运行之前就写入冻结模块表。若告警里出现的是别的包名,那才值得深究。 ## 快速上手 1. 在 **运行 DSH 的本机浏览器** 中打开 Web 界面(所有操作均需 Loopback 权限)。 2. 进入 **设置 → Model Hub → 提供方**。选择目标提供方并完成授权登录。 3. 在卡片中点击 **激活** 启用该路由。 4. 切换至 **目录** 选项卡,勾选希望在对话框中使用的模型。 5. 在任意会话中,点击输入框下方的模型选择气泡或输入 `/model` 即可切换模型与思考强度。 ## 配置项 插件在 settings 的 `model-hub` 命名空间下管理配置: ```yaml model-hub: picker: mode: include # 'all' | 'include' (默认展示所有模型) include: # 当 mode 为 'include' 时生效 - { provider: kimi-coding, model: k3 } fastMode: # 已开启加速梯队的路由 - { provider: codex, model: gpt-5.6-sol } preferredEffort: high # 可选:默认思考强度偏好 subagent: inherit # 'inherit' | { provider, model, reasoningEffort? } ``` - **空配置解析**:缺省时解析为 `{ picker: { include: [], fastMode: [] }, subagent: 'inherit' }`。 - **全局默认模型**:仍保存在官方 `agent-default-model` 命名空间中。Host 端仅读取该配置,仅当用户在前端破坏性确认框中明确勾选时才会调用官方 CAS 接口进行修改。 - **内置 Provider 状态**:独立保存在 `model-hub-providers` 命名空间,避免策展策略变更时误触发模型目录缓存失效。 ## 界面预览 截自 DSH 0.1.1-rc.2 冷装隔离 profile,暗色主题。 | Composer 选择气泡(Simple) | Providers 面板 | Catalog 面板 | |---|---|---| | ![Composer 选择气泡与推理力度滑块](https://raw.githubusercontent.com/yhyfhgs/dsh-model-hub/main/docs/screenshots/composer-picker.png) | ![Model Hub 设置页 Providers 面板](https://raw.githubusercontent.com/yhyfhgs/dsh-model-hub/main/docs/screenshots/settings-providers.png) | ![Model Hub 设置页 Catalog 面板](https://raw.githubusercontent.com/yhyfhgs/dsh-model-hub/main/docs/screenshots/settings-catalog.png) | ## 架构概览 插件采用单包同构设计: - **Host 端 (Node.js ESM)**:注册 `/model-hub` RPC 路由、Settings 命名空间、内置 Provider 适配器以及模型拦截分发钩子。 - **Client 端 (按需 CJS)**:在浏览器中动态挂载,提供设置页面、输入框选择组件与 `/model` 弹窗。 ``` src/ ├── index.ts # Host 插件入口:注册 RPC 通道、命名空间与装配生命周期 ├── rpc/ # 单层 Wire 信封定义、路由器与 7 个标准错误码 ├── auth/ # Authorization 交互桥接与 auth.state 两轴状态投影 ├── provider/ # 适配器绑定与四大生命周期动作 (activate/deactivate/logout/useRecord) ├── provider/native/ # 自建 Provider 域 (qwen-code 与 codex 的 OAuth 流程及适配器) ├── catalog/ # 增强型模型目录、策展过滤逻辑与 LRU 缓存 ├── selection/ # 会话模型选择、思考强度决策链与子代理 3 级路由 ├── settings/ # Settings Schema 定义 └── client/ # 浏览器 UI 源码 (设置卡片、输入框气泡、交互面板等) ``` 打包产物位于 `lib/index.js`、`lib/invariant.js`、`lib/client.js`、`lib/types/**` 以及 `cordis.patch.yml`。 > [!IMPORTANT] > **`./client` 导出仅供 Loader 使用。** `lib/client.js` 并不是任何一方去 import 的模块,它的整个函数体就是一次 `window.__ModuleLoader__.load({ ... })` 调用,由宿主把该文件读出来直接投给浏览器,而不是去解析它——无论文件后缀是什么,Node 的 `import()` 与打包器的 `require()` 都会抛错。声明这个导出只是为了让宿主能按名字定位该文件,它不是对外 API,DSH 浏览器运行时之外没有任何一方能消费它。这也正是 `publint` 对本包只报一条发现(建议把该文件改成 `.cjs`)、而这条被明确记录并驳回而非静音的原因:改名只会让告警消失,并不会让这个导出变得可用。完整理由写在 `scripts/verify-manifest.mjs` 里,豁免按报告原文逐字匹配,因此换成另一条发现照样会让门禁变红。 ## 开发与验证 ```sh corepack pnpm install corepack pnpm run build # 编译 lib/types (tsc) 与运行时产物 (tsdown) corepack pnpm run verify # 五道门:产物纯度、Patch 键名、SourceMap 链接、发布路径、manifest corepack pnpm run test # 运行 Vitest 单元测试 corepack pnpm run smoke:p0 # 将 Tarball 冷装至隔离临时 Profile 进行冒烟验证 ``` - `prepack` 会在 `npm pack` 或 `npm publish` 前执行 `verify` 门禁,拦截脏产物或路径泄漏;`prepublishOnly` 还会先清空 `lib/` 重新构建并跑全量测试,因此一次发布不可能带上陈旧产物。 - 冒烟测试 (`smoke:p0`) 使用一次性临时目录 (`DSH_HOME=$(mktemp -d)`),绝对不会影响本地的 `~/.dsh` 配置。 - 关键功能与阻断项审计清单请参见 [`scripts/gate-p1.zh.md`](https://github.com/yhyfhgs/dsh-model-hub/blob/main/scripts/gate-p1.zh.md) 与 [`scripts/gate-p3.zh.md`](https://github.com/yhyfhgs/dsh-model-hub/blob/main/scripts/gate-p3.zh.md)。 - 标准发布流程请参见 [`scripts/release-checklist.zh.md`](https://github.com/yhyfhgs/dsh-model-hub/blob/main/scripts/release-checklist.zh.md)。 > 上述三份文档位于 `scripts/`,该目录刻意不进 npm 发布包,因此这里一律使用绝对链接:无论从 npm 页面还是从代码检出,都能正常打开。 ## 兼容性 | 依赖项 | 支持范围 | | --- | --- | | Node.js | `^22.19 \|\| >=24` | | DeepSeek Harness | `0.1.1-rc.2` | | `@deepseek-ai/*` 依赖 | `^0.1.1-rc.2` | | `@deepseek-ai/cordis` | `^4.0.1` | | `@earendil-works/pi-ai` | `~0.82.1`(Peer 依赖,由 Host 运行闭包提供) | | React | `^18.2.0`(Peer 依赖) | ### 已知限制 1. **仅限本机访问 (Loopback Only)**:非本机请求访问 `/model-hub` 接口均返回 403。远程访问时界面展示只读标识,如需修改模型配置请在运行 DSH 的机器上操作。 2. **官方模型设置接管**:插件生效期间,官方原生 Models 页面与相关向导步骤会被自动禁用。 3. **无后台主动长连接推送**:界面刷新依赖 Host 事件转发以及登录面板活跃期的定时轮询。 4. **零消息会话选择不持久化**:未发送过任何消息的新建会话,其临时选中的模型在服务重启后不会保留(与官方 `/model` 行为一致)。 5. **前端输入拦截边界**:前端输入框的未配置路由拦截仅作用于 Web 界面;Headless 或 SDK 发起的请求由 Host 服务端直接校验并拦截。 6. **Codex 计费显示为零**:由于 OpenAI Codex 属于套餐订阅制,与普通 API Token 计费模式不同,因此用量计费显示为 0。 7. **OAuth 回调端口固定**:内置 Codex 路由只能在其客户端注册的那一个端口上接收浏览器回调(`http://localhost:1455/auth/callback`),因此同一台机器同时只能进行一次该路由的登录;端口被占用时登录会立即以 `LOOPBACK_PORT_IN_USE` 结束而不是干等,且该路由没有设备码回退方式。回调接收器会同时绑定两个回环地址;若 `::1` 无法绑定,登录继续在 `127.0.0.1` 上完成,并记录 `LOOPBACK_IPV6_UNAVAILABLE`。 8. **Grant 可指定 API Host**:OAuth 授权可以带回该账号被路由到的 API Host,但只有其颁发方确实会下发该字段的路由才会采纳(Qwen Code 会,Codex 不会——该路由上存储的 Host 一律忽略)。被采纳的 Host 必须是 HTTPS,且不得带用户名密码、查询串或片段,也不得是另一个内置路由自己的 Host;不满足时该字段被丢弃,路由回落到厂商默认地址。明文 HTTP 一律拒绝,回环地址也不例外。 9. **会话选择与消息准入分属两个 Owner**:在本插件里选模型,和 Core 判定是否接收一条消息,是彼此独立的两套状态。Core 对每条消息都按自己的链(`picked` ▸ 请求头 ▸ 全局默认)判定,而本插件刻意从不写这条链。由此有两个后果:切换到支持图片的模型,并不能让已经放进输入框的那张图片发出去——Core 仍按自己链上的模型判定,拒绝信息里写的也是那个模型,而不是刚选中的;以及当全局默认指向没有任何适配器提供的 Provider 时,对尚未成功发出过请求的会话,Core 会拒绝**所有**消息(文本同样如此,不只是图片),无论这里选了什么,需要先改选或清除全局默认才能解除。 10. **会话选择的持久化边界**:上文*零消息会话选择不持久化*的成因。会话的选择存放在内存引用里,真正的持久记录是 `request/header` 事件,只有成功发出一次请求才会写下。在那次请求之前,任何读取该持久记录的一方都看不到这次选择——包括 Core 自己的准入链——服务重启后该会话回落到全局默认。 11. **被 Core 自身模型切换接管过的会话**:即使官方 Models 页面被禁用,Core 自己的 `session.selectModel` 仍可被 ACP 与 SDK 客户端调用;它写入的那一层在 Core 链中优先级高于请求头,且在该会话的 Agent 存活期内没有任何清除路径。被这样切换过的会话,此后一律按 Core 的选择做准入,在本插件里的选择不再与之收敛,无论该会话之后又成功发出过多少次请求。 ## 开源协议 [MIT](./LICENSE) © 2026 FHGS