From dbdaf35770172139ec1bdd5b8516285b8545d447 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Wed, 24 Dec 2025 10:02:01 +0800 Subject: [PATCH] feat(proxy): implement streaming timeout control with validation - Add first byte timeout (0 or 1-180s) for streaming requests - Add idle timeout (0 or 60-600s) for streaming data gaps - Add non-streaming timeout (0 or 60-1800s) for total request - Implement timeout logic in response processor - Add 1800s global timeout fallback when disabled - Add database schema migration for timeout fields - Add i18n translations for timeout settings --- src-tauri/src/database/dao/proxy.rs | 16 +- src-tauri/src/database/schema.rs | 39 ++++ src-tauri/src/proxy/forwarder.rs | 18 +- src-tauri/src/proxy/handler_context.rs | 22 ++- src-tauri/src/proxy/handlers.rs | 4 + src-tauri/src/proxy/response_processor.rs | 69 ++++++- src-tauri/src/proxy/types.rs | 26 ++- .../proxy/AutoFailoverConfigPanel.tsx | 160 ++++++++++++++- src/components/proxy/ProxySettingsDialog.tsx | 182 ++++++++++++++++-- src/i18n/locales/en.json | 70 +++++++ src/i18n/locales/ja.json | 70 +++++++ src/i18n/locales/zh.json | 70 +++++++ src/types/proxy.ts | 4 + 13 files changed, 713 insertions(+), 37 deletions(-) diff --git a/src-tauri/src/database/dao/proxy.rs b/src-tauri/src/database/dao/proxy.rs index e2573df7d..7f5c24be2 100644 --- a/src-tauri/src/database/dao/proxy.rs +++ b/src-tauri/src/database/dao/proxy.rs @@ -17,7 +17,8 @@ impl Database { let conn = lock_conn!(self.conn); conn.query_row( "SELECT listen_address, listen_port, max_retries, - request_timeout, enable_logging, live_takeover_active + request_timeout, enable_logging, live_takeover_active, + streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout FROM proxy_config WHERE id = 1", [], |row| { @@ -28,6 +29,9 @@ impl Database { request_timeout: row.get::<_, i32>(3)? as u64, enable_logging: row.get::<_, i32>(4)? != 0, live_takeover_active: row.get::<_, i32>(5).unwrap_or(0) != 0, + streaming_first_byte_timeout: row.get::<_, i32>(6).unwrap_or(30) as u64, + streaming_idle_timeout: row.get::<_, i32>(7).unwrap_or(60) as u64, + non_streaming_timeout: row.get::<_, i32>(8).unwrap_or(300) as u64, }) }, ) @@ -51,8 +55,11 @@ impl Database { conn.execute( "INSERT OR REPLACE INTO proxy_config - (id, enabled, listen_address, listen_port, max_retries, request_timeout, enable_logging, live_takeover_active, target_app, created_at, updated_at) - VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, + (id, enabled, listen_address, listen_port, max_retries, request_timeout, + enable_logging, live_takeover_active, target_app, + streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout, + created_at, updated_at) + VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, COALESCE((SELECT created_at FROM proxy_config WHERE id = 1), datetime('now')), datetime('now'))", rusqlite::params![ @@ -64,6 +71,9 @@ impl Database { if config.enable_logging { 1 } else { 0 }, if config.live_takeover_active { 1 } else { 0 }, "claude", // 兼容旧字段,写入默认值 + config.streaming_first_byte_timeout as i32, + config.streaming_idle_timeout as i32, + config.non_streaming_timeout as i32, ], ) .map_err(|e| AppError::Database(e.to_string()))?; diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 4c1602781..5e63436bf 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -135,6 +135,9 @@ impl Database { request_timeout INTEGER NOT NULL DEFAULT 300, enable_logging INTEGER NOT NULL DEFAULT 1, target_app TEXT NOT NULL DEFAULT 'claude', + streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 30, + streaming_idle_timeout INTEGER NOT NULL DEFAULT 60, + non_streaming_timeout INTEGER NOT NULL DEFAULT 300, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) )", @@ -313,6 +316,20 @@ impl Database { [], ); + // 尝试添加超时配置列到 proxy_config 表(v3 新增) + let _ = conn.execute( + "ALTER TABLE proxy_config ADD COLUMN streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 30", + [], + ); + let _ = conn.execute( + "ALTER TABLE proxy_config ADD COLUMN streaming_idle_timeout INTEGER NOT NULL DEFAULT 60", + [], + ); + let _ = conn.execute( + "ALTER TABLE proxy_config ADD COLUMN non_streaming_timeout INTEGER NOT NULL DEFAULT 300", + [], + ); + // 确保 in_failover_queue 列存在(对于已存在的 v2 数据库) Self::add_column_if_missing( conn, @@ -475,6 +492,28 @@ impl Database { "BOOLEAN NOT NULL DEFAULT 0", )?; + // 添加代理超时配置字段(在 v2 迁移中直接添加) + if Self::table_exists(conn, "proxy_config")? { + Self::add_column_if_missing( + conn, + "proxy_config", + "streaming_first_byte_timeout", + "INTEGER NOT NULL DEFAULT 30", + )?; + Self::add_column_if_missing( + conn, + "proxy_config", + "streaming_idle_timeout", + "INTEGER NOT NULL DEFAULT 60", + )?; + Self::add_column_if_missing( + conn, + "proxy_config", + "non_streaming_timeout", + "INTEGER NOT NULL DEFAULT 300", + )?; + } + // 删除旧的 failover_queue 表(如果存在) conn.execute("DROP INDEX IF EXISTS idx_failover_queue_order", []) .map_err(|e| AppError::Database(format!("删除 failover_queue 索引失败: {e}")))?; diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index f5b27d482..5e562265b 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -39,7 +39,7 @@ pub struct RequestForwarder { failover_manager: Arc, /// AppHandle,用于发射事件和更新托盘 app_handle: Option, - /// 请求开始时的“当前供应商 ID”(用于判断是否需要同步 UI/托盘) + /// 请求开始时的"当前供应商 ID"(用于判断是否需要同步 UI/托盘) current_provider_id_at_start: String, } @@ -47,17 +47,27 @@ impl RequestForwarder { #[allow(clippy::too_many_arguments)] pub fn new( router: Arc, - timeout_secs: u64, + non_streaming_timeout: u64, max_retries: u8, status: Arc>, current_providers: Arc>>, failover_manager: Arc, app_handle: Option, current_provider_id_at_start: String, + _streaming_first_byte_timeout: u64, + _streaming_idle_timeout: u64, ) -> Self { + // 全局超时设置为 1800 秒(30 分钟),确保业务层超时配置能正常工作 + // 参考 Claude Code Hub 的 undici 全局超时设计 + const GLOBAL_TIMEOUT_SECS: u64 = 1800; + let mut client_builder = Client::builder(); - if timeout_secs > 0 { - client_builder = client_builder.timeout(Duration::from_secs(timeout_secs)); + if non_streaming_timeout > 0 { + // 使用配置的非流式超时 + client_builder = client_builder.timeout(Duration::from_secs(non_streaming_timeout)); + } else { + // 禁用超时时使用全局超时作为保底 + client_builder = client_builder.timeout(Duration::from_secs(GLOBAL_TIMEOUT_SECS)); } let client = client_builder diff --git a/src-tauri/src/proxy/handler_context.rs b/src-tauri/src/proxy/handler_context.rs index a8eae231b..3105b7eb0 100644 --- a/src-tauri/src/proxy/handler_context.rs +++ b/src-tauri/src/proxy/handler_context.rs @@ -9,6 +9,15 @@ use crate::proxy::{ }; use std::time::Instant; +/// 流式超时配置 +#[derive(Debug, Clone, Copy)] +pub struct StreamingTimeoutConfig { + /// 首字节超时(秒),0 表示禁用 + pub first_byte_timeout: u64, + /// 静默期超时(秒),0 表示禁用 + pub idle_timeout: u64, +} + /// 请求上下文 /// /// 贯穿整个请求生命周期,包含: @@ -135,13 +144,15 @@ impl RequestContext { pub fn create_forwarder(&self, state: &ProxyState) -> RequestForwarder { RequestForwarder::new( state.provider_router.clone(), - self.config.request_timeout, + self.config.non_streaming_timeout, self.config.max_retries, state.status.clone(), state.current_providers.clone(), state.failover_manager.clone(), state.app_handle.clone(), self.current_provider_id.clone(), + self.config.streaming_first_byte_timeout, + self.config.streaming_idle_timeout, ) } @@ -157,4 +168,13 @@ impl RequestContext { pub fn latency_ms(&self) -> u64 { self.start_time.elapsed().as_millis() as u64 } + + /// 获取流式超时配置 + #[inline] + pub fn streaming_timeout_config(&self) -> StreamingTimeoutConfig { + StreamingTimeoutConfig { + first_byte_timeout: self.config.streaming_first_byte_timeout, + idle_timeout: self.config.streaming_idle_timeout, + } + } } diff --git a/src-tauri/src/proxy/handlers.rs b/src-tauri/src/proxy/handlers.rs index 4df94ea15..20512dc70 100644 --- a/src-tauri/src/proxy/handlers.rs +++ b/src-tauri/src/proxy/handlers.rs @@ -170,10 +170,14 @@ async fn handle_claude_transform( }) }; + // 获取流式超时配置 + let timeout_config = ctx.streaming_timeout_config(); + let logged_stream = create_logged_passthrough_stream( sse_stream, "Claude/OpenRouter", Some(usage_collector), + timeout_config, ); let mut headers = axum::http::HeaderMap::new(); diff --git a/src-tauri/src/proxy/response_processor.rs b/src-tauri/src/proxy/response_processor.rs index dbac8731a..714302b3c 100644 --- a/src-tauri/src/proxy/response_processor.rs +++ b/src-tauri/src/proxy/response_processor.rs @@ -3,8 +3,11 @@ //! 统一处理流式和非流式 API 响应 use super::{ - handler_config::UsageParserConfig, handler_context::RequestContext, server::ProxyState, - usage::parser::TokenUsage, ProxyError, + handler_config::UsageParserConfig, + handler_context::{RequestContext, StreamingTimeoutConfig}, + server::ProxyState, + usage::parser::TokenUsage, + ProxyError, }; use axum::response::Response; use bytes::Bytes; @@ -17,6 +20,7 @@ use std::{ atomic::{AtomicBool, Ordering}, Arc, }, + time::Duration, }; use tokio::sync::Mutex; @@ -60,8 +64,12 @@ pub async fn handle_streaming( // 创建使用量收集器 let usage_collector = create_usage_collector(ctx, state, status.as_u16(), parser_config); - // 创建带日志的透传流 - let logged_stream = create_logged_passthrough_stream(stream, ctx.tag, Some(usage_collector)); + // 获取流式超时配置 + let timeout_config = ctx.streaming_timeout_config(); + + // 创建带日志和超时的透传流 + let logged_stream = + create_logged_passthrough_stream(stream, ctx.tag, Some(usage_collector), timeout_config); let body = axum::body::Body::from_stream(logged_stream); builder.body(body).unwrap() @@ -348,21 +356,60 @@ async fn log_usage_internal( } } -/// 创建带日志记录的透传流 +/// 创建带日志记录和超时控制的透传流 pub fn create_logged_passthrough_stream( stream: impl Stream> + Send + 'static, tag: &'static str, usage_collector: Option, + timeout_config: StreamingTimeoutConfig, ) -> impl Stream> + Send { async_stream::stream! { let mut buffer = String::new(); let mut collector = usage_collector; + let mut is_first_chunk = true; + + // 超时配置 + let first_byte_timeout = if timeout_config.first_byte_timeout > 0 { + Some(Duration::from_secs(timeout_config.first_byte_timeout)) + } else { + None + }; + let idle_timeout = if timeout_config.idle_timeout > 0 { + Some(Duration::from_secs(timeout_config.idle_timeout)) + } else { + None + }; tokio::pin!(stream); - while let Some(chunk) = stream.next().await { - match chunk { - Ok(bytes) => { + loop { + // 选择超时时间:首字节超时或静默期超时 + let timeout_duration = if is_first_chunk { + first_byte_timeout + } else { + idle_timeout + }; + + let chunk_result = match timeout_duration { + Some(duration) => { + match tokio::time::timeout(duration, stream.next()).await { + Ok(Some(chunk)) => Some(chunk), + Ok(None) => None, // 流结束 + Err(_) => { + // 超时 + let timeout_type = if is_first_chunk { "首字节" } else { "静默期" }; + log::error!("[{tag}] 流式响应{}超时 ({}秒)", timeout_type, duration.as_secs()); + yield Err(std::io::Error::other(format!("流式响应{timeout_type}超时"))); + break; + } + } + } + None => stream.next().await, // 无超时限制 + }; + + match chunk_result { + Some(Ok(bytes)) => { + is_first_chunk = false; let text = String::from_utf8_lossy(&bytes); buffer.push_str(&text); @@ -398,11 +445,15 @@ pub fn create_logged_passthrough_stream( yield Ok(bytes); } - Err(e) => { + Some(Err(e)) => { log::error!("[{tag}] 流错误: {e}"); yield Err(std::io::Error::other(e.to_string())); break; } + None => { + // 流正常结束 + break; + } } } diff --git a/src-tauri/src/proxy/types.rs b/src-tauri/src/proxy/types.rs index f00af8de5..7a1a3efeb 100644 --- a/src-tauri/src/proxy/types.rs +++ b/src-tauri/src/proxy/types.rs @@ -9,13 +9,34 @@ pub struct ProxyConfig { pub listen_port: u16, /// 最大重试次数 pub max_retries: u8, - /// 请求超时时间(秒) + /// 请求超时时间(秒)- 已废弃,保留兼容 pub request_timeout: u64, /// 是否启用日志 pub enable_logging: bool, /// 是否正在接管 Live 配置 #[serde(default)] pub live_takeover_active: bool, + /// 流式首字超时(秒)- 等待首个数据块的最大时间 + #[serde(default = "default_streaming_first_byte_timeout")] + pub streaming_first_byte_timeout: u64, + /// 流式静默超时(秒)- 两个数据块之间的最大间隔 + #[serde(default = "default_streaming_idle_timeout")] + pub streaming_idle_timeout: u64, + /// 非流式总超时(秒)- 非流式请求的总超时时间 + #[serde(default = "default_non_streaming_timeout")] + pub non_streaming_timeout: u64, +} + +fn default_streaming_first_byte_timeout() -> u64 { + 30 +} + +fn default_streaming_idle_timeout() -> u64 { + 60 +} + +fn default_non_streaming_timeout() -> u64 { + 600 } impl Default for ProxyConfig { @@ -27,6 +48,9 @@ impl Default for ProxyConfig { request_timeout: 300, enable_logging: true, live_takeover_active: false, + streaming_first_byte_timeout: 30, + streaming_idle_timeout: 60, + non_streaming_timeout: 600, } } } diff --git a/src/components/proxy/AutoFailoverConfigPanel.tsx b/src/components/proxy/AutoFailoverConfigPanel.tsx index 182dbce5c..726d669e9 100644 --- a/src/components/proxy/AutoFailoverConfigPanel.tsx +++ b/src/components/proxy/AutoFailoverConfigPanel.tsx @@ -10,6 +10,7 @@ import { useCircuitBreakerConfig, useUpdateCircuitBreakerConfig, } from "@/lib/query/failover"; +import { useProxyConfig } from "@/hooks/useProxyConfig"; export interface AutoFailoverConfigPanelProps { enabled?: boolean; @@ -26,6 +27,12 @@ export function AutoFailoverConfigPanel({ const { t } = useTranslation(); const { data: config, isLoading, error } = useCircuitBreakerConfig(); const updateConfig = useUpdateCircuitBreakerConfig(); + const { + config: proxyConfig, + isLoading: isProxyLoading, + updateConfig: updateProxyConfig, + isUpdating: isProxyUpdating, + } = useProxyConfig(); const [formData, setFormData] = useState({ failureThreshold: 5, @@ -34,6 +41,11 @@ export function AutoFailoverConfigPanel({ errorRateThreshold: 0.5, minRequests: 10, }); + const [timeoutConfig, setTimeoutConfig] = useState({ + streaming_first_byte_timeout: 30, + streaming_idle_timeout: 60, + non_streaming_timeout: 600, + }); useEffect(() => { if (config) { @@ -43,6 +55,17 @@ export function AutoFailoverConfigPanel({ } }, [config]); + useEffect(() => { + if (proxyConfig) { + setTimeoutConfig({ + streaming_first_byte_timeout: + proxyConfig.streaming_first_byte_timeout ?? 30, + streaming_idle_timeout: proxyConfig.streaming_idle_timeout ?? 60, + non_streaming_timeout: proxyConfig.non_streaming_timeout ?? 300, + }); + } + }, [proxyConfig]); + const handleSave = async () => { try { await updateConfig.mutateAsync({ @@ -52,6 +75,15 @@ export function AutoFailoverConfigPanel({ errorRateThreshold: formData.errorRateThreshold, minRequests: formData.minRequests, }); + if (proxyConfig) { + await updateProxyConfig({ + ...proxyConfig, + streaming_first_byte_timeout: + timeoutConfig.streaming_first_byte_timeout, + streaming_idle_timeout: timeoutConfig.streaming_idle_timeout, + non_streaming_timeout: timeoutConfig.non_streaming_timeout, + }); + } toast.success( t("proxy.autoFailover.configSaved", "自动故障转移配置已保存"), { closeButton: true }, @@ -69,6 +101,14 @@ export function AutoFailoverConfigPanel({ ...config, }); } + if (proxyConfig) { + setTimeoutConfig({ + streaming_first_byte_timeout: + proxyConfig.streaming_first_byte_timeout ?? 30, + streaming_idle_timeout: proxyConfig.streaming_idle_timeout ?? 60, + non_streaming_timeout: proxyConfig.non_streaming_timeout ?? 300, + }); + } }; if (isLoading) { @@ -255,20 +295,134 @@ export function AutoFailoverConfigPanel({ + {/* 代理请求超时配置 */} +
+

+ {t("proxy.settings.timeout.title", { + defaultValue: "超时设置", + })} +

+ +
+
+ + { + const val = parseInt(e.target.value) || 0; + if (val === 0 || (val >= 1 && val <= 180)) { + setTimeoutConfig({ + ...timeoutConfig, + streaming_first_byte_timeout: val, + }); + } + }} + disabled={!enabled || isProxyLoading || isProxyUpdating} + /> +

+ {t( + "proxy.settings.fields.streamingFirstByteTimeout.description", + "等待首个数据块的最大时间(0 禁用,范围 1-180 秒)", + )} +

+
+ +
+ + { + const val = parseInt(e.target.value) || 0; + if (val === 0 || (val >= 60 && val <= 600)) { + setTimeoutConfig({ + ...timeoutConfig, + streaming_idle_timeout: val, + }); + } + }} + disabled={!enabled || isProxyLoading || isProxyUpdating} + /> +

+ {t( + "proxy.settings.fields.streamingIdleTimeout.description", + "数据块之间的最大间隔(0 禁用,范围 60-600 秒)", + )} +

+
+ +
+ + { + const val = parseInt(e.target.value) || 0; + if (val === 0 || (val >= 60 && val <= 1800)) { + setTimeoutConfig({ + ...timeoutConfig, + non_streaming_timeout: val, + }); + } + }} + disabled={!enabled || isProxyLoading || isProxyUpdating} + /> +

+ {t( + "proxy.settings.fields.nonStreamingTimeout.description", + "非流式请求的总超时时间(0 禁用,范围 60-1800 秒)", + )} +

+
+
+
+ {/* 操作按钮 */}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 051d4affc..5bfba75f1 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -981,6 +981,76 @@ } }, "settings": { + "title": "Proxy Service Settings", + "description": "Configure local proxy server listening address, port and runtime parameters. Changes take effect immediately after saving.", + "alert": { + "autoApply": "Changes will be automatically synced to the running proxy service without manual restart." + }, + "basic": { + "title": "Basic Settings", + "description": "Configure proxy service listening address and port." + }, + "advanced": { + "title": "Advanced Parameters", + "description": "Control request stability and logging." + }, + "timeout": { + "title": "Timeout Settings", + "description": "Configure timeout for streaming and non-streaming requests." + }, + "fields": { + "listenAddress": { + "label": "Listen Address", + "placeholder": "127.0.0.1", + "description": "IP address the proxy server listens on (recommended: 127.0.0.1)" + }, + "listenPort": { + "label": "Listen Port", + "placeholder": "5000", + "description": "Port number the proxy server listens on (1024 ~ 65535)" + }, + "maxRetries": { + "label": "Max Retries", + "placeholder": "3", + "description": "Number of retries on request failure (0 ~ 10)" + }, + "requestTimeout": { + "label": "Request Timeout (sec)", + "placeholder": "0 (unlimited) or 300", + "description": "Maximum wait time for a single request (0 = unlimited, or 10 ~ 600 seconds)" + }, + "enableLogging": { + "label": "Enable Logging", + "description": "Log all proxy requests for troubleshooting" + }, + "streamingFirstByteTimeout": { + "label": "Streaming First Byte Timeout (sec)", + "description": "Maximum time to wait for the first data chunk" + }, + "streamingIdleTimeout": { + "label": "Streaming Idle Timeout (sec)", + "description": "Maximum interval between data chunks" + }, + "nonStreamingTimeout": { + "label": "Non-Streaming Timeout (sec)", + "description": "Total timeout for non-streaming requests" + } + }, + "validation": { + "addressInvalid": "Please enter a valid IP address", + "portMin": "Port must be greater than 1024", + "portMax": "Port must be less than 65535", + "retryMin": "Retry count cannot be negative", + "retryMax": "Retry count cannot exceed 10", + "timeoutNonNegative": "Timeout cannot be negative", + "timeoutMax": "Timeout cannot exceed 600 seconds", + "timeoutRange": "Please enter 0 or a value between 10-600", + "streamingTimeoutMin": "Timeout must be at least 5 seconds", + "streamingTimeoutMax": "Timeout cannot exceed 300 seconds" + }, + "actions": { + "save": "Save Configuration" + }, "toast": { "saved": "Proxy configuration saved", "saveFailed": "Save failed: {{error}}" diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 6554839b7..6ea1a9065 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -981,6 +981,76 @@ } }, "settings": { + "title": "プロキシサービス設定", + "description": "ローカルプロキシサーバーのリッスンアドレス、ポート、実行パラメータを設定します。保存後すぐに反映されます。", + "alert": { + "autoApply": "変更は実行中のプロキシサービスに自動的に同期され、手動での再起動は不要です。" + }, + "basic": { + "title": "基本設定", + "description": "プロキシサービスのリッスンアドレスとポートを設定します。" + }, + "advanced": { + "title": "詳細パラメータ", + "description": "リクエストの安定性とログ記録を制御します。" + }, + "timeout": { + "title": "タイムアウト設定", + "description": "ストリーミングと非ストリーミングリクエストのタイムアウトを設定します。" + }, + "fields": { + "listenAddress": { + "label": "リッスンアドレス", + "placeholder": "127.0.0.1", + "description": "プロキシサーバーがリッスンするIPアドレス(推奨: 127.0.0.1)" + }, + "listenPort": { + "label": "リッスンポート", + "placeholder": "5000", + "description": "プロキシサーバーがリッスンするポート番号(1024 ~ 65535)" + }, + "maxRetries": { + "label": "最大リトライ回数", + "placeholder": "3", + "description": "リクエスト失敗時のリトライ回数(0 ~ 10)" + }, + "requestTimeout": { + "label": "リクエストタイムアウト(秒)", + "placeholder": "0(無制限)または 300", + "description": "単一リクエストの最大待機時間(0 = 無制限、または 10 ~ 600 秒)" + }, + "enableLogging": { + "label": "ログ記録を有効化", + "description": "トラブルシューティングのためにすべてのプロキシリクエストを記録" + }, + "streamingFirstByteTimeout": { + "label": "ストリーミング初回バイトタイムアウト(秒)", + "description": "最初のデータチャンクを待つ最大時間" + }, + "streamingIdleTimeout": { + "label": "ストリーミングアイドルタイムアウト(秒)", + "description": "データチャンク間の最大間隔" + }, + "nonStreamingTimeout": { + "label": "非ストリーミングタイムアウト(秒)", + "description": "非ストリーミングリクエストの総タイムアウト" + } + }, + "validation": { + "addressInvalid": "有効なIPアドレスを入力してください", + "portMin": "ポートは1024より大きい必要があります", + "portMax": "ポートは65535より小さい必要があります", + "retryMin": "リトライ回数は負の値にできません", + "retryMax": "リトライ回数は10を超えることはできません", + "timeoutNonNegative": "タイムアウトは負の値にできません", + "timeoutMax": "タイムアウトは600秒を超えることはできません", + "timeoutRange": "0または10-600の間の値を入力してください", + "streamingTimeoutMin": "タイムアウトは少なくとも5秒必要です", + "streamingTimeoutMax": "タイムアウトは300秒を超えることはできません" + }, + "actions": { + "save": "設定を保存" + }, "toast": { "saved": "プロキシ設定を保存しました", "saveFailed": "保存に失敗しました: {{error}}" diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 2b7d7cf5e..73cfab25c 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -981,6 +981,76 @@ } }, "settings": { + "title": "代理服务设置", + "description": "配置本地代理服务器的监听地址、端口和运行参数,保存后立即生效。", + "alert": { + "autoApply": "保存后将自动同步到正在运行的代理服务,无需手动重启。" + }, + "basic": { + "title": "基础设置", + "description": "配置代理服务监听的地址与端口。" + }, + "advanced": { + "title": "高级参数", + "description": "控制请求的稳定性和日志记录。" + }, + "timeout": { + "title": "超时设置", + "description": "配置流式和非流式请求的超时时间。" + }, + "fields": { + "listenAddress": { + "label": "监听地址", + "placeholder": "127.0.0.1", + "description": "代理服务器监听的 IP 地址(推荐 127.0.0.1)" + }, + "listenPort": { + "label": "监听端口", + "placeholder": "5000", + "description": "代理服务器监听的端口号(1024 ~ 65535)" + }, + "maxRetries": { + "label": "最大重试次数", + "placeholder": "3", + "description": "请求失败时的重试次数(0 ~ 10)" + }, + "requestTimeout": { + "label": "请求超时(秒)", + "placeholder": "0(不限)或 300", + "description": "单个请求的最大等待时间(0 表示不限制,或设置 10 ~ 600 秒)" + }, + "enableLogging": { + "label": "启用日志记录", + "description": "记录所有代理请求,便于排查问题" + }, + "streamingFirstByteTimeout": { + "label": "流式首字超时(秒)", + "description": "等待首个数据块的最大时间" + }, + "streamingIdleTimeout": { + "label": "流式静默超时(秒)", + "description": "数据块之间的最大间隔" + }, + "nonStreamingTimeout": { + "label": "非流式超时(秒)", + "description": "非流式请求的总超时时间" + } + }, + "validation": { + "addressInvalid": "请输入有效的IP地址", + "portMin": "端口必须大于1024", + "portMax": "端口必须小于65535", + "retryMin": "重试次数不能为负", + "retryMax": "重试次数不能超过10", + "timeoutNonNegative": "超时时间不能为负数", + "timeoutMax": "超时时间最多600秒", + "timeoutRange": "请输入 0 或 10-600 之间的数值", + "streamingTimeoutMin": "超时时间至少5秒", + "streamingTimeoutMax": "超时时间最多300秒" + }, + "actions": { + "save": "保存配置" + }, "toast": { "saved": "代理配置已保存", "saveFailed": "保存失败: {{error}}" diff --git a/src/types/proxy.ts b/src/types/proxy.ts index cc809dfeb..fac5ecb8e 100644 --- a/src/types/proxy.ts +++ b/src/types/proxy.ts @@ -5,6 +5,10 @@ export interface ProxyConfig { request_timeout: number; enable_logging: boolean; live_takeover_active?: boolean; + // 超时配置 + streaming_first_byte_timeout: number; + streaming_idle_timeout: number; + non_streaming_timeout: number; } export interface ProxyStatus {