From f36bc02124eccb40acb102fe3f2e30360034ffb3 Mon Sep 17 00:00:00 2001 From: Chiron <598621670@qq.com> Date: Wed, 7 Jan 2026 15:27:22 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=20IFlow=20provide?= =?UTF-8?q?r=20=E5=AF=B9=20OpenAI=20=E6=A0=BC=E5=BC=8F=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 provider_calls.rs 中实现 IFlowOAuth 和 IFlowCookie 的处理逻辑 - 修复 deepseek-chat 模型请求返回 400 Bad Request 的问题 - 支持 IFlow 的 token 缓存和自动刷新机制 --- .../src/server/handlers/provider_calls.rs | 143 +++++++++++++++++- 1 file changed, 139 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/server/handlers/provider_calls.rs b/src-tauri/src/server/handlers/provider_calls.rs index ca7a30b2b..a7dfe1149 100644 --- a/src-tauri/src/server/handlers/provider_calls.rs +++ b/src-tauri/src/server/handlers/provider_calls.rs @@ -59,7 +59,8 @@ use crate::models::anthropic::AnthropicMessagesRequest; use crate::models::openai::ChatCompletionRequest; use crate::models::provider_pool_model::{CredentialData, ProviderCredential}; use crate::providers::{ - AntigravityProvider, ClaudeCustomProvider, KiroProvider, OpenAICustomProvider, VertexProvider, + AntigravityProvider, ClaudeCustomProvider, IFlowProvider, KiroProvider, OpenAICustomProvider, + VertexProvider, }; use crate::server::AppState; use crate::server_utils::{ @@ -1919,11 +1920,145 @@ pub async fn call_provider_openai( .into_response() } } + // IFlow 凭证类型 - 支持 OpenAI 格式 + CredentialData::IFlowOAuth { creds_file_path } => { + let db = match &state.db { + Some(db) => db, + None => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": "Database not available"}})), + ) + .into_response(); + } + }; + + // 获取缓存的 token + let token = match state + .token_cache + .get_valid_token(db, &credential.uuid) + .await + { + Ok(t) => t, + Err(e) => { + tracing::warn!("[POOL] IFlow token cache miss, loading from source: {}", e); + let mut iflow = IFlowProvider::new(); + if let Err(e) = iflow.load_credentials_from_path(creds_file_path).await { + let _ = state.pool_service.mark_unhealthy( + db, + &credential.uuid, + Some(&format!("Failed to load IFlow credentials: {}", e)), + ); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": format!("Failed to load IFlow credentials: {}", e)}})), + ) + .into_response(); + } + if let Err(e) = iflow.ensure_valid_token().await { + let _ = state.pool_service.mark_unhealthy( + db, + &credential.uuid, + Some(&format!("IFlow token refresh failed: {}", e)), + ); + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({"error": {"message": format!("IFlow token refresh failed: {}", e)}})), + ) + .into_response(); + } + iflow.credentials.access_token.unwrap_or_default() + } + }; + + let mut iflow = IFlowProvider::new(); + iflow.credentials.access_token = Some(token); + + let request_json = serde_json::to_value(request).unwrap_or_default(); + match iflow.call_api(&request_json).await { + Ok(response) => { + let status = response.status(); + let headers = response.headers().clone(); + + match response.bytes().await { + Ok(body) => { + let mut response_builder = Response::builder().status(status); + for (key, value) in headers.iter() { + response_builder = response_builder.header(key, value); + } + response_builder.body(Body::from(body)).unwrap_or_else(|_| { + (StatusCode::INTERNAL_SERVER_ERROR, "Failed to build response").into_response() + }) + } + Err(e) => { + tracing::error!("[IFlow] Failed to read response body: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": format!("Failed to read IFlow response: {}", e)}})), + ) + .into_response() + } + } + } + Err(e) => { + tracing::error!("[IFlow] API call failed: {}", e); + ( + StatusCode::BAD_GATEWAY, + Json(serde_json::json!({"error": {"message": format!("IFlow API call failed: {}", e)}})), + ) + .into_response() + } + } + } + CredentialData::IFlowCookie { creds_file_path } => { + let mut iflow = IFlowProvider::new(); + if let Err(e) = iflow.load_credentials_from_path(creds_file_path).await { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": format!("Failed to load IFlow credentials: {}", e)}})), + ) + .into_response(); + } + + let request_json = serde_json::to_value(request).unwrap_or_default(); + match iflow.call_api(&request_json).await { + Ok(response) => { + let status = response.status(); + let headers = response.headers().clone(); + + match response.bytes().await { + Ok(body) => { + let mut response_builder = Response::builder().status(status); + for (key, value) in headers.iter() { + response_builder = response_builder.header(key, value); + } + response_builder.body(Body::from(body)).unwrap_or_else(|_| { + (StatusCode::INTERNAL_SERVER_ERROR, "Failed to build response").into_response() + }) + } + Err(e) => { + tracing::error!("[IFlow] Failed to read response body: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": format!("Failed to read IFlow response: {}", e)}})), + ) + .into_response() + } + } + } + Err(e) => { + tracing::error!("[IFlow] API call failed: {}", e); + ( + StatusCode::BAD_GATEWAY, + Json(serde_json::json!({"error": {"message": format!("IFlow API call failed: {}", e)}})), + ) + .into_response() + } + } + } // 新增的凭证类型暂不支持 OpenAI 格式 CredentialData::CodexOAuth { .. } - | CredentialData::ClaudeOAuth { .. } - | CredentialData::IFlowOAuth { .. } - | CredentialData::IFlowCookie { .. } => { + | CredentialData::ClaudeOAuth { .. } => { ( StatusCode::BAD_REQUEST, Json(serde_json::json!({"error": {"message": "This credential type does not support OpenAI format yet"}})), From d5e7772c0e8b54921bc40fae442d8db8c8bd4a0c Mon Sep 17 00:00:00 2001 From: Chiron <598621670@qq.com> Date: Fri, 9 Jan 2026 20:30:08 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20API=20Server?= =?UTF-8?q?=20=E7=BD=91=E7=BB=9C=E7=9B=91=E5=90=AC=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端改进: - 改进网络 IP 获取逻辑,使用 if-addrs 获取所有网络接口 - 智能过滤 VPN 和虚拟网卡地址(如 198.18.0.0/15) - 允许绑定到私有网络地址(10.x.x.x, 172.16-31.x.x, 192.168.x.x) - 更新地址验证逻辑,支持局域网 IP 绑定 - 前端改进: - 使用 Radix UI Select 组件美化监听地址选择器 - 支持选择 127.0.0.1(仅本机)、0.0.0.0(所有接口)或具体局域网 IP - 动态显示真实的局域网 IP 地址 - 添加安全提示:局域网访问需要非默认 API Key - 优化界面布局,状态信息移至标题旁边 - 依赖更新: - 添加 if-addrs = "0.13" 用于获取网络接口信息 --- src-tauri/Cargo.lock | 11 ++ src-tauri/Cargo.toml | 1 + src-tauri/src/app/utils.rs | 35 ++++- src-tauri/src/commands/network_cmd.rs | 52 ++++++- src-tauri/src/config/hot_reload.rs | 35 ++++- src/components/api-server/ApiServerPage.tsx | 150 +++++++++++++++----- src/hooks/useTauri.ts | 1 + 7 files changed, 244 insertions(+), 41 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a36a6f2e0..cb89c8183 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2425,6 +2425,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "if-addrs" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b2eeee38fef3aa9b4cc5f1beea8a2444fc00e7377cafae396de3f5c2065e24" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "image" version = "0.25.9" @@ -3926,6 +3936,7 @@ dependencies = [ "futures", "glob", "hex", + "if-addrs", "indexmap 2.12.1", "md5", "mouse_position", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index df218e665..3db4d7319 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -75,6 +75,7 @@ sysinfo = "0.32" whoami = "1" mouse_position = "0.1.4" window-vibrancy = "0.7.1" +if-addrs = "0.13" # Platform specific dependencies for browser interceptor diff --git a/src-tauri/src/app/utils.rs b/src-tauri/src/app/utils.rs index dffe61f96..8b26f3f38 100644 --- a/src-tauri/src/app/utils.rs +++ b/src-tauri/src/app/utils.rs @@ -21,18 +21,47 @@ pub fn is_loopback_host(host: &str) -> bool { } /// 检查是否为有效的绑定地址 -/// 允许回环地址和 0.0.0.0(监听所有接口) +/// 允许回环地址、0.0.0.0 和私有网络地址 pub fn is_valid_bind_host(host: &str) -> bool { if is_loopback_host(host) { return true; } // 允许 0.0.0.0 和 :: (监听所有接口) - host == "0.0.0.0" || host == "::" + if host == "0.0.0.0" || host == "::" { + return true; + } + + // 允许私有网络地址 + if let Ok(addr) = host.parse::() { + if let std::net::IpAddr::V4(ipv4) = addr { + // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + let octets = ipv4.octets(); + return octets[0] == 10 + || (octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31)) + || (octets[0] == 192 && octets[1] == 168); + } + } + + false } /// 检查是否为非本地绑定地址(需要强 API Key) pub fn is_non_local_bind(host: &str) -> bool { - host == "0.0.0.0" || host == "::" + if host == "0.0.0.0" || host == "::" { + return true; + } + + // 私有网络地址也算非本地绑定 + if let Ok(addr) = host.parse::() { + if let std::net::IpAddr::V4(ipv4) = addr { + let octets = ipv4.octets(); + return octets[0] == 10 + || (octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31)) + || (octets[0] == 192 && octets[1] == 168); + } + } + + false } /// 掩码敏感 Token diff --git a/src-tauri/src/commands/network_cmd.rs b/src-tauri/src/commands/network_cmd.rs index 3448dd8fa..38aabc72f 100644 --- a/src-tauri/src/commands/network_cmd.rs +++ b/src-tauri/src/commands/network_cmd.rs @@ -3,7 +3,7 @@ //! 提供获取本地网络接口信息的功能 use serde::Serialize; -use std::net::UdpSocket; +use std::net::{IpAddr, UdpSocket}; /// 网络接口信息 #[derive(Debug, Clone, Serialize)] @@ -12,6 +12,8 @@ pub struct NetworkInfo { pub localhost: String, /// 内网 IP 地址(局域网) pub lan_ip: Option, + /// 所有可用的网络接口 IP 地址 + pub all_ips: Vec, } /// 获取本地网络信息 @@ -20,10 +22,12 @@ pub struct NetworkInfo { #[tauri::command] pub fn get_network_info() -> Result { let lan_ip = get_local_ip(); + let all_ips = get_all_local_ips(); Ok(NetworkInfo { localhost: "127.0.0.1".to_string(), lan_ip, + all_ips, }) } @@ -38,3 +42,49 @@ fn get_local_ip() -> Option { let local_addr = socket.local_addr().ok()?; Some(local_addr.ip().to_string()) } + +/// 获取所有本地网络接口的 IP 地址 +/// +/// 返回所有非回环的 IPv4 地址,过滤掉 VPN 和虚拟网卡 +fn get_all_local_ips() -> Vec { + use std::net::Ipv4Addr; + + let mut ips = Vec::new(); + + // 使用 if-addrs crate 获取所有网络接口 + if let Ok(interfaces) = if_addrs::get_if_addrs() { + for iface in interfaces { + // 只处理 IPv4 地址 + if let IpAddr::V4(ipv4) = iface.ip() { + // 过滤掉回环地址 + if ipv4.is_loopback() { + continue; + } + + // 过滤掉链路本地地址 (169.254.x.x) + if ipv4.octets()[0] == 169 && ipv4.octets()[1] == 254 { + continue; + } + + // 过滤掉常见的 VPN 地址段 + // 198.18.0.0/15 (用于基准测试) + if ipv4.octets()[0] == 198 && (ipv4.octets()[1] == 18 || ipv4.octets()[1] == 19) { + continue; + } + + // 只保留私有网络地址 + // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + let is_private = ipv4.octets()[0] == 10 + || (ipv4.octets()[0] == 172 + && (ipv4.octets()[1] >= 16 && ipv4.octets()[1] <= 31)) + || (ipv4.octets()[0] == 192 && ipv4.octets()[1] == 168); + + if is_private { + ips.push(ipv4.to_string()); + } + } + } + } + + ips +} diff --git a/src-tauri/src/config/hot_reload.rs b/src-tauri/src/config/hot_reload.rs index cd2062d2a..acb04afc2 100644 --- a/src-tauri/src/config/hot_reload.rs +++ b/src-tauri/src/config/hot_reload.rs @@ -489,18 +489,47 @@ fn is_localhost_host(host: &str) -> bool { } /// 检查是否为有效的绑定地址 -/// 允许回环地址和 0.0.0.0(监听所有接口) +/// 允许回环地址、0.0.0.0 和私有网络地址 fn is_valid_bind_host(host: &str) -> bool { if is_localhost_host(host) { return true; } // 允许 0.0.0.0 和 :: (监听所有接口) - host == "0.0.0.0" || host == "::" + if host == "0.0.0.0" || host == "::" { + return true; + } + + // 允许私有网络地址 + if let Ok(addr) = host.parse::() { + if let std::net::IpAddr::V4(ipv4) = addr { + // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + let octets = ipv4.octets(); + return octets[0] == 10 + || (octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31)) + || (octets[0] == 192 && octets[1] == 168); + } + } + + false } /// 检查是否为非本地绑定地址(需要强 API Key) fn is_non_local_bind(host: &str) -> bool { - host == "0.0.0.0" || host == "::" + if host == "0.0.0.0" || host == "::" { + return true; + } + + // 私有网络地址也算非本地绑定 + if let Ok(addr) = host.parse::() { + if let std::net::IpAddr::V4(ipv4) = addr { + let octets = ipv4.octets(); + return octets[0] == 10 + || (octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31)) + || (octets[0] == 192 && octets[1] == 168); + } + } + + false } /// 热重载状态 diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index d60f2056b..f43729efd 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -7,6 +7,7 @@ import { ChevronUp, RefreshCw, } from "lucide-react"; +import * as Select from "@radix-ui/react-select"; import { LogsTab } from "./LogsTab"; import { RoutesTab } from "./RoutesTab"; import { ProviderIcon } from "@/icons/providers"; @@ -65,6 +66,7 @@ export function ApiServerPage() { // Config editing const [editPort, setEditPort] = useState(""); + const [editHost, setEditHost] = useState(""); const [editApiKey, setEditApiKey] = useState(""); const [defaultProvider, setDefaultProviderState] = useState("kiro"); @@ -100,6 +102,7 @@ export function ApiServerPage() { const c = await getConfig(); setConfig(c); setEditPort(c.server.port.toString()); + setEditHost(c.server.host); setEditApiKey(c.server.api_key); } catch (e) { console.error(e); @@ -172,6 +175,7 @@ export function ApiServerPage() { ...config, server: { ...config.server, + host: editHost, port: parseInt(editPort) || 8999, api_key: editApiKey, }, @@ -635,36 +639,23 @@ export function ApiServerPage() {
-

