{"record":{"id":"29460f610890e9b6","repo":"BigPizzaV3/CodexPlusPlus","slug":"config-toml-29460f","errorCode":null,"errorMessage":"供应商「{}」缺少独立 config.toml，已停止切换，避免继续显示上一套配置文件。","messagePattern":"供应商「(.+?)」缺少独立 config\\.toml，已停止切换，避免继续显示上一套配置文件。","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/codex-plus-core/src/relay_switch.rs","lineNumber":169,"sourceCode":"            settings.computer_use_guard_enabled,\n        )?\n    };\n    let status = relay_config_status_from_home(home);\n    if relay.relay_mode == RelayMode::PureApi && !status.configured {\n        anyhow::bail!(\n            \"纯 API 配置写入后未检测到完整 custom provider，请检查 config.toml 和供应商 API Key。\"\n        );\n    }\n    Ok(RelaySwitchResult {\n        settings: settings.clone(),\n        configured: status.configured,\n        backup_path: result.backup_path,\n    })\n}\n\nfn validate_switch_profile_files(profile: &crate::settings::RelayProfile) -> anyhow::Result<()> {\n    if profile.relay_mode != RelayMode::Aggregate && profile.config_contents.trim().is_empty() {\n        anyhow::bail!(\n            \"供应商「{}」缺少独立 config.toml，已停止切换，避免继续显示上一套配置文件。\",\n            if profile.name.trim().is_empty() {\n                profile.id.as_str()\n            } else {\n                profile.name.as_str()\n            }\n        );\n    }\n    if profile.relay_mode == RelayMode::Official\n        && serde_json::from_str::<serde_json::Value>(&profile.auth_contents)\n            .ok()\n            .and_then(|value| {\n                value\n                    .get(\"OPENAI_API_KEY\")\n                    .and_then(serde_json::Value::as_str)\n                    .map(str::trim)\n                    .map(str::is_empty)\n            })","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/relay_switch.rs#L151-L187","documentation":"validate_switch_profile_files 在真正写入前检查目标 profile：非 Aggregate 模式的 profile 必须自带非空 config_contents，否则 bail 并停止切换。设计意图是保护现场——不切过去，避免 Codex 继续显示上一家供应商的 config.toml/auth.json 造成「界面是 A、实际是 B」的错位。此时未发生任何写入。","triggerScenarios":"switch_relay_profile_in_home 切换到 relay_mode != Aggregate 且 config_contents.trim() 为空的 profile（Relay / PureApi / Official 等需要独立 config 的模式）。消息中的名称取 profile.name，为空则回退用 profile.id。","commonSituations":"新建供应商后没点过「保存/应用」，config_contents 从未生成；导入的 profile 数据不完整；settings.json 手工编辑时丢失 config_contents 字段；数据迁移未带上该字段。","solutions":["先对该 profile 打开编辑器并保存一次，让其生成 config.toml 内容","检查 settings.json 中该 profile 的 config_contents 字段是否存在且非空","若确属聚合型供应商，把 relay_mode 改为 Aggregate（聚合模式不要求独立 config）"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"fn profile_has_standalone_config(profile: &RelayProfile) -> bool {\n    profile.relay_mode == RelayMode::Aggregate\n        || !profile.config_contents.trim().is_empty()\n}\nanyhow::ensure!(profile_has_standalone_config(&profile), \"供应商「{}」缺少独立 config.toml\", profile.name);","typeGuard":"fn is_switchable_profile(profile: &RelayProfile) -> bool {\n    profile.relay_mode == RelayMode::Aggregate || !profile.config_contents.trim().is_empty()\n}","tryCatchPattern":"match switch_relay_profile_in_home(&store, &home, next, &prev) {\n    Ok(r) => Ok(r),\n    Err(err) if err.to_string().contains(\"缺少独立 config.toml\") => {\n        // 安全拒绝：未写入任何文件。引导用户先保存/应用该 profile 生成 config 再切换\n        Err(err)\n    }\n    Err(err) => Err(err),\n}","preventionTips":["新建供应商后立即保存一次，确保 config_contents 生成","导入/迁移 profile 时校验 config_contents 字段完整","UI 在 profile 缺 config 时禁用「切换到该供应商」并提示先保存"],"tags":["relay-switch","profile-validation","config-toml","guard"],"backgroundTag":"missing-required-config","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}