mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 12:22:09 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68e07b350d | |||
| 26486d543c | |||
| fe49a0c189 | |||
| 813d6adb06 | |||
| d745ab58c4 | |||
| c2adb1af46 | |||
| fdd539759e | |||
| 054a5e9e3b | |||
| cba8e8fdb3 | |||
| b18be24384 | |||
| 86288ee77e | |||
| c1e27d3cf2 | |||
| c5d3732b9f | |||
| d33f99aca4 | |||
| 42d1d23618 | |||
| 7bc9dbbbb0 | |||
| e002bdba25 | |||
| 5694353798 | |||
| 17c3dffe8c | |||
| 07ba3df0f4 | |||
| 1fe16aa388 | |||
| f738871ad1 | |||
| 753190a879 |
@@ -36,13 +36,6 @@ pub struct StreamCheckConfig {
|
|||||||
pub codex_model: String,
|
pub codex_model: String,
|
||||||
/// Gemini 测试模型
|
/// Gemini 测试模型
|
||||||
pub gemini_model: String,
|
pub gemini_model: String,
|
||||||
/// 检查提示词
|
|
||||||
#[serde(default = "default_test_prompt")]
|
|
||||||
pub test_prompt: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_test_prompt() -> String {
|
|
||||||
"Who are you?".to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for StreamCheckConfig {
|
impl Default for StreamCheckConfig {
|
||||||
@@ -54,7 +47,6 @@ impl Default for StreamCheckConfig {
|
|||||||
claude_model: "claude-haiku-4-5-20251001".to_string(),
|
claude_model: "claude-haiku-4-5-20251001".to_string(),
|
||||||
codex_model: "gpt-5.1-codex@low".to_string(),
|
codex_model: "gpt-5.1-codex@low".to_string(),
|
||||||
gemini_model: "gemini-3-pro-preview".to_string(),
|
gemini_model: "gemini-3-pro-preview".to_string(),
|
||||||
test_prompt: default_test_prompt(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +110,7 @@ impl StreamCheckService {
|
|||||||
Ok(last_result.unwrap_or_else(|| StreamCheckResult {
|
Ok(last_result.unwrap_or_else(|| StreamCheckResult {
|
||||||
status: HealthStatus::Failed,
|
status: HealthStatus::Failed,
|
||||||
success: false,
|
success: false,
|
||||||
message: "Check failed".to_string(),
|
message: "检查失败".to_string(),
|
||||||
response_time_ms: None,
|
response_time_ms: None,
|
||||||
http_status: None,
|
http_status: None,
|
||||||
model_used: String::new(),
|
model_used: String::new(),
|
||||||
@@ -138,18 +130,17 @@ impl StreamCheckService {
|
|||||||
|
|
||||||
let base_url = adapter
|
let base_url = adapter
|
||||||
.extract_base_url(provider)
|
.extract_base_url(provider)
|
||||||
.map_err(|e| AppError::Message(format!("Failed to extract base_url: {e}")))?;
|
.map_err(|e| AppError::Message(format!("提取 base_url 失败: {e}")))?;
|
||||||
|
|
||||||
let auth = adapter
|
let auth = adapter
|
||||||
.extract_auth(provider)
|
.extract_auth(provider)
|
||||||
.ok_or_else(|| AppError::Message("API Key not found".to_string()))?;
|
.ok_or_else(|| AppError::Message("未找到 API Key".to_string()))?;
|
||||||
|
|
||||||
// 使用全局 HTTP 客户端(已包含代理配置)
|
// 使用全局 HTTP 客户端(已包含代理配置)
|
||||||
let client = crate::proxy::http_client::get();
|
let client = crate::proxy::http_client::get();
|
||||||
let request_timeout = std::time::Duration::from_secs(config.timeout_secs);
|
let request_timeout = std::time::Duration::from_secs(config.timeout_secs);
|
||||||
|
|
||||||
let model_to_test = Self::resolve_test_model(app_type, provider, config);
|
let model_to_test = Self::resolve_test_model(app_type, provider, config);
|
||||||
let test_prompt = &config.test_prompt;
|
|
||||||
|
|
||||||
let result = match app_type {
|
let result = match app_type {
|
||||||
AppType::Claude => {
|
AppType::Claude => {
|
||||||
@@ -158,21 +149,13 @@ impl StreamCheckService {
|
|||||||
&base_url,
|
&base_url,
|
||||||
&auth,
|
&auth,
|
||||||
&model_to_test,
|
&model_to_test,
|
||||||
test_prompt,
|
|
||||||
request_timeout,
|
request_timeout,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
AppType::Codex => {
|
AppType::Codex => {
|
||||||
Self::check_codex_stream(
|
Self::check_codex_stream(&client, &base_url, &auth, &model_to_test, request_timeout)
|
||||||
&client,
|
.await
|
||||||
&base_url,
|
|
||||||
&auth,
|
|
||||||
&model_to_test,
|
|
||||||
test_prompt,
|
|
||||||
request_timeout,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
AppType::Gemini => {
|
AppType::Gemini => {
|
||||||
Self::check_gemini_stream(
|
Self::check_gemini_stream(
|
||||||
@@ -180,7 +163,6 @@ impl StreamCheckService {
|
|||||||
&base_url,
|
&base_url,
|
||||||
&auth,
|
&auth,
|
||||||
&model_to_test,
|
&model_to_test,
|
||||||
test_prompt,
|
|
||||||
request_timeout,
|
request_timeout,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -197,7 +179,7 @@ impl StreamCheckService {
|
|||||||
Ok(StreamCheckResult {
|
Ok(StreamCheckResult {
|
||||||
status: health_status,
|
status: health_status,
|
||||||
success: true,
|
success: true,
|
||||||
message: "Check succeeded".to_string(),
|
message: "检查成功".to_string(),
|
||||||
response_time_ms: Some(response_time),
|
response_time_ms: Some(response_time),
|
||||||
http_status: Some(status_code),
|
http_status: Some(status_code),
|
||||||
model_used: model,
|
model_used: model,
|
||||||
@@ -219,68 +201,32 @@ impl StreamCheckService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Claude 流式检查
|
/// Claude 流式检查
|
||||||
///
|
|
||||||
/// 严格按照 Claude CLI 真实请求格式构建请求
|
|
||||||
async fn check_claude_stream(
|
async fn check_claude_stream(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
base_url: &str,
|
base_url: &str,
|
||||||
auth: &AuthInfo,
|
auth: &AuthInfo,
|
||||||
model: &str,
|
model: &str,
|
||||||
test_prompt: &str,
|
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
) -> Result<(u16, String), AppError> {
|
) -> Result<(u16, String), AppError> {
|
||||||
let base = base_url.trim_end_matches('/');
|
let base = base_url.trim_end_matches('/');
|
||||||
// URL 必须包含 ?beta=true 参数(某些中转服务依赖此参数验证请求来源)
|
|
||||||
let url = if base.ends_with("/v1") {
|
let url = if base.ends_with("/v1") {
|
||||||
format!("{base}/messages?beta=true")
|
format!("{base}/messages")
|
||||||
} else {
|
} else {
|
||||||
format!("{base}/v1/messages?beta=true")
|
format!("{base}/v1/messages")
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = json!({
|
let body = json!({
|
||||||
"model": model,
|
"model": model,
|
||||||
"max_tokens": 1,
|
"max_tokens": 1,
|
||||||
"messages": [{ "role": "user", "content": test_prompt }],
|
"messages": [{ "role": "user", "content": "hi" }],
|
||||||
"stream": true
|
"stream": true
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取本地系统信息
|
|
||||||
let os_name = Self::get_os_name();
|
|
||||||
let arch_name = Self::get_arch_name();
|
|
||||||
|
|
||||||
// 严格按照 Claude CLI 请求格式设置 headers
|
|
||||||
let response = client
|
let response = client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
// 认证 headers(双重认证)
|
|
||||||
.header("authorization", format!("Bearer {}", auth.api_key))
|
|
||||||
.header("x-api-key", &auth.api_key)
|
.header("x-api-key", &auth.api_key)
|
||||||
// Anthropic 必需 headers
|
|
||||||
.header("anthropic-version", "2023-06-01")
|
.header("anthropic-version", "2023-06-01")
|
||||||
.header(
|
.header("Content-Type", "application/json")
|
||||||
"anthropic-beta",
|
|
||||||
"claude-code-20250219,interleaved-thinking-2025-05-14",
|
|
||||||
)
|
|
||||||
.header("anthropic-dangerous-direct-browser-access", "true")
|
|
||||||
// 内容类型 headers
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.header("accept", "application/json")
|
|
||||||
.header("accept-encoding", "identity")
|
|
||||||
.header("accept-language", "*")
|
|
||||||
// 客户端标识 headers
|
|
||||||
.header("user-agent", "claude-cli/2.1.2 (external, cli)")
|
|
||||||
.header("x-app", "cli")
|
|
||||||
// x-stainless SDK headers(动态获取本地系统信息)
|
|
||||||
.header("x-stainless-lang", "js")
|
|
||||||
.header("x-stainless-package-version", "0.70.0")
|
|
||||||
.header("x-stainless-os", os_name)
|
|
||||||
.header("x-stainless-arch", arch_name)
|
|
||||||
.header("x-stainless-runtime", "node")
|
|
||||||
.header("x-stainless-runtime-version", "v22.20.0")
|
|
||||||
.header("x-stainless-retry-count", "0")
|
|
||||||
.header("x-stainless-timeout", "600")
|
|
||||||
// 其他 headers
|
|
||||||
.header("sec-fetch-mode", "cors")
|
|
||||||
.header("connection", "keep-alive")
|
|
||||||
.timeout(timeout)
|
.timeout(timeout)
|
||||||
.json(&body)
|
.json(&body)
|
||||||
.send()
|
.send()
|
||||||
@@ -299,63 +245,52 @@ impl StreamCheckService {
|
|||||||
if let Some(chunk) = stream.next().await {
|
if let Some(chunk) = stream.next().await {
|
||||||
match chunk {
|
match chunk {
|
||||||
Ok(_) => Ok((status, model.to_string())),
|
Ok(_) => Ok((status, model.to_string())),
|
||||||
Err(e) => Err(AppError::Message(format!("Stream read failed: {e}"))),
|
Err(e) => Err(AppError::Message(format!("读取流失败: {e}"))),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(AppError::Message("No response data received".to_string()))
|
Err(AppError::Message("未收到响应数据".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Codex 流式检查
|
/// Codex 流式检查
|
||||||
///
|
|
||||||
/// 严格按照 Codex CLI 真实请求格式构建请求 (Responses API)
|
|
||||||
async fn check_codex_stream(
|
async fn check_codex_stream(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
base_url: &str,
|
base_url: &str,
|
||||||
auth: &AuthInfo,
|
auth: &AuthInfo,
|
||||||
model: &str,
|
model: &str,
|
||||||
test_prompt: &str,
|
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
) -> Result<(u16, String), AppError> {
|
) -> Result<(u16, String), AppError> {
|
||||||
let base = base_url.trim_end_matches('/');
|
let base = base_url.trim_end_matches('/');
|
||||||
// Codex CLI 使用 /v1/responses 端点 (OpenAI Responses API)
|
|
||||||
let url = if base.ends_with("/v1") {
|
let url = if base.ends_with("/v1") {
|
||||||
format!("{base}/responses")
|
format!("{base}/chat/completions")
|
||||||
} else {
|
} else {
|
||||||
format!("{base}/v1/responses")
|
format!("{base}/v1/chat/completions")
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解析模型名和推理等级 (支持 model@level 或 model#level 格式)
|
// 解析模型名和推理等级 (支持 model@level 或 model#level 格式)
|
||||||
let (actual_model, reasoning_effort) = Self::parse_model_with_effort(model);
|
let (actual_model, reasoning_effort) = Self::parse_model_with_effort(model);
|
||||||
|
|
||||||
// 获取本地系统信息
|
|
||||||
let os_name = Self::get_os_name();
|
|
||||||
let arch_name = Self::get_arch_name();
|
|
||||||
|
|
||||||
// Responses API 请求体格式 (input 必须是数组)
|
|
||||||
let mut body = json!({
|
let mut body = json!({
|
||||||
"model": actual_model,
|
"model": actual_model,
|
||||||
"input": [{ "role": "user", "content": test_prompt }],
|
"messages": [
|
||||||
|
{ "role": "system", "content": "" },
|
||||||
|
{ "role": "assistant", "content": "" },
|
||||||
|
{ "role": "user", "content": "hi" }
|
||||||
|
],
|
||||||
|
"max_tokens": 1,
|
||||||
|
"temperature": 0,
|
||||||
"stream": true
|
"stream": true
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果是推理模型,添加 reasoning_effort
|
// 如果是推理模型,添加 reasoning_effort
|
||||||
if let Some(effort) = reasoning_effort {
|
if let Some(effort) = reasoning_effort {
|
||||||
body["reasoning"] = json!({ "effort": effort });
|
body["reasoning_effort"] = json!(effort);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 严格按照 Codex CLI 请求格式设置 headers
|
|
||||||
let response = client
|
let response = client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.header("authorization", format!("Bearer {}", auth.api_key))
|
.header("Authorization", format!("Bearer {}", auth.api_key))
|
||||||
.header("content-type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("accept", "text/event-stream")
|
|
||||||
.header("accept-encoding", "identity")
|
|
||||||
.header(
|
|
||||||
"user-agent",
|
|
||||||
format!("codex_cli_rs/0.80.0 ({os_name} 15.7.2; {arch_name}) Terminal"),
|
|
||||||
)
|
|
||||||
.header("originator", "codex_cli_rs")
|
|
||||||
.timeout(timeout)
|
.timeout(timeout)
|
||||||
.json(&body)
|
.json(&body)
|
||||||
.send()
|
.send()
|
||||||
@@ -373,10 +308,10 @@ impl StreamCheckService {
|
|||||||
if let Some(chunk) = stream.next().await {
|
if let Some(chunk) = stream.next().await {
|
||||||
match chunk {
|
match chunk {
|
||||||
Ok(_) => Ok((status, model.to_string())),
|
Ok(_) => Ok((status, model.to_string())),
|
||||||
Err(e) => Err(AppError::Message(format!("Stream read failed: {e}"))),
|
Err(e) => Err(AppError::Message(format!("读取流失败: {e}"))),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(AppError::Message("No response data received".to_string()))
|
Err(AppError::Message("未收到响应数据".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +321,6 @@ impl StreamCheckService {
|
|||||||
base_url: &str,
|
base_url: &str,
|
||||||
auth: &AuthInfo,
|
auth: &AuthInfo,
|
||||||
model: &str,
|
model: &str,
|
||||||
test_prompt: &str,
|
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
) -> Result<(u16, String), AppError> {
|
) -> Result<(u16, String), AppError> {
|
||||||
let base = base_url.trim_end_matches('/');
|
let base = base_url.trim_end_matches('/');
|
||||||
@@ -394,7 +328,7 @@ impl StreamCheckService {
|
|||||||
|
|
||||||
let body = json!({
|
let body = json!({
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": [{ "role": "user", "content": test_prompt }],
|
"messages": [{ "role": "user", "content": "hi" }],
|
||||||
"max_tokens": 1,
|
"max_tokens": 1,
|
||||||
"temperature": 0,
|
"temperature": 0,
|
||||||
"stream": true
|
"stream": true
|
||||||
@@ -421,10 +355,10 @@ impl StreamCheckService {
|
|||||||
if let Some(chunk) = stream.next().await {
|
if let Some(chunk) = stream.next().await {
|
||||||
match chunk {
|
match chunk {
|
||||||
Ok(_) => Ok((status, model.to_string())),
|
Ok(_) => Ok((status, model.to_string())),
|
||||||
Err(e) => Err(AppError::Message(format!("Stream read failed: {e}"))),
|
Err(e) => Err(AppError::Message(format!("读取流失败: {e}"))),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(AppError::Message("No response data received".to_string()))
|
Err(AppError::Message("未收到响应数据".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +373,7 @@ impl StreamCheckService {
|
|||||||
/// 解析模型名和推理等级 (支持 model@level 或 model#level 格式)
|
/// 解析模型名和推理等级 (支持 model@level 或 model#level 格式)
|
||||||
/// 返回 (实际模型名, Option<推理等级>)
|
/// 返回 (实际模型名, Option<推理等级>)
|
||||||
fn parse_model_with_effort(model: &str) -> (String, Option<String>) {
|
fn parse_model_with_effort(model: &str) -> (String, Option<String>) {
|
||||||
|
// 查找 @ 或 # 分隔符
|
||||||
if let Some(pos) = model.find('@').or_else(|| model.find('#')) {
|
if let Some(pos) = model.find('@').or_else(|| model.find('#')) {
|
||||||
let actual_model = model[..pos].to_string();
|
let actual_model = model[..pos].to_string();
|
||||||
let effort = model[pos + 1..].to_string();
|
let effort = model[pos + 1..].to_string();
|
||||||
@@ -451,14 +386,17 @@ impl StreamCheckService {
|
|||||||
|
|
||||||
fn should_retry(msg: &str) -> bool {
|
fn should_retry(msg: &str) -> bool {
|
||||||
let lower = msg.to_lowercase();
|
let lower = msg.to_lowercase();
|
||||||
lower.contains("timeout") || lower.contains("abort") || lower.contains("timed out")
|
lower.contains("timeout")
|
||||||
|
|| lower.contains("abort")
|
||||||
|
|| lower.contains("中断")
|
||||||
|
|| lower.contains("超时")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_request_error(e: reqwest::Error) -> AppError {
|
fn map_request_error(e: reqwest::Error) -> AppError {
|
||||||
if e.is_timeout() {
|
if e.is_timeout() {
|
||||||
AppError::Message("Request timeout".to_string())
|
AppError::Message("请求超时".to_string())
|
||||||
} else if e.is_connect() {
|
} else if e.is_connect() {
|
||||||
AppError::Message(format!("Connection failed: {e}"))
|
AppError::Message(format!("连接失败: {e}"))
|
||||||
} else {
|
} else {
|
||||||
AppError::Message(e.to_string())
|
AppError::Message(e.to_string())
|
||||||
}
|
}
|
||||||
@@ -505,26 +443,6 @@ impl StreamCheckService {
|
|||||||
.map(|m| m.as_str().trim().to_string())
|
.map(|m| m.as_str().trim().to_string())
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取操作系统名称(映射为 Claude CLI 使用的格式)
|
|
||||||
fn get_os_name() -> &'static str {
|
|
||||||
match std::env::consts::OS {
|
|
||||||
"macos" => "MacOS",
|
|
||||||
"linux" => "Linux",
|
|
||||||
"windows" => "Windows",
|
|
||||||
other => other,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取 CPU 架构名称(映射为 Claude CLI 使用的格式)
|
|
||||||
fn get_arch_name() -> &'static str {
|
|
||||||
match std::env::consts::ARCH {
|
|
||||||
"aarch64" => "arm64",
|
|
||||||
"x86_64" => "x86_64",
|
|
||||||
"x86" => "x86",
|
|
||||||
other => other,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -549,10 +467,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_retry() {
|
fn test_should_retry() {
|
||||||
assert!(StreamCheckService::should_retry("Request timeout"));
|
assert!(StreamCheckService::should_retry("请求超时"));
|
||||||
assert!(StreamCheckService::should_retry("request timed out"));
|
assert!(StreamCheckService::should_retry("request timeout"));
|
||||||
assert!(StreamCheckService::should_retry("connection abort"));
|
assert!(!StreamCheckService::should_retry("API Key 无效"));
|
||||||
assert!(!StreamCheckService::should_retry("API Key invalid"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -580,33 +497,4 @@ mod tests {
|
|||||||
assert_eq!(model, "gpt-4o-mini");
|
assert_eq!(model, "gpt-4o-mini");
|
||||||
assert_eq!(effort, None);
|
assert_eq!(effort, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_os_name() {
|
|
||||||
let os_name = StreamCheckService::get_os_name();
|
|
||||||
// 确保返回非空字符串
|
|
||||||
assert!(!os_name.is_empty());
|
|
||||||
// 在 macOS 上应该返回 "MacOS"
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
assert_eq!(os_name, "MacOS");
|
|
||||||
// 在 Linux 上应该返回 "Linux"
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
assert_eq!(os_name, "Linux");
|
|
||||||
// 在 Windows 上应该返回 "Windows"
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
assert_eq!(os_name, "Windows");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_arch_name() {
|
|
||||||
let arch_name = StreamCheckService::get_arch_name();
|
|
||||||
// 确保返回非空字符串
|
|
||||||
assert!(!arch_name.is_empty());
|
|
||||||
// 在 ARM64 上应该返回 "arm64"
|
|
||||||
#[cfg(target_arch = "aarch64")]
|
|
||||||
assert_eq!(arch_name, "arm64");
|
|
||||||
// 在 x86_64 上应该返回 "x86_64"
|
|
||||||
#[cfg(target_arch = "x86_64")]
|
|
||||||
assert_eq!(arch_name, "x86_64");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState, useEffect, useRef } from "react";
|
import { useMemo, useState, useEffect } from "react";
|
||||||
import { GripVertical, ChevronDown, ChevronUp } from "lucide-react";
|
import { GripVertical, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type {
|
import type {
|
||||||
@@ -149,10 +149,6 @@ export function ProviderCard({
|
|||||||
// 多套餐默认展开
|
// 多套餐默认展开
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
// 操作按钮容器 ref,用于动态计算宽度
|
|
||||||
const actionsRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [actionsWidth, setActionsWidth] = useState(0);
|
|
||||||
|
|
||||||
// 当检测到多套餐时自动展开
|
// 当检测到多套餐时自动展开
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasMultiplePlans) {
|
if (hasMultiplePlans) {
|
||||||
@@ -160,20 +156,6 @@ export function ProviderCard({
|
|||||||
}
|
}
|
||||||
}, [hasMultiplePlans]);
|
}, [hasMultiplePlans]);
|
||||||
|
|
||||||
// 动态获取操作按钮宽度
|
|
||||||
useEffect(() => {
|
|
||||||
if (actionsRef.current) {
|
|
||||||
const updateWidth = () => {
|
|
||||||
const width = actionsRef.current?.offsetWidth || 0;
|
|
||||||
setActionsWidth(width);
|
|
||||||
};
|
|
||||||
updateWidth();
|
|
||||||
// 监听窗口大小变化
|
|
||||||
window.addEventListener("resize", updateWidth);
|
|
||||||
return () => window.removeEventListener("resize", updateWidth);
|
|
||||||
}
|
|
||||||
}, [onTest, onOpenTerminal]); // 按钮数量可能变化时重新计算
|
|
||||||
|
|
||||||
const handleOpenWebsite = () => {
|
const handleOpenWebsite = () => {
|
||||||
if (!isClickableUrl) {
|
if (!isClickableUrl) {
|
||||||
return;
|
return;
|
||||||
@@ -299,13 +281,10 @@ export function ProviderCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="relative flex items-center ml-auto min-w-0">
|
||||||
className="relative flex items-center ml-auto min-w-0 gap-3"
|
|
||||||
style={{ "--actions-width": `${actionsWidth || 320}px` } as React.CSSProperties}
|
|
||||||
>
|
|
||||||
{/* 用量信息区域 - hover 时向左移动,为操作按钮腾出空间 */}
|
{/* 用量信息区域 - hover 时向左移动,为操作按钮腾出空间 */}
|
||||||
<div className="ml-auto">
|
<div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[14.5rem] group-focus-within:-translate-x-[14.5rem] sm:group-hover:-translate-x-[16rem] sm:group-focus-within:-translate-x-[16rem]">
|
||||||
<div className="flex items-center gap-1 transition-transform duration-200 group-hover:-translate-x-[var(--actions-width)] group-focus-within:-translate-x-[var(--actions-width)]">
|
<div className="flex items-center gap-1">
|
||||||
{/* 多套餐时显示套餐数量,单套餐时显示详细信息 */}
|
{/* 多套餐时显示套餐数量,单套餐时显示详细信息 */}
|
||||||
{hasMultiplePlans ? (
|
{hasMultiplePlans ? (
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||||
@@ -350,11 +329,8 @@ export function ProviderCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮区域 - 绝对定位在右侧,hover 时滑入,与用量信息保持间距 */}
|
{/* 操作按钮区域 - 绝对定位在右侧,hover 时滑入 */}
|
||||||
<div
|
<div className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0">
|
||||||
ref={actionsRef}
|
|
||||||
className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 pl-3 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0"
|
|
||||||
>
|
|
||||||
<ProviderActions
|
<ProviderActions
|
||||||
isCurrent={isCurrent}
|
isCurrent={isCurrent}
|
||||||
isTesting={isTesting}
|
isTesting={isTesting}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Save, Loader2 } from "lucide-react";
|
import { Save, Loader2 } from "lucide-react";
|
||||||
@@ -26,7 +25,6 @@ export function ModelTestConfigPanel() {
|
|||||||
claudeModel: "claude-haiku-4-5-20251001",
|
claudeModel: "claude-haiku-4-5-20251001",
|
||||||
codexModel: "gpt-5.1-codex@low",
|
codexModel: "gpt-5.1-codex@low",
|
||||||
geminiModel: "gemini-3-pro-preview",
|
geminiModel: "gemini-3-pro-preview",
|
||||||
testPrompt: "Who are you?",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -45,7 +43,6 @@ export function ModelTestConfigPanel() {
|
|||||||
claudeModel: data.claudeModel,
|
claudeModel: data.claudeModel,
|
||||||
codexModel: data.codexModel,
|
codexModel: data.codexModel,
|
||||||
geminiModel: data.geminiModel,
|
geminiModel: data.geminiModel,
|
||||||
testPrompt: data.testPrompt || "Who are you?",
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(String(e));
|
setError(String(e));
|
||||||
@@ -69,7 +66,6 @@ export function ModelTestConfigPanel() {
|
|||||||
claudeModel: config.claudeModel,
|
claudeModel: config.claudeModel,
|
||||||
codexModel: config.codexModel,
|
codexModel: config.codexModel,
|
||||||
geminiModel: config.geminiModel,
|
geminiModel: config.geminiModel,
|
||||||
testPrompt: config.testPrompt || "Who are you?",
|
|
||||||
};
|
};
|
||||||
await saveStreamCheckConfig(parsed);
|
await saveStreamCheckConfig(parsed);
|
||||||
toast.success(t("streamCheck.configSaved"), {
|
toast.success(t("streamCheck.configSaved"), {
|
||||||
@@ -193,21 +189,6 @@ export function ModelTestConfigPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 检查提示词配置 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="testPrompt">{t("streamCheck.testPrompt")}</Label>
|
|
||||||
<Textarea
|
|
||||||
id="testPrompt"
|
|
||||||
value={config.testPrompt}
|
|
||||||
onChange={(e) =>
|
|
||||||
setConfig({ ...config, testPrompt: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="Who are you?"
|
|
||||||
rows={2}
|
|
||||||
className="min-h-[60px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -1192,8 +1192,7 @@
|
|||||||
"checkParams": "Check Parameters",
|
"checkParams": "Check Parameters",
|
||||||
"timeout": "Timeout (seconds)",
|
"timeout": "Timeout (seconds)",
|
||||||
"maxRetries": "Max Retries",
|
"maxRetries": "Max Retries",
|
||||||
"degradedThreshold": "Degraded Threshold (ms)",
|
"degradedThreshold": "Degraded Threshold (ms)"
|
||||||
"testPrompt": "Test Prompt"
|
|
||||||
},
|
},
|
||||||
"proxyConfig": {
|
"proxyConfig": {
|
||||||
"proxyEnabled": "Proxy Enabled",
|
"proxyEnabled": "Proxy Enabled",
|
||||||
|
|||||||
@@ -1186,8 +1186,7 @@
|
|||||||
"checkParams": "チェックパラメーター",
|
"checkParams": "チェックパラメーター",
|
||||||
"timeout": "タイムアウト(秒)",
|
"timeout": "タイムアウト(秒)",
|
||||||
"maxRetries": "最大リトライ回数",
|
"maxRetries": "最大リトライ回数",
|
||||||
"degradedThreshold": "劣化しきい値(ミリ秒)",
|
"degradedThreshold": "劣化しきい値(ミリ秒)"
|
||||||
"testPrompt": "テストプロンプト"
|
|
||||||
},
|
},
|
||||||
"proxyConfig": {
|
"proxyConfig": {
|
||||||
"proxyEnabled": "プロキシ有効",
|
"proxyEnabled": "プロキシ有効",
|
||||||
|
|||||||
@@ -1192,8 +1192,7 @@
|
|||||||
"checkParams": "检查参数",
|
"checkParams": "检查参数",
|
||||||
"timeout": "超时时间(秒)",
|
"timeout": "超时时间(秒)",
|
||||||
"maxRetries": "最大重试次数",
|
"maxRetries": "最大重试次数",
|
||||||
"degradedThreshold": "降级阈值(毫秒)",
|
"degradedThreshold": "降级阈值(毫秒)"
|
||||||
"testPrompt": "检查提示词"
|
|
||||||
},
|
},
|
||||||
"proxyConfig": {
|
"proxyConfig": {
|
||||||
"proxyEnabled": "代理总开关",
|
"proxyEnabled": "代理总开关",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export interface StreamCheckConfig {
|
|||||||
claudeModel: string;
|
claudeModel: string;
|
||||||
codexModel: string;
|
codexModel: string;
|
||||||
geminiModel: string;
|
geminiModel: string;
|
||||||
testPrompt: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StreamCheckResult {
|
export interface StreamCheckResult {
|
||||||
|
|||||||
Reference in New Issue
Block a user