mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 19:12:04 +08:00
fix(grokbuild): complete proxy and deep-link integrations (#5677)
* fix(grokbuild): complete proxy integration * fix(deeplink): preview GrokBuild configs safely * test(app): stabilize provider integration suite * fix(grokbuild): address review feedback * fix(grokbuild): resolve remaining review findings * fix(grokbuild): use native sessions and harden previews
This commit is contained in:
@@ -287,7 +287,8 @@ pub fn apply_proxy_takeover(
|
||||
token_placeholder: &str,
|
||||
) -> Result<String, AppError> {
|
||||
let updated = update_selected_model_string(config_toml, "base_url", proxy_base_url)?;
|
||||
update_selected_model_string(&updated, "api_key", token_placeholder)
|
||||
let updated = update_selected_model_string(&updated, "api_key", token_placeholder)?;
|
||||
update_selected_model_string(&updated, "api_backend", DEFAULT_API_BACKEND)
|
||||
}
|
||||
|
||||
pub fn update_api_key(config_toml: &str, api_key: &str) -> Result<String, AppError> {
|
||||
@@ -512,8 +513,12 @@ context_window = 500000
|
||||
|
||||
#[test]
|
||||
fn takeover_preserves_env_key_profile_and_injects_inline_placeholder() {
|
||||
let direct_config = valid_env_key_config().replace(
|
||||
"api_backend = \"responses\"",
|
||||
"api_backend = \"chat_completions\"",
|
||||
);
|
||||
let updated = apply_proxy_takeover(
|
||||
valid_env_key_config(),
|
||||
&direct_config,
|
||||
"http://127.0.0.1:15721/grokbuild/v1",
|
||||
"PROXY_MANAGED",
|
||||
)
|
||||
@@ -523,6 +528,7 @@ context_window = 500000
|
||||
assert_eq!(selected.profile, "grok-env");
|
||||
assert_eq!(selected.env_key.as_deref(), Some("GROK_TEST_API_KEY"));
|
||||
assert_eq!(selected.api_key.as_deref(), Some("PROXY_MANAGED"));
|
||||
assert_eq!(selected.api_backend, DEFAULT_API_BACKEND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+130
-10
@@ -7,6 +7,7 @@
|
||||
//! 支持从客户端请求中提取 Session ID,用于关联同一对话的多个请求:
|
||||
//! - Claude: 从 `metadata.user_id` (格式: `user_xxx_session_yyy`) 或 `metadata.session_id` 提取
|
||||
//! - Codex: 从 headers 中的 `session_id` / `x-session-id` 或 `metadata.session_id` 提取
|
||||
//! - Grok Build: 从 headers 中的 `x-grok-conv-id` / `x-grok-session-id` 提取
|
||||
//! - 其他: 生成新的 UUID
|
||||
|
||||
use axum::http::HeaderMap;
|
||||
@@ -23,7 +24,7 @@ pub enum SessionIdSource {
|
||||
MetadataUserId,
|
||||
/// 从 metadata.session_id 提取
|
||||
MetadataSessionId,
|
||||
/// 从 headers 提取 (Codex)
|
||||
/// 从 headers 提取
|
||||
Header,
|
||||
/// 新生成
|
||||
Generated,
|
||||
@@ -56,6 +57,11 @@ pub struct SessionIdResult {
|
||||
/// 2. `metadata.session_id`
|
||||
/// 3. 生成新 UUID
|
||||
///
|
||||
/// ### Grok Build 请求
|
||||
/// 1. Headers: `x-grok-conv-id` 或 `x-grok-session-id`
|
||||
/// 2. `metadata.session_id`
|
||||
/// 3. 生成新 UUID
|
||||
///
|
||||
/// ## 示例
|
||||
///
|
||||
/// ```ignore
|
||||
@@ -73,9 +79,15 @@ pub fn extract_session_id(
|
||||
}
|
||||
}
|
||||
|
||||
// Codex 请求特殊处理
|
||||
if client_format == "codex" || client_format == "openai" {
|
||||
if let Some(result) = extract_codex_session(headers, body) {
|
||||
// Responses 请求特殊处理。Grok Build 使用与 Codex 相同的客户端协议,
|
||||
// 但保留独立前缀,避免统计和缓存键跨应用碰撞。
|
||||
if matches!(client_format, "codex" | "openai" | "grokbuild") {
|
||||
let prefix = if client_format == "grokbuild" {
|
||||
"grokbuild"
|
||||
} else {
|
||||
"codex"
|
||||
};
|
||||
if let Some(result) = extract_responses_session(headers, body, prefix) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -111,16 +123,28 @@ fn extract_claude_session(
|
||||
extract_from_metadata(body)
|
||||
}
|
||||
|
||||
/// 提取 Codex Session ID
|
||||
fn extract_codex_session(headers: &HeaderMap, body: &serde_json::Value) -> Option<SessionIdResult> {
|
||||
/// 提取 Responses 客户端的 Session ID
|
||||
fn extract_responses_session(
|
||||
headers: &HeaderMap,
|
||||
body: &serde_json::Value,
|
||||
prefix: &str,
|
||||
) -> Option<SessionIdResult> {
|
||||
// 1. 从 headers 提取
|
||||
for header_name in &["session_id", "x-session-id"] {
|
||||
let header_names: &[&str] = if prefix == "grokbuild" {
|
||||
// Conversation ID 跨多轮请求保持稳定;session ID 作为客户端缺少
|
||||
// conversation ID 时的回退。x-grok-req-id 是逐请求 ID,不能用于聚合。
|
||||
&["x-grok-conv-id", "x-grok-session-id"]
|
||||
} else {
|
||||
&["session_id", "x-session-id"]
|
||||
};
|
||||
for header_name in header_names {
|
||||
if let Some(value) = headers.get(*header_name) {
|
||||
if let Ok(session_id) = value.to_str() {
|
||||
// Codex Session ID 通常较长(UUID 格式)
|
||||
let session_id = session_id.trim();
|
||||
// Responses 客户端的 Session ID 通常较长(UUID 格式)
|
||||
if session_id.len() > 20 {
|
||||
return Some(SessionIdResult {
|
||||
session_id: format!("codex_{session_id}"),
|
||||
session_id: format!("{prefix}_{session_id}"),
|
||||
source: SessionIdSource::Header,
|
||||
client_provided: true,
|
||||
});
|
||||
@@ -135,9 +159,10 @@ fn extract_codex_session(headers: &HeaderMap, body: &serde_json::Value) -> Optio
|
||||
.and_then(|m| m.get("session_id"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
let session_id = session_id.trim();
|
||||
if session_id.len() > 10 {
|
||||
return Some(SessionIdResult {
|
||||
session_id: format!("codex_{session_id}"),
|
||||
session_id: format!("{prefix}_{session_id}"),
|
||||
source: SessionIdSource::MetadataSessionId,
|
||||
client_provided: true,
|
||||
});
|
||||
@@ -302,6 +327,101 @@ mod tests {
|
||||
assert!(!result.client_provided);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codex_keeps_existing_response_session_headers() {
|
||||
let body = json!({ "input": "Write a function" });
|
||||
|
||||
for header_name in ["session_id", "x-session-id"] {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
header_name,
|
||||
"d937243f-2702-4f20-97b6-c9682235ab81".parse().unwrap(),
|
||||
);
|
||||
|
||||
let result = extract_session_id(&headers, &body, "codex");
|
||||
|
||||
assert_eq!(
|
||||
result.session_id,
|
||||
"codex_d937243f-2702-4f20-97b6-c9682235ab81"
|
||||
);
|
||||
assert_eq!(result.source, SessionIdSource::Header);
|
||||
assert!(result.client_provided);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grokbuild_prefers_conversation_header() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"x-grok-conv-id",
|
||||
"conv-724f4275-584e-43af-ad46-b5e7509a3ca2".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
"x-grok-session-id",
|
||||
"session-d937243f-2702-4f20-97b6-c9682235ab81"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
let body = json!({ "input": "Write a function" });
|
||||
|
||||
let result = extract_session_id(&headers, &body, "grokbuild");
|
||||
|
||||
assert_eq!(
|
||||
result.session_id,
|
||||
"grokbuild_conv-724f4275-584e-43af-ad46-b5e7509a3ca2"
|
||||
);
|
||||
assert_eq!(result.source, SessionIdSource::Header);
|
||||
assert!(result.client_provided);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grokbuild_falls_back_to_session_header() {
|
||||
let body = json!({ "input": "Write a function" });
|
||||
|
||||
for conversation_id in ["", " "] {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-grok-conv-id", conversation_id.parse().unwrap());
|
||||
headers.insert(
|
||||
"x-grok-session-id",
|
||||
"session-d937243f-2702-4f20-97b6-c9682235ab81"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let result = extract_session_id(&headers, &body, "grokbuild");
|
||||
|
||||
assert_eq!(
|
||||
result.session_id,
|
||||
"grokbuild_session-d937243f-2702-4f20-97b6-c9682235ab81"
|
||||
);
|
||||
assert_eq!(result.source, SessionIdSource::Header);
|
||||
assert!(result.client_provided);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grokbuild_ignores_request_and_codex_session_headers() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"x-grok-req-id",
|
||||
"request-724f4275-584e-43af-ad46-b5e7509a3ca2"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
"x-session-id",
|
||||
"codex-d937243f-2702-4f20-97b6-c9682235ab81"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
let body = json!({ "input": "Write a function" });
|
||||
|
||||
let result = extract_session_id(&headers, &body, "grokbuild");
|
||||
|
||||
assert_eq!(result.source, SessionIdSource::Generated);
|
||||
assert!(!result.client_provided);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_session_generates_new_when_not_found() {
|
||||
let headers = HeaderMap::new();
|
||||
|
||||
@@ -31,25 +31,46 @@ pub fn check_env_conflicts(app: &str) -> Result<Vec<EnvConflict>, String> {
|
||||
Ok(conflicts)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum EnvKeyword {
|
||||
Exact(&'static str),
|
||||
Prefix(&'static str),
|
||||
}
|
||||
|
||||
/// Get relevant keywords for each app
|
||||
fn get_keywords_for_app(app: &str) -> Vec<&str> {
|
||||
fn get_keywords_for_app(app: &str) -> Vec<EnvKeyword> {
|
||||
match app.to_lowercase().as_str() {
|
||||
"claude" => vec!["ANTHROPIC"],
|
||||
"codex" => vec!["OPENAI"],
|
||||
"gemini" => vec!["GEMINI", "GOOGLE_GEMINI"],
|
||||
"claude" => vec![EnvKeyword::Prefix("ANTHROPIC")],
|
||||
"codex" => vec![EnvKeyword::Prefix("OPENAI")],
|
||||
"gemini" => vec![
|
||||
EnvKeyword::Prefix("GEMINI"),
|
||||
EnvKeyword::Prefix("GOOGLE_GEMINI"),
|
||||
],
|
||||
"grokbuild" | "grok" => vec![
|
||||
EnvKeyword::Exact("XAI_API_KEY"),
|
||||
EnvKeyword::Exact("GROK_DEFAULT_MODEL"),
|
||||
],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_env_keyword(name: &str, keywords: &[EnvKeyword]) -> bool {
|
||||
let upper_name = name.to_uppercase();
|
||||
keywords.iter().any(|keyword| match keyword {
|
||||
EnvKeyword::Exact(name) => upper_name == *name,
|
||||
EnvKeyword::Prefix(prefix) => upper_name.starts_with(prefix),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check system environment variables (Windows Registry or Unix env)
|
||||
#[cfg(target_os = "windows")]
|
||||
fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
||||
fn check_system_env(keywords: &[EnvKeyword]) -> Result<Vec<EnvConflict>, String> {
|
||||
let mut conflicts = Vec::new();
|
||||
|
||||
// Check HKEY_CURRENT_USER\Environment
|
||||
if let Ok(hkcu) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") {
|
||||
for (name, value) in hkcu.enum_values().filter_map(Result::ok) {
|
||||
if keywords.iter().any(|k| name.to_uppercase().contains(k)) {
|
||||
if matches_env_keyword(&name, keywords) {
|
||||
conflicts.push(EnvConflict {
|
||||
var_name: name.clone(),
|
||||
var_value: value.to_string(),
|
||||
@@ -65,7 +86,7 @@ fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
||||
.open_subkey("SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment")
|
||||
{
|
||||
for (name, value) in hklm.enum_values().filter_map(Result::ok) {
|
||||
if keywords.iter().any(|k| name.to_uppercase().contains(k)) {
|
||||
if matches_env_keyword(&name, keywords) {
|
||||
conflicts.push(EnvConflict {
|
||||
var_name: name.clone(),
|
||||
var_value: value.to_string(),
|
||||
@@ -80,12 +101,12 @@ fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
||||
fn check_system_env(keywords: &[EnvKeyword]) -> Result<Vec<EnvConflict>, String> {
|
||||
let mut conflicts = Vec::new();
|
||||
|
||||
// Check current process environment
|
||||
for (key, value) in std::env::vars() {
|
||||
if keywords.iter().any(|k| key.to_uppercase().contains(k)) {
|
||||
if matches_env_keyword(&key, keywords) {
|
||||
conflicts.push(EnvConflict {
|
||||
var_name: key,
|
||||
var_value: value,
|
||||
@@ -100,7 +121,7 @@ fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
||||
|
||||
/// Check shell configuration files for environment variable exports (Unix only)
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn check_shell_configs(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
||||
fn check_shell_configs(keywords: &[EnvKeyword]) -> Result<Vec<EnvConflict>, String> {
|
||||
let mut conflicts = Vec::new();
|
||||
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
@@ -131,7 +152,7 @@ fn check_shell_configs(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
||||
let var_value = export_line[eq_pos + 1..].trim();
|
||||
|
||||
// Check if variable name contains any keyword
|
||||
if keywords.iter().any(|k| var_name.to_uppercase().contains(k)) {
|
||||
if matches_env_keyword(var_name, keywords) {
|
||||
conflicts.push(EnvConflict {
|
||||
var_name: var_name.to_string(),
|
||||
var_value: var_value
|
||||
@@ -157,12 +178,58 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_keywords() {
|
||||
assert_eq!(get_keywords_for_app("claude"), vec!["ANTHROPIC"]);
|
||||
assert_eq!(get_keywords_for_app("codex"), vec!["OPENAI"]);
|
||||
assert_eq!(
|
||||
get_keywords_for_app("claude"),
|
||||
vec![EnvKeyword::Prefix("ANTHROPIC")]
|
||||
);
|
||||
assert_eq!(
|
||||
get_keywords_for_app("codex"),
|
||||
vec![EnvKeyword::Prefix("OPENAI")]
|
||||
);
|
||||
assert_eq!(
|
||||
get_keywords_for_app("gemini"),
|
||||
vec!["GEMINI", "GOOGLE_GEMINI"]
|
||||
vec![
|
||||
EnvKeyword::Prefix("GEMINI"),
|
||||
EnvKeyword::Prefix("GOOGLE_GEMINI")
|
||||
]
|
||||
);
|
||||
assert_eq!(get_keywords_for_app("unknown"), Vec::<&str>::new());
|
||||
assert_eq!(
|
||||
get_keywords_for_app("grokbuild"),
|
||||
vec![
|
||||
EnvKeyword::Exact("XAI_API_KEY"),
|
||||
EnvKeyword::Exact("GROK_DEFAULT_MODEL")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
get_keywords_for_app("grok"),
|
||||
get_keywords_for_app("grokbuild")
|
||||
);
|
||||
assert_eq!(get_keywords_for_app("unknown"), Vec::<EnvKeyword>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grok_keywords_only_match_credentials() {
|
||||
let keywords = get_keywords_for_app("grokbuild");
|
||||
|
||||
assert!(matches_env_keyword("XAI_API_KEY", &keywords));
|
||||
assert!(matches_env_keyword("xai_api_key", &keywords));
|
||||
assert!(matches_env_keyword("GROK_DEFAULT_MODEL", &keywords));
|
||||
assert!(matches_env_keyword("grok_default_model", &keywords));
|
||||
assert!(!matches_env_keyword("MY_XAI_API_KEY", &keywords));
|
||||
assert!(!matches_env_keyword("XAI_API_KEY_BACKUP", &keywords));
|
||||
assert!(!matches_env_keyword("MY_GROK_DEFAULT_MODEL", &keywords));
|
||||
assert!(!matches_env_keyword("GROK_DEFAULT_MODEL_BACKUP", &keywords));
|
||||
assert!(!matches_env_keyword("GROK_BIN_DIR", &keywords));
|
||||
assert!(!matches_env_keyword("GROK_HOME", &keywords));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broad_app_keywords_match_only_at_the_start() {
|
||||
let keywords = get_keywords_for_app("claude");
|
||||
|
||||
assert!(matches_env_keyword("ANTHROPIC_API_KEY", &keywords));
|
||||
assert!(matches_env_keyword("anthropic_base_url", &keywords));
|
||||
assert!(!matches_env_keyword("MY_ANTHROPIC_API_KEY", &keywords));
|
||||
assert!(!matches_env_keyword("NOT_ANTHROPIC", &keywords));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user