mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 02:51:17 +08:00
fix(codex): strip synced [mcp_servers] from provider snapshots on backfill
MCP servers are owned by the DB mcp_servers table; the [mcp_servers] section in live config.toml is only a projection re-synced after every live write. When switching away baked that projection into the stored provider snapshot, servers deleted in the app were resurrected the next time that provider was activated -- per-entry reconcile only knows rows that still exist in the DB, so it can never clean up such orphans. Strip [mcp_servers] (and the legacy [mcp.servers] form) from the live settings during switch-away backfill. Previously polluted snapshots self-heal the next time the user switches away from them.
This commit is contained in:
@@ -1473,6 +1473,45 @@ pub fn strip_codex_unified_session_bucket_from_settings(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Backfill helper: strip `[mcp_servers]` from a live `{ auth, config }`
|
||||
/// settings object before it is stored back to the DB.
|
||||
///
|
||||
/// MCP 服务器的 SSOT 是 DB 的 mcp_servers 表,live `config.toml` 里的
|
||||
/// `[mcp_servers]` 只是每次写 live 之后由 MCP 同步重新投影的产物。若回填时
|
||||
/// 烙进供应商存储配置,已在应用里删除的服务器会随下次激活该供应商被写回
|
||||
/// live,而逐条 reconcile 只认识 DB 现存条目、永远清不掉这种孤儿。
|
||||
pub fn strip_codex_mcp_servers_from_settings(settings: &mut Value) -> Result<(), AppError> {
|
||||
let Some(config_text) = settings
|
||||
.get("config")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(str::to_string)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
if !config_text.contains("mcp") {
|
||||
return Ok(());
|
||||
}
|
||||
let mut doc = config_text
|
||||
.parse::<DocumentMut>()
|
||||
.map_err(|e| AppError::Message(format!("Invalid Codex config.toml: {e}")))?;
|
||||
let mut changed = doc.as_table_mut().remove("mcp_servers").is_some();
|
||||
// 历史错误格式 [mcp.servers] 一并清理(live 侧 MCP 同步也做同样迁移)
|
||||
if let Some(mcp_tbl) = doc.get_mut("mcp").and_then(|item| item.as_table_like_mut()) {
|
||||
if mcp_tbl.remove("servers").is_some() {
|
||||
changed = true;
|
||||
}
|
||||
if mcp_tbl.is_empty() {
|
||||
doc.as_table_mut().remove("mcp");
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
if let Some(obj) = settings.as_object_mut() {
|
||||
obj.insert("config".to_string(), Value::String(doc.to_string()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Route a Codex live write between full auth+config or config-only.
|
||||
///
|
||||
/// Official providers with usable login material own `auth.json`. Third-party
|
||||
@@ -1803,6 +1842,41 @@ requires_openai_auth = true
|
||||
assert!(settings.pointer("/auth/tokens/access_token").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_mcp_servers_from_settings_removes_table_and_legacy_form() {
|
||||
let mut settings = json!({
|
||||
"auth": { "OPENAI_API_KEY": "sk-test" },
|
||||
"config": "# user comment\nmodel = \"gpt-5.5\"\n\n[mcp_servers.echo]\ntype = \"stdio\"\ncommand = \"echo\"\n\n[mcp.servers.legacy]\ncommand = \"noop\"\n",
|
||||
});
|
||||
strip_codex_mcp_servers_from_settings(&mut settings).expect("strip mcp");
|
||||
let config = settings
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.expect("config text");
|
||||
assert!(!config.contains("mcp_servers"), "got: {config}");
|
||||
assert!(
|
||||
!config.contains("[mcp"),
|
||||
"legacy [mcp.servers] gone: {config}"
|
||||
);
|
||||
assert!(config.contains("# user comment"), "comments preserved");
|
||||
assert!(config.contains("model = \"gpt-5.5\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_mcp_servers_from_settings_is_noop_without_mcp() {
|
||||
let original = "# comment\nmodel = \"gpt-5.5\"\n";
|
||||
let mut settings = json!({
|
||||
"auth": {},
|
||||
"config": original,
|
||||
});
|
||||
strip_codex_mcp_servers_from_settings(&mut settings).expect("strip mcp");
|
||||
assert_eq!(
|
||||
settings.get("config").and_then(|v| v.as_str()),
|
||||
Some(original),
|
||||
"config text must be byte-identical when nothing is stripped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_base_url_prefers_active_provider_section() {
|
||||
let input = r#"model_provider = "azure"
|
||||
|
||||
Reference in New Issue
Block a user