dsh-plugin-ai-switch
Pool AI provider accounts per platform and route them through a local loopback proxy that bridges the OpenAI, OpenAI-Responses, Anthropic and Gemini protocols, then write the proxy into Codex, Claude Code, Gemini CLI, Grok, OpenCode, OpenClaw and Hermes config files with snapshot-and-rollback safe w
Install
dsh plugin --profile web add dsh-plugin-ai-switch
Sandbox test
This plugin has not been tested yet. Submit a test →
README
dsh-plugin-ai-switch · AI 切换
把 ai-switch 搬到 DSH web GUI 上:七个平台的账号池、一个本地路由代理(带四种协议之间的双向桥接)、以及对十七个客户端配置文件的安全直写。
界面是原样复刻的 —— 参考实现那 33.5k 行 React 一个字没改地搬了过来(唯一改动的是 src/lib/transport/index.ts 那一个文件,理由写在它的头注释里),用 Vite 打包成单页应用,由插件自己的 host 路由提供服务。换掉的是下面那一半:107k 行 Rust 变成约 1.1 万行 JavaScript,SQLite 变成 JSON 文档,Tauri IPC 变成 HTTP。
┌ 侧栏 ─────────┬ 主区 ─────────────────────────────────────────────────┐ │ ▸ Codex │ 账号池 (Codex) ● 代理运行中 127.0.0.1:19527 │ │ Claude ├───────────────────────────────────────────────────────┤ │ Gemini │ ┌──┬─────────────┬──────────┬────────┬──────────────┐ │ │ Grok │ │≡ │ 中转 A │ 优先级 1 │ 3 并发 │ 98% · 1.2s │ │ │ OpenCode │ │≡ │ 中转 B │ 优先级 3 │ 0 并发 │ 冷却 47s │ │ │ OpenClaw │ │≡ │ 官方账号 │ 优先级 3 │ 1 并发 │ 周额度 62% │ │ │ Hermes │ └──┴─────────────┴──────────┴────────┴──────────────┘ │ │ ▸ 会话 │ [ 池内 | 池外 | 已归档 ] 写入客户端配置 ⌄ │ │ ▸ 用量 │ 实时日志 200 gpt-5.6-sol → glm-5.3 via 中转A 1.1s │ └───────────────┴───────────────────────────────────────────────────────┘
一个插件 = 两个半边:
- host 半边(
exports ".")跑在 dsh 的 Node 进程里:账号存储、本地路由代理、协议桥接、客户端配置直写、面板事件 WebSocket、静态面板产物。它不注册任何 agent 工具,也不写系统提示 —— 普通会话里 agent 的行为完全不变。 - browser 半边(
exports "./client")跑在 web GUI 里:往侧栏放一个图标,点开后在中间栏挂一个同源 iframe。350 行,界面本身在 iframe 里。
它到底做什么
一句话:让本机的 agent CLI 指向一个本地地址,由这个插件决定每个请求实际走哪个上游账号。
1. 你在面板里加若干账号(中转站的 API key,或导入的官方登录态),拖成一个有序的池。
2. 插件在 127.0.0.1:19527 起一个代理,并按平台各发一把 sk-ai-switch-… 的 key。
3. 点「写入客户端配置」,插件就地改 ~/.codex/config.toml、~/.claude/settings.json 等等,把地址和 key 填进去 —— 只加自己那一段,别的一个字节不动。
4. 之后 codex / claude 跑起来打的就是这个代理:它挑账号、改写模型名、必要时翻译协议、失败了换下一个账号重试、把用量记进账本。
五件跟参考实现不一样的事
1. 后端是 Node,不是 Rust。107k 行变成约 1.1 万行。原样保留了「会动别人文件」那部分的纪律:快照、原子替换、并发修改检测、带哈希守卫的回滚,以及两个保留格式的编辑器(configwrite/toml.js、configwrite/yaml.js)—— 参考实现用 toml_edit 做到「改三个键、注释一个不掉」,JS 生态没有能往回写的 TOML 库,所以自己写了一个按行编辑的。会吃掉注释的配置写入,是一次之后就不敢再用的配置写入。
2. SQLite 变成 JSON 文档。数据集就是几百个账号行加一个池顺序,开面板时读、用户动作时写;28 个迁移和 sqlx 校验和账本在这里买不到任何东西。原子写和 0600 保留了 —— 这些文件里有 API key 和 OAuth refresh token。
3. 路由代理自己占端口(19527,被占就往上找),不挂在 dsh 的 web server 上。三个理由,按重要性排:CLI 需要一个不随 dsh 端口变化的固定地址;dsh 的 server 后面有一道这些 CLI 过不去的鉴权;上游的流式响应挂在同一个 origin 上会吃掉浏览器那六条连接里的一条。
4. 面板事件走 WebSocket,同样是为了那六条连接 —— 这个仓库里九个插件已经把额度用满过一次,症状不是报错而是整个 GUI 不动。细节见 host/socket.js 的头注释。
5. 桌面独占的那一半没有了:Tailscale 私网、本地 HTTPS(要往系统信任库装根证书)、ccswitch:// 协议注册、开机自启、带签名的自动更新。每一样都需要一个「已安装的应用程序」,而这是个插件。面板里对应的按钮不是坏的,是按参考实现自己的 isDesktop() 分支不显示。
平台支持
平台 · 路由账号与 API 路由 · 原生配置写入 · 官方导入与额度
Codex · 支持 · 支持 · 支持
Claude Code · 支持 · 支持 · 上游账号流程允许的范围内支持
Gemini CLI · 支持 · 支持 · 支持导入;不声称支持官方额度
Grok · 支持 · 支持 · 上游账号流程允许的范围内支持
OpenCode · 部分:API 账号必须显式给 base URL 和接口格式 · 支持 · 不支持
OpenClaw · 部分:同上 · 支持 · 不支持
Hermes · 部分:同上 · 支持 · 不支持
后三个是 agent harness 不是模型厂商,没有自己的官方登录态 —— 「部分支持」的全部含义就是这个。
写入哪些文件
十七个客户端目标,每个都是「加一段 provider,别的不动」:
目标 · 文件 · 格式
Codex · ~/.codex/config.toml + ai-switch-model-catalog.json · TOML(保留注释)
Claude Code / Gemini CLI / Grok · ~/.claude、~/.gemini、~/.grok 下的 settings.json · JSON
DeepSeek Harness · ~/.dsh/settings.yaml 的 llm-pi-ai.providers · YAML(保留注释)
ZCode(Codex / Claude) · ~/.zcode/v2/config.json · JSON
WorkBuddy / CodeBuddy CLI(各两个平台) · ~/.workbuddy、~/.codebuddy 下的 models.json · JSON
Qoder CLI(Codex / Claude) · ~/.qoder/settings.json · JSON
OpenCode · ~/.config/opencode/opencode.json · JSON
OpenClaw · ~/.openclaw/openclaw.json · JSON
Hermes · $HERMES_HOME/config.yaml · YAML(保留注释)
第三行值得单独说:它写的是 dsh 自己的配置。写完之后 dsh 的 agent 就跑在这个池上,这也是这个插件装在 dsh 里比装在别处更有意思的地方。
协议桥接
本地入口四种、上游四种,4×4 全实现(host/bridge/):
· → chat · → responses · → anthropic · → gemini
chat(/v1/chat/completions) · 直通 · ✓ · ✓ · ✓
responses(/v1/responses,Codex) · ✓ · 直通 · ✓ · ✓
anthropic(/v1/messages,Claude Code) · ✓ · ✓ · 直通 · ✓
gemini(:generateContent) · ✓ · ✓ · ✓ · 直通
非流式和流式都译,工具调用、思维链、图片、用量计数都过。对角线(from === to)是字节直通,故意的:绕一趟中间表示会把中间表示没有字段的东西悄悄吃掉(Responses 的 encrypted_content、Gemini 的 safetyRatings、Anthropic 的 cache_control)。
安全边界
- 凭据:API key 和 OAuth token 明文存在
<DSH 家目录>/dsh-plugin-ai-switch/accounts.json,0600,原子写。不加密是有意的(密钥只能放同一块盘上);真正起作用的是文件权限,以及浏览器永远拿不到明文 —— 列表接口把secret_payload_json换成{"__masked":true},编辑时 host 把没重发的密文合回去。 - 配置直写:路径必须是绝对路径、父目录必须是真目录、目标必须是普通文件 —— 软链接(含 Windows junction)直接拒绝,那是一次我们没有授权的重定向。写前后各算一次 sha256,写之前再比一次;被别人改过就报
config.concurrent_modification而不是覆盖。原字节留一份 0600 备份,回滚是逐字节还原,且回滚前要求文件仍是我们写的那个哈希。 - 代理只听回环,
127.0.0.1。它是一个本地凭据代理,不是网关。请求靠 bearer token 认平台,认不出就 401。 - 客户端 header 不整体转发给上游 —— 客户端的
authorization是我们自己的代理 key,绝不能出网。只放过user-agent(有中转站按它计费,且不含凭据)。 - Gemini 与 Grok 的配置里不写凭据:这两个 CLI 读哪个环境变量当自定义端点的 key,参考实现自己也没验证过,写一个猜的等于白往文件里放一把 key。
- 会话文件只读,且路径受限:
get_session_messages收到的路径要求解析后仍在扫描根内(比较时带路径分隔符,<root>-secrets过不去)。参考实现这里是任意文件读。 - 终端会真的起进程:Vibe 界面能在你选的目录里跑
codex/claude。这是它的用途,也是它的风险;插件退出时会杀掉所有会话,不留孤儿进程。
与参考实现的行为差异
~/.ai-switch/不共用。桌面版在那里放 SQLite 和自己的 settings;两个不同 schema 的进程写同一个目录、两个路由代理抢同一个端口,是等着发生的数据丢失。插件只拥有<DSH 家目录>/dsh-plugin-ai-switch/。要迁移就用面板里的导入。secret_storage报file而不是keyring。插件没有 OS keyring 服务可用,这个仓库里每个插件的密钥都在 0600 文件里。字段还在,值是老实的。ccswitch_deeplink_compat_supported永远是 false,注册 URL scheme 需要一个已安装的应用。- HTTPS 那组命令不提供。给本地代理上 HTTPS 需要往系统信任库(以及 Linux 上的 NSS)装一张根证书,插件不该做这件事。面板的 HTTPS 面板按
isDesktop()不显示。 node-pty是可选依赖。装上了终端就是真 TTY;没装退化成管道模式,host 侧回显输入,并在终端里直接写明「全屏 CLI 不会正常显示」—— 而不是给一个黑框让人猜。- YAML 的块序列缩进与 serde_yaml 不同(
-在父键下缩进,参考实现平齐)。两者解析结果一致,且陈旧检测比较的是我们自己的输出和自己的渲染,所以只是外观差异。 - 额度刷新一次也不会整批失败:单个账号出错变成一条带 message 的 outcome,这和参考实现一致,但值得写出来 —— 面板是按平台整批刷的。
安装
npm 上还没有,先从源码装:
cd plugins/dsh-plugin-ai …(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::1,最后更新 2026-09-19