mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-04-28 05:33:10 +08:00
Compare commits
4 Commits
codex/issu
...
fix/proxy-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce9e5e25d3 | ||
|
|
01eee0ae22 | ||
|
|
37239f292c | ||
|
|
d954df0c76 |
@@ -37,7 +37,7 @@ tauri-plugin-deep-link = "2"
|
|||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
toml_edit = "0.22"
|
toml_edit = "0.22"
|
||||||
reqwest = { version = "0.12", features = ["rustls-tls", "json", "stream"] }
|
reqwest = { version = "0.12", features = ["rustls-tls", "json", "stream", "socks"] }
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ impl Database {
|
|||||||
Ok(GlobalProxyConfig {
|
Ok(GlobalProxyConfig {
|
||||||
proxy_enabled: false,
|
proxy_enabled: false,
|
||||||
listen_address: "127.0.0.1".to_string(),
|
listen_address: "127.0.0.1".to_string(),
|
||||||
listen_port: 5000,
|
listen_port: 15721,
|
||||||
enable_logging: true,
|
enable_logging: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ impl Database {
|
|||||||
conn.execute("CREATE TABLE IF NOT EXISTS proxy_config (
|
conn.execute("CREATE TABLE IF NOT EXISTS proxy_config (
|
||||||
app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')),
|
app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')),
|
||||||
proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
||||||
listen_port INTEGER NOT NULL DEFAULT 5000, enable_logging INTEGER NOT NULL DEFAULT 1,
|
listen_port INTEGER NOT NULL DEFAULT 15721, enable_logging INTEGER NOT NULL DEFAULT 1,
|
||||||
enabled INTEGER NOT NULL DEFAULT 0, auto_failover_enabled INTEGER NOT NULL DEFAULT 0,
|
enabled INTEGER NOT NULL DEFAULT 0, auto_failover_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
max_retries INTEGER NOT NULL DEFAULT 3, streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 60,
|
max_retries INTEGER NOT NULL DEFAULT 3, streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 60,
|
||||||
streaming_idle_timeout INTEGER NOT NULL DEFAULT 120, non_streaming_timeout INTEGER NOT NULL DEFAULT 600,
|
streaming_idle_timeout INTEGER NOT NULL DEFAULT 120, non_streaming_timeout INTEGER NOT NULL DEFAULT 600,
|
||||||
@@ -253,7 +253,7 @@ impl Database {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
let _ = conn.execute(
|
let _ = conn.execute(
|
||||||
"ALTER TABLE proxy_config ADD COLUMN listen_port INTEGER NOT NULL DEFAULT 5000",
|
"ALTER TABLE proxy_config ADD COLUMN listen_port INTEGER NOT NULL DEFAULT 15721",
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
let _ = conn.execute(
|
let _ = conn.execute(
|
||||||
@@ -469,7 +469,7 @@ impl Database {
|
|||||||
conn,
|
conn,
|
||||||
"proxy_config",
|
"proxy_config",
|
||||||
"listen_port",
|
"listen_port",
|
||||||
"INTEGER NOT NULL DEFAULT 5000",
|
"INTEGER NOT NULL DEFAULT 15721",
|
||||||
)?;
|
)?;
|
||||||
Self::add_column_if_missing(
|
Self::add_column_if_missing(
|
||||||
conn,
|
conn,
|
||||||
@@ -664,7 +664,7 @@ impl Database {
|
|||||||
conn.execute("CREATE TABLE proxy_config_new (
|
conn.execute("CREATE TABLE proxy_config_new (
|
||||||
app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')),
|
app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')),
|
||||||
proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
||||||
listen_port INTEGER NOT NULL DEFAULT 5000, enable_logging INTEGER NOT NULL DEFAULT 1,
|
listen_port INTEGER NOT NULL DEFAULT 15721, enable_logging INTEGER NOT NULL DEFAULT 1,
|
||||||
enabled INTEGER NOT NULL DEFAULT 0, auto_failover_enabled INTEGER NOT NULL DEFAULT 0,
|
enabled INTEGER NOT NULL DEFAULT 0, auto_failover_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
max_retries INTEGER NOT NULL DEFAULT 3, streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 60,
|
max_retries INTEGER NOT NULL DEFAULT 3, streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 60,
|
||||||
streaming_idle_timeout INTEGER NOT NULL DEFAULT 120, non_streaming_timeout INTEGER NOT NULL DEFAULT 600,
|
streaming_idle_timeout INTEGER NOT NULL DEFAULT 120, non_streaming_timeout INTEGER NOT NULL DEFAULT 600,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::{app_config::AppType, provider::Provider};
|
|||||||
use reqwest::{Client, Response};
|
use reqwest::{Client, Response};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
/// Headers 黑名单 - 不透传到上游的 Headers
|
/// Headers 黑名单 - 不透传到上游的 Headers
|
||||||
@@ -81,7 +81,8 @@ pub struct ForwardError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct RequestForwarder {
|
pub struct RequestForwarder {
|
||||||
client: Client,
|
client: Option<Client>,
|
||||||
|
client_init_error: Option<String>,
|
||||||
/// 共享的 ProviderRouter(持有熔断器状态)
|
/// 共享的 ProviderRouter(持有熔断器状态)
|
||||||
router: Arc<ProviderRouter>,
|
router: Arc<ProviderRouter>,
|
||||||
status: Arc<RwLock<ProxyStatus>>,
|
status: Arc<RwLock<ProxyStatus>>,
|
||||||
@@ -111,21 +112,41 @@ impl RequestForwarder {
|
|||||||
// 参考 Claude Code Hub 的 undici 全局超时设计
|
// 参考 Claude Code Hub 的 undici 全局超时设计
|
||||||
const GLOBAL_TIMEOUT_SECS: u64 = 1800;
|
const GLOBAL_TIMEOUT_SECS: u64 = 1800;
|
||||||
|
|
||||||
let mut client_builder = Client::builder();
|
let timeout_secs = if non_streaming_timeout > 0 {
|
||||||
if non_streaming_timeout > 0 {
|
non_streaming_timeout
|
||||||
// 使用配置的非流式超时
|
|
||||||
client_builder = client_builder.timeout(Duration::from_secs(non_streaming_timeout));
|
|
||||||
} else {
|
} else {
|
||||||
// 禁用超时时使用全局超时作为保底
|
GLOBAL_TIMEOUT_SECS
|
||||||
client_builder = client_builder.timeout(Duration::from_secs(GLOBAL_TIMEOUT_SECS));
|
};
|
||||||
}
|
|
||||||
|
|
||||||
let client = client_builder
|
// 注意:这里不能用 expect/unwrap。
|
||||||
|
// release 配置为 panic=abort,一旦 build 失败会导致整个应用闪退。
|
||||||
|
// 常见原因:用户环境变量里存在不合法/不支持的代理(HTTP(S)_PROXY/ALL_PROXY 等)。
|
||||||
|
let (client, client_init_error) = match Client::builder()
|
||||||
|
.timeout(Duration::from_secs(timeout_secs))
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to create HTTP client");
|
{
|
||||||
|
Ok(client) => (Some(client), None),
|
||||||
|
Err(e) => {
|
||||||
|
// 降级:忽略系统/环境代理,避免因代理配置问题导致整个应用崩溃
|
||||||
|
match Client::builder()
|
||||||
|
.timeout(Duration::from_secs(timeout_secs))
|
||||||
|
.no_proxy()
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(client) => (Some(client), Some(e.to_string())),
|
||||||
|
Err(fallback_err) => (
|
||||||
|
None,
|
||||||
|
Some(format!(
|
||||||
|
"Failed to create HTTP client: {e}; no_proxy fallback failed: {fallback_err}"
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
client,
|
client,
|
||||||
|
client_init_error,
|
||||||
router,
|
router,
|
||||||
status,
|
status,
|
||||||
current_providers,
|
current_providers,
|
||||||
@@ -162,12 +183,6 @@ impl RequestForwarder {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"[{}] 故障转移链: {} 个可用供应商",
|
|
||||||
app_type_str,
|
|
||||||
providers.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
let mut last_provider = None;
|
let mut last_provider = None;
|
||||||
let mut attempted_providers = 0usize;
|
let mut attempted_providers = 0usize;
|
||||||
@@ -190,25 +205,11 @@ impl RequestForwarder {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !allowed {
|
if !allowed {
|
||||||
log::debug!(
|
|
||||||
"[{}] Provider {} 熔断器拒绝本次请求,跳过",
|
|
||||||
app_type_str,
|
|
||||||
provider.name
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
attempted_providers += 1;
|
attempted_providers += 1;
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"[{}] 尝试 {}/{} - 使用Provider: {} (sort_index: {})",
|
|
||||||
app_type_str,
|
|
||||||
attempted_providers,
|
|
||||||
providers.len(),
|
|
||||||
provider.name,
|
|
||||||
provider.sort_index.unwrap_or(999999)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 更新状态中的当前Provider信息
|
// 更新状态中的当前Provider信息
|
||||||
{
|
{
|
||||||
let mut status = self.status.write().await;
|
let mut status = self.status.write().await;
|
||||||
@@ -218,18 +219,14 @@ impl RequestForwarder {
|
|||||||
status.last_request_at = Some(chrono::Utc::now().to_rfc3339());
|
status.last_request_at = Some(chrono::Utc::now().to_rfc3339());
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
// 转发请求(每个 Provider 只尝试一次,重试由客户端控制)
|
// 转发请求(每个 Provider 只尝试一次,重试由客户端控制)
|
||||||
match self
|
match self
|
||||||
.forward(provider, endpoint, &body, &headers, adapter.as_ref())
|
.forward(provider, endpoint, &body, &headers, adapter.as_ref())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
let latency = start.elapsed().as_millis() as u64;
|
|
||||||
|
|
||||||
// 成功:记录成功并更新熔断器
|
// 成功:记录成功并更新熔断器
|
||||||
if let Err(e) = self
|
let _ = self
|
||||||
.router
|
.router
|
||||||
.record_result(
|
.record_result(
|
||||||
&provider.id,
|
&provider.id,
|
||||||
@@ -238,10 +235,7 @@ impl RequestForwarder {
|
|||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
{
|
|
||||||
log::warn!("Failed to record success: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新当前应用类型使用的 provider
|
// 更新当前应用类型使用的 provider
|
||||||
{
|
{
|
||||||
@@ -261,12 +255,6 @@ impl RequestForwarder {
|
|||||||
self.current_provider_id_at_start.as_str() != provider.id.as_str();
|
self.current_provider_id_at_start.as_str() != provider.id.as_str();
|
||||||
if should_switch {
|
if should_switch {
|
||||||
status.failover_count += 1;
|
status.failover_count += 1;
|
||||||
log::info!(
|
|
||||||
"[{}] 代理目标已切换到 Provider: {} (耗时: {}ms)",
|
|
||||||
app_type_str,
|
|
||||||
provider.name,
|
|
||||||
latency
|
|
||||||
);
|
|
||||||
|
|
||||||
// 异步触发供应商切换,更新 UI/托盘,并把“当前供应商”同步为实际使用的 provider
|
// 异步触发供应商切换,更新 UI/托盘,并把“当前供应商”同步为实际使用的 provider
|
||||||
let fm = self.failover_manager.clone();
|
let fm = self.failover_manager.clone();
|
||||||
@@ -276,10 +264,7 @@ impl RequestForwarder {
|
|||||||
let at = app_type_str.to_string();
|
let at = app_type_str.to_string();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = fm.try_switch(ah.as_ref(), &at, &pid, &pname).await
|
let _ = fm.try_switch(ah.as_ref(), &at, &pid, &pname).await;
|
||||||
{
|
|
||||||
log::error!("[Failover] 切换供应商失败: {e}");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 重新计算成功率
|
// 重新计算成功率
|
||||||
@@ -290,23 +275,14 @@ impl RequestForwarder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"[{}] 请求成功 - Provider: {} - {}ms",
|
|
||||||
app_type_str,
|
|
||||||
provider.name,
|
|
||||||
latency
|
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(ForwardResult {
|
return Ok(ForwardResult {
|
||||||
response,
|
response,
|
||||||
provider: provider.clone(),
|
provider: provider.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let latency = start.elapsed().as_millis() as u64;
|
|
||||||
|
|
||||||
// 失败:记录失败并更新熔断器
|
// 失败:记录失败并更新熔断器
|
||||||
if let Err(record_err) = self
|
let _ = self
|
||||||
.router
|
.router
|
||||||
.record_result(
|
.record_result(
|
||||||
&provider.id,
|
&provider.id,
|
||||||
@@ -315,10 +291,7 @@ impl RequestForwarder {
|
|||||||
false,
|
false,
|
||||||
Some(e.to_string()),
|
Some(e.to_string()),
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
{
|
|
||||||
log::warn!("Failed to record failure: {record_err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分类错误
|
// 分类错误
|
||||||
let category = self.categorize_proxy_error(&e);
|
let category = self.categorize_proxy_error(&e);
|
||||||
@@ -332,14 +305,6 @@ impl RequestForwarder {
|
|||||||
Some(format!("Provider {} 失败: {}", provider.name, e));
|
Some(format!("Provider {} 失败: {}", provider.name, e));
|
||||||
}
|
}
|
||||||
|
|
||||||
log::warn!(
|
|
||||||
"[{}] Provider {} 失败(可重试): {} - {}ms",
|
|
||||||
app_type_str,
|
|
||||||
provider.name,
|
|
||||||
e,
|
|
||||||
latency
|
|
||||||
);
|
|
||||||
|
|
||||||
last_error = Some(e);
|
last_error = Some(e);
|
||||||
last_provider = Some(provider.clone());
|
last_provider = Some(provider.clone());
|
||||||
// 继续尝试下一个供应商
|
// 继续尝试下一个供应商
|
||||||
@@ -357,12 +322,6 @@ impl RequestForwarder {
|
|||||||
* 100.0;
|
* 100.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log::error!(
|
|
||||||
"[{}] Provider {} 失败(不可重试): {}",
|
|
||||||
app_type_str,
|
|
||||||
provider.name,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return Err(ForwardError {
|
return Err(ForwardError {
|
||||||
error: e,
|
error: e,
|
||||||
provider: Some(provider.clone()),
|
provider: Some(provider.clone()),
|
||||||
@@ -401,12 +360,6 @@ impl RequestForwarder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::error!(
|
|
||||||
"[{}] 所有 {} 个供应商都失败了",
|
|
||||||
app_type_str,
|
|
||||||
providers.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
Err(ForwardError {
|
Err(ForwardError {
|
||||||
error: last_error.unwrap_or(ProxyError::MaxRetriesExceeded),
|
error: last_error.unwrap_or(ProxyError::MaxRetriesExceeded),
|
||||||
provider: last_provider,
|
provider: last_provider,
|
||||||
@@ -424,7 +377,6 @@ impl RequestForwarder {
|
|||||||
) -> Result<Response, ProxyError> {
|
) -> Result<Response, ProxyError> {
|
||||||
// 使用适配器提取 base_url
|
// 使用适配器提取 base_url
|
||||||
let base_url = adapter.extract_base_url(provider)?;
|
let base_url = adapter.extract_base_url(provider)?;
|
||||||
log::info!("[{}] base_url: {}", adapter.name(), base_url);
|
|
||||||
|
|
||||||
// 检查是否需要格式转换
|
// 检查是否需要格式转换
|
||||||
let needs_transform = adapter.needs_transform(provider);
|
let needs_transform = adapter.needs_transform(provider);
|
||||||
@@ -439,36 +391,13 @@ impl RequestForwarder {
|
|||||||
// 使用适配器构建 URL
|
// 使用适配器构建 URL
|
||||||
let url = adapter.build_url(&base_url, effective_endpoint);
|
let url = adapter.build_url(&base_url, effective_endpoint);
|
||||||
|
|
||||||
// 记录原始请求 JSON
|
|
||||||
log::info!(
|
|
||||||
"[{}] ====== 请求开始 ======\n>>> 原始请求 JSON:\n{}",
|
|
||||||
adapter.name(),
|
|
||||||
serde_json::to_string_pretty(body).unwrap_or_else(|_| body.to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// 应用模型映射(独立于格式转换)
|
// 应用模型映射(独立于格式转换)
|
||||||
let (mapped_body, _original_model, mapped_model) =
|
let (mapped_body, _original_model, _mapped_model) =
|
||||||
super::model_mapper::apply_model_mapping(body.clone(), provider);
|
super::model_mapper::apply_model_mapping(body.clone(), provider);
|
||||||
|
|
||||||
if let Some(ref mapped) = mapped_model {
|
|
||||||
log::info!(
|
|
||||||
"[{}] >>> 模型映射后的请求 JSON:\n{}",
|
|
||||||
adapter.name(),
|
|
||||||
serde_json::to_string_pretty(&mapped_body).unwrap_or_default()
|
|
||||||
);
|
|
||||||
log::info!("[{}] 模型已映射到: {}", adapter.name(), mapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换请求体(如果需要)
|
// 转换请求体(如果需要)
|
||||||
let request_body = if needs_transform {
|
let request_body = if needs_transform {
|
||||||
log::info!("[{}] 转换请求格式 (Anthropic → OpenAI)", adapter.name());
|
adapter.transform_request(mapped_body, provider)?
|
||||||
let transformed = adapter.transform_request(mapped_body, provider)?;
|
|
||||||
log::info!(
|
|
||||||
"[{}] >>> 转换后的请求 JSON:\n{}",
|
|
||||||
adapter.name(),
|
|
||||||
serde_json::to_string_pretty(&transformed).unwrap_or_default()
|
|
||||||
);
|
|
||||||
transformed
|
|
||||||
} else {
|
} else {
|
||||||
mapped_body
|
mapped_body
|
||||||
};
|
};
|
||||||
@@ -477,71 +406,27 @@ impl RequestForwarder {
|
|||||||
// 默认使用空白名单,过滤所有 _ 前缀字段
|
// 默认使用空白名单,过滤所有 _ 前缀字段
|
||||||
let filtered_body = filter_private_params_with_whitelist(request_body, &[]);
|
let filtered_body = filter_private_params_with_whitelist(request_body, &[]);
|
||||||
|
|
||||||
// ========== 请求体日志(截断显示) ==========
|
|
||||||
let body_str = serde_json::to_string_pretty(&filtered_body)
|
|
||||||
.unwrap_or_else(|_| filtered_body.to_string());
|
|
||||||
let body_preview = if body_str.len() > 2000 {
|
|
||||||
format!(
|
|
||||||
"{}...\n[截断,总长度: {} 字符]",
|
|
||||||
&body_str[..2000],
|
|
||||||
body_str.len()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
body_str
|
|
||||||
};
|
|
||||||
log::info!(
|
|
||||||
"[{}] ====== 最终请求体 ======\n{}",
|
|
||||||
adapter.name(),
|
|
||||||
body_preview
|
|
||||||
);
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"[{}] 转发请求: {} -> {}",
|
|
||||||
adapter.name(),
|
|
||||||
provider.name,
|
|
||||||
url
|
|
||||||
);
|
|
||||||
|
|
||||||
// 构建请求
|
// 构建请求
|
||||||
let mut request = self.client.post(&url);
|
let client = self.client.as_ref().ok_or_else(|| {
|
||||||
|
ProxyError::ForwardFailed(
|
||||||
// ========== 详细 Headers 日志 ==========
|
self.client_init_error
|
||||||
log::info!("[{}] ====== 客户端原始 Headers ======", adapter.name());
|
.clone()
|
||||||
for (key, value) in headers {
|
.unwrap_or_else(|| "HTTP client is not initialized".to_string()),
|
||||||
log::info!(
|
)
|
||||||
"[{}] {}: {:?}",
|
})?;
|
||||||
adapter.name(),
|
let mut request = client.post(&url);
|
||||||
key.as_str(),
|
|
||||||
value.to_str().unwrap_or("<binary>")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤黑名单 Headers,保护隐私并避免冲突
|
// 过滤黑名单 Headers,保护隐私并避免冲突
|
||||||
let mut filtered_headers: Vec<String> = Vec::new();
|
|
||||||
let mut passed_headers: Vec<(String, String)> = Vec::new();
|
|
||||||
|
|
||||||
for (key, value) in headers {
|
for (key, value) in headers {
|
||||||
let key_str = key.as_str().to_lowercase();
|
if HEADER_BLACKLIST
|
||||||
if HEADER_BLACKLIST.contains(&key_str.as_str()) {
|
.iter()
|
||||||
filtered_headers.push(key_str);
|
.any(|h| key.as_str().eq_ignore_ascii_case(h))
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let value_str = value.to_str().unwrap_or("<binary>").to_string();
|
|
||||||
passed_headers.push((key.as_str().to_string(), value_str.clone()));
|
|
||||||
request = request.header(key, value);
|
request = request.header(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !filtered_headers.is_empty() {
|
|
||||||
log::info!(
|
|
||||||
"[{}] ====== 被过滤的 Headers ({}) ======",
|
|
||||||
adapter.name(),
|
|
||||||
filtered_headers.len()
|
|
||||||
);
|
|
||||||
for h in &filtered_headers {
|
|
||||||
log::info!("[{}] - {}", adapter.name(), h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 anthropic-beta Header(仅 Claude)
|
// 处理 anthropic-beta Header(仅 Claude)
|
||||||
// 关键:确保包含 claude-code-20250219 标记,这是上游服务验证请求来源的依据
|
// 关键:确保包含 claude-code-20250219 标记,这是上游服务验证请求来源的依据
|
||||||
// 如果客户端发送的 beta 标记中没有包含 claude-code-20250219,需要补充
|
// 如果客户端发送的 beta 标记中没有包含 claude-code-20250219,需要补充
|
||||||
@@ -564,55 +449,27 @@ impl RequestForwarder {
|
|||||||
CLAUDE_CODE_BETA.to_string()
|
CLAUDE_CODE_BETA.to_string()
|
||||||
};
|
};
|
||||||
request = request.header("anthropic-beta", &beta_value);
|
request = request.header("anthropic-beta", &beta_value);
|
||||||
passed_headers.push(("anthropic-beta".to_string(), beta_value.clone()));
|
|
||||||
log::info!("[{}] 设置 anthropic-beta: {}", adapter.name(), beta_value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 客户端 IP 透传(默认开启)
|
// 客户端 IP 透传(默认开启)
|
||||||
if let Some(xff) = headers.get("x-forwarded-for") {
|
if let Some(xff) = headers.get("x-forwarded-for") {
|
||||||
if let Ok(xff_str) = xff.to_str() {
|
if let Ok(xff_str) = xff.to_str() {
|
||||||
request = request.header("x-forwarded-for", xff_str);
|
request = request.header("x-forwarded-for", xff_str);
|
||||||
passed_headers.push(("x-forwarded-for".to_string(), xff_str.to_string()));
|
|
||||||
log::debug!("[{}] 透传 x-forwarded-for: {}", adapter.name(), xff_str);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(real_ip) = headers.get("x-real-ip") {
|
if let Some(real_ip) = headers.get("x-real-ip") {
|
||||||
if let Ok(real_ip_str) = real_ip.to_str() {
|
if let Ok(real_ip_str) = real_ip.to_str() {
|
||||||
request = request.header("x-real-ip", real_ip_str);
|
request = request.header("x-real-ip", real_ip_str);
|
||||||
passed_headers.push(("x-real-ip".to_string(), real_ip_str.to_string()));
|
|
||||||
log::debug!("[{}] 透传 x-real-ip: {}", adapter.name(), real_ip_str);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 禁用压缩,避免 gzip 流式响应解析错误
|
// 禁用压缩,避免 gzip 流式响应解析错误
|
||||||
// 参考 CCH: undici 在连接提前关闭时会对不完整的 gzip 流抛出错误
|
// 参考 CCH: undici 在连接提前关闭时会对不完整的 gzip 流抛出错误
|
||||||
request = request.header("accept-encoding", "identity");
|
request = request.header("accept-encoding", "identity");
|
||||||
passed_headers.push(("accept-encoding".to_string(), "identity".to_string()));
|
|
||||||
|
|
||||||
// 使用适配器添加认证头
|
// 使用适配器添加认证头
|
||||||
if let Some(auth) = adapter.extract_auth(provider) {
|
if let Some(auth) = adapter.extract_auth(provider) {
|
||||||
log::debug!(
|
|
||||||
"[{}] 使用认证: {:?} (key: {})",
|
|
||||||
adapter.name(),
|
|
||||||
auth.strategy,
|
|
||||||
auth.masked_key()
|
|
||||||
);
|
|
||||||
request = adapter.add_auth_headers(request, &auth);
|
request = adapter.add_auth_headers(request, &auth);
|
||||||
// 记录认证头(脱敏)
|
|
||||||
passed_headers.push((
|
|
||||||
"authorization".to_string(),
|
|
||||||
format!("Bearer {}...", &auth.api_key[..8.min(auth.api_key.len())]),
|
|
||||||
));
|
|
||||||
passed_headers.push((
|
|
||||||
"x-api-key".to_string(),
|
|
||||||
format!("{}...", &auth.api_key[..8.min(auth.api_key.len())]),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
log::error!(
|
|
||||||
"[{}] 未找到 API Key!Provider: {}",
|
|
||||||
adapter.name(),
|
|
||||||
provider.name
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// anthropic-version 统一处理(仅 Claude):优先使用客户端的版本号,否则使用默认值
|
// anthropic-version 统一处理(仅 Claude):优先使用客户端的版本号,否则使用默认值
|
||||||
@@ -623,28 +480,10 @@ impl RequestForwarder {
|
|||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.unwrap_or("2023-06-01");
|
.unwrap_or("2023-06-01");
|
||||||
request = request.header("anthropic-version", version_str);
|
request = request.header("anthropic-version", version_str);
|
||||||
passed_headers.push(("anthropic-version".to_string(), version_str.to_string()));
|
|
||||||
log::info!(
|
|
||||||
"[{}] 设置 anthropic-version: {}",
|
|
||||||
adapter.name(),
|
|
||||||
version_str
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 最终发送的 Headers 日志 ==========
|
|
||||||
log::info!(
|
|
||||||
"[{}] ====== 最终发送的 Headers ({}) ======",
|
|
||||||
adapter.name(),
|
|
||||||
passed_headers.len()
|
|
||||||
);
|
|
||||||
for (k, v) in &passed_headers {
|
|
||||||
log::info!("[{}] {}: {}", adapter.name(), k, v);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
log::info!("[{}] 发送请求到: {}", adapter.name(), url);
|
|
||||||
let response = request.json(&filtered_body).send().await.map_err(|e| {
|
let response = request.json(&filtered_body).send().await.map_err(|e| {
|
||||||
log::error!("[{}] 请求失败: {}", adapter.name(), e);
|
|
||||||
if e.is_timeout() {
|
if e.is_timeout() {
|
||||||
ProxyError::Timeout(format!("请求超时: {e}"))
|
ProxyError::Timeout(format!("请求超时: {e}"))
|
||||||
} else if e.is_connect() {
|
} else if e.is_connect() {
|
||||||
@@ -656,19 +495,12 @@ impl RequestForwarder {
|
|||||||
|
|
||||||
// 检查响应状态
|
// 检查响应状态
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
log::info!("[{}] 响应状态: {}", adapter.name(), status);
|
|
||||||
|
|
||||||
if status.is_success() {
|
if status.is_success() {
|
||||||
Ok(response)
|
Ok(response)
|
||||||
} else {
|
} else {
|
||||||
let status_code = status.as_u16();
|
let status_code = status.as_u16();
|
||||||
let body_text = response.text().await.ok();
|
let body_text = response.text().await.ok();
|
||||||
log::error!(
|
|
||||||
"[{}] 上游错误 ({}): {:?}",
|
|
||||||
adapter.name(),
|
|
||||||
status_code,
|
|
||||||
body_text
|
|
||||||
);
|
|
||||||
|
|
||||||
Err(ProxyError::UpstreamError {
|
Err(ProxyError::UpstreamError {
|
||||||
status: status_code,
|
status: status_code,
|
||||||
|
|||||||
@@ -291,7 +291,10 @@ async fn handle_claude_transform(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let body = axum::body::Body::from(response_body);
|
let body = axum::body::Body::from(response_body);
|
||||||
Ok(builder.body(body).unwrap())
|
builder.body(body).map_err(|e| {
|
||||||
|
log::error!("[Claude] 构建响应失败: {e}");
|
||||||
|
ProxyError::Internal(format!("Failed to build response: {e}"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -38,13 +38,20 @@ impl AuthInfo {
|
|||||||
///
|
///
|
||||||
/// 显示前4位和后4位,中间用 `...` 代替
|
/// 显示前4位和后4位,中间用 `...` 代替
|
||||||
/// 如果 key 长度不足8位,则返回 `***`
|
/// 如果 key 长度不足8位,则返回 `***`
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn masked_key(&self) -> String {
|
pub fn masked_key(&self) -> String {
|
||||||
if self.api_key.len() > 8 {
|
if self.api_key.chars().count() > 8 {
|
||||||
format!(
|
let prefix: String = self.api_key.chars().take(4).collect();
|
||||||
"{}...{}",
|
let suffix: String = self
|
||||||
&self.api_key[..4],
|
.api_key
|
||||||
&self.api_key[self.api_key.len() - 4..]
|
.chars()
|
||||||
)
|
.rev()
|
||||||
|
.take(4)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.collect();
|
||||||
|
format!("{prefix}...{suffix}")
|
||||||
} else {
|
} else {
|
||||||
"***".to_string()
|
"***".to_string()
|
||||||
}
|
}
|
||||||
@@ -54,8 +61,17 @@ impl AuthInfo {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn masked_access_token(&self) -> Option<String> {
|
pub fn masked_access_token(&self) -> Option<String> {
|
||||||
self.access_token.as_ref().map(|token| {
|
self.access_token.as_ref().map(|token| {
|
||||||
if token.len() > 8 {
|
if token.chars().count() > 8 {
|
||||||
format!("{}...{}", &token[..4], &token[token.len() - 4..])
|
let prefix: String = token.chars().take(4).collect();
|
||||||
|
let suffix: String = token
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.take(4)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.collect();
|
||||||
|
format!("{prefix}...{suffix}")
|
||||||
} else {
|
} else {
|
||||||
"***".to_string()
|
"***".to_string()
|
||||||
}
|
}
|
||||||
@@ -126,6 +142,13 @@ mod tests {
|
|||||||
assert_eq!(auth.masked_key(), "1234...6789");
|
assert_eq!(auth.masked_key(), "1234...6789");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_masked_key_utf8_safe() {
|
||||||
|
let auth = AuthInfo::new("测试⚠️1234567890".to_string(), AuthStrategy::Bearer);
|
||||||
|
let masked = auth.masked_key();
|
||||||
|
assert!(!masked.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_auth_strategy_equality() {
|
fn test_auth_strategy_equality() {
|
||||||
assert_eq!(AuthStrategy::Anthropic, AuthStrategy::Anthropic);
|
assert_eq!(AuthStrategy::Anthropic, AuthStrategy::Anthropic);
|
||||||
@@ -160,6 +183,14 @@ mod tests {
|
|||||||
assert_eq!(auth.masked_access_token(), Some("ya29...cdef".to_string()));
|
assert_eq!(auth.masked_access_token(), Some("ya29...cdef".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_masked_access_token_utf8_safe() {
|
||||||
|
let auth =
|
||||||
|
AuthInfo::with_access_token("refresh".to_string(), "令牌⚠️1234567890".to_string());
|
||||||
|
let masked = auth.masked_access_token().unwrap();
|
||||||
|
assert!(!masked.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_masked_access_token_short() {
|
fn test_masked_access_token_short() {
|
||||||
let auth = AuthInfo::with_access_token("refresh".to_string(), "short".to_string());
|
let auth = AuthInfo::with_access_token("refresh".to_string(), "short".to_string());
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use super::{
|
|||||||
usage::parser::TokenUsage,
|
usage::parser::TokenUsage,
|
||||||
ProxyError,
|
ProxyError,
|
||||||
};
|
};
|
||||||
use axum::response::Response;
|
use axum::response::{IntoResponse, Response};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::stream::{Stream, StreamExt};
|
use futures::stream::{Stream, StreamExt};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
@@ -72,7 +72,13 @@ pub async fn handle_streaming(
|
|||||||
create_logged_passthrough_stream(stream, ctx.tag, Some(usage_collector), timeout_config);
|
create_logged_passthrough_stream(stream, ctx.tag, Some(usage_collector), timeout_config);
|
||||||
|
|
||||||
let body = axum::body::Body::from_stream(logged_stream);
|
let body = axum::body::Body::from_stream(logged_stream);
|
||||||
builder.body(body).unwrap()
|
match builder.body(body) {
|
||||||
|
Ok(resp) => resp,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("[{}] 构建流式响应失败: {e}", ctx.tag);
|
||||||
|
ProxyError::Internal(format!("Failed to build streaming response: {e}")).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理非流式响应
|
/// 处理非流式响应
|
||||||
@@ -155,7 +161,10 @@ pub async fn handle_non_streaming(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let body = axum::body::Body::from(body_bytes);
|
let body = axum::body::Body::from(body_bytes);
|
||||||
Ok(builder.body(body).unwrap())
|
builder.body(body).map_err(|e| {
|
||||||
|
log::error!("[{}] 构建响应失败: {e}", ctx.tag);
|
||||||
|
ProxyError::Internal(format!("Failed to build response: {e}"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通用响应处理入口
|
/// 通用响应处理入口
|
||||||
|
|||||||
@@ -65,8 +65,11 @@ impl<'a> UsageLogger<'a> {
|
|||||||
|
|
||||||
let created_at = SystemTime::now()
|
let created_at = SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.unwrap()
|
.map(|d| d.as_secs() as i64)
|
||||||
.as_secs() as i64;
|
.unwrap_or_else(|e| {
|
||||||
|
log::warn!("SystemTime is before UNIX_EPOCH, falling back to 0: {e}");
|
||||||
|
0
|
||||||
|
});
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO proxy_request_logs (
|
"INSERT INTO proxy_request_logs (
|
||||||
|
|||||||
@@ -441,8 +441,21 @@ impl ProxyService {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// 至少写入一份可用的 Token
|
// 至少写入一份可用的 Token
|
||||||
provider.settings_config["env"] =
|
if provider.settings_config.is_null() {
|
||||||
json!({ token_key: token });
|
provider.settings_config = json!({});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(root) = provider.settings_config.as_object_mut()
|
||||||
|
{
|
||||||
|
root.insert(
|
||||||
|
"env".to_string(),
|
||||||
|
json!({ token_key: token }),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Claude provider settings_config 格式异常(非对象),跳过写入 Token (provider: {provider_id})"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,9 +498,20 @@ impl ProxyService {
|
|||||||
{
|
{
|
||||||
auth_obj.insert("OPENAI_API_KEY".to_string(), json!(token));
|
auth_obj.insert("OPENAI_API_KEY".to_string(), json!(token));
|
||||||
} else {
|
} else {
|
||||||
provider.settings_config["auth"] = json!({
|
if provider.settings_config.is_null() {
|
||||||
"OPENAI_API_KEY": token
|
provider.settings_config = json!({});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if let Some(root) = provider.settings_config.as_object_mut() {
|
||||||
|
root.insert(
|
||||||
|
"auth".to_string(),
|
||||||
|
json!({ "OPENAI_API_KEY": token }),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Codex provider settings_config 格式异常(非对象),跳过写入 Token (provider: {provider_id})"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = self.db.update_provider_settings_config(
|
if let Err(e) = self.db.update_provider_settings_config(
|
||||||
@@ -526,9 +550,20 @@ impl ProxyService {
|
|||||||
{
|
{
|
||||||
env_obj.insert("GEMINI_API_KEY".to_string(), json!(token));
|
env_obj.insert("GEMINI_API_KEY".to_string(), json!(token));
|
||||||
} else {
|
} else {
|
||||||
provider.settings_config["env"] = json!({
|
if provider.settings_config.is_null() {
|
||||||
"GEMINI_API_KEY": token
|
provider.settings_config = json!({});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if let Some(root) = provider.settings_config.as_object_mut() {
|
||||||
|
root.insert(
|
||||||
|
"env".to_string(),
|
||||||
|
json!({ "GEMINI_API_KEY": token }),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Gemini provider settings_config 格式异常(非对象),跳过写入 Token (provider: {provider_id})"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = self.db.update_provider_settings_config(
|
if let Err(e) = self.db.update_provider_settings_config(
|
||||||
@@ -1526,7 +1561,30 @@ impl ProxyService {
|
|||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Err("Claude 配置文件不存在".to_string());
|
return Err("Claude 配置文件不存在".to_string());
|
||||||
}
|
}
|
||||||
read_json_file(&path).map_err(|e| format!("读取 Claude 配置失败: {e}"))
|
|
||||||
|
let mut value: Value =
|
||||||
|
read_json_file(&path).map_err(|e| format!("读取 Claude 配置失败: {e}"))?;
|
||||||
|
|
||||||
|
if value.is_null() {
|
||||||
|
value = json!({});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !value.is_object() {
|
||||||
|
let kind = match &value {
|
||||||
|
Value::Null => "null",
|
||||||
|
Value::Bool(_) => "boolean",
|
||||||
|
Value::Number(_) => "number",
|
||||||
|
Value::String(_) => "string",
|
||||||
|
Value::Array(_) => "array",
|
||||||
|
Value::Object(_) => "object",
|
||||||
|
};
|
||||||
|
return Err(format!(
|
||||||
|
"Claude 配置文件格式错误:根节点必须是 JSON 对象(当前为 {kind}),路径: {}",
|
||||||
|
path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_claude_live(&self, config: &Value) -> Result<(), String> {
|
fn write_claude_live(&self, config: &Value) -> Result<(), String> {
|
||||||
|
|||||||
@@ -269,7 +269,11 @@ async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<
|
|||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
let preview = if text.len() > 200 {
|
let preview = if text.len() > 200 {
|
||||||
format!("{}...", &text[..200])
|
let mut safe_cut = 200usize;
|
||||||
|
while !text.is_char_boundary(safe_cut) {
|
||||||
|
safe_cut = safe_cut.saturating_sub(1);
|
||||||
|
}
|
||||||
|
format!("{}...", &text[..safe_cut])
|
||||||
} else {
|
} else {
|
||||||
text.clone()
|
text.clone()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function ProxyPanel() {
|
|||||||
|
|
||||||
// 监听地址/端口的本地状态
|
// 监听地址/端口的本地状态
|
||||||
const [listenAddress, setListenAddress] = useState("127.0.0.1");
|
const [listenAddress, setListenAddress] = useState("127.0.0.1");
|
||||||
const [listenPort, setListenPort] = useState(5000);
|
const [listenPort, setListenPort] = useState(15721);
|
||||||
|
|
||||||
// 同步全局配置到本地状态
|
// 同步全局配置到本地状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -389,7 +389,9 @@ export function ProxyPanel() {
|
|||||||
id="listen-address"
|
id="listen-address"
|
||||||
value={listenAddress}
|
value={listenAddress}
|
||||||
onChange={(e) => setListenAddress(e.target.value)}
|
onChange={(e) => setListenAddress(e.target.value)}
|
||||||
placeholder="127.0.0.1"
|
placeholder={t("proxy.settings.fields.listenAddress.placeholder", {
|
||||||
|
defaultValue: "127.0.0.1",
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("proxy.settings.fields.listenAddress.description", {
|
{t("proxy.settings.fields.listenAddress.description", {
|
||||||
@@ -410,9 +412,11 @@ export function ProxyPanel() {
|
|||||||
type="number"
|
type="number"
|
||||||
value={listenPort}
|
value={listenPort}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setListenPort(parseInt(e.target.value) || 5000)
|
setListenPort(parseInt(e.target.value) || 15721)
|
||||||
}
|
}
|
||||||
placeholder="5000"
|
placeholder={t("proxy.settings.fields.listenPort.placeholder", {
|
||||||
|
defaultValue: "15721",
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("proxy.settings.fields.listenPort.description", {
|
{t("proxy.settings.fields.listenPort.description", {
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ export function ProxyToggle({ className, activeApp }: ProxyToggleProps) {
|
|||||||
useProxyStatus();
|
useProxyStatus();
|
||||||
|
|
||||||
const handleToggle = async (checked: boolean) => {
|
const handleToggle = async (checked: boolean) => {
|
||||||
await setTakeoverForApp({ appType: activeApp, enabled: checked });
|
try {
|
||||||
|
await setTakeoverForApp({ appType: activeApp, enabled: checked });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[ProxyToggle] Toggle takeover failed:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const takeoverEnabled = takeoverStatus?.[activeApp] || false;
|
const takeoverEnabled = takeoverStatus?.[activeApp] || false;
|
||||||
|
|||||||
@@ -1058,7 +1058,7 @@
|
|||||||
},
|
},
|
||||||
"listenPort": {
|
"listenPort": {
|
||||||
"label": "Listen Port",
|
"label": "Listen Port",
|
||||||
"placeholder": "5000",
|
"placeholder": "15721",
|
||||||
"description": "Port number the proxy server listens on (1024 ~ 65535)"
|
"description": "Port number the proxy server listens on (1024 ~ 65535)"
|
||||||
},
|
},
|
||||||
"maxRetries": {
|
"maxRetries": {
|
||||||
|
|||||||
@@ -1058,7 +1058,7 @@
|
|||||||
},
|
},
|
||||||
"listenPort": {
|
"listenPort": {
|
||||||
"label": "リッスンポート",
|
"label": "リッスンポート",
|
||||||
"placeholder": "5000",
|
"placeholder": "15721",
|
||||||
"description": "プロキシサーバーがリッスンするポート番号(1024 ~ 65535)"
|
"description": "プロキシサーバーがリッスンするポート番号(1024 ~ 65535)"
|
||||||
},
|
},
|
||||||
"maxRetries": {
|
"maxRetries": {
|
||||||
|
|||||||
@@ -1058,7 +1058,7 @@
|
|||||||
},
|
},
|
||||||
"listenPort": {
|
"listenPort": {
|
||||||
"label": "监听端口",
|
"label": "监听端口",
|
||||||
"placeholder": "5000",
|
"placeholder": "15721",
|
||||||
"description": "代理服务器监听的端口号(1024 ~ 65535)"
|
"description": "代理服务器监听的端口号(1024 ~ 65535)"
|
||||||
},
|
},
|
||||||
"maxRetries": {
|
"maxRetries": {
|
||||||
|
|||||||
Reference in New Issue
Block a user