为模型列表补上厂商图标,并管理多组服务商密钥
30.7/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/20 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add koliy82/dsh-rustvy该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · model
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 12 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 30 处,展示前 26 条)
- 读取本地文件lib/index.js:105
return readdirSync(HERE) - 读取本地文件lib/index.js:108
const stats = statSync(join(HERE, entry)) - 发起网络请求client/client.js:1292
const response = await fetch(`${API}/catalog.json`, { headers: { accept: 'application/json' } }) - 发起网络请求client/client.js:1323
const probe = await fetch(`${API}/logo/${encodeURIComponent(icon)}`, { headers: { accept: 'image/svg+xml' } }) - 发起网络请求client/client.js:1446
const response = await fetch(`${API}/uptime.json?range=${encodeURIComponent(want)}`, { - 发起网络请求client/client.js:1615
const response = await fetch(`${API}${path}`, { headers: { accept: 'application/json' } }) - 发起网络请求client/client.js:1662
const response = await fetch(`${API}/keys`, { - 发起网络请求client/client.js:1700
const response = await fetch(`${API}/session-key`, { - 写入剪贴板client/client.js:1948
await navigator.clipboard.writeText(String(text)) - 读取本地文件lib/errors.js:71
const manifest = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8')) - 写入本地文件lib/errors.js:741
mkdirSync(dirname(file), { recursive: true }) - 写入本地文件lib/errors.js:743
writeFileSync(temporary, data) - 写入本地文件lib/errors.js:744
renameSync(temporary, file) - 读取本地文件lib/errors.js:764
const stored = JSON.parse(readFileSync(file, 'utf8')) - 写入本地文件lib/host.js:376
mkdirSync(dirname(file), { recursive: true }) - 写入本地文件lib/host.js:378
writeFileSync(temporary, data) - 写入本地文件lib/host.js:379
renameSync(temporary, file) - 读取本地文件lib/host.js:389
return JSON.parse(readFileSync(file, 'utf8')) - 读取本地文件lib/host.js:608
return readdirSync(logosDir()).filter(entry => entry.toLowerCase().endsWith('.svg')).length - 读取本地文件lib/host.js:622
return statSync(file).isFile() ? readFileSync(file) : undefined - 读取本地文件lib/host.js:622
return statSync(file).isFile() ? readFileSync(file) : undefined - 读取本地文件lib/host.js:1306
backup: existsSync(modelsBackupFile()) ? readJsonFile(modelsBackupFile())?.at ?? null : null, - 读取本地文件lib/host.js:4501
if (existsSync(stateDir())) mirror.logos = countCachedLogos() - 发起网络请求lib/net.js:264
const request = transport.request({ - 发起网络请求lib/net.js:354
const request = transport.request({ - 发起网络请求lib/net.js:419
const response = await fetch(url, {
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
DeepSeek Harness 模型选择器里的厂商标记,以及一份与服务商保持同步的模型列表
README
由 DeepSeek 最新模型翻译生成通过作者的推荐链接注册 Rustvy: <https://rustvy.xyz/r/Z6LJC8FH> 通过此链接注册,可获得 10% 折扣和奖励 token。
活动详情:<https://rustvy.xyz/dashboard/referral>
Rustvy(dsh-rustvy)
俄语 | 英语
DeepSeek Harness 模型列表里的厂商图标,一份与服务商同样新鲜的模型列表,以及同一配置档里的多个服务商密钥——每个会话用自己选中的那个密钥发请求。一个插件,一份镜像,无需重启。
它做什么
插件有三项任务。前两项共用同一份服务商公开目录镜像;第三项不需要镜像,只需要配置档的路由。
图标。 选择列表里每行模型前面一个单色厂商图标,画法与厂商自己的面板一致:CSS 遮罩,用 currentColor 填充,所以每个厂商一个文件就能覆盖两套主题和行的所有状态——悬停、选中、禁用——不用做颜色变体。
| 位置 | 出现什么 |
| 输入框的模型菜单 | 每行模型前一个 16 px 单色图标 |
| 输入框的模型按钮 | 所选模型的图标,在名称前(14 px) |
| /model 命令的弹窗 | 每行同样的图标 |
| 重绘列表的皮肤(dsh-claude-style) | 同样的图标;该行里皮肤自带的徽标会隐藏,避免两个徽标撞在一起 |
模型列表。
展开完整文档(README 剩余 1 节)
通过作者的推荐链接注册 Rustvy: <https://rustvy.xyz/r/Z6LJC8FH> 通过此链接注册,可获得 10% 折扣和奖励 token。
活动详情:<https://rustvy.xyz/dashboard/referral>
Rustvy(dsh-rustvy)
俄语 | 英语
DeepSeek Harness 模型列表里的厂商图标,一份与服务商同样新鲜的模型列表,以及同一配置档里的多个服务商密钥——每个会话用自己选中的那个密钥发请求。一个插件,一份镜像,无需重启。
它做什么
插件有三项任务。前两项共用同一份服务商公开目录镜像;第三项不需要镜像,只需要配置档的路由。
图标。 选择列表里每行模型前面一个单色厂商图标,画法与厂商自己的面板一致:CSS 遮罩,用 currentColor 填充,所以每个厂商一个文件就能覆盖两套主题和行的所有状态——悬停、选中、禁用——不用做颜色变体。
| 位置 | 出现什么 |
| 输入框的模型菜单 | 每行模型前一个 16 px 单色图标 |
| 输入框的模型按钮 | 所选模型的图标,在名称前(14 px) |
| /model 命令的弹窗 | 每行同样的图标 |
| 重绘列表的皮肤(dsh-claude-style) | 同样的图标;该行里皮肤自带的徽标会隐藏,避免两个徽标撞在一起 |
模型列表。 配置档通过手动声明的路由 @deepseek-ai/dsh-llm-pi-ai 接入第三方服务商,这条路由的 models 列表决定了界面上到底能选什么:只有路由返回的模型,harness 才会提供。手写的列表在服务商发布新模型那天就过时了——新模型看不见,已下架的模型仍可选,直到发请求才报错。插件读取路由自带端点返回的内容,转成适配器记录的确切格式,显示与已安装列表的差异,并按命令通过 ctx.configEditor 写回去。
密钥。 同一配置档里的多个服务商密钥,以及单个会话自己的密钥——在输入框工具栏里选择,就在访问模式列表后面。密钥在插件里直接添加,点 「+」 按钮:不用碰 .env,不用碰 cordis.patch.yml,也不用碰 Models 页面,选择在重启后依然保留。DeepSeek 账号不受影响:替换只作用于本插件的路由。
| 位置 | 出现的内容 |
| 设置 → Rustvy | 「API 密钥」分区:密钥列表、「+」 按钮、替换值、重命名、检查、设为默认密钥、删除 |
| 输入框,紧跟在访问模式列表之后 | 当前会话密钥的紧凑切换器,带菜单和 「+ 添加密钥」 一行 |
模型菜单和 /model 窗口里没有密钥分区:密钥选择只存在于一处。模型菜单则不再显示那些与当前密钥环路由重复的服务商分组——否则同一份模型列表会按每个密钥重复一次。
为什么需要这个
因为手动维护的路由有两处故障,而且都是静默的。服务商新增的模型不会出现,也没人说明原因:没有报错,只有缺失。服务商下架的模型仍然可见,选中它会报网络错误,看起来像连接问题,而不是 YAML 文件里过时的一行。
这两种情况都靠读取服务商的数据来解决,而不是重写数据。这个插件写入的列表带有真实的上下文窗口大小、输出上限、输入模态和推理等级,所以某个模型获得图像支持或新的思考等级时,当天就能被正确描述,而不是继承某个谁当年猜的备用值。
又因为图标取自同一份镜像,全新模型到来时已经带着自己厂商的缓存图标:镜像在端点提到它此前没见过的模型时重读目录——也就是发布的那一刻。
第三个任务关乎另一种静默。服务商的密钥是独立的限额和独立的计费,而配置档只有一个,所以没有插件时「每个会话用自己的密钥」意味着要么开第二个配置档,要么在 YAML 里加第二行、由人手动维护。手动维护的路由副本会按和手写模型列表相同的规则过时——在发布当天。密钥环自己写入副本,而且和写模型列表是同一次处理,所以密钥路由不会落后于配置档路由,用另一个密钥发请求的会话看到的模型选择也和邻居完全一样。
这些都不绑定到某一个服务商。源站点、路由和配置档行都是设置;https://rustvy.xyz 和 rustvy 是默认值。
安装
dsh plugin --profile web add dsh-rustvy
或者直接从 GitHub 安装,如果你要最新修订而不是已发布版本:
dsh plugin --profile web add github:koliy82/dsh-rustvy
裸写 koliy82/dsh-rustvy 不带协议前缀不会被接受:安装源解析认得注册表名、git 规格、tarball 和本地绝对路径,而 git 规格需要协议前缀或 github: 简写。
两种情况都不会请求确认构建:包既没有 install 脚本也没有依赖,pnpm 没什么可构建的。
从该仓库的本地副本:
pwsh -File .\install.ps1
install.ps1 把包复制到 <профиль>\node_modules\dsh-rustvy,作为真实目录,并把名字追加到配置档清单的 dsh.profile.bundles。运行中的 harness 会在清单变更时读取这一行。
之后重启一次 harness,或刷新页面:图标和设置页会即时出现,加载器那一行在组合时读取。
写入模型列表的路由
图标不依赖其他部分也能用。模型列表那一半需要一个可写入的路由 @deepseek-ai/dsh-llm-pi-ai。把它加到配置档的 cordis.patch.yml,或在 设置 → 插件 → 插件配置 中编辑:
- id: llm-pi-ai
name: "@deepseek-ai/dsh-llm-pi-ai"
config:
providers:
rustvy:
displayName: rustvy
apiKeyEnv: RUSTVY_API_KEY
api: openai-completions
baseURL: https://rustvy.xyz/v1
models:
- id: placeholder
name: placeholder
models 列表只需能解析:这个插件会替换它。其余字段在重新生成时原样保留,因为协议兼容开关或凭据引用属于你对部署的了解,任何目录里都没有。
apiKeyEnv 指定路由从哪里取密钥;每次请求都通过 harness 的凭据机制解析——先看进程继承的环境,再看 $DSH_HOME/.credentials.yaml,最后看启动目录或 $DSH_HOME 下的 .env。密钥本身不会进入配置文件。插件读取模型列表时,只有当进程环境里已经有同名变量才填入密钥;它面向的端点不带密钥也能响应。
把模型设为主模型:
- id: agent-default-model
name: "@deepseek-ai/dsh-agent-default-model"
config:
provider: rustvy
model: claude-opus-5-5
reasoningEffort: high
使用方法
打开 设置 → Rustvy。页面显示镜像持有什么、服务商返回什么,以及两者之间的确切差异。「刷新目录和图标」 按钮刷新两半,「写入模型列表」 写入生成的列表。除非你在设置里主动要求,否则不点这两个按钮就不会写入任何内容。
同一页面的 「API 密钥」 区域维护密钥环:「+」 按钮打开「名称 + 值」的行内表单,每条密钥行都有替换值、重命名、设为默认密钥、校验和删除。默认密钥不进入设置也能用:组合器工具栏里的开关对当前打开的聊天做同样的事。
设置
设置 → Rustvy
图标:
| 参数 | 默认值 | 含义 |
| 显示模型图标 | 开 | 图标总开关;关闭后移除所有图标,不改动模型列表 |
| 所选模型按钮中的图标 | 开 | 模型按钮里也显示图标,而不只是菜单行 |
| 隐藏其他皮肤的标志 | 开 | 在图标由本插件绘制的行里,隐藏皮肤自带的底板 |
| 图标尺寸,px | 16 | 10 到 32 |
模型列表:
| 参数 | 默认值 | 含义 |
| 保持路由的模型列表为最新 | 开 | 总开关;关闭后不改动路由的 YAML |
| 写入列表 | 按下「更新」时 | 仅通过单独按钮 — 从不自行写入 · 按下「更新」时 — 按更新按钮时写入 · 自行,每次更新时 — 后台每日更新时也写入 |
| 模型名称 | 与服务商网站一致 | 显示服务商的名称,或原始标识符 |
| 提供「无推理」档位 | 开 | 为未声明该档位的推理模型添加 off 档位 |
| 保留服务商不再提供的模型 | 关 | 保留已安装但端点不再提供的模型,以应对临时出错的列表,而非确实下架的模型 |
| 服务商路由 | rustvy | 写入 providers 内的哪个键 |
| 配置档行 | llm-pi-ai | 哪个加载器行包含它 |
| 服务商网站 | https://rustvy.xyz | 从何处读取目录和标志 |
默认写入模式刻意取中间值:按下按钮是人的决定,而后台到期不是,在没人要求时改写运行中配置档的加载器配置,这种事值得先征求许可。
插件涉及的每个地址和文件都能在页面上看到:网站、目录页和模型列表端点以链接形式给出,在新标签页打开;镜像、设置、回滚副本和配置档补丁的路径以文本形式给出,并带复制按钮。「显示手动编辑块」 按钮给出的正是自动写入会得到的 YAML,想把它粘进 cordis.patch.yml 的人不必自己重写一份简略转述。
有一个字段没有控件,因为它属于配置档的 YAML,而不属于表单:modelZones(默认 ["standard"])——端点不可用、列表构成由镜像决定时,允许的目录区域。
键:
| 参数 | 默认值 | 含义 |
| 多聊天共用键 | 开 | 键环总开关;关闭后不应用路由替换,每个请求都走配置档的路由 |
关闭不会删除任何东西:键的路由留在配置档配置里,值留在凭据存储里,聊天选择留在自己的文件里,所以重新开启后,该功能会从关闭时的位置继续。开关位于「API 密钥」分区,而不是图标开关和模型列表:它属于环,同一页面也解释了为什么主机不提供所需服务时无法添加密钥。
设置存放在 <профиль>\.dsh-rustvy\config.json。两个环境变量可以覆盖它们——用于把镜像指向别处、或想让插件挂载后不工作的部署:DSH_RUSTVY_ORIGIN 和 DSH_RUSTVY_DISABLED=1。
图片从哪来
包里没有内置任何东西。宿主侧把服务商网站上的两个公开对象镜像到配置档,浏览器侧则从本地地址读取它们:
1. 模型目录 —— 网站用自己页面里的 JSON 岛()渲染模型表格,所以未认证的读取者拿到的正是面板看到的那份数据。镜像到 <профиль>\.dsh-rustvy\catalog.json。
2. logo 文件(/logos/<имя>.svg)—— 首次访问时复制到 <профиль>\.dsh-rustvy\logos\。
两者都走进程自己的出站路由(lib/net.js):先走活的全局调度器,以遵守 dsh-proxy-gui 或进程里任何其他路由设置的代理;再走这里用 HTTPS_PROXY / ALL_PROXY / HTTP_PROXY 构建的连接器,用于调度器未安装的情况。两种情况都考虑 NO_PROXY。页面本身不访问外网:浏览器向 127.0.0.1 要图标,正因如此,浏览器根本够不到源的机器上也能正常工作。
目录每 24 小时更新一次,也可按需更新;冷镜像最多等 6 秒,过期的镜像在后台刷新,期间各行继续显示已有图标。源不可达时,磁盘缓存仍然给出它里面的一切。
模型列表从哪来
两个来源,顺序如此,因为它们回答的是不同问题:
1. 路由自己的端点(GET {baseURL}/models)—— 组成的权威列表:正好是请求里能点名的那些模型,每个都带自己的 context_k、max_output_tokens、modalities 和 reasoning_levels。适配器自带的发现机制轮询的也是这个端点。
2. 目录岛镜像 —— 名称和兜底。它是唯一携带显示名、厂商和 logo 文件名的来源,所以按标识符与返回的列表拼接。端点不响应时,组成由岛自己按 enabled 和 zone 字段决定,这样路由在故障期间也保持最新,而不是冻结在最后一次成功读取上。源健康时,两个列表完全一致。
端点返回、而镜像从未见过的模型,说明岛落后于线上 API,而这恰好发生在发布时刻,所以这时会重读岛;等待时限与冷镜像相同,同样很短。
具体写入什么
每条生成的记录只带适配器配置 schema 里声明过的字段,因为路由配置档的 models 列表会替换路由目录,未知字段最多只能被丢弃:
| 字段 | 来源 |
| id | 对外提供的模型标识符,也就是请求里要写的名字 |
| name | 来自模型池的显示名;没有名字时用标识符 |
| contextWindow | context_k × 1000 |
| maxTokens | max_output_tokens |
| input | modalities,过滤到只剩 text/image |
| reasoningEfforts | reasoning_levels,映射到适配器的推理档位;不推理的模型则为 false |
| compat | 永远不生成,已有的值总是原样带过来 |
没人表态的字段会跳过,不填默认值,这样适配器自己的兜底值仍然生效,手写的值也不会被悄悄固定下来。compat 是唯一原样带过来的东西:协议兼容开关属于网关知识,任何目录里都没有。
推理档位映射到适配器的词表(off、minimal、low、medium、high、xhigh、max);适配器不认识的档位会被丢弃,并附一条说明,而不是整条记录拒收。适配器不支持的档位就是没声明,而声明了 off 却没有实际值,意思是「只提供选项,什么都不发」——所以这个开关默认打开。
记录会拒绝什么
空列表永远不写:唯一能得到它的途径是读取失败,而没有模型的路由什么都服务不了。内容没变的列表也不写,因为 configEditor.edit() 会对整个加载器做一次协调,为「没有变化」花掉一次协调就等于白白重载正在用的配置档。这里说的「没变」不只看配置档自己的路由:配置档本身没问题,但密钥环里某个克隆的模型列表落后了,也要写——否则用另一把密钥发请求的聊天看到的模型会比邻居少。每次写入都把上一份文档存到 <配置档>\.dsh-rustvy\models-backup.json,「回滚到上一版」按钮把它放回去;回滚挤掉的状态自己会成为下一份副本,所以回滚也能再回滚。
行如何与图标匹配
行按可见名称匹配,顺序如下:
1. 镜像目录里的模型标识符(claude-opus-5-5、abyss/glm-5.3……);
2. 它的显示名(OPUS 5.5);
3. 名称开头的标识符——皮肤可能在同一元素里留下自己的描述行,所以名称常常只是前缀,而不是整行;匹配成功的最长标识符胜出,这样 claude-opus-5-5 永远不会落到 claude-opus-5 头上;
4. 标识符的形状——用于镜像一次都没见过的模型: claude- → Anthropic,gpt-/o1…o9 → OpenAI,glm- → Zhipu, kimi-/k3 → Moonshot,qwen- → Alibaba Qwen,deepseek- → DeepSeek, ahmia- → Ahmia,mimo- → Xiaomi,fable- → Anthropic,gemini- → Google,grok- → xAI。
匹配不上任何规则的模型干脆不给图标:凭空造一个图标是错误答案,而不是缺失的答案。匹配上了、但镜像拿不到 logo 的模型,不给空方块,而是给一块带字母组合的标牌——取厂商名的首字母:URL 失效的遮罩会糊成一个实心方块。
努力等级行、厂商标题、菜单根单元格、搜索框、目录加载失败行都不匹配任何规则,保持无图标。
API 密钥环
同一个配置档里放多个服务商密钥,每个聊天用自己用户选中的那个密钥发请求。选择项放在 composer 工具栏里——挨着访问模式列表,而不是放在模型菜单里——只对一个聊天生效,并且重启后仍然保留。本节任何位置都不涉及 DeepSeek 账号。
为什么一个密钥就是一条路由
@deepseek-ai/dsh-llm-pi-ai 从 profile.apiKeyEnv 取密钥——那是名字,不是值——每次请求都通过 harness 的凭据机制解析它,不做任何缓存。调用配置(LlmCallConfig)里没有密钥字段,生成参数(GenerateOptions)里也没有,而 baseURL 和请求头被烤进配置档快照。没有东西能让适配器「在这次请求里」收到密钥,所以给聊天换一个密钥的唯一诚实做法就是换一条路由:克隆一份,api、baseURL、models 都不变,只换自己的 apiKeyEnv。
整个设计都由此而来。添加密钥时,把值写进凭据存储,用新名字;通过 ctx.configEditor 克隆配置档路由;再把这条记录记进密钥环。聊天的选择在 agent/request 瀑布上生效,做法是替换调用配置里的路由名——用的手法和接缝,跟 harness 给单个聊天应用模型选择时一模一样,所以它恰好在模型选择已经生效的地方生效,也不会另开一条通往请求的路径。
| 密钥 | 凭据名 | 路由 | 路由显示名 |
| 第一个(主密钥) | RUSTVY_API_KEY——按配置档里声明的 | rustvy——按配置档里声明的 | rustvy |
| 第二个 | RUSTVY_API_KEY_2 | rustvy-k2 | rustvy · <密钥名称> |
| 第 n 个 | RUSTVY_API_KEY_<n> | rustvy-k<n> | rustvy · <密钥名称> |
名字从设置里的路由生成,不是写死的:modelRoute: rustvy 时第二个密钥拿到 rustvy-k2,路由名不同就用自己的后缀。克隆会整份复制路由配置档,只改 apiKeyEnv 和显示名,所以手写的协议兼容开关、以及关于自家网关的其他所有知识,都会跟着进到每个密钥里,而不是在第一次添加时就丢掉。
上限是 12 个密钥:这是一个环,不是注册表。新密钥取第一个序号,该序号下凭据名和路由都空闲,所以远程密钥的名字不会被复用,而别人偶然落在该名字上的行也不会被覆盖。
主密钥
环里的第一个密钥不是谁添加的记录,而是配置档里已经声明的路由:它继承该路由的 apiKeyEnv 和名字,并且永远是主密钥。在此功能出现之前就工作的配置档继续照常工作——只有一个密钥的环不会重命名任何东西,也不往任何地方写入。主密钥不能删除:它属于配置档的路由,删掉它会让每个聊天的服务商都坏掉,包括那些从未用过这个环的聊天。拒绝时会说明这一点,并建议替换值——这个操作对任何密钥都可用,也不涉及配置,因为凭据名在替换时不变。
各部分存放位置
| 内容 | 位置 |
| 密钥值 | $DSH_HOME\.credentials.yaml,refs 段——与 harness 的 Models 页面写入、适配器读取的是同一个存储 |
| 环的元数据 | <配置档>\.dsh-rustvy\keys.json——id、名称、凭据名、路由、主密钥标记、序号、创建日期和 defaultKeyId |
| 聊天选择 | <配置档>\.dsh-rustvy\session-keys.json——sessionId → keyId |
插件文件里永远没有密钥值:keys.json 里没有,session-keys.json 里没有,设置里没有,回滚用的配置档副本里也没有。Loopback 路由返回掩码——前三个和最后两个字符——以及八字符的 SHA-256 指纹;这足以在列表里认出密钥、发现两条记录用了同一个值,但不足以拿它来用。值本身只在添加或替换它的那一个请求体里出现,从不出现在请求行里:URL 会写进日志、进缓存、进浏览器历史,而密钥不该沾上这三样中的任何一样。
选择存在磁盘上,而不是会话日志里:这是插件自己的路由偏好,而插件若把自身事件写进长期日志,就会把它们留在每一个曾经挂载过它的会话里。文件里只存与默认密钥不同的偏离,所以默认密钥更换之前创建的聊天会跟随新密钥,而不是守着旧的。
界面里能看到什么
两个界面:一个管理环,另一个为自己的聊天选密钥。切换器只在有得选时才出现:只有一个密钥时它根本不显示,因为一个密钥算不上选择。
设置 → Rustvy →「API-ключи」。 密钥列表:名称、掩码、凭据名、路由、该路由提供多少模型、多少聊天选了这个密钥。分区标题里的 「+」 按钮打开内联表单「名称 + 值」,值字段隐藏,可按需显示。每行都有替换值、重命名(失焦后保存)、设为默认键、两步完成校验与删除;主键没有删除按钮,行内会说明原因。下方是「键路由」区块:配置档行与包、列表地址、「经替换发出的请求」计数、自带选择项的会话数、缺失与滞后路由列表、两个文件的路径及复制按钮,以及 「修复路由」 按钮——只有当某个键的路由确实丢失时,它才会出现。
输入区。 工具栏里一个紧凑按钮,紧跟在访问模式列表之后,注册在槽位 conversation.input.left:显示当前活动键的名称;如果会话没有自己的选择,则显示「默认键」。菜单里每个键一行,活动键带勾选标记,另有「默认」一行和 「+ 添加键」 一行,后者带同样的内联表单;从会话里添加的键会立即被该会话选中,因为从对话里添加它,正是为了用它发请求。槽位没有传入 sessionId 之前,按钮不绘制:选择属于会话,一个会把选择存到错误会话上的切换器,比没有切换器更糟。
模型选择菜单与 /model 弹窗。 这里没有键的分区——既没有行,也没有标题:选择住在输入区里,菜单里再放一份同样的列表,只会多出一个容易出错的地方。装饰器在这里做的是相反的事——隐藏宿主在 hiddenGroups 里点名的服务商分组,也就是这个环的路由克隆,免得同一份模型列表在每个键下各显示一遍。宿主不能不声明它们:克隆需要自己的模型列表,否则路由替换之后,请求在里面找不到自己的模型。隐藏是结构化且谨慎的:分组标题按形状识别,而不是按类名,因为这个插件看不到宿主基础组件的类名,而且这些类名在不同版本之间并不稳定;隐藏的要么是包含该分组的最小元素,要么是标题连同其后直到下一个标题之前的各行。没有把握时,什么都不隐藏:错误地藏掉一行模型,等于夺走一个选项;多显示一次分组,只是重复。
添加键时发生什么
操作顺序是刻意安排的。先把值写入凭据存储:一个名字解析不到任何东西的路由,会让每个点名它的请求失败。然后是配置写入,如果写入失败,值会被收回,所以失败的添加不会留下任何痕迹。环文档最后写入,此时值和路由都已存在。
配置写入是真实且受支持的:它走 ctx.configEditor,由后者协调配置档,harness 的 Models 页面往配置档里添加服务商时做的也正是这件事。configEditor.edit() 会协调整个加载器,所以环的写入排成了队列:同时添加两个键——这是两次连续的修改,不是针对另一份刚刚替换掉彼此的文档所做的两次修改。当路由表已经与环一致时,根本不会发生写入——这个插件拒绝把协调浪费在没有变化的事情上,在模型列表里也是如此。
可以不发送请求就校验密钥:校验会用该密钥发起 GET {baseURL}/models,并报告状态、返回的模型数量,以及服务商原样返回的错误文本。
替换的范围有多窄
替换只改动调用配置中的一个字段——路由名——而且只在下列条件全部满足时才会发生:
1. 该能力已通过 apiKeys 设置开启;
2. 请求带有会话标识——没有它就没有可替换的对象;
3. 环中有两个或更多密钥;
4. 该聊天的密钥——它自己的选择,若没有则为默认密钥——不是主密钥;
5. 请求本来就走在该环的某条路由上(rustvy、rustvy-k);
6. 目标路由已在配置档中设置;
7. 目标路由提供所请求的模型。
其余所有情况下,配置都按原样返回。deepseek-account 以及任何其他服务商——第二道网关、相邻行、带密钥的他人记录——永远不会被改名:第五个条件检查的是路由是否属于该环,而不是它看起来像不像。
消失或不提供所需模型的克隆不会让这一步失败:请求会走配置档中的路由,原因会被记录下来。它显示在 GET /keys 的 fallback 字段中——设置页面会在密钥列表上方以警告形式绘制它——也会在日志中留下一行,在原因改变之前不会重复。这里的逻辑是:密钥路由不工作属于配置问题,正确的回应是让请求完成并显示可见的原因,而不是让一步失败却不说为什么。替换内部的异常会被捕获,请求照常进行。
模型列表对环的所有路由一次性写入——基础路由以及环在配置档中看到的每个克隆——因此克隆不会落后于基础路由,而用于手动编辑的块(/models.yaml)包含克隆及其凭据名称,这样想往 cordis.patch.yml 里插入一行的人不必自己根据描述重建它们。
GET /keys 上的两个计数器回答了一个否则看起来像「密钥不工作」的问题:routing.seen——harness 总共让多少请求通过了 agent/request 瀑布,routing.applied——其中有多少被改名为密钥的路由。有流量时为零意味着瀑布没有到达插件,这一点立刻可见。
路由
在 harness 自己的 Web 服务器上注册一次前缀。插件的路由对主机的请求过滤器不可见,因此每条路由都自行检查对方:仅本地地址、仅本地 Host、仅 same-origin 的 Sec-Fetch-Site,以及仅匹配的 Origin。
| 路由 | 方法 | 响应 |
| /dsh-rustvy/catalog.json | GET | 镜像目录,外加浏览器端渲染时所用的配置 |
| /dsh-rustvy/logo/<名称>.svg | GET、HEAD | 单个已缓存的 logo,首次请求时从源站拉取;源站没有该图标时返回 404,名称不匹配 [A-Za-z0-9._-]+.svg 时返回 400 |
| /dsh-rustvy/models.json | GET | сгенерированный список моделей и его расхождение с установленным; ?cached=1 отдаёт последний вычисленный план вместо повторного чтения эндпоинта |
| /dsh-rustvy/models.yaml | GET, HEAD | тот же список в виде готовой к вставке строки cordis.patch.yml; ?cached=1 как выше |
| /dsh-rustvy/state | GET | состояние зеркала: версия, число моделей, кешированные логотипы, маршрут, прокси (учётные данные маскируются), все пути к файлам, последняя ошибка, последнее вычисленное расхождение и сводка кольца ключей. Собственных запросов не делает; сводка кольца синхронная и без чтения учётных данных, поэтому масок в ней нет — только счётчики и маршруты |
| /dsh-rustvy/refresh | POST | перечитать каталог, заново прогреть все названные им логотипы и сообщить — либо, когда режим записи позволяет, применить — список моделей |
| /dsh-rustvy/apply-models | POST | записать сгенерированный список в профиль маршрута через ctx.configEditor |
| /dsh-rustvy/revert-models | POST | вернуть сохранённый профиль маршрута |
| /dsh-rustvy/config | POST | записать настройки; сети не касается |
| /dsh-rustvy/keys | GET, POST | GET: кольцо целиком — ключи с маской и отпечатком, ключ по умолчанию, скрытые группы провайдеров, отсутствующие и отставшие маршруты, счётчики подмены, последний отказ, пути к обоим файлам, доступность credentials и configEditor. POST: одно действие над кольцом (add, set, rename, default, remove, repair, test); успешный ответ — свежий GET плюс поле результата, поэтому один POST перерисовывает всё. Отказ — 409 с { ok: false, error }, неизвестное действие — 400. Значение ключа принимается только в теле запроса |
| /dsh-rustvy/session-key | GET, POST | GET с ?sessionId=: каким ключом этот чат отправляет запросы (keyId), есть ли у него собственный выбор (picked), и целиком тот же ответ, что у GET /keys; без идентификатора — 400. POST { sessionId, keyId } записывает выбор и отвечает тем же payload; keyId: null возвращает чат к ключу по умолчанию |
Без перезапусков
Хост-половина перезагружает себя сама, тем же способом, что и dsh-proxy-gui:
строка загрузчика называет этот пакет, чья точка входа импортирует реализацию через
запрос со сбрасывающим кеш параметром ?rev=, а опрос каталога lib раз в секунду
останавливает предыдущую редакцию и запускает следующую, как только изменившийся
отпечаток повторится. Поэтому lib/host.js, lib/models.js, lib/keys.js и
lib/net.js перезагружаются на лету, а перезапуск нужен только при правке самой
точки входа.
Браузерная половина приходит в открытую страницу через собственную живую композицию
плагинов harness; обновление страницы тоже работает.
Название, описание и значок в меню плагинов читаются из пакета на композиции,
поэтому изменение locale/.json или assets/icon.svg требует перезапуска.
Требования и деградация
Проверено на dsh 0.2.1-rc.2. Пакет не объявляет ни engines.dsh, ни
peer-зависимостей, потому что не импортирует ни одного модуля @deepseek-ai/ — у него ноль рантайм-зависимостей, а непроверенное требование к версии хоста дало бы в маркете лишь ложные предупреждения о несовместимости.
Каждая возможность деградирует самостоятельно:
| Чего нет | Что происходит |
| configEditor | расхождение по-прежнему вычисляется и показывается; кнопки записи и отката объясняют, что хост не отдаёт редактор |
| маршрут dsh-llm-pi-ai | значки работают; половина со списком сообщает, что маршрут не найден |
| источник | кеш на диске отдаёт каждый значок и каждую модель, которые в нём есть |
| эндпоинт при живом каталоге | состав определяет остров, и страница об этом говорит |
| credentials | значение ключа некуда записать: кольцо остаётся с одним основным ключом, добавление недоступно, и раздел «API-ключи» называет причину вместо того, чтобы показать форму, которая откажет |
| configEditor — для кольца | значение существующего ключа заменить можно, маршрут для нового добавить нельзя: форма добавления выключена, и раздел объясняет почему |
| маршрут ключа, потерянный после ручной правки cordis.patch.yml или отката списка моделей | ключ остаётся в кольце, его маршрут числится в routes.missing, запросы этого чата идут маршрутом профиля с видимой причиной, а кнопка «Починить маршруты» ({ action: 'repair' }) записывает маршруты кольца заново |
Структура
| Файл | Роль |
| lib/index.js | стабильная точка входа загрузчика; владеет циклом перезагрузки |
| lib/host.js | маршруты, зеркало каталога, кеш логотипов, синхронизация моделей, кольцо ключей и подмена маршрута на agent/request, настройки |
| lib/models.js | чистое отображение: что публикует источник → одна запись профиля маршрута, расхождение двух списков и YAML для ручной правки |
| lib/keys.js | чистая арифметика кольца: названия, имена учётных данных и маршрутов, документы кольца и выбора чатов, сверка карты маршрутов с кольцом |
| lib/net.js | один исходящий GET по маршруту процесса, затем по прокси из окружения |
| client/client.js | браузерный набор, написанный вручную: декоратор, страница настроек, переключатель ключа в композере и скрытие групп-клонов в меню моделей |
| locale/en.json, ru.json, zh.json | название и описание для меню плагинов |
| assets/icon.svg | значок для меню плагинов |
| cordis.patch.yml | строка загрузчика, которую подтягивает список bundles профиля |
| install.ps1 | установка в профиль |
| scripts/prepublish-check.mjs | гейт публикации: всё, что может установиться и при этом быть молча неправильным |
| tests/ | проверки отображения, эмиттера YAML, кольца ключей, хост- и браузерной половин |
lib/models.js и lib/keys.js намеренно чистые — без сети, файловой системы и Cordis, — поэтому отображение можно проверить на реальном ответе источника, а арифметику кольца на выдуманном профиле, ничего не монтируя. lib/host.js владеет всем, у чего есть время жизни, и именно его заменяет перезагрузка.
Тесты
node tests\models-check.mjs # чистое отображение: живой источник -> записи, расхождение, аудит схемы
node tests\yaml-roundtrip-test.mjs # эмиттер против двух настоящих YAML-парсеров
node tests\model-sync-host-test.mjs # хост: подставные webServer и configEditor, настоящий запрос к источнику
node tests\api-keys-host-test.mjs # кольцо ключей: подставные credentials, configEditor и шина событий, без сети
node tests\model-sync-client-test.mjs # браузер: мини-React и подставной DOM, фиктивный /state
node scripts\prepublish-check.mjs # гейт публикации
models-check.mjs читает эндпоинт и остров каталога источника, генерирует список и
проверяет каждую запись против схемы адаптера: только объявленные поля,
положительные целые размеры, известные модальности и словарь рассуждения, который
предлагает уровень помимо off, но даёт боевое значение каждому уровню, кроме
off. Он же проверяет, что запасной путь по острову выбирает ровно те модели,
которые отдаёт эндпоинт, — именно это делает запасной путь безопасным для записи.
yaml-roundtrip-test.mjs разбирает настоящую строку cordis.patch.yml профиля (или
синтетическую той же формы, если профиля на машине нет), заново выдаёт её эмиттером
этого пакета и проверяет, что два независимых парсера — js-yaml и yaml — читают её обратно одинаково, что все поля подключения сохраняются, что подстановка строки оставляет соседей байт в байт и что расстановка кавычек переживает обратный ход: зарезервированные слова, числа, ведущие индикаторы, концевой пробел и кавычки внутри строки.
Хост-тест прогоняет зарегистрированные маршруты на подставных парах запрос/ответ и
подставном configEditor, наполненном маршрутом в том виде, в каком он есть у
профиля, и проверяет: план читает эндпоинт; модель, которой зеркало не видело, всё
равно получает настоящее отображаемое имя, а не идентификатор; написанный вручную
compat переживает перегенерацию и не изобретается для другой модели;
/models.yaml выдаёт строку патча, сохраняющую поля подключения и отбрасывающую
заменяемый список; запись сохраняет displayName, apiKeyEnv, api и baseURL и
заменяет только models; копия ложится на диск; повторное применение ничего не
делает и конфигурацию не трогает; /state перестаёт сообщать расхождение, которое
запись только что устранила; откат возвращает снятые модели и их compat;
нелокальный собеседник получает 403, а неверный метод — 405.
api-keys-host-test.mjs — 84 проверки кольца ключей на той же host-половине, с подставным хранилищем учётных данных, подставным configEditor и подставной шиной событий, без сети вовсе: эндпоинт маршрута смотрит на закрытый порт, поэтому и чтение списка моделей, и проверка ключа детерминированно заканчиваются отказом, а состав списка определяет зеркальный остров. Проверяется подмена: выбранный ключ переименовывает маршрут, а модель, уровень рассуждения и лимит вывода переживают её без изменений; чат без собственного выбора идёт маршрутом профиля; deepseek-account и соседний маршрут с чужим ключом не переименовываются никогда; запрос без сессии остаётся как пришёл; модель, которой клон не отдаёт, оставляет запрос на базовом маршруте и попадает в fallback; чат без выбора следует за ключом по умолчанию, а keyId: null возвращает его обратно. Проверяются отказы: пустой ключ, ключ с пробелом, неизвестное действие (400), удаление основного ключа (409). Проверяется удаление ключа: маршрут клона исчезает, сосед и базовый маршрут выживают, значение стирается из хранилища, выбор чатов с этого ключа сбрасывается, а сам чат продолжает ходить базовым маршрутом. Проверяются записи и счётчики: одно добавление — одна правка конфигурации, замена значения не правит её вовсе, запись списка моделей накрывает клон и сохраняет его собственное имя учётных данных, блок для ручной правки содержит клон, repair возвращает потерянный маршрут, в keys.json нет секрета, выгрузка снимает и маршрут, и слушателя, а с выключенной возможностью не переименовывается ничего.
Браузерный тест рисует страницу настроек мини-реактом, чьи ячейки хуков привязаны к
экземпляру компонента, и проверяет: раздел называется Rustvy; три внешних адреса — настоящие ссылки, открывающиеся вне приложения; каждый путь к файлу показан с кнопкой копирования; маршрут, строка профиля, сайт и размер значка редактируются и сохраняются по потере фокуса; оба выпадающих списка сохраняются по изменению; панель ручной правки читает и показывает блок; расхождение называет новые, снятые и изменённые модели; кнопка записи включена при наличии расхождения, выключена при актуальном списке и выключена, когда хост не отдаёт редактор; откат остаётся выключенным, пока копии нет; каждое изменение уходит на нужный маршрут и сообщает, что именно сдвинулось.
Тот же тест рисует и кольцо ключей: раздел «API-ключи» со списком, масками,
переименованием по потере фокуса, инлайн-формой «+», проверкой, назначением
ключом по умолчанию и удалением в два шага; объяснения на каждый отказ — нет credentials, host-половина устарела, возможность выключена — вместе с возвращением способности, когда причина устранена; переключатель «Ключи на несколько чатов», который читает ответ хоста и пишет своё поле в /config; блок «Маршруты ключей» со счётчиками подмены, числом чатов со своим выбором, путями к обоим файлам, отсутствующим и отставшим маршрутами и кнопкой «Починить маршруты»; предел в 12 ключей; переключатель композера, который не рисуется без sessionId и при одном ключе, читает выбор своего чата, публикует его в /session-key и предлагает добавить ключ; меню выбора моделей и окно /model, которые не получают ни раздела ключей, ни его строк; стоимость обращений к хосту — два взаимодействия подряд стоят одного чтения, открытие переключателя читает всегда, а выбор обходится одним POST без дочитывания; и выгрузка, после которой в меню не остаётся ничего своего. Отдельно проверяется, что значение ключа не попадает ни в один URL и не остаётся на странице после добавления.
Половина, рисующая значки, автоматическим набором в этом репозитории не
покрыта — и это стоит знать заранее. Она работает с DOM уже открытой страницы, поэтому проверяется глазами: какие строки получают значок и какой именно, что строки уровней усилия, заголовки производителей и поле поиска остаются пустыми, что вместо неполученного логотипа появляется плашка с монограммой, что добавленная позже строка тоже получает значок, что повторный проход ничего не удваивает и что выгрузка убирает добавленное. model-sync-client-test.mjs покрывает страницу настроек и переключатель ключа в композере, но не рисование значков; скрытие групп-клонов в меню — та же работа с DOM уже открытой страницы, и набор проверяет там только то, что меню не получает ни раздела ключей, ни его строк.
Лицензия
MIT — см. LICENSE.预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
koliy82/dsh-rustvy
为模型列表补上厂商图标,并管理多组服务商密钥
相关插件 · 模型接入
API 中继安全审计toby-bridges/api-relay-audit
面向 AI API 中转与 LLM 代理的本地安全审计。
★ 897已汉化DSH
69.4
免费搜索插件DDDMUC/dsh-free-search
DeepSeek Harness 免费搜索插件 —— 无需 API key,零成本,多引擎可切换。 一个给…
★ 761已汉化DSH
68.7
免费联网搜索liustack/modsearch
在官方 App 里模型能联网,切到 API 就不能了。ModSearch 把联网补回来:网页搜索、X…
★ 863已汉化DSH
68.5
OpenCode 免费模型FishBottle7/opencode2dsh
在 DSHDeepSeek Harness里原生使用 OpenCode Zen 的免费匿名模型。
★ 379已汉化DSH
67.8
AI 模型网关tingly-dev/tingly-box
Tingly Box Web UI 演示
★ 373已汉化DSH
67.4
WorkBuddy 账号池XDTrees/dsh-xdpool
把你能白嫖的模型额度接进 DSH 同一个池:WorkBuddy 多账号自动容错、十三家扫码登录、76…
★ 359已汉化DSH
67.1