mirror of
https://github.com/yynxxxxx/Codex-X.git
synced 2026-10-18 05:59:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81d259e276 | ||
|
|
ed3d8cff88 |
@@ -2,6 +2,18 @@
|
||||
|
||||
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、指令提示词、供应商、会话管理等页面切换时右侧滚动条闪现/消失造成的视觉抖动。
|
||||
- 外层页面滚动容器改为稳定滚动并隐藏外层滚动条,保留 TOML 编辑器、会话列表等内部区域自己的滚动条。
|
||||
|
||||
## [v0.2.24] - 2026-07-06
|
||||
|
||||
- 修复切换供应商 / 会话管理等页面时右侧滚动条短暂出现又消失的问题:页面切换动画改为纯透明度过渡,不再用 `translateY` 造成瞬时溢出。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codex-x",
|
||||
"version": "0.2.24",
|
||||
"version": "0.2.26",
|
||||
"private": true,
|
||||
"description": "Codex Switch & Instruct desktop manager",
|
||||
"type": "module",
|
||||
|
||||
Generated
+1
-1
@@ -346,7 +346,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codex-x"
|
||||
version = "0.2.24"
|
||||
version = "0.2.26"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "codex-x"
|
||||
version = "0.2.24"
|
||||
version = "0.2.26"
|
||||
description = "Codex Switch & Instruct desktop manager"
|
||||
authors = ["yynxxxxx"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Codex-X",
|
||||
"version": "0.2.24",
|
||||
"version": "0.2.26",
|
||||
"identifier": "com.yynxxxxx.codexx",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -689,10 +689,16 @@ html, body, #root {
|
||||
}
|
||||
.content {
|
||||
height: calc(100vh - var(--shell-vertical-pad));
|
||||
overflow-y: auto;
|
||||
/* Keep one stable scroll container for every page. The outer page scrollbar is
|
||||
intentionally hidden because route changes can otherwise flash a temporary
|
||||
right-side scrollbar while page content mounts/measures. Inner editors/lists
|
||||
keep their own visible scrollbars. */
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 4px;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.content::-webkit-scrollbar,
|
||||
.provider-row-list::-webkit-scrollbar,
|
||||
@@ -702,6 +708,11 @@ html, body, #root {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
/* Hide only the outer page scrollbar; nested TOML/session/editor scrollbars remain visible. */
|
||||
.content::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.content::-webkit-scrollbar-thumb,
|
||||
.provider-row-list::-webkit-scrollbar-thumb,
|
||||
.backup-list::-webkit-scrollbar-thumb,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "codex-x-workspace",
|
||||
"private": true,
|
||||
"version": "0.2.24",
|
||||
"version": "0.2.26",
|
||||
"scripts": {
|
||||
"dev": "pnpm --dir apps/desktop tauri dev",
|
||||
"build": "pnpm --dir apps/desktop tauri build",
|
||||
|
||||
Reference in New Issue
Block a user