From 75d78da920db3ee9d6bed08bfe1ceabd392e1a0b Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Thu, 5 Feb 2026 16:48:44 +0800 Subject: [PATCH] fix(proxy): preserve URL query/fragment and improve error handling Extract shared URL utilities to handle query strings and fragments correctly. Add race condition fix for URL preview, improve proxy toggle error feedback, and support Codex chat format proxy endpoint. --- src-tauri/src/proxy/mod.rs | 1 + src-tauri/src/proxy/providers/claude.rs | 17 +-- src-tauri/src/proxy/providers/codex.rs | 13 +-- src-tauri/src/proxy/providers/gemini.rs | 6 +- src-tauri/src/proxy/url_builder.rs | 103 ++++++++++++++---- src-tauri/src/proxy/url_utils.rs | 96 ++++++++++++++++ .../providers/forms/shared/EndpointField.tsx | 6 +- src/components/proxy/ProxyToggle.tsx | 30 +++-- src/hooks/useProviderActions.ts | 26 ++++- src/i18n/locales/en.json | 10 +- src/i18n/locales/ja.json | 10 +- src/i18n/locales/zh.json | 10 +- 12 files changed, 274 insertions(+), 54 deletions(-) create mode 100644 src-tauri/src/proxy/url_utils.rs diff --git a/src-tauri/src/proxy/mod.rs b/src-tauri/src/proxy/mod.rs index 49337795b..21055715d 100644 --- a/src-tauri/src/proxy/mod.rs +++ b/src-tauri/src/proxy/mod.rs @@ -24,6 +24,7 @@ pub mod session; pub mod thinking_rectifier; pub(crate) mod types; pub mod url_builder; +pub(crate) mod url_utils; pub mod usage; // 公开导出给外部使用(commands, services等模块需要) diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index 8fe3ee307..5630052c7 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -14,6 +14,7 @@ use super::{AuthInfo, AuthStrategy, ProviderAdapter, ProviderType}; use crate::provider::Provider; use crate::proxy::error::ProxyError; +use crate::proxy::url_utils::{dedup_v1_v1_boundary_safe, split_url_suffix}; use reqwest::RequestBuilder; /// Claude 适配器 @@ -252,7 +253,8 @@ impl ProviderAdapter for ClaudeAdapter { // 现在 OpenRouter 已推出 Claude Code 兼容接口,因此默认直接透传 endpoint。 // 如需回退旧逻辑,可在 forwarder 中根据 needs_transform 改写 endpoint。 - let base_trimmed = base_url.trim_end_matches('/'); + let (base, suffix) = split_url_suffix(base_url); + let base_trimmed = base.trim_end_matches('/'); let endpoint_trimmed = endpoint.trim_start_matches('/'); // 检测 base_url 是否已经以 API 路径结尾(用户填写了完整路径) @@ -269,28 +271,27 @@ impl ProviderAdapter for ClaudeAdapter { .any(|pattern| base_trimmed.to_lowercase().ends_with(pattern)); // 如果 base_url 已经以 API 路径结尾,直接使用 base_url,不再追加 endpoint - let mut base = if base_ends_with_api_path { + let base = if base_ends_with_api_path { base_trimmed.to_string() } else { format!("{base_trimmed}/{endpoint_trimmed}") }; // 去除重复的 /v1/v1(可能由 base_url 与 endpoint 都带版本导致) - while base.contains("/v1/v1") { - base = base.replace("/v1/v1", "/v1"); - } + let base = dedup_v1_v1_boundary_safe(base); // 为 Claude 相关端点添加 ?beta=true 参数 // 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数 // 注:openai_chat 模式下会转发到 /v1/chat/completions,此处也需要保持一致 // 检查最终 URL 是否包含需要 beta 参数的路径,且没有查询参数 let needs_beta = (base.contains("/v1/messages") || base.contains("/v1/chat/completions")) - && !base.contains('?'); + && !base.contains('?') + && !suffix.starts_with('?'); if needs_beta { - format!("{base}?beta=true") + format!("{base}?beta=true{suffix}") } else { - base + format!("{base}{suffix}") } } diff --git a/src-tauri/src/proxy/providers/codex.rs b/src-tauri/src/proxy/providers/codex.rs index 5e16afb5f..3d4351d14 100644 --- a/src-tauri/src/proxy/providers/codex.rs +++ b/src-tauri/src/proxy/providers/codex.rs @@ -8,6 +8,7 @@ use super::{AuthInfo, AuthStrategy, ProviderAdapter}; use crate::provider::Provider; use crate::proxy::error::ProxyError; +use crate::proxy::url_utils::{dedup_v1_v1_boundary_safe, split_url_suffix}; use regex::Regex; use reqwest::RequestBuilder; use std::sync::LazyLock; @@ -138,7 +139,8 @@ impl ProviderAdapter for CodexAdapter { } fn build_url(&self, base_url: &str, endpoint: &str) -> String { - let base_trimmed = base_url.trim_end_matches('/'); + let (base, suffix) = split_url_suffix(base_url); + let base_trimmed = base.trim_end_matches('/'); let endpoint_trimmed = endpoint.trim_start_matches('/'); // 检测 base_url 是否已经以 API 路径结尾(用户填写了完整路径) @@ -156,7 +158,7 @@ impl ProviderAdapter for CodexAdapter { // 如果 base_url 已经以 API 路径结尾,直接使用 base_url,不再追加 endpoint if base_ends_with_api_path { - return base_trimmed.to_string(); + return format!("{base_trimmed}{suffix}"); } // OpenAI/Codex 的 base_url 可能是: @@ -185,11 +187,8 @@ impl ProviderAdapter for CodexAdapter { }; // 去除重复的 /v1/v1(可能由 base_url 与 endpoint 都带版本导致) - while url.contains("/v1/v1") { - url = url.replace("/v1/v1", "/v1"); - } - - url + url = dedup_v1_v1_boundary_safe(url); + format!("{url}{suffix}") } fn add_auth_headers(&self, request: RequestBuilder, auth: &AuthInfo) -> RequestBuilder { diff --git a/src-tauri/src/proxy/providers/gemini.rs b/src-tauri/src/proxy/providers/gemini.rs index 646d237f7..bb43ec6fc 100644 --- a/src-tauri/src/proxy/providers/gemini.rs +++ b/src-tauri/src/proxy/providers/gemini.rs @@ -9,6 +9,7 @@ use super::{AuthInfo, AuthStrategy, ProviderAdapter, ProviderType}; use crate::provider::Provider; use crate::proxy::error::ProxyError; +use crate::proxy::url_utils::split_url_suffix; use reqwest::RequestBuilder; /// Gemini 适配器 @@ -200,7 +201,8 @@ impl ProviderAdapter for GeminiAdapter { } fn build_url(&self, base_url: &str, endpoint: &str) -> String { - let base_trimmed = base_url.trim_end_matches('/'); + let (base, suffix) = split_url_suffix(base_url); + let base_trimmed = base.trim_end_matches('/'); let endpoint_trimmed = endpoint.trim_start_matches('/'); let mut url = format!("{base_trimmed}/{endpoint_trimmed}"); @@ -214,7 +216,7 @@ impl ProviderAdapter for GeminiAdapter { } } - url + format!("{url}{suffix}") } fn add_auth_headers(&self, request: RequestBuilder, auth: &AuthInfo) -> RequestBuilder { diff --git a/src-tauri/src/proxy/url_builder.rs b/src-tauri/src/proxy/url_builder.rs index 2524ecf65..e9fe14fbe 100644 --- a/src-tauri/src/proxy/url_builder.rs +++ b/src-tauri/src/proxy/url_builder.rs @@ -3,6 +3,7 @@ //! 提供统一的 URL 构建逻辑,供前端预览和后端代理使用。 use crate::app_config::AppType; +use crate::proxy::url_utils::{dedup_v1_v1_boundary_safe, split_url_suffix}; use serde::{Deserialize, Serialize}; /// URL 预览结果 @@ -46,12 +47,33 @@ impl ApiPathPatterns { } } - fn for_codex(_api_format: Option<&str>) -> Self { - // Codex 只支持 /responses 端点 - Self { - direct_endpoint: "/responses", - proxy_endpoint: "/responses", - full_url_patterns: &["/v1/responses", "/responses"], + fn for_codex(api_format: Option<&str>) -> Self { + // Codex: + // - Direct mode (Codex CLI) hard-codes Responses API: `/responses` + // - Proxy mode might target different upstream formats (e.g. Chat Completions) + let is_chat = matches!(api_format, Some("chat")); + if is_chat { + Self { + direct_endpoint: "/responses", + proxy_endpoint: "/v1/chat/completions", + full_url_patterns: &[ + "/v1/responses", + "/responses", + "/v1/chat/completions", + "/chat/completions", + ], + } + } else { + Self { + direct_endpoint: "/responses", + proxy_endpoint: "/responses", + full_url_patterns: &[ + "/v1/responses", + "/responses", + "/v1/chat/completions", + "/chat/completions", + ], + } } } @@ -66,9 +88,8 @@ impl ApiPathPatterns { /// 检测 URL 是否以指定的 API 路径结尾 fn url_ends_with_api_path(url: &str, patterns: &[&str]) -> bool { - let trimmed = url.trim_end_matches('/').to_lowercase(); - // 移除查询参数 - let path_part = trimmed.split('?').next().unwrap_or(&trimmed); + let (base, _) = split_url_suffix(url); + let path_part = base.trim_end_matches('/').to_lowercase(); patterns .iter() .any(|pattern| path_part.ends_with(&pattern.to_lowercase())) @@ -78,34 +99,31 @@ fn url_ends_with_api_path(url: &str, patterns: &[&str]) -> bool { /// /// 始终将 endpoint 拼接到 base_url 后面,不做任何智能检测或去重。 fn build_direct_url(base_url: &str, endpoint: &str) -> String { - let base_trimmed = base_url.trim_end_matches('/'); + let (base, suffix) = split_url_suffix(base_url); + let base_trimmed = base.trim_end_matches('/'); let endpoint_trimmed = endpoint.trim_start_matches('/'); // 直接拼接,不做任何去重 - format!("{base_trimmed}/{endpoint_trimmed}") + format!("{base_trimmed}/{endpoint_trimmed}{suffix}") } /// 智能构建 URL(用于代理地址) /// /// 如果 base_url 已经以 API 路径结尾,直接返回;否则追加 endpoint。 pub fn build_smart_url(base_url: &str, endpoint: &str, full_url_patterns: &[&str]) -> String { - let base_trimmed = base_url.trim_end_matches('/'); + let (base, suffix) = split_url_suffix(base_url); + let base_trimmed = base.trim_end_matches('/'); let endpoint_trimmed = endpoint.trim_start_matches('/'); // 检测 base_url 是否已经以 API 路径结尾 if url_ends_with_api_path(base_trimmed, full_url_patterns) { - return base_trimmed.to_string(); + return format!("{base_trimmed}{suffix}"); } // 拼接 URL - let mut url = format!("{base_trimmed}/{endpoint_trimmed}"); - - // 去重 /v1/v1 - while url.contains("/v1/v1") { - url = url.replace("/v1/v1", "/v1"); - } - - url + let url = format!("{base_trimmed}/{endpoint_trimmed}"); + let url = dedup_v1_v1_boundary_safe(url); + format!("{url}{suffix}") } /// 构建 URL 预览 @@ -239,6 +257,17 @@ mod tests { assert!(!preview.is_full_url); } + #[test] + fn test_build_url_preview_codex_chat_proxy_endpoint() { + let preview = build_url_preview(&AppType::Codex, "https://api.openai.com", Some("chat")); + assert_eq!(preview.direct_url, "https://api.openai.com/responses"); + assert_eq!( + preview.proxy_url, + "https://api.openai.com/v1/chat/completions" + ); + assert!(!preview.is_full_url); + } + #[test] fn test_build_url_preview_codex_full_url() { // 全链接时:直连会硬拼接后缀,代理保持原地址 @@ -315,6 +344,38 @@ mod tests { assert_eq!(preview.proxy_url, "https://api.example.com/v1/messages"); } + #[test] + fn test_query_suffix_preserved_in_preview() { + let preview = build_url_preview( + &AppType::Claude, + "https://api.example.com?beta=true", + Some("anthropic"), + ); + assert_eq!( + preview.direct_url, + "https://api.example.com/v1/messages?beta=true" + ); + assert_eq!( + preview.proxy_url, + "https://api.example.com/v1/messages?beta=true" + ); + assert!(!preview.is_full_url); + } + + #[test] + fn test_fragment_suffix_preserved_and_full_url_detected() { + let preview = build_url_preview( + &AppType::Claude, + "https://api.example.com/v1/messages#frag", + Some("anthropic"), + ); + assert_eq!( + preview.proxy_url, + "https://api.example.com/v1/messages#frag" + ); + assert!(preview.is_full_url); + } + #[test] fn test_full_url_detection_by_api_format() { // anthropic 格式:只有 /messages 结尾才是全链接 diff --git a/src-tauri/src/proxy/url_utils.rs b/src-tauri/src/proxy/url_utils.rs new file mode 100644 index 000000000..1e9b85758 --- /dev/null +++ b/src-tauri/src/proxy/url_utils.rs @@ -0,0 +1,96 @@ +//! URL utilities shared across proxy modules. +//! +//! This module intentionally avoids full URL parsing to keep behavior aligned with +//! existing "stringly-typed" base_url configurations while fixing common edge cases +//! (query/fragment handling and safe path de-duplication). + +/// Split a URL-like string into `(base, suffix)` where suffix starts with `?` or `#`. +/// +/// Example: +/// - `https://x/v1?token=1` => (`https://x/v1`, `?token=1`) +/// - `https://x/v1#frag` => (`https://x/v1`, `#frag`) +pub(crate) fn split_url_suffix(input: &str) -> (&str, &str) { + match input.find(['?', '#']) { + Some(idx) => (&input[..idx], &input[idx..]), + None => (input, ""), + } +} + +/// De-duplicate repeated `/v1/v1` only when it occurs on a segment boundary. +/// +/// This avoids corrupting valid paths such as `/v1/v1beta/...`. +pub(crate) fn dedup_v1_v1_boundary_safe(mut url: String) -> String { + const NEEDLE: &str = "/v1/v1"; + let mut search_start = 0usize; + + loop { + let Some(rel_pos) = url[search_start..].find(NEEDLE) else { + break; + }; + let pos = search_start + rel_pos; + let after = pos + NEEDLE.len(); + + let boundary_ok = after == url.len() + || matches!( + url.as_bytes().get(after), + Some(b'/') | Some(b'?') | Some(b'#') + ); + + if boundary_ok { + url.replace_range(pos..after, "/v1"); + // Continue searching from the same position in case we created a new boundary match. + search_start = pos; + } else { + // Skip forward to find later occurrences that might be valid boundary matches. + search_start = pos + 1; + } + } + + url +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn split_url_suffix_handles_query() { + let (base, suffix) = split_url_suffix("https://example.com/v1?token=1"); + assert_eq!(base, "https://example.com/v1"); + assert_eq!(suffix, "?token=1"); + } + + #[test] + fn split_url_suffix_handles_fragment() { + let (base, suffix) = split_url_suffix("https://example.com/v1#frag"); + assert_eq!(base, "https://example.com/v1"); + assert_eq!(suffix, "#frag"); + } + + #[test] + fn dedup_v1_v1_only_on_boundary() { + let url = "https://example.com/v1/v1/messages".to_string(); + assert_eq!( + dedup_v1_v1_boundary_safe(url), + "https://example.com/v1/messages" + ); + } + + #[test] + fn dedup_v1_v1_does_not_corrupt_v1beta() { + let url = "https://example.com/v1/v1beta/models".to_string(); + assert_eq!( + dedup_v1_v1_boundary_safe(url.clone()), + "https://example.com/v1/v1beta/models" + ); + } + + #[test] + fn dedup_v1_v1_skips_v1beta_but_dedups_later_occurrence() { + let url = "https://example.com/v1/v1beta/v1/v1/messages".to_string(); + assert_eq!( + dedup_v1_v1_boundary_safe(url), + "https://example.com/v1/v1beta/v1/messages" + ); + } +} diff --git a/src/components/providers/forms/shared/EndpointField.tsx b/src/components/providers/forms/shared/EndpointField.tsx index 6c0e13990..51b20807e 100644 --- a/src/components/providers/forms/shared/EndpointField.tsx +++ b/src/components/providers/forms/shared/EndpointField.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { FormLabel } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; @@ -40,6 +40,7 @@ export function EndpointField({ }: EndpointFieldProps) { const { t } = useTranslation(); const [urlPreview, setUrlPreview] = useState(null); + const lastRequestIdRef = useRef(0); const defaultManageLabel = t("providerForm.manageAndTest", { defaultValue: "管理和测速", @@ -54,15 +55,18 @@ export function EndpointField({ // 防抖:延迟 300ms 后请求 const timer = setTimeout(async () => { + const requestId = ++lastRequestIdRef.current; try { const preview = await proxyApi.buildUrlPreview( appType, value, apiFormat, ); + if (requestId !== lastRequestIdRef.current) return; setUrlPreview(preview); } catch (error) { console.error("Failed to build URL preview:", error); + if (requestId !== lastRequestIdRef.current) return; setUrlPreview(null); } }, 300); diff --git a/src/components/proxy/ProxyToggle.tsx b/src/components/proxy/ProxyToggle.tsx index 588f44780..8d9a4c3cb 100644 --- a/src/components/proxy/ProxyToggle.tsx +++ b/src/components/proxy/ProxyToggle.tsx @@ -34,7 +34,7 @@ function extractBaseUrl(provider: Provider, appId: AppId): string | null { return typeof envUrl === "string" ? envUrl.trim() : null; } - if (appId === "codex") { + if (appId === "codex" || appId === "opencode") { const tomlConfig = config?.config; if (typeof tomlConfig === "string") { const match = tomlConfig.match(/base_url\s*=\s*(['"])([^'"]+)\1/); @@ -85,16 +85,19 @@ export function ProxyToggle({ ); if (proxyRequirement) { - // 显示警告但仍允许关闭 + const warningKey = + proxyRequirement === "openai_chat_format" + ? "notifications.openAIChatFormatWarningOnDisable" + : proxyRequirement === "url_mismatch" + ? "notifications.urlMismatchWarningOnDisable" + : "notifications.fullUrlWarningOnDisable"; + toast.warning( - t("notifications.fullUrlWarningOnDisable", { + t(warningKey, { defaultValue: - "当前供应商配置了完整 API 路径或特殊格式,关闭代理后可能无法正常工作。建议更换为基础地址配置或保持代理开启。", + "当前供应商配置可能依赖代理模式,关闭代理后可能无法正常工作。建议更换为基础地址配置或保持代理开启。", }), - { - duration: 6000, - closeButton: true, - }, + { duration: 6000, closeButton: true }, ); } } catch (error) { @@ -107,6 +110,17 @@ export function ProxyToggle({ await setTakeoverForApp({ appType: activeApp, enabled: checked }); } catch (error) { console.error("[ProxyToggle] Toggle takeover failed:", error); + toast.error( + t("proxy.takeover.toggleFailed", { + defaultValue: "切换接管状态失败", + }), + { + description: t("proxy.takeover.toggleFailedDesc", { + defaultValue: "请检查代理服务状态与权限,然后重试。", + }), + closeButton: true, + }, + ); } }; diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts index a907f92c0..c8d894d0e 100644 --- a/src/hooks/useProviderActions.ts +++ b/src/hooks/useProviderActions.ts @@ -37,7 +37,7 @@ function extractBaseUrl(provider: Provider, appId: AppId): string | null { return typeof envUrl === "string" ? envUrl.trim() : null; } - if (appId === "codex") { + if (appId === "codex" || appId === "opencode") { // Codex: base_url 存储在 config 字段中(TOML 字符串) const tomlConfig = config?.config; if (typeof tomlConfig === "string") { @@ -165,6 +165,7 @@ export function useProviderActions( // 调用后端 API 检查是否需要代理(前后端使用相同逻辑) let proxyRequirement: string | null = null; + let proxyRequirementCheckFailed = false; if (baseUrl) { try { proxyRequirement = await proxyApi.checkProxyRequirement( @@ -174,6 +175,7 @@ export function useProviderActions( ); } catch (error) { console.error("Failed to check proxy requirement:", error); + proxyRequirementCheckFailed = true; } } @@ -220,9 +222,25 @@ export function useProviderActions( const defaultMessage = activeApp === "opencode" ? "已添加到配置" : "切换成功!"; - toast.success(t(messageKey, { defaultValue: defaultMessage }), { - closeButton: true, - }); + if (proxyRequirementCheckFailed && baseUrl) { + toast.success( + t("notifications.switchAppliedUnverified", { + defaultValue: "切换已应用(未验证直连兼容性)", + }), + { + description: t("notifications.switchAppliedUnverifiedDesc", { + defaultValue: + "未能验证该端点是否需要代理。如果切换后无法正常使用,请开启代理并接管当前应用。", + }), + closeButton: true, + duration: 6000, + }, + ); + } else { + toast.success(t(messageKey, { defaultValue: defaultMessage }), { + closeButton: true, + }); + } } catch { // 错误提示由 mutation 处理 } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index aaea3a2d9..ec1a724ec 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -160,7 +160,11 @@ "openAIChatFormatRequiresProxy": "This provider uses OpenAI Chat format and requires the proxy service for format conversion. Please enable the proxy and takeover the current app first.", "fullUrlRequiresProxy": "This provider is configured with a full API path. The client may append duplicate paths in direct connection mode. Please enable the proxy and takeover the current app first.", "urlMismatchRequiresProxy": "This provider's request URL configuration does not match the API format. It cannot work properly in direct connection mode. Please enable the proxy and takeover the current app first.", - "fullUrlWarningOnDisable": "The current provider is configured with a full API path or special format. It may not work properly after disabling the proxy. Consider switching to a base URL configuration or keep the proxy enabled." + "fullUrlWarningOnDisable": "The current provider is configured with a full API path or special format. It may not work properly after disabling the proxy. Consider switching to a base URL configuration or keep the proxy enabled.", + "openAIChatFormatWarningOnDisable": "The current provider uses OpenAI Chat format. It may not work properly after disabling the proxy. Consider keeping the proxy enabled.", + "urlMismatchWarningOnDisable": "The current provider's request URL configuration may rely on the proxy. It may not work properly after disabling the proxy. Consider switching to a base URL configuration or keep the proxy enabled.", + "switchAppliedUnverified": "Switch applied (direct mode not verified)", + "switchAppliedUnverifiedDesc": "Unable to verify whether this endpoint requires the proxy. If it doesn't work after switching, enable the proxy and takeover the current app." }, "confirm": { "deleteProvider": "Delete Provider", @@ -1291,6 +1295,10 @@ } }, "proxy": { + "takeover": { + "toggleFailed": "Failed to toggle takeover", + "toggleFailedDesc": "Check proxy service status and permissions, then try again." + }, "panel": { "serviceAddress": "Service Address", "addressCopied": "Address copied", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 8f367e43a..7c2af658f 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -160,7 +160,11 @@ "openAIChatFormatRequiresProxy": "このプロバイダーは OpenAI Chat フォーマットを使用しており、フォーマット変換のためにプロキシサービスが必要です。先にプロキシを有効にして、現在のアプリをテイクオーバーしてください。", "fullUrlRequiresProxy": "このプロバイダーは完全な API パスで構成されています。直接接続モードではクライアントがパスを重複追加する可能性があります。先にプロキシを有効にして、現在のアプリをテイクオーバーしてください。", "urlMismatchRequiresProxy": "このプロバイダーのリクエスト URL 設定が API フォーマットと一致しません。直接接続モードでは正常に動作しません。先にプロキシを有効にして、現在のアプリをテイクオーバーしてください。", - "fullUrlWarningOnDisable": "現在のプロバイダーは完全な API パスまたは特殊なフォーマットで構成されています。プロキシを無効にすると正常に動作しない可能性があります。ベース URL 設定に変更するか、プロキシを有効のままにすることをお勧めします。" + "fullUrlWarningOnDisable": "現在のプロバイダーは完全な API パスまたは特殊なフォーマットで構成されています。プロキシを無効にすると正常に動作しない可能性があります。ベース URL 設定に変更するか、プロキシを有効のままにすることをお勧めします。", + "openAIChatFormatWarningOnDisable": "現在のプロバイダーは OpenAI Chat フォーマットを使用しています。プロキシを無効にすると正常に動作しない可能性があります。プロキシを有効のままにすることをお勧めします。", + "urlMismatchWarningOnDisable": "現在のプロバイダーのリクエスト URL 設定はプロキシに依存している可能性があります。プロキシを無効にすると正常に動作しない可能性があります。ベース URL 設定に変更するか、プロキシを有効のままにすることをお勧めします。", + "switchAppliedUnverified": "切り替えを適用しました(直接接続は未検証)", + "switchAppliedUnverifiedDesc": "このエンドポイントがプロキシを必要とするか検証できませんでした。切り替え後に動作しない場合は、プロキシを有効にして現在のアプリをテイクオーバーしてください。" }, "confirm": { "deleteProvider": "プロバイダーを削除", @@ -1282,6 +1286,10 @@ } }, "proxy": { + "takeover": { + "toggleFailed": "テイクオーバーの切り替えに失敗しました", + "toggleFailedDesc": "プロキシサービスの状態と権限を確認して、もう一度お試しください。" + }, "panel": { "serviceAddress": "サービスアドレス", "addressCopied": "アドレスをコピーしました", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index b0d6af8e9..12f787388 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -160,7 +160,11 @@ "openAIChatFormatRequiresProxy": "此供应商使用 OpenAI Chat 格式,需要开启代理服务进行格式转换才能正常使用。请先开启代理并接管当前应用。", "fullUrlRequiresProxy": "此供应商配置了完整 API 路径,直连模式下客户端可能会重复追加路径。请先开启代理并接管当前应用。", "urlMismatchRequiresProxy": "此供应商的请求地址配置与 API 格式不匹配,直连模式下无法正常工作。请先开启代理并接管当前应用。", - "fullUrlWarningOnDisable": "当前供应商配置了完整 API 路径或特殊格式,关闭代理后可能无法正常工作。建议更换为基础地址配置或保持代理开启。" + "fullUrlWarningOnDisable": "当前供应商配置了完整 API 路径或特殊格式,关闭代理后可能无法正常工作。建议更换为基础地址配置或保持代理开启。", + "openAIChatFormatWarningOnDisable": "当前供应商使用 OpenAI Chat 格式,关闭代理后可能无法正常工作。建议保持代理开启。", + "urlMismatchWarningOnDisable": "当前供应商的请求地址配置可能依赖代理模式,关闭代理后可能无法正常工作。建议更换为基础地址配置或保持代理开启。", + "switchAppliedUnverified": "切换已应用(未验证直连兼容性)", + "switchAppliedUnverifiedDesc": "未能验证该端点是否需要代理。如果切换后无法正常使用,请开启代理并接管当前应用。" }, "confirm": { "deleteProvider": "删除供应商", @@ -1291,6 +1295,10 @@ } }, "proxy": { + "takeover": { + "toggleFailed": "切换接管状态失败", + "toggleFailedDesc": "请检查代理服务状态与权限,然后重试。" + }, "panel": { "serviceAddress": "服务地址", "addressCopied": "地址已复制",