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
11 changes: 4 additions & 7 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
*.sh text eol=lf
.github/workflows/*.yml text eol=lf

# Preserve the original Snow Skin asset line endings for byte-exact bundling.
assets/inject/upstream/snow-skin/*.js text eol=lf
assets/inject/upstream/snow-skin/*.css text eol=lf

# Keep byte-exact macOS theme assets identical on every checkout platform.
assets/inject/upstream/*/macos/*.js text eol=lf
assets/inject/upstream/*/macos/*.css text eol=lf
# Keep every byte-exact upstream theme asset stable on all checkout platforms.
assets/inject/upstream/**/*.js text eol=lf
assets/inject/upstream/**/*.css text eol=lf
assets/inject/upstream/skin-packs/packs/*/theme.json text eol=lf
18 changes: 14 additions & 4 deletions apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
type ImageHandling,
type ModelWindowRow,
} from "./model-windows";
import { relayAuthForLiveDraft } from "./relay-live-files";
import { resolveProviderSyncCompletion } from "./provider-sync-flow";
import {
defaultDreamSkinTheme,
Expand Down Expand Up @@ -5517,17 +5518,22 @@ function RelayProfileDetail({
const isActive = !isNew && profile.id === form.activeRelayId;
const profileUsesLiveFiles = relayProfileUsesLiveFiles(profile);
useEffect(() => {
const nextDraft = isAggregateRelayProfile(profile)
const useLiveFiles = isActive && profileUsesLiveFiles && relayFiles;
const liveDraft = isAggregateRelayProfile(profile)
? normalizeAggregateRelayProfile(profile, form)
: deriveRelayProfileFromFiles(
isActive && profileUsesLiveFiles && relayFiles
useLiveFiles
? {
...profile,
configContents: relayFiles.configContents,
authContents: relayFiles.authContents,
authContents: relayAuthForLiveDraft(profile, relayFiles.authContents),
}
: profile,
);
const storedApiKey = useLiveFiles ? profile.apiKey.trim() : "";
const nextDraft = useLiveFiles && !isAggregateRelayProfile(liveDraft)
? applyRelayProfilePatchToFiles(liveDraft, { apiKey: storedApiKey })
: liveDraft;
setDraft(nextDraft);
setModelWindowRows(modelWindowRowsFromProfile(nextDraft.modelList, nextDraft.modelWindows || "", nextDraft.modelVlm));
}, [profile.id, profile.modelList, profile.modelWindows, profileUsesLiveFiles, isActive, isNew, relayFiles?.configContents, relayFiles?.authContents]);
Expand Down Expand Up @@ -6512,7 +6518,11 @@ function RelayFileEditors({
<div className="relay-file-head">
<div>
<strong>auth.json</strong>
<span>{isActive ? t("当前使用中:打开时从 ~/.codex/auth.json 回填,保存后会作为此供应商 auth 存档") : t("切换到此供应商时会写入 ~/.codex/auth.json")}</span>
<span>{isActive
? profile.relayMode === "pureApi"
? t("当前使用中:保留此供应商的 auth 存档,避免 Codex 登录密钥覆盖供应商密钥")
: t("当前使用中:打开时从 ~/.codex/auth.json 回填,保存后会作为此供应商 auth 存档")
: t("切换到此供应商时会写入 ~/.codex/auth.json")}</span>
</div>
</div>
<SyncedTextarea
Expand Down
20 changes: 20 additions & 0 deletions apps/codex-plus-manager/src/relay-live-files.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import assert from "node:assert";
import { describe, it } from "node:test";
import { relayAuthForLiveDraft } from "./relay-live-files.ts";

describe("relayAuthForLiveDraft", () => {
it("preserves the complete pure API provider auth snapshot", () => {
assert.strictEqual(relayAuthForLiveDraft({
relayMode: "pureApi",
authContents: '{"OPENAI_API_KEY":"provider-key","vendor":"stored"}',
}, '{"OPENAI_API_KEY":"login-key","tokens":"live"}'),
'{"OPENAI_API_KEY":"provider-key","vendor":"stored"}');
});

it("keeps the current official auth state for mixed API mode", () => {
assert.strictEqual(relayAuthForLiveDraft({
relayMode: "official",
authContents: '{"tokens":"stored"}',
}, '{"tokens":"live"}'), '{"tokens":"live"}');
});
});
16 changes: 16 additions & 0 deletions apps/codex-plus-manager/src/relay-live-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type RelayProfileFileSnapshot = {
relayMode: "official" | "pureApi" | "mixedApi" | "aggregate";
authContents: string;
};

/**
* Pure API credentials belong to the stored provider, not Codex's current
* login state. Other live-file modes still use the current auth.json so an
* official login refresh is not replaced by an archived token set.
*/
export function relayAuthForLiveDraft(
profile: RelayProfileFileSnapshot,
liveAuthContents: string,
): string {
return profile.relayMode === "pureApi" ? profile.authContents : liveAuthContents;
}
75 changes: 63 additions & 12 deletions crates/codex-plus-core/src/relay_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ pub fn backfill_relay_profile_from_home_with_common(
let live_config = read_optional_text(&home.join("config.toml"))?;
let template_config = profile.config_contents.clone();
let template_auth = profile.auth_contents.clone();
let template_api_key = relay_profile_api_key(profile);
let template_base_url = relay_profile_base_url(profile);
profile.config_contents = if profile.use_common_config {
strip_common_config_from_config(&live_config, common_config_contents)?
Expand All @@ -772,8 +773,13 @@ pub fn backfill_relay_profile_from_home_with_common(
profile.config_contents =
move_model_providers_before_profiles(&ensure_trailing_newline(doc.to_string()));
}
profile.auth_contents = read_optional_text(&home.join("auth.json"))?;
restore_profile_auth_from_live_config(profile, &template_auth)?;
let live_auth = read_optional_text(&home.join("auth.json"))?;
restore_profile_credentials_after_backfill(
profile,
&template_auth,
&template_api_key,
&live_auth,
)?;
sync_profile_mode_from_backfilled_live(profile);
sync_context_limits_from_config(profile, &live_config);
if profile.model.trim().is_empty() {
Expand Down Expand Up @@ -2026,20 +2032,35 @@ fn provider_id_with_table_from_config(config_text: &str) -> anyhow::Result<Optio
Ok(provider_table_exists(&doc, &provider_id).then_some(provider_id))
}

fn restore_profile_auth_from_live_config(
fn restore_profile_credentials_after_backfill(
profile: &mut RelayProfile,
template_auth: &str,
template_api_key: &str,
live_auth: &str,
) -> anyhow::Result<()> {
let Some(token) = experimental_bearer_token_from_config(&profile.config_contents)? else {
if profile.relay_mode == crate::settings::RelayMode::PureApi {
profile.config_contents =
remove_experimental_bearer_token_from_config(&profile.config_contents)?;
profile.auth_contents =
set_openai_api_key_in_auth_contents(template_auth, template_api_key)?;
profile.api_key = template_api_key.trim().to_string();
return Ok(());
};
profile.api_key = token.clone();
}

if profile.relay_mode == crate::settings::RelayMode::Official && profile.official_mix_api_key {
profile.auth_contents = remove_openai_api_key_from_auth_contents(&profile.auth_contents)?;
profile.auth_contents = remove_openai_api_key_from_auth_contents(live_auth)?;
profile.config_contents =
set_experimental_bearer_token_in_config(&profile.config_contents, template_api_key)?;
profile.api_key = template_api_key.trim().to_string();
return Ok(());
}

profile.auth_contents = live_auth.to_string();
let Some(token) = experimental_bearer_token_from_config(&profile.config_contents)? else {
return Ok(());
};
profile.api_key = token.clone();

if !profile.auth_contents.trim().is_empty() {
if codex_auth_api_key(&profile.auth_contents).is_none() {
return Ok(());
Expand All @@ -2051,22 +2072,52 @@ fn restore_profile_auth_from_live_config(

profile.config_contents =
remove_experimental_bearer_token_from_config(&profile.config_contents)?;
profile.auth_contents = set_openai_api_key_in_auth_contents(template_auth, &token)?;
Ok(())
}

let mut auth = if template_auth.trim().is_empty() {
fn set_openai_api_key_in_auth_contents(
auth_contents: &str,
api_key: &str,
) -> anyhow::Result<String> {
let mut auth = if auth_contents.trim().is_empty() {
json!({})
} else {
serde_json::from_str::<Value>(template_auth).with_context(|| "auth.json JSON 解析失败")?
serde_json::from_str::<Value>(auth_contents).with_context(|| "auth.json JSON 解析失败")?
};
if !auth.is_object() {
auth = json!({});
}
if let Some(auth_object) = auth.as_object_mut() {
auth_object.insert("OPENAI_API_KEY".to_string(), Value::String(token));
if api_key.trim().is_empty() {
auth_object.remove("OPENAI_API_KEY");
} else {
auth_object.insert(
"OPENAI_API_KEY".to_string(),
Value::String(api_key.trim().to_string()),
);
}
} else {
anyhow::bail!("auth.json 必须是 JSON 对象");
}
profile.auth_contents = serde_json::to_string_pretty(&auth)?;
Ok(())
Ok(serde_json::to_string_pretty(&auth)?)
}

fn set_experimental_bearer_token_in_config(
config_contents: &str,
api_key: &str,
) -> anyhow::Result<String> {
let mut doc = parse_toml_document(config_contents)?;
let provider_id = active_or_default_provider_id(&doc);
let provider = ensure_provider_table(&mut doc, &provider_id)?;
if api_key.trim().is_empty() {
provider.remove("experimental_bearer_token");
} else {
provider["experimental_bearer_token"] = toml_edit::value(api_key.trim());
}
Ok(move_model_providers_before_profiles(
&ensure_trailing_newline(doc.to_string()),
))
}

fn sync_profile_mode_from_backfilled_live(profile: &mut RelayProfile) {
Expand Down
Loading
Loading