支持web端访问更新/web管理接口

This commit is contained in:
xintaofei
2026-03-25 23:07:59 +08:00
parent c4809bec46
commit 21e51dabf3
5 changed files with 112 additions and 31 deletions

View File

@@ -9,3 +9,4 @@ pub mod version_control;
pub mod folder_commands; pub mod folder_commands;
pub mod mcp; pub mod mcp;
pub mod git; pub mod git;
pub mod web_server;

View File

@@ -0,0 +1,52 @@
use axum::{extract::Extension, Json};
use serde::{Deserialize, Serialize};
use tauri::Manager;
use crate::app_error::AppCommandError;
use crate::web::{do_get_web_server_status, do_start_web_server, do_stop_web_server};
use crate::web::{WebServerInfo, WebServerState};
pub async fn get_web_server_status(
Extension(app): Extension<tauri::AppHandle>,
) -> Result<Json<Option<WebServerInfo>>, AppCommandError> {
let state = app.state::<WebServerState>();
Ok(Json(do_get_web_server_status(&state)))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StartWebServerParams {
pub port: Option<u16>,
pub host: Option<String>,
}
pub async fn start_web_server(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<StartWebServerParams>,
) -> Result<Json<WebServerInfo>, AppCommandError> {
let state = app.state::<WebServerState>();
let info = do_start_web_server(&app, &state, params.port, params.host).await?;
Ok(Json(info))
}
pub async fn stop_web_server(
Extension(app): Extension<tauri::AppHandle>,
) -> Result<Json<()>, AppCommandError> {
let state = app.state::<WebServerState>();
do_stop_web_server(&state);
Ok(Json(()))
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppUpdateCheckResult {
pub current_version: &'static str,
pub update: Option<()>,
}
pub async fn check_app_update() -> Json<AppUpdateCheckResult> {
Json(AppUpdateCheckResult {
current_version: env!("CARGO_PKG_VERSION"),
update: None,
})
}

View File

@@ -40,11 +40,11 @@ pub struct WebServerInfo {
pub addresses: Vec<String>, pub addresses: Vec<String>,
} }
fn generate_random_token() -> String { pub(crate) fn generate_random_token() -> String {
uuid::Uuid::new_v4().to_string().replace('-', "") uuid::Uuid::new_v4().to_string().replace('-', "")
} }
fn find_static_dir(app: &tauri::AppHandle) -> PathBuf { pub(crate) fn find_static_dir(app: &tauri::AppHandle) -> PathBuf {
// 1. Production: Tauri bundles frontendDist into the resource directory. // 1. Production: Tauri bundles frontendDist into the resource directory.
let resource = app.path().resource_dir().ok(); let resource = app.path().resource_dir().ok();
if let Some(ref dir) = resource { if let Some(ref dir) = resource {
@@ -83,7 +83,7 @@ fn find_static_dir(app: &tauri::AppHandle) -> PathBuf {
cwd_out cwd_out
} }
fn get_local_addresses(port: u16) -> Vec<String> { pub(crate) fn get_local_addresses(port: u16) -> Vec<String> {
let mut addrs = vec![format!("http://127.0.0.1:{}", port)]; let mut addrs = vec![format!("http://127.0.0.1:{}", port)];
// Try to get LAN IPs // Try to get LAN IPs
if let Ok(interfaces) = std::net::UdpSocket::bind("0.0.0.0:0") { if let Ok(interfaces) = std::net::UdpSocket::bind("0.0.0.0:0") {
@@ -97,14 +97,14 @@ fn get_local_addresses(port: u16) -> Vec<String> {
addrs addrs
} }
#[tauri::command] // ── Core logic (shared by Tauri commands and web handlers) ──
pub async fn start_web_server(
app: tauri::AppHandle, pub(crate) async fn do_start_web_server(
state: tauri::State<'_, WebServerState>, app: &tauri::AppHandle,
state: &WebServerState,
port: Option<u16>, port: Option<u16>,
host: Option<String>, host: Option<String>,
) -> Result<WebServerInfo, AppCommandError> { ) -> Result<WebServerInfo, AppCommandError> {
// Check if already running
if state.running.load(Ordering::Relaxed) { if state.running.load(Ordering::Relaxed) {
return Err(AppCommandError::new( return Err(AppCommandError::new(
AppErrorCode::AlreadyExists, AppErrorCode::AlreadyExists,
@@ -116,11 +116,7 @@ pub async fn start_web_server(
let host = host.unwrap_or_else(|| "0.0.0.0".to_string()); let host = host.unwrap_or_else(|| "0.0.0.0".to_string());
let token = generate_random_token(); let token = generate_random_token();
// Determine static directory for serving the frontend. let static_dir = find_static_dir(app);
// In production: files are bundled into the resource directory.
// In dev: the "out/" directory is at the project root (one level above src-tauri/).
let static_dir = find_static_dir(&app);
let router = router::build_router(app.clone(), token.clone(), static_dir); let router = router::build_router(app.clone(), token.clone(), static_dir);
let addr: SocketAddr = format!("{}:{}", host, port) let addr: SocketAddr = format!("{}:{}", host, port)
@@ -142,14 +138,12 @@ pub async fn start_web_server(
} }
}); });
// Store state
*state.handle.lock().unwrap() = Some(handle); *state.handle.lock().unwrap() = Some(handle);
state.port.store(actual_port, Ordering::Relaxed); state.port.store(actual_port, Ordering::Relaxed);
*state.token.lock().unwrap() = token.clone(); *state.token.lock().unwrap() = token.clone();
state.running.store(true, Ordering::Relaxed); state.running.store(true, Ordering::Relaxed);
let addresses = get_local_addresses(actual_port); let addresses = get_local_addresses(actual_port);
Ok(WebServerInfo { Ok(WebServerInfo {
port: actual_port, port: actual_port,
token, token,
@@ -157,10 +151,7 @@ pub async fn start_web_server(
}) })
} }
#[tauri::command] pub(crate) fn do_stop_web_server(state: &WebServerState) {
pub async fn stop_web_server(
state: tauri::State<'_, WebServerState>,
) -> Result<(), AppCommandError> {
if let Some(handle) = state.handle.lock().unwrap().take() { if let Some(handle) = state.handle.lock().unwrap().take() {
handle.abort(); handle.abort();
} }
@@ -168,6 +159,39 @@ pub async fn stop_web_server(
state.port.store(0, Ordering::Relaxed); state.port.store(0, Ordering::Relaxed);
*state.token.lock().unwrap() = String::new(); *state.token.lock().unwrap() = String::new();
eprintln!("[WEB] Web server stopped"); eprintln!("[WEB] Web server stopped");
}
pub(crate) fn do_get_web_server_status(state: &WebServerState) -> Option<WebServerInfo> {
if !state.running.load(Ordering::Relaxed) {
return None;
}
let port = state.port.load(Ordering::Relaxed);
let token = state.token.lock().unwrap().clone();
let addresses = get_local_addresses(port);
Some(WebServerInfo {
port,
token,
addresses,
})
}
// ── Tauri commands (thin wrappers) ──
#[tauri::command]
pub async fn start_web_server(
app: tauri::AppHandle,
state: tauri::State<'_, WebServerState>,
port: Option<u16>,
host: Option<String>,
) -> Result<WebServerInfo, AppCommandError> {
do_start_web_server(&app, &state, port, host).await
}
#[tauri::command]
pub async fn stop_web_server(
state: tauri::State<'_, WebServerState>,
) -> Result<(), AppCommandError> {
do_stop_web_server(&state);
Ok(()) Ok(())
} }
@@ -175,15 +199,5 @@ pub async fn stop_web_server(
pub async fn get_web_server_status( pub async fn get_web_server_status(
state: tauri::State<'_, WebServerState>, state: tauri::State<'_, WebServerState>,
) -> Result<Option<WebServerInfo>, AppCommandError> { ) -> Result<Option<WebServerInfo>, AppCommandError> {
if !state.running.load(Ordering::Relaxed) { Ok(do_get_web_server_status(&state))
return Ok(None);
}
let port = state.port.load(Ordering::Relaxed);
let token = state.token.lock().unwrap().clone();
let addresses = get_local_addresses(port);
Ok(Some(WebServerInfo {
port,
token,
addresses,
}))
} }

View File

@@ -167,6 +167,11 @@ pub fn build_router(app: tauri::AppHandle, token: String, static_dir: std::path:
.route("/acp_read_agent_skill", post(handlers::acp::acp_read_agent_skill)) .route("/acp_read_agent_skill", post(handlers::acp::acp_read_agent_skill))
.route("/acp_save_agent_skill", post(handlers::acp::acp_save_agent_skill)) .route("/acp_save_agent_skill", post(handlers::acp::acp_save_agent_skill))
.route("/acp_delete_agent_skill", post(handlers::acp::acp_delete_agent_skill)) .route("/acp_delete_agent_skill", post(handlers::acp::acp_delete_agent_skill))
// ─── Web Server ───
.route("/get_web_server_status", post(handlers::web_server::get_web_server_status))
.route("/start_web_server", post(handlers::web_server::start_web_server))
.route("/stop_web_server", post(handlers::web_server::stop_web_server))
.route("/check_app_update", post(handlers::web_server::check_app_update))
// ─── Terminal ─── // ─── Terminal ───
.route("/terminal_spawn", post(handlers::terminal::terminal_spawn)) .route("/terminal_spawn", post(handlers::terminal::terminal_spawn))
.route("/terminal_write", post(handlers::terminal::terminal_write)) .route("/terminal_write", post(handlers::terminal::terminal_write))

View File

@@ -1,3 +1,5 @@
import { getTransport, isDesktop } from "./transport"
// All updater imports are dynamic to avoid crashing in non-Tauri browsers. // All updater imports are dynamic to avoid crashing in non-Tauri browsers.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type Update = any type Update = any
@@ -20,15 +22,22 @@ export interface AppUpdateErrorInfo {
} }
export async function getCurrentAppVersion(): Promise<string> { export async function getCurrentAppVersion(): Promise<string> {
if (!isDesktop()) {
const result = await getTransport().call<AppUpdateCheckResult>("check_app_update")
return result.currentVersion
}
try { try {
const { getVersion } = await import("@tauri-apps/api/app") const { getVersion } = await import("@tauri-apps/api/app")
return await getVersion() return await getVersion()
} catch { } catch {
return "web" return "unknown"
} }
} }
export async function checkAppUpdate(): Promise<AppUpdateCheckResult> { export async function checkAppUpdate(): Promise<AppUpdateCheckResult> {
if (!isDesktop()) {
return getTransport().call<AppUpdateCheckResult>("check_app_update")
}
const { getVersion } = await import("@tauri-apps/api/app") const { getVersion } = await import("@tauri-apps/api/app")
const { check } = await import("@tauri-apps/plugin-updater") const { check } = await import("@tauri-apps/plugin-updater")
const [currentVersion, update] = await Promise.all([getVersion(), check()]) const [currentVersion, update] = await Promise.all([getVersion(), check()])