Skip to content

Commit 42540cf

Browse files
committed
fix(ci): repair GitHub flow failures
Root causes fixed: - Collapse Codex model fetch Tauri/service arguments into request structs so Volcengine AgentPlan fields no longer trip clippy::too_many_arguments in Backend Checks. - Preserve live model_context_window as user-owned Codex display/context state when a provider omits it. - Treat same-provider live tables containing local proxy base_url or PROXY_MANAGED as takeover residue and replace them from the provider backup so restored configs do not keep proxy routing fields. - Build and verify the Windows codex-history-repairer sidecar before Tauri bundling, fixing the NSIS missing sidecar failure. - Cross-build Windows ARM64 release assets on windows-2022 because windows-11-arm can compile the app but WiX v3 light.exe fails there. - Mark GitHub releases as formal releases with prerelease=false. Validation: - cargo fmt --manifest-path src-tauri/Cargo.toml - cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings - cargo test --manifest-path src-tauri/Cargo.toml - cargo build --manifest-path src-tauri/Cargo.toml --bin codex-history-repairer --features history-repairer --release - pnpm typecheck - pnpm test:unit - pnpm exec prettier --check src/lib/api/model-fetch.ts .github/workflows/release.yml memory.md - git diff --check Notes: - actionlint is not installed on this machine, so workflow syntax still needs GitHub-side validation after pushing to fork/main. - Untracked output/release-* and scripts/logs directories were intentionally left untouched.
1 parent 6adc3b2 commit 42540cf

6 files changed

Lines changed: 156 additions & 55 deletions

File tree

.github/workflows/release.yml

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ jobs:
2020
matrix:
2121
include:
2222
- os: windows-2022
23-
- os: windows-11-arm
23+
# GitHub 的 Windows ARM64 runner 能编译主程序,但 WiX v3 light.exe 在该
24+
# 环境中无法可靠启动;ARM64 安装包改由 x64 Windows runner 交叉打包。
25+
- os: windows-2022
2426
arch: arm64
2527
- os: ubuntu-22.04
2628
- os: ubuntu-22.04-arm
@@ -95,14 +97,14 @@ jobs:
9597
|| sudo apt-get install -y --no-install-recommends libsoup2.4-dev
9698
9799
- name: Setup pnpm
98-
if: runner.os != 'Windows' || matrix.arch != 'arm64'
100+
if: runner.os != 'Windows' || runner.arch != 'ARM64'
99101
uses: pnpm/action-setup@v6
100102
with:
101103
version: 10.12.3
102104
run_install: false
103105

104106
- name: Setup pnpm (Windows ARM64)
105-
if: runner.os == 'Windows' && matrix.arch == 'arm64'
107+
if: runner.os == 'Windows' && runner.arch == 'ARM64'
106108
shell: pwsh
107109
run: |
108110
$ErrorActionPreference = 'Stop'
@@ -112,13 +114,13 @@ jobs:
112114
pnpm --version
113115
114116
- name: Get pnpm store directory
115-
if: runner.os != 'Windows' || matrix.arch != 'arm64'
117+
if: runner.os != 'Windows' || runner.arch != 'ARM64'
116118
id: pnpm-store
117119
shell: bash
118120
run: echo "path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
119121

