{"record":{"id":"8f4b908dcdb94e08","repo":"BigPizzaV3/CodexPlusPlus","slug":"key-8f4b90","errorCode":null,"errorMessage":"中转 Key 不能为空","messagePattern":"中转 Key 不能为空","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/relay_config.rs","lineNumber":280,"sourceCode":"        RelayProtocol::Responses,\n        crate::protocol_proxy::DEFAULT_PROTOCOL_PROXY_PORT,\n    )\n}\n\npub fn apply_relay_config_to_home_with_protocol(\n    home: &Path,\n    base_url: &str,\n    bearer_token: &str,\n    protocol: RelayProtocol,\n    proxy_port: u16,\n) -> anyhow::Result<RelayApplyResult> {\n    let base_url = base_url.trim();\n    if base_url.is_empty() {\n        anyhow::bail!(\"中转 Base URL 不能为空\");\n    }\n    let bearer_token = bearer_token.trim();\n    if bearer_token.is_empty() {\n        anyhow::bail!(\"中转 Key 不能为空\");\n    }\n    let codex_base_url = codex_base_url_for_protocol(base_url, protocol, proxy_port);\n    let updated = upsert_model_provider_config(\"\", &codex_base_url, bearer_token, true)?;\n    let auth_contents = serde_json::to_string_pretty(&json!({\n        \"OPENAI_API_KEY\": bearer_token\n    }))?;\n    let backup_path =\n        write_codex_live_atomic(home, Some(&updated), Some(auth_contents.as_bytes()), false)?;\n    let status = relay_config_status_from_home(home);\n    Ok(RelayApplyResult {\n        config_path: status.config_path,\n        backup_path,\n        configured: status.configured,\n    })\n}\n\npub fn apply_pure_api_config_to_home(\n    home: &Path,","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/relay_config.rs#L262-L298","documentation":"在 apply_relay_config_to_home_with_protocol 写入 Codex home 之前，对 bearer_token 做 trim 后非空校验。为空时立即 bail，不会产生任何 config.toml / auth.json 写入或备份。这是把「中转 Base URL + Key」一键投影为 Codex 配置的入口函数（apply_relay_config_to_home 的带协议参数变体）。","triggerScenarios":"调用 apply_relay_config_to_home_with_protocol(home, base_url, bearer_token, protocol, proxy_port) 时 bearer_token.trim() 为空字符串（空串或纯空白字符）。注意 base_url 会先被校验，所以本错误只在 base_url 非空而 key 为空时出现。","commonSituations":"前端表单 Key 输入框留空白就点「应用」；RelayProfile 的 api_key 字段为空（relay_profile_api_key 解析后为空）；从环境变量读取 key 但变量未设置；误以为可以只配 base_url 不配 key（ Responses 协议中转仍需要 key 写入 auth.json 的 OPENAI_API_KEY）。","solutions":["在调用前 trim 并检查 bearer_token 非空，为空时在 UI 层拦截","检查 RelayProfile 的 api_key / auth_contents 字段是否为空（relay_profile_api_key 会从其中解析）","若确实不需要写入 key，改用直接写 config.toml 的 apply_relay_config_file_to_home 接口","从环境变量取 key 时确认变量已设置且值不是纯空白"],"exampleFix":"// before\nlet result = apply_relay_config_to_home_with_protocol(&home, &base_url, \"\", protocol, port)?;\n\n// after\nlet token = bearer_token.trim();\nif token.is_empty() {\n    return Err(anyhow::anyhow!(\"中转 Key 不能为空\"));\n}\nlet result = apply_relay_config_to_home_with_protocol(&home, base_url, token, protocol, port)?;","handlingStrategy":"validation","validationCode":"let token = bearer_token.trim();\nif token.is_empty() {\n    return Err(anyhow::anyhow!(\"中转 Key 不能为空\"));\n}\napply_relay_config_to_home_with_protocol(&home, base_url, token, protocol, proxy_port)?;","typeGuard":null,"tryCatchPattern":"match apply_relay_config_to_home_with_protocol(&home, base_url, token, protocol, port) {\n    Ok(result) => { /* ... */ }\n    Err(err) if err.to_string().contains(\"中转 Key 不能为空\") => {\n        // 定位到 Key 输入框提示用户，不重试\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["UI 表单提交前 trim 并非空校验 Key 字段，为空禁用提交按钮","从 RelayProfile 取值时统一走 relay_profile_api_key 再校验","环境变量读取后立即检查空值，不要把空串静默传下去"],"tags":["relay","api-key","validation","codex-plus-core"],"backgroundTag":"missing-api-key","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}