cc-switch 的手机遥控面板 + 上游路由器,作为一个 Paseo workspace service 跑在你的 Mac 上。
手机(或任何外网设备)打开一个网页,就能看各个 LLM 应用当前用的是哪个上游、一键切换、测速,还能把 /proxy/<app>/... 当作统一入口转发到当前选中的上游。
零 npm 依赖:只用 Node 内置的 node:http + node:sqlite。
cc-switch 桌面应用本身没有 HTTP 控制接口——切换上游是一个只存在于桌面 App 内部的 Tauri IPC 命令(switch_provider),它自带的 :15721 代理还走一套内存里的故障转移状态机。所以光改 SQLite 里的 is_current,并不能确定地把流量切过去。
于是这个 sidecar 自己接管路由:
selection.json是每个 app 当前上游的权威。/proxy/<app>/...转发到那个上游;开启自动故障转移后,失败会按队列顺序切换(见下节)。- cc-switch 的 DB 只是被镜像(
is_current)让桌面 UI 显示一致,路由从不依赖它。
数据来自 ~/.cc-switch/cc-switch.db(providers + proxy_request_logs)。
四个 app 页签:claude / codex / gemini / grokbuild。
实现了官方文档 4.2 应用路由 / 4.3 故障转移 的完整语义,直接读写同一个 cc-switch.db,所以手机端和桌面版看到的是同一份配置。
每个 app 一个开关。关闭时 /proxy/<app>/... 直接返回 503 routing disabled,不转发。
对应桌面版「设置 → 高级 → 路由服务 → 应用路由」。
把若干备用 provider 加进队列,按 sort_index 排序(序号越小优先级越高)。
节点卡片上的「队列」按钮即加入/移除,对应桌面版供应商卡片的故障转移开关。
| 状态 | 行为 |
|---|---|
| 关闭 | 仅记录失败,不自动切换 |
| 开启 | 主节点失败时按队列顺序自动切到下一个,最多 max_retries 次 |
判定为失败:网络不可达,或上游返回 5xx / 429。切换时同步更新 selection.json。
- 连续失败达
circuit_failure_threshold→ 该节点跳闸,故障转移时跳过它。 - 跳闸
circuit_timeout_seconds后进入半开,放一次试通;单次成功即恢复。 - 健康状态镜像进
provider_health表(is_healthy/consecutive_failures/last_error)。
所有这些开关写库前都会先自动备份
cc-switch.db。
| 组成 | 为什么必须 |
|---|---|
| token 鉴权 | /proxy/* 会带着存在 DB 里的各家 API key 转发到上游。一旦把端口开放绑定,等于把钥匙发出去。所以凡是非回环来源必须出示共享 token(timingSafeEqual 比对)。回环(127.0.0.1)被信任,因为那是 Paseo 自己的代理和健康检查。 |
?token= → Cookie 交换 |
手机第一次带 token 打开时,服务器 302 把它换成 HttpOnly Cookie,让 token 不再留在浏览器历史和日志里。 |
selection.json |
路由权威。写它=改路由,不依赖 cc-switch 那套不确定的状态机。 |
| 写前自动备份 DB | 每个进程第一次写 is_current 之前,先把 cc-switch.db 复制成 *.bak-paseo-mobile-<时间戳>。 |
HOST=0.0.0.0 |
默认只绑 127.0.0.1。要让手机/局域网能连,必须绑到所有接口。 |
| Tailscale Funnel | 手机不在同一局域网时,把 127.0.0.1:16810 安全地暴露到公网,自带 HTTPS。token 校验照常生效。 |
⚠️ 安全提示:带 token 的完整 URL 就等于登录态,别随手贴到公开渠道。公网暴露后任何拿到该 URL 的人都能操作切换和查看用量。
- Node ≥ 22(用到
node:sqlite的DatabaseSync) - 已安装 cc-switch,存在
~/.cc-switch/cc-switch.db(没有就直接退出) - 想公网访问才需要 Tailscale
| 变量 | 默认 | 说明 |
|---|---|---|
PORT / PASEO_PORT |
16891 |
监听端口(Paseo 配置里被覆盖成 16810) |
HOST |
127.0.0.1 |
绑定地址;对外要设 0.0.0.0 |
CC_SWITCH_DB |
~/.cc-switch/cc-switch.db |
cc-switch 数据库路径 |
CCSWITCH_MOBILE_STATE |
~/.paseo/ccswitch-mobile |
状态目录(selection.json + token 存这里) |
CCSWITCH_MOBILE_TOKEN |
自动生成 | 访问令牌;不设则首次启动生成一个 32 位 hex 存到状态目录 |
PASEO_URL |
— | 外部访问基础 URL,仅用于日志里打印手机地址 |
token 存哪:~/.paseo/ccswitch-mobile/token(权限 0600)。~/.cc-switch/、~/.paseo/ 都在 home 目录,不会进这个仓库。
paseo.json(放在 workspace 根目录):
{
"scripts": {
"ccswitch-mobile": {
"type": "service",
"command": "HOST=0.0.0.0 node paseo-ccswitch-mobile/server.mjs",
"port": 16810
}
}
}然后在 Paseo 里启动这个 script。它会监听 *:16810,启动日志会直接打印带 token 的局域网/外网 URL。
HOST=0.0.0.0 PORT=16810 node server.mjs
# 启动日志会打印:http://<你的IP>:16810/?token=<自动生成的token># 1) 服务已在 127.0.0.1:16810 跑起来
# 2) 第一次要先在浏览器里给该节点开启 Funnel(每个 tailnet 一次)
tailscale funnel --bg --https=443 http://127.0.0.1:16810
# 若提示未启用,打开它给的 https://login.tailscale.com/f/funnel?... 点 Enable
# 3) 拿到公网域名
tailscale funnel status之后外网/手机直接开:
https://<你的节点>.ts.net/?token=<token>
| 方法 / 路径 | 作用 |
|---|---|
GET /api/state |
所有 app 总览:节点数、当前选中、厂商启用、路由/自动转移/队列数 |
GET /api/nodes?app=claude&hours=24 |
某 app 的节点列表 + 近 N 小时用量(调用数/错误/成本/最近时间) |
POST /api/select {app,id} |
切换某 app 的上游;写 selection.json 并镜像 is_current |
POST /api/ping {app,id} |
测上游可达性:GET {base}/v1/models(gemini 用 /v1beta/models),非 200 报 unknown 而非误判为挂 |
GET /api/vendors |
读厂商启用状态(Claude / Codex / Gemini / Grok,可同时开) |
POST /api/vendors {app,enabled} 或 {vendors:{claude:true,...}} |
开/关某个或一批厂商;关后底部页签隐藏,状态落盘 vendors.json |
GET /api/proxy-config[?app=X] |
读某 app(或全部)的路由/自动转移/重试/熔断配置 |
POST /api/proxy-config {app,proxy_enabled?,auto_failover_enabled?,max_retries?} |
改路由/自动转移开关或重试次数,写 proxy_config |
GET /api/failover-queue?app=X |
某 app 的故障转移队列(含熔断/健康状态) |
POST /api/failover-queue {app,id,enabled} |
把节点加入/移出队列,写 in_failover_queue |
GET /api/health |
{status:"ok", selection} |
GET|POST /proxy/<app>/... |
转发到当前选中上游,自动带鉴权头(Anthropic 官方走 x-api-key,Gemini 走 x-goog-api-key,其余 Authorization: Bearer);路由关闭返回 503,自动转移开启时失败按队列切换 |
鉴权(非回环):URL ?token=、x-ccswitch-token 头、或 ccswitch_token Cookie 三选一。
server.mjs 全部服务逻辑(单文件)
public/index.html 手机界面(含设置 → 厂商开关)
public/app.js
public/app.css
状态(不在仓库):~/.paseo/ccswitch-mobile/{selection.json,vendors.json,token}