120122
- name: Setup pnpm cache
121-
if: runner.os != 'Windows' || matrix.arch != 'arm64'
123+
if: runner.os != 'Windows' || runner.arch != 'ARM64'
122124
uses: actions/cache@v5
123125
with:
124126
path: ${{ steps.pnpm-store.outputs.path }}
@@ -292,6 +294,24 @@ jobs:
292294
sleep "$sleep_seconds"
293295
done
294296
297+
- name: Build Windows history repair sidecar
298+
if: runner.os == 'Windows'
299+
shell: pwsh
300+
env:
301+
WINDOWS_RELEASE_ARCH: ${{ matrix.arch || 'x86_64' }}
302+
run: |
303+
$ErrorActionPreference = 'Stop'
304+
if ($env:WINDOWS_RELEASE_ARCH -eq 'arm64') {
305+
cargo build --manifest-path src-tauri/Cargo.toml --bin codex-history-repairer --features history-repairer --target aarch64-pc-windows-msvc --release
306+
$sidecar = 'src-tauri/target/aarch64-pc-windows-msvc/release/codex-history-repairer.exe'
307+
} else {
308+
cargo build --manifest-path src-tauri/Cargo.toml --bin codex-history-repairer --features history-repairer --release
309+
$sidecar = 'src-tauri/target/release/codex-history-repairer.exe'
310+
}
311+
if (-not (Test-Path $sidecar)) {
312+
throw "codex-history-repairer sidecar was not produced: $sidecar"
313+
}
314+
295315
- name: Build Tauri App (Windows)
296316
if: runner.os == 'Windows'
297317
shell: pwsh
@@ -686,7 +706,7 @@ jobs:
686706
with:
687707
tag_name: ${{ github.ref_name }}
688708
name: CC Switch ${{ github.ref_name }}
689-
prerelease: true
709+
prerelease: false
690710
body: |
691711
## CC Switch ${{ github.ref_name }}
692712

memory.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# CC Switch Repository Memory
22

3+
## 2026-07-03 GitHub Flow CI/Release Failure Root Fix
4+
5+
- `v3.16.4-7` 推到 `BigStrongSun/ccswitchmulti` 后 GitHub Actions 有两类真实失败:CI 的 `Backend Checks` 卡在 Rust Clippy,Release 的 Windows 打包卡在 Windows 资产构建。不要把它归因为 GitHub 本身抽风,也不要只重跑 workflow。
6+
- CI 根因之一是火山 AgentPlan 模型列表支持把 `fetch_models_for_config` / `model_fetch::fetch_models` 扩成 8 个并列参数,触发 `clippy::too_many_arguments`。修复边界是把 Tauri 命令入参收敛成 `FetchModelsForConfigRequest`,后端服务收敛成 `FetchModelsRequest` / `VolcengineModelListRequest`,前端 `invoke` 改为 `{ request: ... }`
7+
- CI 继续暴露的 backend test 根因是 Codex provider 配置合并边界不清:`model_context_window` 应视为用户界面/上下文显示偏好,provider 未声明时不能从 live config 删除;同名 custom provider 表如果 live 里仍带本地代理 `base_url``PROXY_MANAGED`,恢复 provider 备份时必须按 takeover 残留整表替换,否则本地代理字段会继续劫持后续请求。
8+
- Release Windows x64 根因是 Tauri bundler 需要 `codex-history-repairer.exe` sidecar,但 workflow 只构建主程序,NSIS 阶段报 `codex-history-repairer.exe` 不存在。修复是在 Windows Tauri build 前显式按架构构建并校验 history repair sidecar。
9+
- Release Windows ARM64 根因是 `windows-11-arm` runner 能编译主程序,但 WiX v3 `light.exe` 在该 ARM runner 环境无法可靠启动;正式 workflow 应在 `windows-2022` x64 runner 上交叉构建 `aarch64-pc-windows-msvc`,同时 pnpm setup/cache 条件要看 `runner.arch`,不要看目标 `matrix.arch`
10+
- `release.yml` 的正式发布语义必须是 `prerelease: false`;否则 GitHub latest 不会按正式 release 晋升,即使 tag 和资产都存在。
11+
- 本轮本地验证覆盖:`cargo clippy --manifest-path src-tauri\Cargo.toml -- -D warnings``cargo test --manifest-path src-tauri\Cargo.toml``pnpm typecheck``pnpm test:unit``pnpm exec prettier --check src/lib/api/model-fetch.ts .github/workflows/release.yml``actionlint` 本机未安装,后续若继续改 workflow,优先补装或在 CI 侧验证 workflow 语法。
12+
313
## 2026-07-02 CCSwitchMulti v3.16.4-7 Formal Release
414

515
- `v3.16.4-7``v3.16.4-6` 后的 MultiRouter 路由热修复正式发布,核心变更是修复第三方 GPT alias 只出现在聚合 `modelCatalog`、没有回到第三方 route `match.models/modelMap` 时被官方 `gpt` 前缀 route 抢走的问题;版本提交为 `755b69e4ee0b5a91461558e4b7a8d8753b38bc5e``chore(release): bump CCSwitchMulti to v3.16.4-7`)。

