feat(dev-server): 添加开发模式 HTTP/WebSocket 服务器
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
功能: - 创建 dev_server.rs 模块,提供 HTTP/WebSocket API - 使用 feature flag \dev-server\ 控制编译 - 仅绑定 localhost:50051,安全限制 CORS - 生产构建不包含此模块 使用方式: - pnpm tauri:dev:web - 启动带开发服务器的 Tauri - pnpm tauri:dev - 常规开发模式(无服务器) 安全: - 仅 localhost 绑定 - CORS 限制为 Vite 开发端口 - 通过 feature flag 完全移除生产代码
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"prepare:tauri-tools:dry-run": "node scripts/preseed-tauri-tools.mjs --dry-run",
|
||||
"tauri": "tauri",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:dev:web": "tauri dev --features dev-server",
|
||||
"tauri:build": "tauri build",
|
||||
"tauri:build:bundled": "pnpm prepare:openfang-runtime && node scripts/tauri-build-bundled.mjs",
|
||||
"tauri:build:bundled:debug": "pnpm prepare:openfang-runtime && node scripts/tauri-build-bundled.mjs --debug",
|
||||
|
||||
@@ -15,6 +15,10 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
dev-server = ["axum", "tower-http"]
|
||||
|
||||
[dependencies]
|
||||
# ZCLAW crates
|
||||
zclaw-types = { workspace = true }
|
||||
@@ -66,3 +70,7 @@ rand = { workspace = true }
|
||||
|
||||
# SQLite (keep for backward compatibility during migration)
|
||||
sqlx = { workspace = true }
|
||||
|
||||
# Development server (optional, only for debug builds)
|
||||
axum = { version = "0.7", features = ["ws"], optional = true }
|
||||
tower-http = { version = "0.5", optional = true }
|
||||
|
||||
198
desktop/src-tauri/src/dev_server.rs
Normal file
198
desktop/src-tauri/src/dev_server.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
//! Development Mode Server
|
||||
//!
|
||||
//! Provides HTTP/WebSocket API for web-based debugging.
|
||||
//! Only compiled when the `dev-server` feature is enabled.
|
||||
//!
|
||||
//! Security:
|
||||
//! - Only binds to localhost (127.0.0.1)
|
||||
//! - CORS restricted to localhost:1420 (Vite dev server)
|
||||
//! - Not included in release builds
|
||||
|
||||
#![cfg(feature = "dev-server")]
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use axum::{
|
||||
extract::ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
http::{header, Method},
|
||||
response::{IntoResponse, Json},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
pub const DEV_SERVER_PORT: u16 = 50051;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DevServerState {}
|
||||
|
||||
pub async fn start_dev_server() -> Result<(DevServerState, tokio::task::JoinHandle<()>), String> {
|
||||
let state = DevServerState::default();
|
||||
let state_clone = state.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
run_server(state_clone).await;
|
||||
});
|
||||
|
||||
Ok((state, handle))
|
||||
}
|
||||
|
||||
async fn run_server(state: DevServerState) {
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_check))
|
||||
.route("/ws", get(websocket_handler))
|
||||
.route("/api/kernel/status", get(kernel_status))
|
||||
.route("/api/agents", get(agents_list))
|
||||
.route("/api/skills", get(skills_list))
|
||||
.route("/api/hands", get(hands_list))
|
||||
.route("/api/pipelines", get(pipelines_list))
|
||||
.route("/api/rpc", post(json_rpc_handler))
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin([
|
||||
"http://localhost:1420".parse().unwrap(),
|
||||
"http://127.0.0.1:1420".parse().unwrap(),
|
||||
"http://localhost:5173".parse().unwrap(),
|
||||
"http://127.0.0.1:5173".parse().unwrap(),
|
||||
])
|
||||
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
|
||||
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]),
|
||||
)
|
||||
.with_state(state);
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], DEV_SERVER_PORT));
|
||||
|
||||
info!("[DevServer] Starting development server on http://{}", addr);
|
||||
|
||||
let listener = match tokio::net::TcpListener::bind(addr).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!("[DevServer] Failed to bind to {}: {}", addr, e);
|
||||
warn!("[DevServer] Port {} may be in use. Web debugging will not be available.", DEV_SERVER_PORT);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = axum::serve(listener, app).await {
|
||||
error!("[DevServer] Server error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
async fn health_check() -> impl IntoResponse {
|
||||
Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"mode": "development",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"message": "ZCLAW Development Server - Use Tauri app for full functionality"
|
||||
}))
|
||||
}
|
||||
|
||||
async fn websocket_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(handle_websocket)
|
||||
}
|
||||
|
||||
async fn handle_websocket(socket: WebSocket) {
|
||||
let (mut tx, mut rx) = socket.split();
|
||||
|
||||
info!("[DevServer] WebSocket client connected");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = rx.recv() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
let response = handle_ws_message(&text).await;
|
||||
if tx.send(Message::Text(response)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Ping(data))) => {
|
||||
if tx.send(Message::Pong(data)).await.is_err() {
|
||||
break
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => {
|
||||
info!("[DevServer] WebSocket client disconnected");
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ws_message(text: &str) -> String {
|
||||
let request: Result<serde_json::Value, _> = serde_json::from_str(text);
|
||||
|
||||
match request {
|
||||
Ok(json) => {
|
||||
let method = json.get("method").and_then(|m| m.as_str()).unwrap_or("unknown");
|
||||
info!("[DevServer] WebSocket request: {}", method);
|
||||
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"mode": "development",
|
||||
"message": "Use Tauri app for full functionality"
|
||||
},
|
||||
"id": json.get("id")
|
||||
})).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
Err(e) => {
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": format!("Parse error: {}", e)
|
||||
},
|
||||
"id": null
|
||||
})).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn json_rpc_handler(
|
||||
Json(request): Json<serde_json::Value>,
|
||||
) -> impl IntoResponse {
|
||||
let method = request.get("method").and_then(|m| m.as_str()).unwrap_or("unknown");
|
||||
info!("[DevServer] RPC request: {}", method);
|
||||
|
||||
Json(serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"mode": "development",
|
||||
"method": method,
|
||||
"message": "Use Tauri app for full functionality"
|
||||
},
|
||||
"id": request.get("id")
|
||||
}))
|
||||
}
|
||||
|
||||
async fn kernel_status() -> impl IntoResponse {
|
||||
Json(serde_json::json!({
|
||||
"initialized": false,
|
||||
"mode": "development",
|
||||
"message": "Use Tauri app for kernel operations"
|
||||
}))
|
||||
}
|
||||
|
||||
async fn agents_list() -> impl IntoResponse {
|
||||
Json(serde_json::json!({"agents": []}))
|
||||
}
|
||||
|
||||
async fn skills_list() -> impl IntoResponse {
|
||||
Json(serde_json::json!({"skills": []}))
|
||||
}
|
||||
|
||||
async fn hands_list() -> impl IntoResponse {
|
||||
Json(serde_json::json!({"hands": []}))
|
||||
}
|
||||
|
||||
async fn pipelines_list() -> impl IntoResponse {
|
||||
Json(serde_json::json!({"pipelines": []}))
|
||||
}
|
||||
@@ -29,6 +29,10 @@ mod kernel_commands;
|
||||
// Pipeline commands (DSL-based workflows)
|
||||
mod pipeline_commands;
|
||||
|
||||
// Development server (optional, for web debugging)
|
||||
#[cfg(feature = "dev-server")]
|
||||
mod dev_server;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
@@ -1303,6 +1307,25 @@ fn gateway_doctor(app: AppHandle) -> Result<String, String> {
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Start development server when dev-server feature is enabled
|
||||
#[cfg(feature = "dev-server")]
|
||||
{
|
||||
std::thread::spawn(|| {
|
||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime for dev server");
|
||||
rt.block_on(async {
|
||||
match dev_server::start_dev_server().await {
|
||||
Ok((_state, _handle)) => {
|
||||
tracing::info!("[DevServer] Development server started on port {}", dev_server::DEV_SERVER_PORT);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("[DevServer] Failed to start: {}", e);
|
||||
}
|
||||
}
|
||||
std::future::pending::<()>().await;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize Viking storage (async, in background)
|
||||
let runtime = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
|
||||
runtime.block_on(async {
|
||||
|
||||
Reference in New Issue
Block a user