DSH XD Pool
XDTrees/dsh-xdpool把本机 WorkBuddy 桌面 App 登录过的所有账号自动并入 DSH 模型池:多账号轮换自动容错、实时积分,模型目录按模型标注积分倍率与限时免费 / 夜间折扣。
把 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 │
└───────────────┴───────────────────────────────────────────────────────┘
一个插件 = 两个半边:
exports ".")跑在 dsh 的 Node 进程里:账号存储、本地路由代理、协议桥接、客户端配置直写、面板事件 WebSocket、静态面板产物。它不注册任何 agent 工具,也不写系统提示 —— 普通会话里 agent 的行为完全不变。exports "./client")跑在 web GUI 里:往侧栏放一个图标,点开后在中间栏挂一个同源 iframe。350 行,界面本身在 iframe 里。一句话:让本机的 agent CLI 指向一个本地地址,由这个插件决定每个请求实际走哪个上游账号。
127.0.0.1:19527 起一个代理,并按平台各发一把 sk-ai-switch-… 的 key。~/.codex/config.toml、~/.claude/settings.json 等等,把地址和 key 填进去 —— 只加自己那一段,别的一个字节不动。codex / claude 跑起来打的就是这个代理:它挑账号、改写模型名、必要时翻译协议、失败了换下一个账号重试、把用量记进账本。configwrite/toml.js、configwrite/yaml.js)—— 参考实现用 toml_edit 做到「改三个键、注释一个不掉」,JS 生态没有能往回写的 TOML 库,所以自己写了一个按行编辑的。会吃掉注释的配置写入,是一次之后就不敢再用的配置写入。host/socket.js 的头注释。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)。
<DSH 家目录>/dsh-plugin-ai-switch/accounts.json,0600,原子写。不加密是有意的(密钥只能放同一块盘上);真正起作用的是文件权限,以及浏览器永远拿不到明文 —— 列表接口把 secret_payload_json 换成 {"__masked":true},编辑时 host 把没重发的密文合回去。config.concurrent_modification 而不是覆盖。原字节留一份 0600 备份,回滚是逐字节还原,且回滚前要求文件仍是我们写的那个哈希。127.0.0.1。它是一个本地凭据代理,不是网关。请求靠 bearer token 认平台,认不出就 401。authorization 是我们自己的代理 key,绝不能出网。只放过 user-agent(有中转站按它计费,且不含凭据)。get_session_messages 收到的路径要求解析后仍在扫描根内(比较时带路径分隔符,<root>-secrets 过不去)。参考实现这里是任意文件读。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 需要一个已安装的应用。isDesktop() 不显示。node-pty 是可选依赖。装上了终端就是真 TTY;没装退化成管道模式,host 侧回显输入,并在终端里直接写明「全屏 CLI 不会正常显示」—— 而不是给一个黑框让人猜。- 在父键下缩进,参考实现平齐)。两者解析结果一致,且陈旧检测比较的是我们自己的输出和自己的渲染,所以只是外观差异。npm 上还没有,先从源码装:
cd plugins/dsh-plugin-ai-switch
npm install # ws 是运行时依赖,面板构建要 vite/react
npm run build
cd ../..
dsh plugin --profile web add link:plugins/dsh-plugin-ai-switch
装完重启 dsh 服务生效。侧栏会出现一个「AI 切换」图标。
卸载:
dsh plugin --profile web remove dsh-plugin-ai-switch
src/
index.js cordis 入口:只 inject webServer
shared/protocol.js ApiError 信封 + 校验器(面板按 code 分支)
host/
routes.js 命令表(101 条,含桌面功能的诚实禁用状态)+ 路由挂载
http.js 信封、body、静态文件
socket.js 面板事件 WebSocket(SSE 兜底)
sdk.js DSH 家目录、原子写、语言(不 import @deepseek-ai/*)
store.js JSON 文档存储:原子写、串行化更新、坏文件挪到一边
platforms.js 七个平台、能力矩阵、十七个目标、CLI 启动器
accounts.js 账号域 + 池(选号、优先级带、并发、冷却)
models.js 模型目录:别名 → 上游名、上下文窗口、Codex 目录文件
keys.js 每平台一把代理 key,轮换后旧 key 仍受理
proxy.js 本地路由代理:挑号、重试、记账、实时日志
bridge/ 4×4 协议桥(canon/chat/responses/anthropic/gemini/sse)
configwrite/
safewrite.js 快照、原子替换、并发守卫、路径锁
toml.js yaml.js 保留格式的编辑器
adapters.js 十七个客户端适配器
index.js 协调器:准备 → 提交 → 回滚
sessions.js usage.js 读各 CLI 的会话记录与 token 用量
terminals.js agent 终端(node-pty 可选)
settings.js ledger.js 设置、请求账本
client/index.js 侧栏图标 + 同源 iframe
webview/ 参考实现的 React 前端 + 七个 Tauri shim
npm run check # 语法检查 + 重建 lib/(跳过 vite)
npm test # check + node --test
npm run build # 含面板的完整构建
npm run typecheck # 面板的 tsc
lib/ 随提交入库(外壳打包时不跑 vite),所以改完 src/ 或 webview/ 要 npm run build 再提交。
参考实现自带中英双语,语言存在 localStorage['ai-switch.language']。host 半边按 DSH_DESKTOP_LANG / LC_ALL / LANG 判出 zh-CN 或 en,client 半边把它放进 iframe 的 query,面板的 index.html 在 React 启动前种进那个 key。在面板里自己切过语言就以你切的为准。
MIT。参考实现 ai-switch 亦为 MIT,其许可副本随包发布在 LICENSE-ai-switch。