src-tauri/src/codex_config.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2087,6 +2087,39 @@ fn codex_provider_table_has_proxy_placeholder(item: &Item) -> bool {
20872087
== Some(CODEX_PROXY_AUTH_PLACEHOLDER)
20882088
}
20892089

2090+
/// 判断 URL 是否指向本机代理地址。
2091+
///
2092+
/// 这里不依赖 `proxy.rs` 的运行态端口,只识别接管写入时会出现的本机回环地址。
2093+
/// 该判断只用于清理 takeover 残留,不能把用户配置的真实第三方 URL 当成可删除字段。
2094+
fn codex_base_url_is_local_proxy(url: &str) -> bool {
2095+
let lower = url.trim().to_ascii_lowercase();
2096+
let Some((_, rest)) = lower.split_once("://") else {
2097+
return false;
2098+
};
2099+
rest.starts_with("127.")
2100+
|| rest.starts_with("localhost")
2101+
|| rest.starts_with("0.0.0.0")
2102+
|| rest.starts_with("[::1]")
2103+
|| rest.starts_with("[::]")
2104+
|| rest.starts_with("::1")
2105+
|| rest.starts_with("::")
2106+
}
2107+
2108+
/// 判断同名 provider 表是否仍像接管期间生成的临时代理表。
2109+
///
2110+
/// 恢复 provider 备份时,live config 可能还是接管态:同名表里有本地
2111+
/// `base_url` 或 `PROXY_MANAGED` token。此时不能做“provider 键优先”的
2112+
/// 递归合并,否则备份没有声明的接管字段会被保留下来并继续劫持请求。
2113+
fn codex_provider_table_has_takeover_artifact(item: &Item) -> bool {
2114+
item.as_table_like().is_some_and(|table| {
2115+
codex_provider_table_has_proxy_placeholder(item)
2116+
|| table
2117+
.get("base_url")
2118+
.and_then(|url| url.as_str())
2119+
.is_some_and(codex_base_url_is_local_proxy)
2120+
})
2121+
}
2122+
20902123
/// 移除目标 provider 不再使用的 CC Switch 自有 provider 表。
20912124
///
20922125
/// 参数:
@@ -2147,7 +2180,7 @@ fn remove_codex_provider_owned_fields_missing_from_provider(
21472180
remove_active_custom_codex_model_provider_section(live_doc);
21482181
}
21492182

