aidog 加平台 / 改平台
给 aidog 新增一个平台预设,或修改一个平台的默认配置(base_url、端点协议、coding plan、默认模型、余额/配额查询)。本 skill 给出改哪几处、什么顺序、怎么验证,并把调研证实的反直觉陷阱前置,避免照源码直觉走错。
file:line 锚点对应当前代码(2026-06-15 校对)。行号会随代码漂移,定位以函数名/符号名为准,行号仅作快速跳转参考。
0. 四条认知纠偏(动手前必读,最大价值)
平台预设住前端,不在后端。
db.rs没有任何平台 seed 函数。create_platform(src-tauri/src/gateway/db.rs:508)是纯落库——传什么 base_url / endpoints / models 就存什么,对默认配置零知识。所有「选了平台 → 自动填 base_url」的预设逻辑都在前端src/pages/Platforms.tsx的getDefaultEndpoints(:150)。别去 db.rs 找 seed,没有。Protocol 枚举 Rust↔TS 必须逐字双写,无容错,失配整体解析失败。 Rust
Protocol(src-tauri/src/gateway/models.rs:5)每个变体的#[serde(rename="xxx")]字符串,必须与 TS 联合类型(src/services/api.ts:6-27)的字面量"xxx"逐字一致。Protocol没有容错 deserialize(不像ClientType有deserialize_client_type_lenient,models.rs:320)——TS 传一个 Rust 不认识的字符串 → 整个 Platform 反序列化失败而非回退。漏改一侧 = 静默炸。adapter/{glm,kimi,minimax,bailian,codex}.rs是死代码,禁去改。 这 5 个文件全标#[allow(dead_code)],且converter.rs的 dispatch 从不调它们(内部本就super::openai::to_openai直转)。真实协议转换只在convert_request(src-tauri/src/gateway/adapter/converter.rs:10)的 5 个 wire 分支。加平台时不要去这些 adapter 文件加逻辑。coding plan / 余额查询按 base_url 子串分派,不按 Protocol 枚举。
query_quota(src-tauri/src/gateway/quota.rs:373)用base_url.to_lowercase()做if url.contains("...")顺序匹配,与Protocol枚举无关。接余额/配额查询时认 base_url 子串,别去枚举上挂。
1. 先判路径:加平台是哪种?
新平台的上游报文格式是什么?
├─ OpenAI Chat Completions 兼容(绝大多数国内/聚合/中转平台)
│ → endpoint.protocol = "openai",base_url 含 /v1 等前缀
│ → 【路径 1】零 wire 改动,走 converter.rs:34 默认分支 to_openai
├─ Anthropic Messages 兼容(很多平台提供 /anthropic 端点)
│ → endpoint.protocol = "anthropic",base_url 到 host 根(proxy 拼 /v1/messages)
│ → 【路径 1】零 wire 改动,走 converter.rs:12
├─ Gemini / OpenAI Responses / OpenAI Completions
│ → 对应 wire protocol,已有 adapter,零新增 → 【路径 1】
└─ 全新私有 wire 格式(现实中几乎不出现)
→ 【路径 2】新建 adapter + converter match + parse_sse + 入站解析(重活)
90%+ 的「加平台」是路径 1——只是又一个 OpenAI/Anthropic 兼容中转/聚合站,复用现成 wire 协议,给它一个独立枚举名 + 预设 base_url。
关键区分:Protocol 变体有两类语义(
models.rs:6-17注释划分):
- wire protocol(可作 endpoint 协议):仅
anthropic / openai / openai_responses / openai_completions / gemini这 5 个决定请求体格式与 SSE 解析。- 平台类型(仅作平台主协议):其余全部(glm/kimi/deepseek/newapi…),只是身份标签 + 决定 OpenAI 兼容平台的 chat path,不参与 wire 转换。 新平台加的枚举变体几乎总是「平台类型」,wire 复用前 5 个之一。
2. 路径 1:纯 OpenAI/Anthropic 兼容平台(6 处,缺一即失败)
按顺序改。前 3 处是跨层契约,后 3 处是前端预设。
① Rust Protocol 枚举加变体 — src-tauri/src/gateway/models.rs:5
在「平台类型」段加一行:
#[serde(rename = "foo")] // ★ rename 字符串 = DB/JSON 持久值,确定后不可改
Foo,
加完后所有对
Protocol的 exhaustive match 会编译报错指路(见 §4),按提示补。
② TS Protocol 联合类型加字面量 — src/services/api.ts:6-27
| "foo" // ★ 与 ① 的 rename 逐字一致
③ platform_fetch_models 的 | 链 — src-tauri/src/lib.rs:687
新 OpenAI 兼容平台多半归到「走 {base}/models GET」那条 Protocol::A | Protocol::B | ... => 链。编译强制(exhaustive match),不补 cargo build 报错。
④ PROTOCOLS 下拉条目 — src/pages/Platforms.tsx:17
{ value: "foo", label: "Foo", keywords: ["foo", "foo.com"], codingPlan: false },
不补 = 用户在「添加平台」下拉里选不到(软失败,无编译报错)。codingPlan: true 标记编程订阅平台。keywords 供智能粘贴解析器匹配(src/utils/platformPaste.ts matchPlatform)。
⑤ getDefaultEndpoints 预设端点 — src/pages/Platforms.tsx:150
在 base map(Partial<Record<Protocol, PlatformEndpoint[]>>)里加该平台 → PlatformEndpoint[]:
foo: [
{ protocol: "openai", base_url: "https://api.foo.com/v1", client_type: "default", coding_plan: false },
],
铁律:base_url 含版本前缀(如 /v1、/api/paas/v4)。proxy 拼 api_path(OpenAI 系 = /chat/completions,converter.rs:44),禁额外拼接。不补 = 选了平台后 endpoints 为空,用户得手填 base_url(Partial,非 exhaustive,不报错)。
多端点示例(glm,
Platforms.tsx:168-171):同平台可配 openai + anthropic 双端点,运行期按入站协议匹配(proxy.rs:945-957)。coding plan 平台用cp三元切 base_url(kimi,:176-178)。
⑥ 显示名 — src/pages/Platforms.tsx:397 PROTOCOL_LABELS
foo: "Foo",
这是 Record<Protocol, string>,tsc exhaustive 强制,不补 yarn build 报错。(研究里把它叫「显示名 map」——它就是 PROTOCOL_LABELS;DEFAULT_NAMES 在 :470 从它派生。)
可选项
- 颜色
PROTOCOL_COLORS(Platforms.tsx:472):key 是string非Protocol,非 exhaustive,不补走 fallbackvar(--accent)。 - 默认模型
getDefaultModels(Platforms.tsx:371):在 presets map 加一行预填模型槽位,见 §5。强烈建议补(否则选平台后模型槽位空)。 - 余额 / coding plan 查询:见
references/quota-coding-plan.md。
路径 1 不需要改任何 wire header / converter / 鉴权 / adapter——这些按 wire 协议分支,复用现成。后端
db.rs / router.rs / converter.rs / adapter/均无需动。
完整 file:line 触点表见 references/touchpoints-map.md。
3. 路径 2:加新 wire 协议(重活,几乎用不上)
仅当上游报文格式 anthropic/openai/openai_responses/openai_completions/gemini 全不匹配时。除 §2 全部外,还需:
- 新建 adapter
src-tauri/src/gateway/adapter/xxx.rs(参考adapter/openai.rs/anthropic.rs活 adapter,别参考死代码 glm/kimi)+adapter/mod.rs注册 mod。 convert_request加 match 分支 —converter.rs:10(返回(Value, api_path))。parse_sse加 match 分支 —converter.rs:66附近(流式 SSE 解析)。parse_incoming_request(若作入站协议)—converter.rs入站解析。- wire 鉴权头 —
proxy.rsbuild_upstream_headers(:2482附近)/apply_default_headers(:2307附近)按 wire 协议加分支(anthropicx-api-key/ geminix-goog-api-key/ 默认 Bearer)。 - endpoint 协议选项
ENDPOINT_PROTOCOLS—Platforms.tsx:91(只有 wire 协议进这个列表)。 - 默认身份
ClientType::default_for_protocol(models.rs:307,非 exhaustive 有_)+ 前端defaultClientForProtocol(Platforms.tsx:120)。 - 把新 wire 协议在 Protocol 枚举里列为 wire 协议段(
models.rs:6-16区)。
4. 加 Protocol 变体后必补的 Rust match(编译强制)
加一个 Protocol 变体后,所有无 _ 兜底的 exhaustive match 编译失败。已知热点:
platform_fetch_models(lib.rs:687)—|链,必补(见 §2③)。
有 _ 兜底、多数情况不用动的:
convert_request/parse_sse(converter.rs)—_ => to_openai,OpenAI 兼容平台自动兜底。inject_coding_plan_fields(proxy.rs:2553)—_兜底,仅当要注特殊字段才加分支。ClientType::default_for_protocol(models.rs:307)— 有_。
实操:加完变体直接
cargo build,编译器把所有必补 match 指出来,按提示补|链即可。不会漏。
5. 默认模型预设(getDefaultModels)
详见 references/default-model.md。要点:
预设住前端 getDefaultModels(protocol, codingPlan?)(src/pages/Platforms.tsx:371),与 getDefaultEndpoints 并列。加平台时在其 presets map 加一行该平台 → 槽位对象,填单个 model 名/槽位(非列表):
foo: { default: "foo-model-v1" }, // OpenAI 兼容平台多归 default 槽
glm: { default: "glm-4.6" }, // 现有示例
kimi: { default: cp ? "kimi-k2.7-code" : "kimi-k2.6" }, // coding plan 切型号(型号名以源码为准)
- 返回
Partial<Record<ModelSlot, string>>(Partial,未覆盖平台返回{}不报错)。 - 槽位 key 必须 ∈
ModelSlot(default/sonnet/opus/haiku/gpt,api.ts:47/models.rs:235)。 - 准则:取该平台当前主力型号,确定才填,不确定留空(注释
Platforms.tsx:370)。 - 两个消费点已按 Protocol 泛化、无需改:表单 auto-fill(
:1620,切协议时setModels展开预设)、列表卡片回退展示(:1164,已配置 → 上游 available → 预设回退)。
路由消费走 resolve_model(router.rs:317):请求模型名含 opus/sonnet/haiku/gpt → 用对应槽位,否则 default,无 default → 透传(去 [budget] 后缀)。
6. 余额 / coding plan / 价格接入(仅平台支持上游查询时)
完整模板见 references/quota-coding-plan.md。要点:
- 余额查询(按量平台):照搬
query_deepseek_balance(quota.rs:138)骨架 → 新增query_foo_balance+ 在query_quota余额段(quota.rs:392附近)加if url.contains("api.foo.com") { return query_foo_balance(...).await; }。返回balance: Some(BalanceInfo{...}), coding_plan: None。 - coding plan 配额(订阅平台):照搬
query_kimi_coding_plan(quota.rs:243)→ 在 coding plan 段(quota.rs:380附近,优先于余额段)加分派。返回coding_plan: Some(CodingPlanInfo{tiers, level}), balance: None。- 🔴 tier
name硬约束:必须 ∈cycle_ms_for_tier已知集合{"five_hour","weekly_limit","seven_day","mcp_monthly"}(usage_color.rs:30)。未知 name → 无周期 → statusline 配色退 Neutral。
- 🔴 tier
- 价格估算(按量平台):无需改代码。
resolve_price(db.rs:2840)回退链:pricing[platform_type]→ 顶层 →default_platform→ fallback。只要model_price表该模型price_data.pricing含新platform_type键(= Protocol rename 字符串)即命中,否则自动回退。靠价格同步/手填,不动代码。 - 无上游 quota API 的平台:用
manual_budgets(platform 列,JSON)本地限额兜底,与请求驱动预估并行(manual_budget.rs),无需改 quota.rs。
典型新平台只改
quota.rs(1 函数 + 1 分派行),可选改usage_color.rs(新 tier 周期)。command(platform_query_quota,lib.rs)/ api.ts / Platforms.tsx 都已泛化,无需改。
7. client_type 陷阱(协议 ≠ 身份)
- Protocol = 报文格式(决定 URL path / 鉴权 header 名 / 请求体结构)。
- ClientType = 模拟哪个客户端通过上游校验(注入 UA +
X-Stainless-*等指纹头,proxy.rs:2250apply_client_headers)。 - 二者正交:同一
openai协议可配codex_tui或claude_code或default身份。
🔴 coding plan 上游对 client_type 有身份白名单(协议 ≠ 身份):例如 Kimi coding plan 上游只接 Claude Code 身份、拒 Codex。所以 Platforms.tsx 给 Kimi 的 openai-wire coding endpoint 配的是 client_type: "claude_code" 而非 codex_tui——即便协议是 openai,身份也必须填上游接受的那个。配 coding endpoint 的 client_type 要匹配上游实际白名单,别按 wire 协议想当然。
8. coding plan 字段注入(仅需特殊 body 字段时)
inject_coding_plan_fields(body, protocol)(proxy.rs:2553)按平台主协议platform_typematch 注入请求体字段。当前唯一实分支是Protocol::Kimi(注prompt_cache_key),其余走_兜底(不注)。- 新平台 coding plan 需特殊字段 → 在
Protocol::Kimi分支旁、_前加Protocol::Foo =>分支。 - 🔴 parity 要求:proxy(
proxy.rs:1074-1077)与 model-test(lib.rs:837-838)并行调inject_coding_plan_fields+override_coding_plan_path。改注入逻辑两处必须同步,否则 model_test 与实际代理行为不一致(见 memorymodel-test-proxy-parity)。 override_coding_plan_path(proxy.rs:2582)当前是空壳(参数全_前缀,无 match)。各平台靠 base_url 区分 coding/normal,多数不需要动它;需要时先建 match 骨架。
9. URL 构造铁律
base_url含版本前缀(/v1、/api/paas/v4、/api/anthropic等)。provider_api_path()(converter.rs:44)OpenAI 系只返/chat/completions;anthropic 拼/v1/messages,gemini 拼/v1beta/...。- 最终 URL =
base_url + api_path。禁止额外拼接(CLAUDE.md 硬约束 + memoryurl-construction-rule)。 - 即 anthropic 端点 base_url 填到 host 根(如
https://open.bigmodel.cn/api/anthropic,proxy 拼/v1/messages);openai 端点 base_url 含/v1(proxy 拼/chat/completions)。
10. 验证门禁
# 动前端(Platforms.tsx / api.ts)
yarn build # tsc + vite,exhaustive Record 错位会在这里炸
yarn check:i18n # 仅当新增了 i18n 文案 key
# 动后端(models.rs / quota.rs / proxy.rs / lib.rs)
cd src-tauri && cargo build # exhaustive match 漏补在这里炸
cd src-tauri && cargo clippy # warning 必须清零(memory warnings-are-issues)
cd src-tauri && cargo test # 若动 quota/estimate/usage_color
收尾自检:
- Protocol 变体 Rust rename ↔ TS 字面量逐字一致(§0-2)。
-
PROTOCOL_LABELS/PROTOCOLS/getDefaultEndpoints三处都补(否则选不到 / 无预填 / tsc 报错)。 - base_url 含版本前缀,无额外拼接(§9)。
- coding endpoint
client_type匹配上游身份白名单(§7)。 - coding plan tier
name∈cycle_ms_for_tier集合(§6)。 - 改 inject 逻辑则 proxy + model-test 两处同步(§8)。
- 没去改 glm/kimi/minimax/bailian/codex 死代码 adapter(§0-3)。
反例黑名单(不要做)
- ❌ 去
db.rs找/加平台 seed —— 预设住前端getDefaultEndpoints。 - ❌ 只改 Rust Protocol 不改 TS(或反之)—— 无容错,整体解析失败。
- ❌ 去
adapter/{glm,kimi,minimax,bailian,codex}.rs加转换逻辑 —— 死代码,从不被调。 - ❌ base_url 不含版本前缀,或在 base_url 后再拼
/chat/completions—— 双拼接。 - ❌ coding endpoint 按 wire 协议想当然填 client_type —— 上游有身份白名单。
- ❌ coding plan tier 用
cycle_ms_for_tier集合外的 name —— 配色退中性。 - ❌ 改 inject 只改 proxy 不改 model-test —— parity 破坏。
- ❌ 加平台去改定价 UI(
PricingTab.tsx)—— 定价按模型名键,与平台无关,0 触点。
相关
- 触点全表:
references/touchpoints-map.md - 余额/配额模板:
references/quota-coding-plan.md - 默认模型节:
references/default-model.md - 请求链路调试:
aidog-request-inspectskill - 流程/IA:
aidog-flow-iaskill - 主要涉及文件:
src/pages/Platforms.tsx、src/services/api.ts、src-tauri/src/gateway/**