mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
Merge pull request #105 from lwmacct/dev/260111-2255
feat: 添加开发模式 HTTP 桥接,支持浏览器 dev server 与 Tauri 后端通信
This commit is contained in:
@@ -28,7 +28,7 @@ axum = { version = "0.7", features = ["ws"] }
|
||||
axum-server = { version = "0.7", features = ["tls-rustls"] }
|
||||
rustls-pemfile = "2"
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["limit"] }
|
||||
tower-http = { version = "0.5", features = ["limit", "cors"] }
|
||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! HTTP 桥接模块
|
||||
//!
|
||||
//! 仅在开发模式下启用,允许浏览器 dev server 通过 HTTP 调用 Tauri 命令。
|
||||
//!
|
||||
//! 这是一个独立的开发服务器,运行在 3030 端口,与主应用服务器(8999)分离。
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub mod dispatcher;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::HeaderValue,
|
||||
response::{IntoResponse, Response},
|
||||
routing::post,
|
||||
Json, Router,
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(debug_assertions)]
|
||||
use std::sync::Arc;
|
||||
#[cfg(debug_assertions)]
|
||||
use tokio::sync::RwLock;
|
||||
#[cfg(debug_assertions)]
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
use crate::server::AppState;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct InvokeRequest {
|
||||
pub cmd: String,
|
||||
#[serde(default)]
|
||||
pub args: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct InvokeResponse {
|
||||
pub result: Option<serde_json::Value>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 开发桥接服务器配置
|
||||
#[cfg(debug_assertions)]
|
||||
pub struct DevBridgeConfig {
|
||||
/// 监听地址
|
||||
pub host: String,
|
||||
/// 监听端口
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
impl Default for DevBridgeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 3030,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 开发桥接服务器
|
||||
#[cfg(debug_assertions)]
|
||||
pub struct DevBridgeServer;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
impl DevBridgeServer {
|
||||
/// 启动开发桥接服务器
|
||||
///
|
||||
/// 这是一个独立的 HTTP 服务器,仅用于开发模式,
|
||||
/// 允许浏览器 dev server 通过 HTTP 调用 Tauri 命令。
|
||||
///
|
||||
/// 服务器会在后台持续运行,直到应用退出。
|
||||
pub async fn start(
|
||||
app_state: Arc<RwLock<AppState>>,
|
||||
config: Option<DevBridgeConfig>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = config.unwrap_or_default();
|
||||
|
||||
let app = Router::new()
|
||||
.route("/invoke", post(invoke_command))
|
||||
.route("/health", post(health_check))
|
||||
.layer(
|
||||
// CORS 配置 - 允许 localhost:1420 访问
|
||||
CorsLayer::new()
|
||||
.allow_origin("http://localhost:1420".parse::<HeaderValue>().unwrap())
|
||||
.allow_methods([axum::http::Method::POST, axum::http::Method::GET])
|
||||
.allow_headers([axum::http::header::CONTENT_TYPE]),
|
||||
)
|
||||
.with_state(app_state);
|
||||
|
||||
let addr = format!("{}:{}", config.host, config.port);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
|
||||
eprintln!("[DevBridge] 正在监听: http://{}", addr);
|
||||
|
||||
// 直接运行服务器(不使用 graceful_shutdown)
|
||||
// 服务器将持续运行直到应用退出
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.ok();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
async fn invoke_command(
|
||||
State(state): State<Arc<RwLock<AppState>>>,
|
||||
Json(req): Json<InvokeRequest>,
|
||||
) -> Response {
|
||||
// 获取 AppState 的读锁
|
||||
let state_ref = state.read().await;
|
||||
// 调用命令分发器
|
||||
match dispatcher::handle_command(&state_ref, &req.cmd, req.args).await {
|
||||
Ok(result) => Json(InvokeResponse {
|
||||
result: Some(result),
|
||||
error: None,
|
||||
})
|
||||
.into_response(),
|
||||
Err(e) => Json(InvokeResponse {
|
||||
result: None,
|
||||
error: Some(e.to_string()),
|
||||
})
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
async fn health_check() -> impl IntoResponse {
|
||||
Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"service": "DevBridge",
|
||||
"version": "1.0.0"
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
//! 命令分发器
|
||||
//!
|
||||
//! 将 HTTP 请求路由到现有的 Tauri 命令函数。
|
||||
|
||||
use crate::server::AppState;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
/// 处理 HTTP 桥接命令请求
|
||||
///
|
||||
/// 将命令名和参数分发到对应的命令处理函数
|
||||
pub async fn handle_command(
|
||||
state: &AppState,
|
||||
cmd: &str,
|
||||
args: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
match cmd {
|
||||
// ========== P0 - 核心配置 ==========
|
||||
"get_config" => {
|
||||
// 从配置文件读取
|
||||
let config_path = crate::config::ConfigManager::default_config_path();
|
||||
let manager = crate::config::ConfigManager::load(&config_path)?;
|
||||
let config = manager.config();
|
||||
Ok(serde_json::to_value(config)?)
|
||||
}
|
||||
|
||||
"save_config" => {
|
||||
// 保存配置到文件
|
||||
let config: crate::config::Config = serde_json::from_value(args.unwrap_or_default())?;
|
||||
crate::config::save_config(&config)?;
|
||||
Ok(serde_json::json!({ "success": true }))
|
||||
}
|
||||
|
||||
"get_default_provider" => {
|
||||
let provider = state.default_provider.read().await.clone();
|
||||
// 直接返回字符串值,不是对象
|
||||
Ok(serde_json::json!(provider))
|
||||
}
|
||||
|
||||
"get_endpoint_providers" => {
|
||||
let providers = state.endpoint_providers.read().await;
|
||||
Ok(serde_json::to_value(&*providers)?)
|
||||
}
|
||||
|
||||
// ========== P0 - 服务器状态 ==========
|
||||
"get_server_status" => {
|
||||
// 解析 base_url 获取 host 和 port
|
||||
let url_parts: Vec<&str> = state.base_url.split(':').collect();
|
||||
let host = url_parts.get(2).unwrap_or(&"127.0.0.1");
|
||||
let port = url_parts.get(3).and_then(|p| p.parse::<u16>().ok()).unwrap_or(3030);
|
||||
|
||||
let status = serde_json::json!({
|
||||
"running": true, // HTTP 桥接可用说明服务器在运行
|
||||
"host": host,
|
||||
"port": port,
|
||||
"api_key": "***", // 不暴露真实 API key
|
||||
});
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
// ========== P1 - 日志相关 ==========
|
||||
"get_logs" => {
|
||||
let logs = state.logs.read().await;
|
||||
let entries = logs.get_logs();
|
||||
// 限制返回最近 100 条
|
||||
let limit = entries.len().min(100);
|
||||
let recent: Vec<_> = entries.into_iter().rev().take(limit).map(|e| serde_json::json!({
|
||||
"timestamp": e.timestamp,
|
||||
"level": e.level,
|
||||
"message": e.message,
|
||||
})).collect();
|
||||
Ok(serde_json::to_value(recent)?)
|
||||
}
|
||||
|
||||
"clear_logs" => {
|
||||
state.logs.write().await.clear();
|
||||
Ok(serde_json::json!({ "success": true }))
|
||||
}
|
||||
|
||||
// ========== Provider Pool ==========
|
||||
"get_provider_pool_overview" => {
|
||||
// 从数据库获取凭证池概览
|
||||
if let Some(db) = &state.db {
|
||||
let conn = db.lock().map_err(|e| e.to_string())?;
|
||||
let credentials = crate::database::dao::provider_pool::ProviderPoolDao::get_all(&conn)
|
||||
.unwrap_or_default();
|
||||
|
||||
let overview: Vec<JsonValue> = credentials
|
||||
.into_iter()
|
||||
.map(|cred| serde_json::json!({
|
||||
"uuid": cred.uuid,
|
||||
"name": cred.name,
|
||||
"provider_type": cred.provider_type,
|
||||
"enabled": true,
|
||||
}))
|
||||
.collect();
|
||||
Ok(serde_json::to_value(overview)?)
|
||||
} else {
|
||||
Ok(serde_json::json!([]))
|
||||
}
|
||||
}
|
||||
|
||||
"get_api_key_providers" => {
|
||||
// 从 API Key Provider 服务获取
|
||||
if let Some(db) = &state.db {
|
||||
let conn = db.lock().map_err(|e| e.to_string())?;
|
||||
let providers = crate::database::dao::api_key_provider::ApiKeyProviderDao::get_all_providers(&conn)
|
||||
.unwrap_or_default();
|
||||
Ok(serde_json::to_value(providers)?)
|
||||
} else {
|
||||
Ok(serde_json::json!([]))
|
||||
}
|
||||
}
|
||||
|
||||
"get_provider_pool_credentials" => {
|
||||
// 获取所有凭证详细信息
|
||||
if let Some(db) = &state.db {
|
||||
let conn = db.lock().map_err(|e| e.to_string())?;
|
||||
let credentials = crate::database::dao::provider_pool::ProviderPoolDao::get_all(&conn)
|
||||
.unwrap_or_default();
|
||||
Ok(serde_json::to_value(credentials)?)
|
||||
} else {
|
||||
Ok(serde_json::json!([]))
|
||||
}
|
||||
}
|
||||
|
||||
"test_api" => {
|
||||
// 测试 API 连接
|
||||
// 从 args 获取 provider
|
||||
let args = args.ok_or("缺少参数")?;
|
||||
let provider = args
|
||||
.get("provider")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("缺少 provider 参数")?;
|
||||
|
||||
// 选择凭证
|
||||
let credential = if let Some(db) = &state.db {
|
||||
state
|
||||
.pool_service
|
||||
.select_credential(db, provider, None)
|
||||
.ok()
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match credential {
|
||||
Some(cred) => {
|
||||
state
|
||||
.logs
|
||||
.write()
|
||||
.await
|
||||
.add("info", &format!("[DevBridge] 测试 API 使用凭证: {:?}", cred.name));
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"credential_name": cred.name,
|
||||
"provider_type": cred.provider_type,
|
||||
}))
|
||||
}
|
||||
None => Ok(serde_json::json!({
|
||||
"success": false,
|
||||
"error": "未找到可用凭证"
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 模型相关 ==========
|
||||
"get_models" => {
|
||||
// 返回可用模型列表
|
||||
Ok(serde_json::json!({
|
||||
"data": [
|
||||
{"id": "claude-sonnet-4-20250514", "object": "model", "owned_by": "anthropic"},
|
||||
{"id": "claude-opus-4-20250514", "object": "model", "owned_by": "anthropic"},
|
||||
{"id": "claude-haiku-4-20250514", "object": "model", "owned_by": "anthropic"},
|
||||
{"id": "gpt-4o", "object": "model", "owned_by": "openai"},
|
||||
{"id": "gpt-4o-mini", "object": "model", "owned_by": "openai"},
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
// ========== 网络信息 ==========
|
||||
"get_network_info" => {
|
||||
// 返回网络信息
|
||||
Ok(serde_json::json!({
|
||||
"localhost": "127.0.0.1",
|
||||
"lan_ip": null,
|
||||
"all_ips": ["127.0.0.1"]
|
||||
}))
|
||||
}
|
||||
|
||||
_ => Err(format!(
|
||||
"[DevBridge] 未知命令: '{}'. 如需此命令,请将其添加到 dispatcher.rs 的 handle_command 函数中。",
|
||||
cmd
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ mod commands;
|
||||
mod config;
|
||||
mod converter;
|
||||
mod data;
|
||||
#[cfg(debug_assertions)]
|
||||
mod dev_bridge;
|
||||
mod logger;
|
||||
mod models;
|
||||
mod providers;
|
||||
|
||||
@@ -866,6 +866,28 @@ async fn run_server(
|
||||
api_key_service,
|
||||
};
|
||||
|
||||
// ========== 开发模式:启动独立的 HTTP 桥接服务器 ==========
|
||||
// 仅在 debug 模式下,启动一个独立的开发服务器在端口 3030
|
||||
// 允许浏览器 dev server 通过 HTTP 调用 Tauri 命令
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
eprintln!("[DevBridge] ===== 准备启动开发桥接服务器 =====");
|
||||
use tokio::sync::RwLock as TokioRwLock;
|
||||
let dev_bridge_state = Arc::new(TokioRwLock::new(state.clone()));
|
||||
eprintln!("[DevBridge] 状态已克隆,准备启动");
|
||||
tokio::spawn(async move {
|
||||
eprintln!("[DevBridge] spawn 任务开始执行");
|
||||
match crate::dev_bridge::DevBridgeServer::start(dev_bridge_state, None).await {
|
||||
Ok(_) => {
|
||||
eprintln!("[DevBridge] 启动完成");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[DevBridge] 启动失败: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 启动配置文件监控
|
||||
let _file_watcher = if let Some(path) = config_path {
|
||||
start_config_watcher(
|
||||
|
||||
+18
-2
@@ -1,10 +1,12 @@
|
||||
// Import mock invoke directly - Vite alias will replace @tauri-apps/api/core
|
||||
// with the mock module in web mode
|
||||
import { invoke as baseInvoke } from "@tauri-apps/api/core";
|
||||
import { invokeViaHttp, isDevBridgeAvailable } from "@/lib/dev-bridge";
|
||||
|
||||
// Safe Tauri invoke wrapper for web mode compatibility
|
||||
// 支持三种模式:Tauri IPC → HTTP Bridge → Mock
|
||||
const safeInvoke = async (cmd: string, args?: any): Promise<any> => {
|
||||
// Check if Tauri is available via window.__TAURI__
|
||||
// 1. 优先使用 Tauri IPC (生产环境或 Tauri webview 可用时)
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
(window as any).__TAURI__?.core?.invoke
|
||||
@@ -17,7 +19,21 @@ const safeInvoke = async (cmd: string, args?: any): Promise<any> => {
|
||||
return (window as any).__TAURI__.invoke(cmd, args);
|
||||
}
|
||||
|
||||
// Not in Tauri environment - use the imported invoke (mock in web mode)
|
||||
// 2. Dev 模式下尝试 HTTP 桥接(浏览器环境,Tauri 后端在运行)
|
||||
if (isDevBridgeAvailable()) {
|
||||
try {
|
||||
console.log(`[DevBridge] 尝试 HTTP 桥接: ${cmd}`);
|
||||
const result = await invokeViaHttp(cmd, args);
|
||||
console.log(`[DevBridge] HTTP 桥接成功: ${cmd}`);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.warn(`[DevBridge] HTTP 桥接失败: ${cmd}`, e);
|
||||
// 继续尝试 mock
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback 到 mock(Vite alias 会替换 @tauri-apps 导入)
|
||||
console.log(`[DevBridge] 使用 mock: ${cmd}`);
|
||||
return baseInvoke(cmd, args);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 开发桥接 HTTP 客户端
|
||||
*
|
||||
* 在开发模式下,当 Tauri IPC 不可用时(浏览器环境),
|
||||
* 通过 HTTP 与运行中的 Tauri 后端通信。
|
||||
*/
|
||||
|
||||
const BRIDGE_URL = "http://127.0.0.1:3030/invoke";
|
||||
|
||||
export interface InvokeRequest {
|
||||
cmd: string;
|
||||
args?: unknown;
|
||||
}
|
||||
|
||||
export interface InvokeResponse {
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查开发桥接是否可用
|
||||
*
|
||||
* @returns true 如果在 dev 模式且 Tauri 不可用
|
||||
*/
|
||||
export function isDevBridgeAvailable(): boolean {
|
||||
// 检查是否在浏览器环境(非 Tauri webview)
|
||||
const isBrowser =
|
||||
typeof window !== "undefined" &&
|
||||
!(window as any).__TAURI__ &&
|
||||
// 进一步检查是否在开发模式
|
||||
(import.meta.env.DEV ||
|
||||
location.hostname === "localhost" ||
|
||||
location.hostname === "127.0.0.1");
|
||||
|
||||
return isBrowser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 HTTP 桥接调用 Tauri 命令
|
||||
*
|
||||
* @param cmd - 命令名称
|
||||
* @param args - 命令参数
|
||||
* @returns Promise<T> 命令执行结果
|
||||
*/
|
||||
export async function invokeViaHttp<T = unknown>(
|
||||
cmd: string,
|
||||
args?: unknown,
|
||||
): Promise<T> {
|
||||
console.log(`[DevBridge] HTTP 调用: ${cmd}`, args);
|
||||
|
||||
try {
|
||||
const response = await fetch(BRIDGE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ cmd, args } satisfies InvokeRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: InvokeResponse = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
return data.result as T;
|
||||
} catch (e) {
|
||||
console.error(`[DevBridge] HTTP 调用失败: ${cmd}`, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查 - 测试与后端的连接
|
||||
*
|
||||
* @returns Promise<boolean> true 如果连接成功
|
||||
*/
|
||||
export async function healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(BRIDGE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cmd: "get_server_status",
|
||||
} satisfies InvokeRequest),
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取桥接状态信息
|
||||
*/
|
||||
export interface BridgeStatus {
|
||||
available: boolean;
|
||||
connected: boolean;
|
||||
mode: "tauri" | "http" | "mock";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前桥接状态
|
||||
*/
|
||||
export function getBridgeStatus(): BridgeStatus {
|
||||
const hasTauri = typeof window !== "undefined" && (window as any).__TAURI__;
|
||||
const devAvailable = isDevBridgeAvailable();
|
||||
|
||||
return {
|
||||
available: hasTauri || devAvailable,
|
||||
connected: hasTauri, // Tauri 总是连接的,HTTP 需要运行时检查
|
||||
mode: hasTauri ? "tauri" : devAvailable ? "http" : "mock",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 开发桥接模块
|
||||
*
|
||||
* 提供浏览器开发服务器与 Tauri 后端的 HTTP 通信桥接。
|
||||
*
|
||||
* @module dev-bridge
|
||||
*/
|
||||
|
||||
export {
|
||||
invokeViaHttp,
|
||||
isDevBridgeAvailable,
|
||||
healthCheck,
|
||||
getBridgeStatus,
|
||||
} from "./http-client";
|
||||
export type {
|
||||
InvokeRequest,
|
||||
InvokeResponse,
|
||||
BridgeStatus,
|
||||
} from "./http-client";
|
||||
Reference in New Issue
Block a user