2150-
for key in ["model", "model_provider", "model_context_window"] {
2183+
for key in ["model", "model_provider"] {
21512184
if provider_doc.get(key).is_none() {
21522185
live_doc.as_table_mut().remove(key);
21532186
}
@@ -2256,7 +2289,11 @@ pub(crate) fn merge_codex_provider_config_texts(
22562289
{
22572290
match live_providers.get_mut(provider_id) {
22582291
Some(live_item) => {
2259-
merge_codex_toml_item_prefer_provider(live_item, &provider_item)
2292+
if codex_provider_table_has_takeover_artifact(live_item) {
2293+
*live_item = provider_item;
2294+
} else {
2295+
merge_codex_toml_item_prefer_provider(live_item, &provider_item);
2296+
}
22602297
}
22612298
None => {
22622299
live_providers.insert(provider_id, provider_item);

src-tauri/src/commands/model_fetch.rs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ use reqwest::header::{HeaderValue, CONTENT_TYPE, USER_AGENT};
77
use serde::{Deserialize, Serialize};
88
use std::time::Duration;
99

10+
/// 前端模型列表刷新命令的完整请求体。
11+
///
12+
/// 这个结构体保持 Tauri 命令只有一个入参,避免新增供应商专用字段后继续扩散成
13+
/// 多个并列参数;字段名由 `camelCase` 与前端 `invoke` 请求保持一致。
14+
#[derive(Debug, Clone, Deserialize)]
15+
#[serde(rename_all = "camelCase")]
16+
pub struct FetchModelsForConfigRequest {
17+
pub base_url: String,
18+
pub api_key: String,
19+
pub is_full_url: Option<bool>,
20+
pub models_url: Option<String>,
21+
pub custom_user_agent: Option<String>,
22+
pub volcengine_model_list_action: Option<String>,
23+
pub volcengine_access_key_id: Option<String>,
24+
pub volcengine_secret_access_key: Option<String>,
25+
}
26+
1027
/// Codex 上游协议探测结果。
1128
#[derive(Debug, Clone, Serialize, Deserialize)]
1229
#[serde(rename_all = "camelCase")]
@@ -24,29 +41,24 @@ pub struct CodexResponsesProbeResult {
2441
/// 否则对 baseURL 生成候选列表(含「剥离 Anthropic 兼容子路径」兜底),按序尝试。
2542
#[tauri::command(rename_all = "camelCase")]
2643
pub async fn fetch_models_for_config(
27-
base_url: String,
28-
api_key: String,
29-
is_full_url: Option<bool>,
30-
models_url: Option<String>,
31-
custom_user_agent: Option<String>,
32-
volcengine_model_list_action: Option<String>,
33-
volcengine_access_key_id: Option<String>,
34-
volcengine_secret_access_key: Option<String>,
44+
request: FetchModelsForConfigRequest,
3545
) -> Result<Vec<FetchedModel>, String> {
3646
// 与转发 / 检测路径共用 parse_custom_user_agent:非法 UA 静默忽略(不阻断取模型)。
37-
let user_agent = crate::provider::parse_custom_user_agent(custom_user_agent.as_deref())
47+
let user_agent = crate::provider::parse_custom_user_agent(request.custom_user_agent.as_deref())
3848
.ok()
3949
.flatten();
40-
model_fetch::fetch_models(
41-
&base_url,
42-
&api_key,
43-
is_full_url.unwrap_or(false),
44-
models_url.as_deref(),
50+
model_fetch::fetch_models(model_fetch::FetchModelsRequest {
51+
base_url: &request.base_url,
52+
api_key: &request.api_key,
53+
is_full_url: request.is_full_url.unwrap_or(false),
54+
models_url_override: request.models_url.as_deref(),
4555
user_agent,
46-
volcengine_model_list_action.as_deref(),
47-
volcengine_access_key_id.as_deref(),
48-
volcengine_secret_access_key.as_deref(),
49-
)
56+
volcengine: model_fetch::VolcengineModelListRequest {
57+
action: request.volcengine_model_list_action.as_deref(),
58+
access_key_id: request.volcengine_access_key_id.as_deref(),
59+
secret_access_key: request.volcengine_secret_access_key.as_deref(),
60+
},
61+
})
5062
.await
5163
}
5264

src-tauri/src/services/model_fetch.rs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,31 @@ pub struct FetchedModel {
1919
pub context_window: Option<u64>,
2020
}
2121

22+
/// 模型列表获取服务的统一入参。
23+
///
24+
/// 通用 OpenAI-compatible `/models` 字段和火山 Plan 管控面字段放在同一个请求对象里,
25+
/// 让服务入口保持稳定;新增 provider 专用取模逻辑时优先扩展子结构而不是继续增加函数参数。
26+
#[derive(Debug, Clone)]
27+
pub struct FetchModelsRequest<'a> {
28+
pub base_url: &'a str,
29+
pub api_key: &'a str,
30+
pub is_full_url: bool,
31+
pub models_url_override: Option<&'a str>,
32+
pub user_agent: Option<HeaderValue>,
33+
pub volcengine: VolcengineModelListRequest<'a>,
34+
}
35+
36+
/// 火山 Agent/Coding Plan 模型枚举的管控面参数。
37+
///
38+
/// 这些字段不是推理 API 的 bearer key,而是火山 OpenAPI AK/SK;没有 action 时
39+
/// 主流程仍回到普通 OpenAI-compatible `/models` 候选逻辑。
40+
#[derive(Debug, Clone, Copy, Default)]
41+
pub struct VolcengineModelListRequest<'a> {
42+
pub action: Option<&'a str>,
43+
pub access_key_id: Option<&'a str>,
44+
pub secret_access_key: Option<&'a str>,
45+
}
46+
2247
/// OpenAI 兼容的 /v1/models 响应格式
2348
#[derive(Debug, Deserialize)]
2449
struct ModelsResponse {
@@ -132,46 +157,41 @@ async fn fetch_volcengine_plan_models(
132157
/// 获取供应商的可用模型列表
133158
///
134159
/// 使用 OpenAI 兼容的 GET /v1/models 端点,按候选列表顺序尝试。
135-
pub async fn fetch_models(
136-
base_url: &str,
137-
api_key: &str,
138-
is_full_url: bool,
139-
models_url_override: Option<&str>,
140-
user_agent: Option<HeaderValue>,
141-
volcengine_model_list_action: Option<&str>,
142-
volcengine_access_key_id: Option<&str>,
143-
volcengine_secret_access_key: Option<&str>,
144-
) -> Result<Vec<FetchedModel>, String> {
145-
if let Some(action) = normalize_volcengine_model_list_action(volcengine_model_list_action)? {
160+
pub async fn fetch_models(options: FetchModelsRequest<'_>) -> Result<Vec<FetchedModel>, String> {
161+
if let Some(action) = normalize_volcengine_model_list_action(options.volcengine.action)? {
146162
return fetch_volcengine_plan_models(
147-
base_url,
163+
options.base_url,
148164
action,
149-
volcengine_access_key_id,
150-
volcengine_secret_access_key,
165+
options.volcengine.access_key_id,
166+
options.volcengine.secret_access_key,
151167
)
152168
.await;
153169
}
154170

155-
if api_key.is_empty() {
171+
if options.api_key.is_empty() {
156172
return Err("API Key is required to fetch models".to_string());
157173
}
158174

159-
let candidates = build_models_url_candidates(base_url, is_full_url, models_url_override)?;
175+
let candidates = build_models_url_candidates(
176+
options.base_url,
177+
options.is_full_url,
178+
options.models_url_override,
179+
)?;
160180
let client = crate::proxy::http_client::get();
161181
let mut last_err: Option<String> = None;
162182

163183
for url in &candidates {
164184
log::debug!("[ModelFetch] Trying endpoint: {url}");
165-
let mut request = client
185+
let mut request_builder = client
166186
.get(url)
167-
.header("Authorization", format!("Bearer {api_key}"))
187+
.header("Authorization", format!("Bearer {}", options.api_key))
168188
.timeout(Duration::from_secs(FETCH_TIMEOUT_SECS));
169189
// 自定义 User-Agent:部分 /models 端点同样有 UA 白名单(如 Kimi Coding Plan),
170190
// 与转发 / 检测路径共用同一 UA,避免"代理可用但取模型失败"。
171-
if let Some(ua) = &user_agent {
172-
request = request.header(USER_AGENT, ua.clone());
191+
if let Some(ua) = &options.user_agent {
192+
request_builder = request_builder.header(USER_AGENT, ua.clone());
173193
}
174-
let response = match request.send().await {
194+
let response = match request_builder.send().await {
175195
Ok(r) => r,
176196
Err(e) => {
177197
return Err(format!("Request failed: {e}"));

src/lib/api/model-fetch.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,16 @@ export async function fetchModelsForConfig(
3737
volcengineModelList?: VolcengineModelListOptions,
3838
): Promise<FetchedModel[]> {
3939
return invoke("fetch_models_for_config", {
40-
baseUrl,
41-
apiKey,
42-
isFullUrl,
43-
modelsUrl,
44-
customUserAgent,
45-
volcengineModelListAction: volcengineModelList?.action,
46-
volcengineAccessKeyId: volcengineModelList?.accessKeyId,
47-
volcengineSecretAccessKey: volcengineModelList?.secretAccessKey,
40+
request: {
41+
baseUrl,
42+
apiKey,
43+
isFullUrl,
44+
modelsUrl,
45+
customUserAgent,
46+
volcengineModelListAction: volcengineModelList?.action,
47+
volcengineAccessKeyId: volcengineModelList?.accessKeyId,
48+
volcengineSecretAccessKey: volcengineModelList?.secretAccessKey,
49+
},
4850
});
4951
}
5052

0 commit comments

Comments
 (0)