feat: 前端静态文件嵌入二进制
- 添加 rust-embed 依赖 - 创建 static_files.rs 模块,编译时嵌入 static/ 目录 - 修改 gateway 路由,默认使用嵌入文件 - 支持 STATIC_DIR 环境变量切换到磁盘文件(开发模式) - 更新 README 说明 Web UI 和构建流程
This commit is contained in:
parent
1288ba268f
commit
9b6cae0803
@ -47,3 +47,4 @@ rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", branch = "mai
|
|||||||
schemars = "1.0"
|
schemars = "1.0"
|
||||||
http = "1"
|
http = "1"
|
||||||
tower-http = { version = "0.6", features = ["fs"] }
|
tower-http = { version = "0.6", features = ["fs"] }
|
||||||
|
rust-embed = "8"
|
||||||
|
|||||||
43
README.md
43
README.md
@ -846,8 +846,22 @@ silent_agent_task 和 agent_task 使用同一套 Agent 执行能力,但路由
|
|||||||
|
|
||||||
- /health:健康检查
|
- /health:健康检查
|
||||||
- /ws:CLI 客户端连接入口
|
- /ws:CLI 客户端连接入口
|
||||||
|
- /:Web UI 前端(已嵌入二进制,无需外部文件)
|
||||||
|
|
||||||
### 11.3 CLI 使用方式
|
### 11.3 Web UI
|
||||||
|
|
||||||
|
PicoBot 内置 Web 前端,在编译时已打包进二进制文件。启动网关后可直接访问:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:19876/
|
||||||
|
```
|
||||||
|
|
||||||
|
特性:
|
||||||
|
- 前端代码编译时嵌入 exe,无需携带 `static/` 目录
|
||||||
|
- 支持 SPA 前端路由
|
||||||
|
- 开发模式下可通过 `STATIC_DIR` 环境变量使用磁盘文件(支持热更新)
|
||||||
|
|
||||||
|
### 11.4 CLI 使用方式
|
||||||
|
|
||||||
程序提供两个主命令:
|
程序提供两个主命令:
|
||||||
|
|
||||||
@ -989,10 +1003,33 @@ CLI 中已实现的交互命令包括:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 13.2 启动网关
|
### 13.2 构建与启动
|
||||||
|
|
||||||
|
**构建(包含前端):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run -- gateway
|
# 使用 Makefile(推荐)
|
||||||
|
make build
|
||||||
|
|
||||||
|
# 或手动构建
|
||||||
|
cd web && npm install && npm run build
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物:
|
||||||
|
- 前端输出到 `static/` 目录
|
||||||
|
- 编译时通过 `rust-embed` 嵌入二进制文件
|
||||||
|
- 最终 exe 约 25MB,包含完整前端
|
||||||
|
|
||||||
|
**启动网关:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生产模式(使用嵌入的前端)
|
||||||
|
cargo run --release -- gateway
|
||||||
|
|
||||||
|
# 开发模式(使用磁盘文件,支持前端热更新)
|
||||||
|
cd web && npm run dev &
|
||||||
|
STATIC_DIR=static cargo run -- gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
### 13.3 启动本地 CLI
|
### 13.3 启动本地 CLI
|
||||||
|
|||||||
@ -22,6 +22,7 @@ pub mod session_lifecycle;
|
|||||||
pub mod session_message_sender;
|
pub mod session_message_sender;
|
||||||
pub mod session_message_service;
|
pub mod session_message_service;
|
||||||
pub mod session_pool;
|
pub mod session_pool;
|
||||||
|
pub mod static_files;
|
||||||
pub mod tool_registry_factory;
|
pub mod tool_registry_factory;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ use processor::InboundProcessor;
|
|||||||
use runtime::build_session_manager_with_sender;
|
use runtime::build_session_manager_with_sender;
|
||||||
use session_message_sender::BusSessionMessageSender;
|
use session_message_sender::BusSessionMessageSender;
|
||||||
use session::SessionManager;
|
use session::SessionManager;
|
||||||
|
use static_files::static_handler;
|
||||||
|
|
||||||
pub struct GatewayState {
|
pub struct GatewayState {
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
@ -184,13 +186,24 @@ pub async fn run(
|
|||||||
let bind_host = host.unwrap_or_else(|| state.config.gateway.host.clone());
|
let bind_host = host.unwrap_or_else(|| state.config.gateway.host.clone());
|
||||||
let bind_port = port.unwrap_or(state.config.gateway.port);
|
let bind_port = port.unwrap_or(state.config.gateway.port);
|
||||||
|
|
||||||
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "static".to_string());
|
// 使用嵌入的静态文件(编译时打包进二进制)
|
||||||
|
// 开发模式下可通过 STATIC_DIR 环境变量使用磁盘文件
|
||||||
|
let use_embedded = std::env::var("STATIC_DIR").is_err();
|
||||||
|
|
||||||
let app = Router::new()
|
let app = if use_embedded {
|
||||||
.route("/health", routing::get(http::health))
|
Router::new()
|
||||||
.route("/ws", routing::get(ws::ws_handler))
|
.route("/health", routing::get(http::health))
|
||||||
.fallback_service(ServeDir::new(&static_dir))
|
.route("/ws", routing::get(ws::ws_handler))
|
||||||
.with_state(state.clone());
|
.fallback(static_handler)
|
||||||
|
.with_state(state.clone())
|
||||||
|
} else {
|
||||||
|
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "static".to_string());
|
||||||
|
Router::new()
|
||||||
|
.route("/health", routing::get(http::health))
|
||||||
|
.route("/ws", routing::get(ws::ws_handler))
|
||||||
|
.fallback_service(ServeDir::new(&static_dir))
|
||||||
|
.with_state(state.clone())
|
||||||
|
};
|
||||||
|
|
||||||
let addr = format!("{}:{}", bind_host, bind_port);
|
let addr = format!("{}:{}", bind_host, bind_port);
|
||||||
let listener = TcpListener::bind(&addr).await?;
|
let listener = TcpListener::bind(&addr).await?;
|
||||||
|
|||||||
57
src/gateway/static_files.rs
Normal file
57
src/gateway/static_files.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{header, Response, StatusCode, Uri},
|
||||||
|
};
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
|
|
||||||
|
/// 嵌入的静态文件资源
|
||||||
|
/// 在编译时将 static 目录下的所有文件打包进二进制文件
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "static/"]
|
||||||
|
pub struct StaticAssets;
|
||||||
|
|
||||||
|
/// 处理静态文件请求
|
||||||
|
/// 从嵌入的资源中读取文件并返回 HTTP 响应
|
||||||
|
pub async fn static_handler(uri: Uri) -> Response<Body> {
|
||||||
|
let path = uri.path().trim_start_matches('/');
|
||||||
|
|
||||||
|
// 处理根路径,返回 index.html
|
||||||
|
let path = if path.is_empty() {
|
||||||
|
"index.html"
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
match StaticAssets::get(path) {
|
||||||
|
Some(content) => {
|
||||||
|
let mime_type = mime_guess::from_path(path)
|
||||||
|
.first_or_octet_stream()
|
||||||
|
.as_ref()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(header::CONTENT_TYPE, mime_type)
|
||||||
|
.body(Body::from(content.data.into_owned()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// 对于 SPA 应用,如果请求的是页面路由(不是静态资源),返回 index.html
|
||||||
|
// 静态资源通常包含 . (如 .js, .css, .png)
|
||||||
|
if !path.contains('.') {
|
||||||
|
if let Some(index) = StaticAssets::get("index.html") {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(header::CONTENT_TYPE, "text/html")
|
||||||
|
.body(Body::from(index.data.into_owned()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(Body::from("404 Not Found"))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user