- Home
- Plugin
- Modelli e provider
- dsh-aimy
dsh-aimy
sheep1x/dsh-aimy
Manages a relay station's many groups and rates for DSH, switching the station's billing group in place and refreshing the model list and rate on stations that expose /v1/token/group; stations without it fall back to read-only and keep serving models.
Installazione
dsh plugin --profile web add github:sheep1x/dsh-aimyREADME
dsh-Aimy
DSH(DeepSeek Harness)插件 dsh-Aimy:解决中转站多分组多倍率不方便管理的问题——把中转站作为模型来源接入 DSH,支持就地切换中转站分组,同步刷新模型列表与计费倍率。
配合的中转站接口:GET/PUT /v1/token/group(见 ../docs/dsh-aimy.md)。
入口
| 入口 | 插槽 | 说明 |
|---|---|---|
| 首页左侧栏图标 + 主区面板「Aimy-艾米」 | sidebar.panellist(order 30)+ main(key dsh-aimy) | 整页面板,撑满主区、自带滚动 |
- 面板 id、
main的 key、包名一致,都是dsh-aimy;sidebar 点图标时把activePanelId设成该 id,main据此命中。 - 两处注册都在
src/client/app.ts的apply()里,inject = ['slots']。 - client 侧必须用
react.createElement(可变 children);react/jsx-runtime的jsx(type, props, key)第三参是 key,会把h(type, props, ...children)的子节点全部丢掉。 - 面板头部与侧栏图标共用
src/shared/brand.ts里的品牌路径。
为什么不放设置页:宿主
dsh-client-ui-settings-general的navIcon(id)按分区 id 硬编码图标,只认account/models/agent-presets/plugins/archived-sessions, 其余一律回落到齿轮;settings.section插槽也只消费id/order/label,从不读icon, 插件侧无法覆盖。借用已知 id 同样不可行——它们已被内置分区占用,重复 id 会让renderSlot(only: active)同时渲染两个分区。所以只保留侧栏入口,避免设置页出现一行齿轮。
截图(市场详情页)
市场(dsh-market)详情页会展示插件截图。在你自己的仓库里声明,改图无需再提 PR:
// screenshots.json(与 package.json 同级)
["assets/screenshots/01-panel.png", "assets/screenshots/02-group.png"]
- 放图目录:
assets/screenshots/(见该目录下的 README) - 1–8 张;dsh-market 只展示前 6 张
- 支持相对路径(不得以
/开头、不得含..),或 GitHub 托管的 https 绝对 URL (raw.githubusercontent.com/user-images.githubusercontent.com/camo.githubusercontent.com/github.com)。第三方图床会被拒——截图 URL 会带上访问者 IP - 未声明时市场会从本 README 自动抽取图片(回退行为)
自检:npm run check:screenshots(校验格式、数量、主机白名单、文件是否存在)
当前未声明
screenshots.json:保留「从 README 自动抽取」的回退。 想显示真实面板截图时,把图放进assets/screenshots/再声明即可。
状态
- ✅ P1:包骨架、host 路由、侧栏面板、分组读取与切换、旧版降级
- ✅ P2:复用
@deepseek-ai/dsh-llm-pi-ai挂载模型路由,切组后重挂载刷新模型目录 - ✅ P4-1:面板里直接填密钥(写 DSH 凭据库,write-only),保存后立即探测并挂载模型
- ✅ P4-2:「当前选中模型不在新分组」提示(只提示,不自动改选)
- ✅ 双档案:Aimy-艾米(地址隐藏的预设)+ 兼容模式(自填 URL/协议,适配 new-api / sub2api)
- ⏳ 可选:插件市场打包(index 格式 + submission 校验)
安装
一条命令(DSH 自带 CLI)
dsh plugin --profile desktop add github:Sheep1x/dsh-aimy
--profile不能省:DSH 的plugin子命令要求显式指定 profile(省略会报--profile <name> is required)。- 这条命令把参数原样转发给 profile 目录下的 pnpm,装完仍需把
dsh-aimy加进dsh.profile.bundles(见下)。 - 仓库提交了
lib/,安装即得可加载的包,不需要allowBuilds构建授权。
方式一:npm 直接从 GitHub 安装(推荐,无需发布到 registry)
cd ~/.dsh/profiles/desktop
pnpm add github:Sheep1x/dsh-aimy # 或 npm i github:Sheep1x/dsh-aimy
然后在 profile 的 package.json 里把 dsh-aimy 加进 dsh.profile.bundles。
前置条件:本机要有
git(pnpm 的github:依赖靠它取包),且能访问 GitHub。仓库提交了构建产物
lib/,所以源码安装(dsh plugin add/pnpm add github:…/file:)拿到的就是可直接加载的包, 不依赖prepare在装机时构建(file:与dsh plugin add都不会跑prepare)。 改完源码请node build.mjs并一起提交lib/,否则用户装到的是旧代码。
方式二:本地路径
先把源码构建出 lib/,再装:
pnpm add file:D:/openwork/dsh-aimy
git clone https://github.com/Sheep1x/dsh-aimy.git
cd dsh-aimy
npm install # 装 esbuild 等构建依赖
npm run build # 产出 lib/index.js 与 lib/client.js
npm test # 31 项测试(host 27 + client 4)
然后在 DSH profile 里装本地目录,并把 dsh-aimy 加进 dsh.profile.bundles(~/.dsh/profiles/<profile>/package.json)。
cd ~/.dsh/profiles/desktop
pnpm add link:D:/openwork/dsh-aimy # 软链:build 完重启即生效
# 或 pnpm add file:D:/openwork/dsh-aimy # 快照:改完要 pnpm install --force
link:只在node_modules/dsh-aimy建一条软链;file:会按files字段打快照,源码变动 pnpm 未必自动刷新。 两种方式下 peer(@deepseek-ai/*)都由 DSH 的 profile 解析器按包名路由,不依赖本目录的node_modules。
安装(本地开发)
cd dsh-aimy
"C:/Program Files/nodejs/node.exe" build.mjs # 构建 lib/index.js + lib/client.js
然后在 DSH profile(~/.dsh/profiles/<profile>/package.json)里加依赖并加入 bundles:
cd ~/.dsh/profiles/desktop
pnpm add file:D:/openwork/one-hub/dsh-aimy
{
"dsh": { "profile": { "bundles": [ "...", "dsh-aimy" ] } }
}
重启 DSH 后,首页左侧栏出现「Aimy-艾米」图标,点开是整页面板。
两个 Tab / 两个档案
| Tab | 地址 | 密钥引用 | 路由名 | 显示名 | 能力 |
|---|---|---|---|---|---|
| Aimy-艾米(默认) | 内置 https://aimy.sheep1x.top,界面只读展示、不可编辑 | ONEHUB_API_KEY | onehub | Aimy-艾米 | 读模型 + 真实切分组 |
| 兼容模式 | 用户自填(写入 $DSH_HOME/dsh-aimy/profiles.json) | ONEHUB_COMPAT_API_KEY | onehub-compat | 兼容模式 | 读模型 + 调用;分组接口缺失时自动只读 |
- 两个档案互相独立:各自挂各自的 pi-ai 路由,模型选择器里按显示名区分,切 Tab 只是切配置页。
- 兼容模式适配 new-api / sub2api / 任意 OpenAI 兼容网关,可选
anthropic-messages协议。 - 分组接口是可选能力:
/v1/token/group返回 404 时只把supportsSwitch置 false,模型照常拉取与调用。
配置
插件配置在 profile 的 cordis.patch.yml(bundle 自带默认值,按需覆盖):
- id: dsh-aimy
name: dsh-aimy
config:
baseURL: https://aimy.sheep1x.top # 中转站根地址(不要带 /v1)
apiKeyRef: ONEHUB_API_KEY # 凭据引用名
group: '' # 期望分组(空 = 跟随令牌)
密钥不写进配置文件:优先级是
- 面板「API Key → 保存」写入的凭据库值(
ctx.credentials.set(apiKeyRef, …),write-only) - DSH 凭据库里已有的同名值
- 同名环境变量
保存后插件会立刻用它探测一次中转站,成功即挂载模型路由。点「清除」会 unset 该引用,
环境变量兜底重新生效。
兼容范围(重要)
| 站点能力 | 支持情况 |
|---|---|
GET /v1/models(标准 OpenAI 格式) | ✅ 必需。绝大多数中转站都有,new-api / sub2api / one-hub 均满足 |
| 调用模型 | ✅ 三种协议任选(Responses / Chat Completions / Anthropic) |
GET/PUT /v1/token/group(分组切换) | ⚠️ 可选能力。有则支持就地切组;没有则自动降级为只读 |
分组接口是可选扩展,不是所有站点都有。 new-api / sub2api 属于通用 OpenAI 兼容网关, 该接口是各站点自行加的,多数部署并没有实现。插件对此的处理是降级而非报错:
- 接口返回 404、返回
200但data里既无group也无groups、或返回success:false→ 一律视为「该站点不支持切换分组」,supportsSwitch置false; - 模型目录与模型调用完全不受影响,照常可用;
- 界面不会显示一个点了没反应的「切换分组」按钮。
因此:在任何 OpenAI 兼容中转站上都能拉到模型并用;只有提供该接口的站点才能就地切组。
本站(one-hub 系)实测 GET /v1/token/group 正常返回 group / groups / ratio。
前置条件(仅切换分组需要)
以下三条只在你想用分组切换时才需要;只拉模型用则无需满足:
- 站点已升级到包含
/v1/token/group的版本。 - 给 DSH 单独建一个令牌,并勾选 允许客户端切换分组。
- 该令牌的分组必须是「公开分组」或你自己账号所在的分组。
切组是令牌级状态:同一把 key 上其它客户端也会跟着换组,所以建议专用令牌。
路由
host 半侧在 /dsh-aimy-relay 前缀下暴露(仅供本插件的浏览器半侧调用):
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /state | 当前分组、倍率、可选分组、模型目录(命中缓存时不打网络) |
| POST | /refresh | 强制重新拉取分组与模型目录 |
| POST | /switch | { "group": "vip" } 真实切组,随后刷新目录 |
| POST | /credentials | { "apiKey": "sk-…" } 写入凭据库并立即探测/挂载 |
| DELETE | /credentials | 清除凭据库中的密钥 |
测试
"C:/Program Files/nodejs/node.exe" --test test/*.test.mjs
test/host.test.mjs:host 路由与挂载(假中转站 + 假 cordis ctx)test/client.test.mjs:client 半侧(走window.__ModuleLoader__契约,假 hooks 渲染真 React 元素树)- 三条注册齐全、侧栏 id 必须等于
main的 key - 侧栏图标是 svg 且跟随
size/active - 面板展示默认地址并标注「预设地址(不可修改)」
- 点地址 → 写剪贴板 → 提示「已复制」
- 三条注册齐全、侧栏 id 必须等于
host 覆盖(27 项):正常读取 / 真实切组 / 模型路由挂载 / 切组后重挂载 / 同分组不重挂载 / 中转站报错透传 / 空分组拦截 / 旧版站点降级 / 未配置密钥 / 保存密钥后自动挂载 / 空密钥拒绝 / 清除密钥 / 模型失效判定(在目录内 / 切走后失效 / 切回恢复 / 指向别的路由不误报)。
结构
src/
shared/types.ts 两侧共用类型
host/relay.ts 中转站 HTTP 客户端(错误归一)
host/state.ts 模型目录缓存($DSH_HOME/dsh-aimy/state.json,仅非密钥)
host/routes.ts /dsh-aimy-relay/* 的纯函数编排
host/index.ts Cordis 插件入口(name / inject / apply)
client/index.ts __ModuleLoader__ 外壳
client/app.ts 装配层:注册 sidebar.panellist + main
client/section.ts 「中转站」设置分区 UI
协议(三种,与 pi-ai 的 supportedProtocols() 一致)
openai-responses(默认)· openai-completions · anthropic-messages
协议在界面上是下拉框,两个 Tab 都能改:面板状态卡片里的「协议」一行,选中即保存
(POST /config?profile=aimy|compat,只传 api),改完服务端会按新协议重新挂载模型路由。
预设档案只开放协议这一项,地址仍固化在配置里、只读展示;兼容模式则地址与协议都可改,
只改协议时会保留已填地址。
协议变化必须触发重挂载:同一个模型集合在 completions / responses 下是两套线路,
sameModels() 只看模型会误判成「没变化」,所以 forceRemount() 会先丢掉旧路由
再重建(ProviderMount.api 记录当前线路用于比较)。
取值必须与 @deepseek-ai/dsh-llm-pi-ai 的 supportedProtocols() 完全一致,否则 pi-ai 的
Config 校验会报 names api …, which this build cannot serve。面板的协议下拉从
shared/types.ts 的 API_OPTIONS 生成,新增协议只需改那一处。
normalizeApi() 还接受常见别名(responses / completions / anthropic / messages /
claude …),未识别时回退到默认的 Responses。
地址怎么填(/v1 可填可不填)
OpenAI 系与 Anthropic 的 SDK 拼路径口径相反,所以插件按协议归整:
| 协议 | SDK 自动拼的路径 | 归整结果 |
|---|---|---|
openai-responses(默认) | …/responses | baseURL 必须以 /v1 结尾 → 没填自动补,已填不重复 |
openai-completions | …/chat/completions | 同上 |
anthropic-messages | …/v1/messages | baseURL 不能带 /v1 → 填了自动剥掉 |
容错(三种协议都适用):用户若整段粘贴了端点路径(…/v1/chat/completions、…/v1/messages),
会先被剥回根地址;我们自己的模型/分组拉取也会用 apiRoot() 做同样归整,
所以不会出现 /v1/v1/models。
因此面板里填 https://gateway.example.com 或 https://gateway.example.com/v1 都能用。
切组后模型失效怎么处理
切组会改变可用模型集合。若当前默认模型(ctx.agentDefaultModel.currentSelection())指向本插件的路由、
但已不在新分组的目录里,插件会:
- 状态里置
staleModel: true,并附一条 warning:「当前选中的模型 xxx 不在新分组里,请重新选择模型」 - 面板显示红色提示行
绝不自动改选模型——静默替换会让用户在不知情的情况下换了计费对象。
若选择指向其它路由(不是本插件的 routeName),不算失效。
模型怎么进入 DSH 的模型选择器
挂载 = 把官方 @deepseek-ai/dsh-llm-pi-ai 作为子插件注册进本插件 fiber,它会调用 ctx.llm
把 { routeName, displayName, models } 声明成一个 provider 路由;模型选择器读的就是这份声明。
两个关键点(都踩过坑):
-
挂的是模块命名空间,不是
PiAiAdapter类。 真包导出export { Config, PiAiAdapter, apply, inject, name, … },其中PiAiAdapter是class extends LlmAdapter的适配器实现类,没有apply。把它传给ctx.plugin()不会报错,但什么 provider 都不会注册 → 面板一切正常、模型选择器里一个模型都没有。pickPlugin()因此优先挑「带apply的对象/函数插件」,只给类时直接抛错。 -
启动就要挂,不能等面板被打开。 之前只有
/state、/refresh里才syncProvider,所以模型要等用户点开「中转站」面板才出现。 现在apply()结束时用$DSH_HOME/dsh-aimy/state.json里缓存的目录做一次warmStart(): 冷启动立刻挂上(分组信息要联网才知道,预热用最近一次目录即可,面板刷新后按真实分组重挂)。 缓存命中的/state分支也会补一次syncProvider,避免「进程起来了但路由没挂」。
模型路由怎么挂上的
插件把官方 @deepseek-ai/dsh-llm-pi-ai 挂进自己的 fiber:
ctx.plugin(PiAiAdapter, {
providers: {
onehub: {
api: 'openai-responses',
baseURL: 'https://你的站点/v1',
apiKeyEnv: 'ONEHUB_API_KEY',
defaultContextWindow: 1000000,
defaultMaxTokens: 8192,
models: [{ id: 'deepseek-v4.1-flash', contextWindow: 1000000, input: ['text', 'image'] }, ...], // 来自 GET /v1/models
},
},
})
为什么每个模型都要写明 contextWindow 与 input:pi-ai 的解析顺序是
entry.contextWindow ?? 目录值 ?? provider.defaultContextWindow,input 则是
declaredInput(entry.input) ?? provider.defaultInput(declaredInput 对缺省和空数组都返回
undefined)。所以只写 provider 级默认值时,模型在选择器里会显示成默认的小窗口、模态退化成纯文本。
本插件统一给每个模型打上 contextWindow: 1000000 和 input: ['text','image'],
provider 级再兜一层 defaultInput。注意这不只是标签:dsh-compaction-basic 的压缩阈值、
溢出判定、用量压力百分比都是拿 contextWindow 算的,131072 → 1000000 会让压缩推迟约 7.6 倍。
图片模态由面板上的「图片输入」开关控制(默认开),关掉后模型只声明 text,
贴图会在附件阶段就被拒绝,而不是等上游在整轮中途报错。
为什么切组要重挂载:pi-ai 的 providers 是 schemastery 的 volatile 字段,但
createVolatile() 返回的是冻结快照,写入通道是内部 symbol(只有 Loader 能提交),
插件侧无法原地修改。所以目录变化时 dispose 旧 fiber、用新配置再挂一次;
模型集合未变时短路跳过(避免无意义抖动)。
License
MIT © Sheep1x
注意区分用词:本文档里的「协议」若出现在「三种协议 / 线路协议 / OpenAI Responses」 语境下,指的是模型线路协议(wire protocol);开源许可一律写作「License」。
Plugin correlati
dsh-our-free-model
ebony-vinyl/dsh-our-free-model
dsh-routing-suite
yjh051108/dsh-routing-suite
dsh-workbuddy-connect
corrinehu/dsh-workbuddy-connect
dsh-connect-workbuddy
dingminhua/dsh-connect-workbuddy