mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-10-24 01:58:23 +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(
|
||||
|
||||
Reference in New Issue
Block a user