Compare commits

..
1 Commits
Author SHA1 Message Date
yyyyyynnn 81d259e276 Release v0.2.26 2026-07-07 17:21:46 +08:00
8 changed files with 91 additions and 20 deletions
+7
View File
@@ -2,6 +2,13 @@
All notable changes to Codex-X will be documented here.
## [v0.2.26] - 2026-07-07
- 修复第三方 Provider 切换后可能“看起来已切换但实际未按新供应商生效”的问题:不再把所有第三方都写成 `model_provider = "custom"`,而是写入供应商自己的稳定 ID。
- 修复从官方 ChatGPT 登录态切到第三方 API Key 时 `auth.json` 仍保留 `auth_mode = "chatgpt"` 的问题;写入 API Key 时会同步设置 `auth_mode = "api_key"`。
- 第三方供应商默认不再要求 OpenAI 登录态,避免新建/导入 Provider 时错误继承官方 auth 语义。
- 增加 Provider 切换单元测试,覆盖真实 provider key 和 API Key auth mode 的落盘结果。
## [v0.2.25] - 2026-07-06
- 进一步修复 TOML、指令提示词、供应商、会话管理等页面切换时右侧滚动条闪现/消失造成的视觉抖动。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "codex-x",
"version": "0.2.25",
"version": "0.2.26",
"private": true,
"description": "Codex Switch & Instruct desktop manager",
"type": "module",
+1 -1
View File
@@ -346,7 +346,7 @@ dependencies = [
[[package]]
name = "codex-x"
version = "0.2.25"
version = "0.2.26"
dependencies = [
"chrono",
"dirs 5.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "codex-x"
version = "0.2.25"
version = "0.2.26"
description = "Codex Switch & Instruct desktop manager"
authors = ["yynxxxxx"]
license = "MIT"
+69 -7
View File
@@ -109,6 +109,7 @@ struct BackupEntry {
#[serde(rename_all = "camelCase")]
struct ProviderInput {
config_dir: Option<String>,
provider_id: Option<String>,
provider_name: String,
base_url: String,
model: String,
@@ -1980,7 +1981,7 @@ fn extract_ccswitch_codex_provider(
let requires_openai_auth = provider_table
.and_then(|table| table.get("requires_openai_auth"))
.and_then(|item| item.as_bool())
.unwrap_or(true);
.unwrap_or(false);
Some(SavedProvider {
id: sanitize_id(id),
@@ -3965,6 +3966,7 @@ fn save_provider_toml_config_inner(input: ProviderTomlInput) -> Result<ActionRes
auth_value = json!({});
}
auth_value["OPENAI_API_KEY"] = Value::String(api_key);
auth_value["auth_mode"] = Value::String("api_key".to_string());
write_json(&auth, &auth_value)?;
}
@@ -3992,6 +3994,13 @@ fn switch_provider_inner(input: ProviderInput) -> Result<ActionResult> {
let backup_id = create_backup(&codex_dir, "switch-provider")?;
let provider_name = input.provider_name.trim();
let provider_key = input
.provider_id
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(sanitize_id)
.unwrap_or_else(|| sanitize_id(provider_name));
let base_url = input.base_url.trim().trim_end_matches('/');
let model = input.model.trim();
if provider_name.is_empty() {
@@ -4006,17 +4015,17 @@ fn switch_provider_inner(input: ProviderInput) -> Result<ActionResult> {
let text = read_to_string_if_exists(&cfg)?;
let mut doc = parse_toml_document(&cfg, &text)?;
doc["model_provider"] = value("custom");
doc["model_provider"] = value(provider_key.as_str());
doc["model"] = value(model);
set_top_level_defaults(&mut doc);
let root = doc.as_table_mut();
let providers = ensure_table(root, "model_providers")?;
let custom = ensure_table(providers, "custom")?;
custom["name"] = value(provider_name);
custom["base_url"] = value(base_url);
custom["wire_api"] = value(input.wire_api.unwrap_or_else(|| "responses".to_string()));
custom["requires_openai_auth"] = value(input.requires_openai_auth.unwrap_or(true));
let provider_table = ensure_table(providers, provider_key.as_str())?;
provider_table["name"] = value(provider_name);
provider_table["base_url"] = value(base_url);
provider_table["wire_api"] = value(input.wire_api.unwrap_or_else(|| "responses".to_string()));
provider_table["requires_openai_auth"] = value(input.requires_openai_auth.unwrap_or(false));
write_text(&cfg, &doc.to_string())?;
@@ -4035,6 +4044,7 @@ fn switch_provider_inner(input: ProviderInput) -> Result<ActionResult> {
auth_value = json!({});
}
auth_value["OPENAI_API_KEY"] = Value::String(api_key);
auth_value["auth_mode"] = Value::String("api_key".to_string());
write_json(&auth, &auth_value)?;
}
@@ -4181,3 +4191,55 @@ pub fn run() {
.run(tauri::generate_context!())
.expect("error while running Codex-X");
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_codex_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"codex-x-{name}-{}-{}",
std::process::id(),
Local::now().timestamp_nanos_opt().unwrap_or_default()
));
fs::create_dir_all(&dir).expect("create temp codex dir");
dir
}
#[test]
fn switch_provider_writes_real_provider_key_and_api_key_auth_mode() {
let codex_dir = temp_codex_dir("switch-provider");
let result = switch_provider_inner(ProviderInput {
config_dir: Some(codex_dir.display().to_string()),
provider_id: Some("magicai".to_string()),
provider_name: "MagicAI".to_string(),
base_url: "https://example.com/v1/".to_string(),
model: "gpt-5.5".to_string(),
api_key: Some("sk-test".to_string()),
wire_api: Some("responses".to_string()),
requires_openai_auth: None,
})
.expect("switch provider");
assert_eq!(result.state.model_provider.as_deref(), Some("magicai"));
assert_eq!(result.state.model.as_deref(), Some("gpt-5.5"));
let config_text = fs::read_to_string(config_path(&codex_dir)).expect("read config");
assert!(config_text.contains("model_provider = \"magicai\""));
assert!(config_text.contains("[model_providers.magicai]"));
assert!(config_text.contains("requires_openai_auth = false"));
let auth_text = fs::read_to_string(auth_path(&codex_dir)).expect("read auth");
let auth: Value = serde_json::from_str(&auth_text).expect("parse auth");
assert_eq!(
auth.get("OPENAI_API_KEY").and_then(Value::as_str),
Some("sk-test")
);
assert_eq!(
auth.get("auth_mode").and_then(Value::as_str),
Some("api_key")
);
let _ = fs::remove_dir_all(codex_dir);
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Codex-X",
"version": "0.2.25",
"version": "0.2.26",
"identifier": "com.yynxxxxx.codexx",
"build": {
"frontendDist": "../dist",
+10 -8
View File
@@ -279,7 +279,7 @@ const defaultProviderForm: SavedProvider = {
model: "gpt-5.5",
apiKey: "",
wireApi: "responses",
requiresOpenaiAuth: true,
requiresOpenaiAuth: false,
};
const blankProviderForm: SavedProvider = {
@@ -289,7 +289,7 @@ const blankProviderForm: SavedProvider = {
model: "gpt-5.5",
apiKey: "",
wireApi: "responses",
requiresOpenaiAuth: true,
requiresOpenaiAuth: false,
};
const blankPromptForm: SavedPrompt = {
@@ -599,6 +599,7 @@ function extractOpenAiApiKey(authText?: string) {
function buildProviderTomlPreview(provider: SavedProvider, state: CodexState | null) {
const model = provider.model.trim() || "gpt-5.5";
const name = provider.providerName.trim() || "your-provider";
const providerKey = providerId(provider.id || name || provider.baseUrl || "custom");
const baseUrl = provider.baseUrl.trim().replace(/\/+$/, "") || "https://example.com/v1";
const wireApi = provider.wireApi || "responses";
const source = state?.configText?.trimEnd() || "";
@@ -612,7 +613,7 @@ function buildProviderTomlPreview(provider: SavedProvider, state: CodexState | n
const sectionMatch = line.match(/^\s*\[([^\]]+)]\s*$/);
if (sectionMatch) {
currentSection = sectionMatch[1].trim();
skippingCustomProvider = currentSection === "model_providers.custom";
skippingCustomProvider = currentSection === `model_providers.${providerKey}`;
if (skippingCustomProvider) continue;
}
if (skippingCustomProvider) continue;
@@ -637,7 +638,7 @@ function buildProviderTomlPreview(provider: SavedProvider, state: CodexState | n
});
const headerLines = [
'model_provider = "custom"',
`model_provider = "${tomlEscape(providerKey)}"`,
`model = "${tomlEscape(model)}"`,
];
if (!hasReasoningEffort) {
@@ -645,7 +646,7 @@ function buildProviderTomlPreview(provider: SavedProvider, state: CodexState | n
}
const providerLines = [
"[model_providers.custom]",
`[model_providers.${providerKey}]`,
`name = "${tomlEscape(name)}"`,
`base_url = "${tomlEscape(baseUrl)}"`,
`wire_api = "${tomlEscape(wireApi)}"`,
@@ -664,7 +665,7 @@ function buildProviderTomlPreview(provider: SavedProvider, state: CodexState | n
function buildProviderAuthPreview(provider: SavedProvider) {
const key = provider.apiKey?.trim();
return JSON.stringify({ OPENAI_API_KEY: key || null }, null, 2);
return JSON.stringify({ OPENAI_API_KEY: key || null, auth_mode: key ? "api_key" : undefined }, null, 2);
}
@@ -910,7 +911,7 @@ function App() {
baseUrl: p.baseUrl || "",
model: state?.model || "gpt-5.5",
wireApi: p.wireApi || "responses",
requiresOpenaiAuth: p.requiresOpenaiAuth ?? true,
requiresOpenaiAuth: p.requiresOpenaiAuth ?? false,
isCurrent: p.isCurrent,
}));
}, [state]);
@@ -921,7 +922,7 @@ function App() {
source: "local" as const,
isCurrent:
Boolean(currentProvider) &&
currentProvider?.baseUrl === p.baseUrl &&
(currentProvider?.id === p.id || currentProvider?.baseUrl === p.baseUrl) &&
(state?.model || "") === p.model,
}));
}, [savedProviders, currentProvider, state?.model]);
@@ -1227,6 +1228,7 @@ function App() {
invoke<ActionResult>("switch_provider", {
input: {
configDir: configDir || null,
providerId: provider.id,
providerName: provider.providerName,
baseUrl: provider.baseUrl,
model: provider.model,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "codex-x-workspace",
"private": true,
"version": "0.2.25",
"version": "0.2.26",
"scripts": {
"dev": "pnpm --dir apps/desktop tauri dev",
"build": "pnpm --dir apps/desktop tauri build",