API Server

-

- 本地代理服务器,支持 OpenAI/Anthropic 格式 - {networkInfo && ( - <> - {" "} - - {networkInfo.localhost}:{config?.server.port ?? 8999} - - {networkInfo.lan_ip && ( - <> - {" | "} - - {networkInfo.lan_ip}:{config?.server.port ?? 8999} - - (局域网) - - )} - - )} - - {" "} - {status?.running ? "运行中" : "已停止"} - {" · "} - {status?.requests || 0} 请求 - {" · "} +

+

API Server

+
+ + + {status?.running ? "运行中" : "已停止"} + + · + {status?.requests || 0} 请求 + · {defaultProvider} - +
+
+

+ 本地代理服务器,支持 OpenAI/Anthropic 格式

@@ -728,14 +719,89 @@ export function ApiServerPage() { ? "停止服务" : "启动服务"} -
+
+
+ 监听地址: + + + + + + + + + + + + + + + + + 127.0.0.1 + + (仅本机) + + + + + + + + + + + + 0.0.0.0 + + (所有接口) + + + + + + {networkInfo?.all_ips.map((ip) => ( + + + + + + + {ip} + + (局域网) + + + + + ))} + + + + +
端口: setEditPort(e.target.value)} - className="w-20 rounded border bg-background px-2 py-1 text-sm" + className="w-20 rounded-md border border-input bg-background px-3 py-1.5 text-sm shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50" + disabled={status?.running} />
@@ -744,18 +810,34 @@ export function ApiServerPage() { type="text" value={editApiKey} onChange={(e) => setEditApiKey(e.target.value)} - className="w-40 rounded border bg-background px-2 py-1 text-sm" + className="w-40 rounded-md border border-input bg-background px-3 py-1.5 text-sm shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50" + disabled={status?.running} />
+ {status?.running && ( +
+ ℹ️ + 修改配置需要先停止服务 +
+ )} + {(editHost === "0.0.0.0" || + (networkInfo?.all_ips.includes(editHost) ?? false)) && + !status?.running && ( +
+ ⚠️ + 局域网访问需要使用非默认 API Key 以确保安全 +
+ )}
{/* Default Provider - 动态显示有凭证的 Provider */} diff --git a/src/hooks/useTauri.ts b/src/hooks/useTauri.ts index 821feeba4..bfa9e0437 100644 --- a/src/hooks/useTauri.ts +++ b/src/hooks/useTauri.ts @@ -524,6 +524,7 @@ export async function setEndpointProvider( export interface NetworkInfo { localhost: string; lan_ip: string | null; + all_ips: string[]; } /**