Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3136,7 +3136,30 @@ pub async fn native_browser_status() -> NativeBrowserDiagnostics {

#[tauri::command]
pub fn load_settings() -> CommandResult<SettingsPayload> {
settings_payload("设置已加载。", "设置读取失败")
let Ok(_guard) = relay_switch_mutex().lock() else {
return failed(
"供应商切换锁已损坏,请重启管理器后再试。",
fallback_settings_payload(),
);
};
let store = SettingsStore::default();
let home = codex_plus_core::relay_config::default_codex_home_dir();
match codex_plus_core::provider_import::initialize_local_config_provider(&store, &home) {
Ok(settings) => ok(
"设置已加载。",
SettingsPayload {
settings,
settings_path: codex_plus_core::paths::default_settings_path()
.to_string_lossy()
.to_string(),
user_scripts: user_script_inventory(),
},
),
Err(error) => failed(
&format!("读取本机默认供应商失败:{error}"),
fallback_settings_payload(),
),
}
}

#[tauri::command]
Expand Down
4 changes: 3 additions & 1 deletion apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ type BackendSettings = WhaleBalanceSettings & {
providerSyncManualProviders: string[];
providerSyncLastSelectedProvider: string;
relayProfilesEnabled: boolean;
localConfigProviderImported: boolean;
enhancementsEnabled: boolean;
codexAppStandaloneQuotaUnlock: boolean;
codexAppPluginMarketplaceUnlock: boolean;
Expand Down Expand Up @@ -1101,6 +1102,7 @@ const defaultSettings: BackendSettings = {
providerSyncManualProviders: [],
providerSyncLastSelectedProvider: "",
relayProfilesEnabled: true,
localConfigProviderImported: false,
enhancementsEnabled: true,
codexAppStandaloneQuotaUnlock: false,
codexAppPluginMarketplaceUnlock: true,
Expand Down Expand Up @@ -14130,7 +14132,7 @@ function relaySettingsWithDraft(
}

function relayProfileUsesLiveFiles(profile: RelayProfile): boolean {
return profile.relayMode !== "official" || profile.officialMixApiKey;
return profile.relayMode !== "official" || profile.officialMixApiKey || !!profile.configContents.trim();
}

function authJsonHasOpenAiApiKey(contents: string): boolean {
Expand Down
19 changes: 19 additions & 0 deletions apps/codex-plus-manager/src/local-config-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import ts from "typescript";

test("官方本机配置保留可编辑快照并接入现有文件编辑路径", async () => {
const source = await readFile(new URL("./App.tsx", import.meta.url), "utf8");
const file = ts.createSourceFile("App.tsx", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const fn = file.statements.find((node) => ts.isFunctionDeclaration(node)
&& node.name?.text === "relayProfileUsesLiveFiles");
assert.ok(fn);
const compiled = ts.transpileModule(fn.getText(file), {
compilerOptions: { target: ts.ScriptTarget.ES2022 },
}).outputText;
const usesFiles = new Function(`${compiled}; return relayProfileUsesLiveFiles;`)();
assert.equal(usesFiles({ relayMode: "official", officialMixApiKey: false, configContents: 'model = "gpt-5.5"' }), true);
assert.equal(usesFiles({ relayMode: "official", officialMixApiKey: false, configContents: "" }), false);
assert.equal(usesFiles({ relayMode: "pureApi", officialMixApiKey: false, configContents: "" }), true);
});
118 changes: 117 additions & 1 deletion crates/codex-plus-core/src/provider_import.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::settings::{RelayMode, RelayProfile, RelayProtocol, SettingsStore};
use crate::settings::{BackendSettings, RelayMode, RelayProfile, RelayProtocol, SettingsStore};
use anyhow::Context;
use std::path::Path;

Expand Down Expand Up @@ -26,6 +26,122 @@ pub struct ProviderImportResult {
pub profile_name: String,
}

/// 将本机配置保存为普通供应商;后续读取不覆盖用户对该供应商的修改。
pub fn initialize_local_config_provider(
store: &SettingsStore,
home: &Path,
) -> anyhow::Result<BackendSettings> {
let mut settings = store.load()?;
if settings.local_config_provider_imported {
return Ok(settings);
}
if !settings
.relay_profiles
.iter()
.any(|profile| matches!(profile.name.as_str(), "系统默认" | "系统默认配置"))
{
let mut profile = RelayProfile {
use_common_config: false,
..RelayProfile::default()
};
let read_local = |name: &str| -> anyhow::Result<String> {
match std::fs::read_to_string(home.join(name)) {
Ok(contents) => Ok(contents),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
Err(error) => Err(error).with_context(|| format!("无法读取本机 {name}")),
}
};
profile.config_contents = read_local("config.toml")?;
profile.auth_contents = read_local("auth.json")?;
let config: toml::Value = profile
.config_contents
.parse()
.context("本机 config.toml 格式无效")?;
let auth: serde_json::Value = if profile.auth_contents.trim().is_empty() {
serde_json::json!({})
} else {
serde_json::from_str(&profile.auth_contents).context("本机 auth.json 格式无效")?
};
let provider_id = config
.get("model_provider")
.and_then(toml::Value::as_str)
.unwrap_or("openai");
let provider = config
.get("model_providers")
.and_then(|providers| providers.get(provider_id));
let wire_api = provider
.and_then(|value| value.get("wire_api"))
.and_then(toml::Value::as_str);
if matches!(wire_api, Some("chat" | "chat_completions"))
|| config
.get("codex_plus_chat_base_url")
.and_then(toml::Value::as_str)
.is_some_and(|value| !value.trim().is_empty())
{
profile.protocol = RelayProtocol::ChatCompletions;
}
profile.base_url = crate::relay_config::relay_profile_base_url(&profile);
profile.upstream_base_url = profile.base_url.clone();
profile.relay_mode = RelayMode::PureApi;
profile.api_key = crate::relay_config::relay_profile_api_key(&profile);
let api_auth = auth
.get("OPENAI_API_KEY")
.and_then(serde_json::Value::as_str)
.is_some_and(|key| !key.trim().is_empty());
profile.no_auth = !api_auth
&& profile.api_key.is_empty()
&& !profile.base_url.is_empty()
&& provider
.and_then(|value| value.get("requires_openai_auth"))
.and_then(toml::Value::as_bool)
== Some(false);
if !api_auth && profile.api_key.is_empty() && !profile.no_auth {
profile.relay_mode = RelayMode::Official;
profile.official_mix_api_key = !profile.base_url.is_empty();
}

// 本机文件指向工具的协议代理时,复用当前供应商保存的真实上游和凭证。
// 直接把代理地址当上游保存,会在再次切换后形成回环。
if settings.relay_profiles_enabled
&& crate::relay_config::responses_proxy_configured_in_home(home)
{
profile = settings.active_relay_profile();
profile.use_common_config = false;
crate::relay_config::backfill_relay_profile_from_home_with_common(
home,
&mut profile,
&mut String::new(),
)?;
} else {
crate::relay_config::backfill_relay_profile_from_home_with_common_and_policy(
home,
&mut profile,
&mut String::new(),
crate::relay_config::RelayBackfillPolicy::AdoptLiveIdentity,
)?;
}
profile.id = format!("relay-{}", uuid::Uuid::new_v4());
profile.name = "系统默认".to_string();
profile.model_list = profile.model.clone();

if settings.relay_profiles == crate::settings::default_relay_profiles() {
let legacy = settings.active_relay_profile();
if legacy == RelayProfile::default() {
settings.relay_profiles.clear();
} else {
settings.relay_profiles[0] = legacy;
}
}
if settings.relay_profiles.is_empty() {
settings.active_relay_id = profile.id.clone();
}
settings.relay_profiles.insert(0, profile);
}
settings.local_config_provider_imported = true;
store.save(&settings)?;
Ok(settings)
}

pub fn import_provider_from_url(url: &str) -> anyhow::Result<ProviderImportResult> {
let request = request_from_url(url)?;
import_provider(request)
Expand Down
3 changes: 3 additions & 0 deletions crates/codex-plus-core/src/relay_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,9 @@ fn model_list_head(profile: &RelayProfile) -> String {
/// 后续的目标任务对齐,模型漂移重现。值与当前目标任务模型相等时视为工具
/// 写入是安全的:apply 每次都会按最新目标模型重新注入,不需要 backfill 保存。
fn live_model_is_tool_written(home: &Path, profile: &RelayProfile, model: &str) -> bool {
if profile.relay_mode == RelayMode::Official && !profile.official_mix_api_key {
return false;
}
let model = model.trim();
if model.is_empty() {
return false;
Expand Down
4 changes: 4 additions & 0 deletions crates/codex-plus-core/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,9 @@ pub struct BackendSettings {
pub relay_api_key: String,
#[serde(rename = "relayProfiles", default = "default_relay_profiles")]
pub relay_profiles: Vec<RelayProfile>,
/// 本机配置只自动导入一次,删除或重命名后不再补回。
#[serde(rename = "localConfigProviderImported", default)]
pub local_config_provider_imported: bool,
#[serde(rename = "relayCommonConfigContents", default)]
pub relay_common_config_contents: String,
#[serde(rename = "relayContextConfigContents", default)]
Expand Down Expand Up @@ -812,6 +815,7 @@ impl Default for BackendSettings {
relay_base_url: default_relay_base_url(),
relay_api_key: String::new(),
relay_profiles: default_relay_profiles(),
local_config_provider_imported: false,
relay_common_config_contents: String::new(),
relay_context_config_contents: String::new(),
active_relay_id: default_active_relay_id(),
Expand Down
Loading