{"record":{"id":"798a61e5763105be","repo":"BigPizzaV3/CodexPlusPlus","slug":"base-url-798a61","errorCode":null,"errorMessage":"Base URL 不能为空","messagePattern":"Base URL 不能为空","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/relay_config.rs","lineNumber":529,"sourceCode":"    }))?;\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 async fn test_relay_profile(\n    profile: &RelayProfile,\n    model: &str,\n) -> anyhow::Result<RelayProfileTestResult> {\n    let base_url = relay_profile_base_url(profile);\n    let base_url = base_url.trim().trim_end_matches('/');\n    if base_url.is_empty() {\n        anyhow::bail!(\"Base URL 不能为空\");\n    }\n    let api_key = relay_profile_api_key(profile);\n    let api_key = api_key.trim();\n    if api_key.is_empty() {\n        anyhow::bail!(\"API Key 不能为空\");\n    }\n\n    let client = crate::http_client::proxied_client(\"CodexPlusPlus/RelayTest\")?;\n    let endpoint = match profile.protocol {\n        RelayProtocol::Responses => format!(\"{base_url}/responses\"),\n        RelayProtocol::ChatCompletions => format!(\"{base_url}/chat/completions\"),\n    };\n    let test_model = model.trim();\n    if test_model.is_empty() {\n        anyhow::bail!(\"测试模型不能为空\");\n    }\n\n    let payload = relay_profile_test_payload(profile.protocol, test_model);","sourceCodeStart":511,"sourceCodeEnd":547,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/relay_config.rs#L511-L547","documentation":"test_relay_profile 是「测试供应商连通性」的 async 入口：向 {base_url}/responses 或 /chat/completions 发一次最小请求。发送前先取 relay_profile_base_url(profile) 并 trim、去掉尾部斜杠，若结果为空则 bail，不会发起任何网络请求。","triggerScenarios":"调用 test_relay_profile(profile, model) 时 profile 的 base_url 解析结果为空，或仅由斜杠/空白组成（如 \"//\" 或 \"/\"——trim_end_matches('/') 会把斜杠剥光变成空串）。","commonSituations":"在 UI 上没保存 base_url 就点「测试」；base_url 只填了域名后缀斜杠；Aggregate 模式 profile 没有独立 base_url。","solutions":["先保存 profile 的 base_url 再点测试","调用前检查 relay_profile_base_url(profile).trim().trim_end_matches('/') 非空","对 Aggregate 类型 profile 不要直接调用本函数测试"],"exampleFix":"// before\nlet r = test_relay_profile(&empty_profile, \"gpt-5\").await?;\n\n// after\nlet base = relay_profile_base_url(&profile).trim().trim_end_matches('/').to_string();\nif base.is_empty() {\n    return Err(anyhow::anyhow!(\"Base URL 不能为空\"));\n}\nlet r = test_relay_profile(&profile, \"gpt-5\").await?;","handlingStrategy":"validation","validationCode":"let base = relay_profile_base_url(profile).trim().trim_end_matches('/').to_string();\nif base.is_empty() {\n    return Err(anyhow::anyhow!(\"Base URL 不能为空\"));\n}\ntest_relay_profile(profile, model).await?;","typeGuard":null,"tryCatchPattern":"if let Err(err) = test_relay_profile(&profile, &model).await {\n    if err.to_string().contains(\"Base URL 不能为空\") {\n        // 提示先保存 base_url；不发网络重试\n    } else { return Err(err); }\n}","preventionTips":["测试按钮仅在 profile 的 base_url 非空时可用","注意 \"//\" 或 \"/\" 这类值 trim 斜杠后也会判空","Aggregate 类型 profile 不要直接调用单 profile 测试"],"tags":["relay","base-url","connectivity-test","validation"],"backgroundTag":"missing-base-url","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}