提交qiming-mcp-proxy

This commit is contained in:
Codex
2026-06-01 13:03:20 +08:00
parent 9e9486b7c2
commit afb3d9f4e6
394 changed files with 124494 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
use clap::{Parser, Subcommand};
use std::path::PathBuf;
pub mod models;
/// FastEmbed - 文本向量化服务
#[derive(Parser, Debug)]
#[command(name = "fastembed")]
#[command(about = "FastEmbed 文本向量化服务", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// 启动 HTTP 服务
Server(ServerArgs),
/// 模型管理
Models(ModelsCmd),
}
/// HTTP 服务启动参数
#[derive(Parser, Debug)]
pub struct ServerArgs {
/// 监听端口
#[arg(short, long, default_value = "8080")]
pub port: u16,
/// 配置文件路径
#[arg(short, long)]
pub config: Option<PathBuf>,
}
/// 模型管理子命令
#[derive(Parser, Debug)]
pub struct ModelsCmd {
#[command(subcommand)]
pub command: ModelsSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum ModelsSubcommand {
/// 下载模型到本地缓存
Download(DownloadArgs),
/// 列出已下载的模型
List(ListArgs),
}
/// 模型下载参数
#[derive(Parser, Debug)]
pub struct DownloadArgs {
/// 模型类型: text | image | sparse
#[arg(long, default_value = "text")]
pub r#type: String,
/// 内置模型变体名,如 BGELargeZHV15
#[arg(long)]
pub model: Option<String>,
/// Hugging Face 模型代码,如 Xenova/bge-large-zh-v1.5
#[arg(long)]
pub code: Option<String>,
/// BYO 模式ONNX 文件名
#[arg(long)]
pub onnx: Option<String>,
/// BYO 模式Tokenizer 文件名
#[arg(long)]
pub tokenizer: Option<String>,
/// BYO 模式Config 文件名
#[arg(long)]
pub config: Option<String>,
/// BYO 模式Special tokens map 文件名
#[arg(long, alias = "special_tokens")]
pub special_tokens_map: Option<String>,
/// BYO 模式Tokenizer config 文件名
#[arg(long)]
pub tokenizer_config: Option<String>,
/// 缓存目录
#[arg(long, default_value = ".fastembed_cache")]
pub cache_dir: PathBuf,
/// 显示下载进度
#[arg(long, default_value_t = true)]
pub progress: bool,
}
/// 模型列表参数
#[derive(Parser, Debug)]
pub struct ListArgs {
/// 模型类型筛选: text | image | sparse
#[arg(long, default_value = "text")]
pub r#type: String,
/// 缓存目录
#[arg(long, default_value = ".fastembed_cache")]
pub cache_dir: PathBuf,
}

View File

@@ -0,0 +1,96 @@
use super::{DownloadArgs, ListArgs};
use crate::models::{ModelInfo, list_available_models};
use anyhow::Result;
/// 执行模型下载
pub async fn download_model(args: DownloadArgs) -> Result<()> {
use fastembed::{InitOptions, TextEmbedding};
tracing::info!("Start downloading the model...");
// 解析模型
let model = if let Some(model_name) = args.model {
// 使用内置模型变体名
crate::models::parse_model(&model_name)?
} else if let Some(code) = args.code {
// 使用模型代码
crate::models::parse_model(&code)?
} else {
anyhow::bail!("必须指定 --model 或 --code 参数");
};
// 显示下载信息
let model_info = ModelInfo::from_embedding_model(&model);
println!("📦 Download model:");
println!("Variant name: {}", model_info.variant);
println!("Model code: {}", model_info.code);
println!("Vector dimensions: {}", model_info.dim);
println!("Cache directory: {}", args.cache_dir.display());
println!();
// 初始化模型(会自动下载)
let mut options = InitOptions::new(model.clone());
options = options.with_cache_dir(args.cache_dir.clone());
options = options.with_show_download_progress(args.progress);
println!("⬇️ Downloading model files...");
let start = std::time::Instant::now();
let _embedding = TextEmbedding::try_new(options)?;
let elapsed = start.elapsed();
println!();
println!("✅ Model download completed!");
println!("Time taken: {:?}", elapsed);
println!("Cache location: {}", args.cache_dir.display());
// 验证文件
println!();
println!("🔍 Verify model file...");
let available = list_available_models(args.cache_dir.to_str().unwrap())?;
if available.iter().any(|m| m.variant == model_info.variant) {
println!("✅ Model file verification successful!");
} else {
println!("⚠️ WARNING: Model file may be incomplete");
}
Ok(())
}
/// 列出已下载的模型
pub async fn list_models(args: ListArgs) -> Result<()> {
use crate::models::list_available_models;
println!("📋 Query downloaded models...");
println!("Type: {}", args.r#type);
println!("Cache directory: {}", args.cache_dir.display());
println!();
// 检查缓存目录是否存在
if !args.cache_dir.exists() {
println!("⚠️ The cache directory does not exist: {}", args.cache_dir.display());
println!("Tip: Please download the model first");
return Ok(());
}
// 列出可用模型
let models = list_available_models(args.cache_dir.to_str().unwrap())?;
if models.is_empty() {
println!("📭 No downloaded model found");
println!("Tip: Use 'fastembed models download --model BGELargeZHV15' to download the model");
} else {
println!("✅ Found {} downloaded models:", models.len());
println!();
println!("{:<20} {:<40} {:<10}", "Variant", "Model Code", "Dim");
println!("{}", "".repeat(72));
for model in models {
println!("{:<20} {:<40} {:<10}", model.variant, model.code, model.dim);
}
}
Ok(())
}

View File

@@ -0,0 +1,144 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// 服务器配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
/// 监听地址
#[serde(default = "default_host")]
pub host: String,
/// 监听端口
#[serde(default = "default_port")]
pub port: u16,
}
fn default_host() -> String {
"0.0.0.0".to_string()
}
fn default_port() -> u16 {
8080
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: default_host(),
port: default_port(),
}
}
}
/// FastEmbed 配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FastEmbedConfig {
/// 缓存目录
#[serde(default = "default_cache_dir")]
pub cache_dir: String,
/// 默认模型
#[serde(default = "default_model")]
pub default_model: String,
/// 批处理大小
#[serde(default = "default_batch_size")]
pub batch_size: usize,
}
fn default_cache_dir() -> String {
".fastembed_cache".to_string()
}
fn default_model() -> String {
"BGELargeZHV15".to_string()
}
fn default_batch_size() -> usize {
256
}
impl Default for FastEmbedConfig {
fn default() -> Self {
Self {
cache_dir: default_cache_dir(),
default_model: default_model(),
batch_size: default_batch_size(),
}
}
}
/// 应用配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub fastembed: FastEmbedConfig,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
server: ServerConfig::default(),
fastembed: FastEmbedConfig::default(),
}
}
}
impl AppConfig {
/// 从文件加载配置
pub fn from_file(path: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("无法读取配置文件: {:?}", path))?;
let config: AppConfig = serde_yaml::from_str(&content)
.with_context(|| format!("无法解析配置文件: {:?}", path))?;
Ok(config)
}
/// 生成默认配置文件
pub fn generate_default_config(path: &PathBuf) -> Result<()> {
let default_config = AppConfig::default();
let yaml = serde_yaml::to_string(&default_config).context("无法序列化默认配置")?;
std::fs::write(path, yaml).with_context(|| format!("无法写入配置文件: {:?}", path))?;
tracing::info!("Default configuration file has been generated: {:?}", path);
Ok(())
}
/// 应用环境变量覆盖
pub fn apply_env_overrides(&mut self) {
// FASTEMBED_CACHE_DIR 可以覆盖 cache_dir
if let Ok(cache_dir) = std::env::var("FASTEMBED_CACHE_DIR") {
tracing::info!("Environment variable FASTEMBED_CACHE_DIR overrides the cache directory: {}", cache_dir);
self.fastembed.cache_dir = cache_dir;
}
}
/// 加载或生成配置
pub fn load_or_generate(config_path: Option<PathBuf>) -> Result<Self> {
let path = config_path.unwrap_or_else(|| PathBuf::from("./config.yml"));
let mut config = if path.exists() {
tracing::info!("Load configuration from file: {:?}", path);
Self::from_file(&path)?
} else {
tracing::warn!("Configuration file does not exist: {:?}, generate default configuration", path);
Self::generate_default_config(&path)?;
Self::default()
};
// 应用环境变量覆盖
config.apply_env_overrides();
// 打印最终配置
tracing::info!("Final configuration: {:?}", config);
Ok(config)
}
}

View File

@@ -0,0 +1,167 @@
use axum::{Json, extract::State, http::StatusCode};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Instant;
use utoipa::ToSchema;
use crate::models::{ModelInfo, get_or_init_model, parse_model};
use crate::server::AppState;
/// 文本嵌入请求
#[derive(Debug, Deserialize, ToSchema)]
pub struct EmbedRequest {
/// 模型名称(变体名或模型代码)
#[schema(example = "BGELargeZHV15")]
pub model: Option<String>,
/// 待嵌入的文本列表
#[schema(example = json!(["query: 搜索文本", "passage: 文档内容"]))]
pub texts: Vec<String>,
/// 批处理大小
#[schema(example = 256)]
pub batch_size: Option<usize>,
}
/// 文本嵌入响应
#[derive(Debug, Serialize, ToSchema)]
pub struct EmbedResponse {
/// 模型信息
pub model: ModelInfo,
/// 嵌入向量数量
#[schema(example = 2)]
pub count: usize,
/// 嵌入向量列表
#[schema(example = json!([[0.00123, -0.00456], [0.00078, 0.00234]]))]
pub embeddings: Vec<Vec<f32>>,
/// 耗时(毫秒)
#[schema(example = 12)]
pub elapsed_ms: u128,
}
/// 错误响应
#[derive(Debug, Serialize, ToSchema)]
pub struct ErrorResponse {
/// 错误代码
#[schema(example = "INVALID_MODEL")]
pub error: String,
/// 错误消息
#[schema(example = "未知模型")]
pub message: String,
/// HTTP 状态码
#[schema(example = 400)]
pub status: u16,
}
/// 文本嵌入处理器
#[utoipa::path(
post,
path = "/api/embeddings",
tag = "文本嵌入",
request_body = EmbedRequest,
responses(
(status = 200, description = "嵌入成功", body = EmbedResponse),
(status = 400, description = "请求参数错误", body = ErrorResponse),
(status = 413, description = "请求负载过大", body = ErrorResponse),
(status = 500, description = "服务器错误", body = ErrorResponse)
)
)]
pub async fn handle_embed(
State(state): State<Arc<AppState>>,
Json(req): Json<EmbedRequest>,
) -> Result<Json<EmbedResponse>, (StatusCode, Json<ErrorResponse>)> {
let start = Instant::now();
// 参数验证
if req.texts.is_empty() {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "EMPTY_TEXTS".to_string(),
message: "texts 不能为空".to_string(),
status: 400,
}),
));
}
// 检查文本数量限制(最大 1024
if req.texts.len() > 1024 {
return Err((
StatusCode::PAYLOAD_TOO_LARGE,
Json(ErrorResponse {
error: "TOO_MANY_TEXTS".to_string(),
message: format!("texts 数量不能超过 1024当前: {}", req.texts.len()),
status: 413,
}),
));
}
// 解析模型
let model_name = req
.model
.as_deref()
.unwrap_or(&state.config.fastembed.default_model);
let embedding_model = parse_model(model_name).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "INVALID_MODEL".to_string(),
message: format!("未知模型: {}, 错误: {}", model_name, e),
status: 400,
}),
)
})?;
// 获取或初始化模型
let model_arc = get_or_init_model(
embedding_model.clone(),
Some(state.config.fastembed.cache_dir.clone()),
None, // 使用模型默认的 max_length
)
.map_err(|e| {
tracing::error!("Model initialization failed: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "MODEL_INIT_ERROR".to_string(),
message: format!("模型初始化失败: {}", e),
status: 500,
}),
)
})?;
// 执行嵌入
let batch_size = req.batch_size.unwrap_or(state.config.fastembed.batch_size);
let mut model_guard = model_arc.lock().unwrap();
let embeddings = model_guard
.embed(req.texts.clone(), Some(batch_size))
.map_err(|e| {
tracing::error!("Embedding calculation failed: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "EMBED_ERROR".to_string(),
message: format!("嵌入计算失败: {}", e),
status: 500,
}),
)
})?;
// 转换为 Vec<Vec<f32>>
let embeddings_vec: Vec<Vec<f32>> = embeddings.into_iter().map(|e| e.to_vec()).collect();
let elapsed = start.elapsed();
Ok(Json(EmbedResponse {
model: ModelInfo::from_embedding_model(&embedding_model),
count: embeddings_vec.len(),
embeddings: embeddings_vec,
elapsed_ms: elapsed.as_millis(),
}))
}

View File

@@ -0,0 +1,41 @@
use axum::{Json, extract::State};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use utoipa::ToSchema;
use crate::server::AppState;
/// 健康检查响应
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct HealthResponse {
/// 服务状态
#[schema(example = "ok")]
pub status: String,
/// 服务运行时长(毫秒)
#[schema(example = 123456)]
pub uptime_ms: u128,
/// 模型缓存是否就绪
#[schema(example = true)]
pub model_cache_ready: bool,
}
/// 健康检查处理器
#[utoipa::path(
get,
path = "/health",
tag = "健康检查",
responses(
(status = 200, description = "服务健康", body = HealthResponse)
)
)]
pub async fn handle_health(State(state): State<Arc<AppState>>) -> Json<HealthResponse> {
let uptime = state.start_time.elapsed();
Json(HealthResponse {
status: "ok".to_string(),
uptime_ms: uptime.as_millis(),
model_cache_ready: *state.model_cache_ready.lock().unwrap(),
})
}

View File

@@ -0,0 +1,3 @@
pub mod embeddings;
pub mod health;
pub mod models;

View File

@@ -0,0 +1,87 @@
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use utoipa::{IntoParams, ToSchema};
use crate::handlers::embeddings::ErrorResponse;
use crate::models::{ModelInfo, list_available_models};
use crate::server::AppState;
/// 查询参数
#[derive(Debug, Deserialize, IntoParams)]
pub struct ModelsQuery {
/// 模型类型: text | image | sparse
#[serde(rename = "type")]
#[param(example = "text")]
pub model_type: Option<String>,
}
/// 模型列表响应
#[derive(Debug, Serialize, ToSchema)]
pub struct ModelsResponse {
/// 模型类型
#[schema(example = "text")]
pub r#type: String,
/// 模型数量
#[schema(example = 2)]
pub count: usize,
/// 模型列表
pub models: Vec<ModelInfo>,
}
/// 列出可用模型处理器
#[utoipa::path(
get,
path = "/api/models/available",
tag = "模型管理",
params(ModelsQuery),
responses(
(status = 200, description = "模型列表", body = ModelsResponse),
(status = 400, description = "请求参数错误", body = ErrorResponse),
(status = 500, description = "服务器错误", body = ErrorResponse)
)
)]
pub async fn handle_list_models(
State(state): State<Arc<AppState>>,
Query(query): Query<ModelsQuery>,
) -> Result<Json<ModelsResponse>, (StatusCode, Json<ErrorResponse>)> {
// 验证类型参数
let model_type = query.model_type.as_deref().unwrap_or("text");
// 目前仅支持 text 类型
if model_type != "text" {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "INVALID_TYPE".to_string(),
message: format!("不支持的模型类型: {},当前仅支持 text", model_type),
status: 400,
}),
));
}
// 列出可用模型
let models = list_available_models(&state.config.fastembed.cache_dir).map_err(|e| {
tracing::error!("Failed to list available models: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "LIST_ERROR".to_string(),
message: format!("列出可用模型失败: {}", e),
status: 500,
}),
)
})?;
Ok(Json(ModelsResponse {
r#type: model_type.to_string(),
count: models.len(),
models,
}))
}

View File

@@ -0,0 +1,51 @@
mod cli;
mod config;
mod handlers;
mod models;
mod server;
use anyhow::Result;
use clap::Parser;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use cli::{Cli, Commands, ModelsSubcommand};
use config::AppConfig;
#[tokio::main]
async fn main() -> Result<()> {
// 初始化日志
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "fastembed=info,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let cli = Cli::parse();
match cli.command {
Commands::Server(args) => {
// 加载或生成配置
let mut config = AppConfig::load_or_generate(args.config)?;
// 命令行端口覆盖配置文件
if args.port != 8080 {
config.server.port = args.port;
}
// 启动服务器
server::start_server(config).await?;
}
Commands::Models(models_cmd) => match models_cmd.command {
ModelsSubcommand::Download(download_args) => {
cli::models::download_model(download_args).await?;
}
ModelsSubcommand::List(list_args) => {
cli::models::list_models(list_args).await?;
}
},
}
Ok(())
}

View File

@@ -0,0 +1,162 @@
use anyhow::{Context, Result, anyhow};
use dashmap::DashMap;
use fastembed::{EmbeddingModel, InitOptions, TextEmbedding};
use once_cell::sync::Lazy;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
/// 全局模型缓存
pub static MODEL_CACHE: Lazy<DashMap<EmbeddingModel, Arc<Mutex<TextEmbedding>>>> =
Lazy::new(DashMap::new);
/// 解析模型标识(支持变体名和模型代码)
pub fn parse_model(user_input: &str) -> Result<EmbeddingModel> {
// 尝试直接匹配变体名
match user_input {
"BGELargeZHV15" => Ok(EmbeddingModel::BGELargeZHV15),
"BGESmallZHV15" => Ok(EmbeddingModel::BGESmallZHV15),
"BGEBaseENV15" => Ok(EmbeddingModel::BGEBaseENV15),
"BGESmallENV15" => Ok(EmbeddingModel::BGESmallENV15),
"BGELargeENV15" => Ok(EmbeddingModel::BGELargeENV15),
"AllMiniLML6V2" => Ok(EmbeddingModel::AllMiniLML6V2),
"AllMiniLML12V2" => Ok(EmbeddingModel::AllMiniLML12V2),
// 如果不是变体名,尝试使用 FromStr 解析模型代码
other => EmbeddingModel::from_str(other).map_err(|_| anyhow!("未知模型: {}", other)),
}
}
/// 获取或初始化模型
pub fn get_or_init_model(
model: EmbeddingModel,
cache_dir: Option<String>,
max_length: Option<usize>,
) -> Result<Arc<Mutex<TextEmbedding>>> {
// 检查缓存
if let Some(existing) = MODEL_CACHE.get(&model) {
tracing::debug!("Get model from cache: {:?}", model);
return Ok(existing.clone());
}
// 初始化模型
tracing::info!("Initialization model: {:?}", model);
let mut options = InitOptions::new(model.clone());
if let Some(dir) = cache_dir {
options = options.with_cache_dir(PathBuf::from(dir));
}
if let Some(len) = max_length {
options = options.with_max_length(len);
}
// 显示下载进度
options = options.with_show_download_progress(true);
let embedding =
TextEmbedding::try_new(options).with_context(|| format!("无法初始化模型: {:?}", model))?;
let arc = Arc::new(Mutex::new(embedding));
let model_key = model.clone();
MODEL_CACHE.insert(model_key, arc.clone());
tracing::info!("Model initialization successful: {:?}", model);
Ok(arc)
}
/// 模型信息
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct ModelInfo {
/// 模型变体名称
#[schema(example = "BGELargeZHV15")]
pub variant: String,
/// 模型代码Hugging Face 仓库)
#[schema(example = "Xenova/bge-large-zh-v1.5")]
pub code: String,
/// 向量维度
#[schema(example = 1024)]
pub dim: usize,
}
impl ModelInfo {
pub fn from_embedding_model(model: &EmbeddingModel) -> Self {
let (variant, code, dim) = match model {
EmbeddingModel::BGELargeZHV15 => ("BGELargeZHV15", "Xenova/bge-large-zh-v1.5", 1024),
EmbeddingModel::BGESmallZHV15 => ("BGESmallZHV15", "Xenova/bge-small-zh-v1.5", 512),
EmbeddingModel::BGEBaseENV15 => ("BGEBaseENV15", "Xenova/bge-base-en-v1.5", 768),
EmbeddingModel::BGESmallENV15 => ("BGESmallENV15", "Xenova/bge-small-en-v1.5", 384),
EmbeddingModel::BGELargeENV15 => ("BGELargeENV15", "Xenova/bge-large-en-v1.5", 1024),
EmbeddingModel::AllMiniLML6V2 => (
"AllMiniLML6V2",
"sentence-transformers/all-MiniLM-L6-v2",
384,
),
EmbeddingModel::AllMiniLML12V2 => (
"AllMiniLML12V2",
"sentence-transformers/all-MiniLM-L12-v2",
384,
),
_ => ("Unknown", "unknown", 0),
};
Self {
variant: variant.to_string(),
code: code.to_string(),
dim,
}
}
}
/// 列出本地已下载的模型(仅离线检查)
pub fn list_available_models(cache_dir: &str) -> Result<Vec<ModelInfo>> {
let cache_path = PathBuf::from(cache_dir);
// 如果缓存目录不存在,返回空列表
if !cache_path.exists() {
return Ok(vec![]);
}
let all_models = vec![
EmbeddingModel::BGELargeZHV15,
EmbeddingModel::BGESmallZHV15,
EmbeddingModel::BGEBaseENV15,
EmbeddingModel::BGESmallENV15,
EmbeddingModel::BGELargeENV15,
EmbeddingModel::AllMiniLML6V2,
EmbeddingModel::AllMiniLML12V2,
];
let available: Vec<ModelInfo> = all_models
.into_iter()
.filter(|model| check_model_files_exist(&cache_path, model))
.map(|model| ModelInfo::from_embedding_model(&model))
.collect();
Ok(available)
}
/// 检查模型文件是否存在(简化版本)
fn check_model_files_exist(cache_path: &PathBuf, model: &EmbeddingModel) -> bool {
// 这是一个简化实现
// fastembed 使用 hf-hub 的缓存结构
// 例如 "Xenova/bge-large-zh-v1.5" -> "models--Xenova--bge-large-zh-v1.5"
let model_info = ModelInfo::from_embedding_model(model);
let model_code = model_info.code;
// 从模型代码转换为 hf-hub 缓存目录名
// "Xenova/bge-large-zh-v1.5" -> "models--Xenova--bge-large-zh-v1.5"
let model_dir_name = format!("models--{}", model_code.replace('/', "--"));
let model_dir = cache_path.join(&model_dir_name);
// 检查目录是否存在且不为空
if model_dir.exists() && model_dir.is_dir() {
if let Ok(entries) = std::fs::read_dir(&model_dir) {
return entries.count() > 0;
}
}
false
}

View File

@@ -0,0 +1,193 @@
use anyhow::Result;
use axum::{
Router,
routing::{get, post},
};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tokio::signal;
use tower_http::{
cors::{Any, CorsLayer},
limit::RequestBodyLimitLayer,
trace::TraceLayer,
};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use crate::config::AppConfig;
use crate::handlers::{
embeddings::handle_embed, health::handle_health, models::handle_list_models,
};
/// OpenAPI 文档定义
#[derive(OpenApi)]
#[openapi(
info(
title = "FastEmbed API",
version = "0.1.0",
description = "基于 FastEmbed 的文本嵌入服务",
contact(
name = "API Support",
)
),
paths(
crate::handlers::health::handle_health,
crate::handlers::embeddings::handle_embed,
crate::handlers::models::handle_list_models,
),
components(
schemas(
crate::handlers::health::HealthResponse,
crate::handlers::embeddings::EmbedRequest,
crate::handlers::embeddings::EmbedResponse,
crate::handlers::embeddings::ErrorResponse,
crate::handlers::models::ModelsResponse,
crate::models::ModelInfo,
)
),
tags(
(name = "健康检查", description = "服务健康状态监控"),
(name = "文本嵌入", description = "文本向量化接口"),
(name = "模型管理", description = "模型列表与管理"),
)
)]
struct ApiDoc;
/// 应用状态
#[derive(Clone)]
pub struct AppState {
pub config: AppConfig,
pub start_time: Instant,
pub model_cache_ready: Arc<Mutex<bool>>,
}
impl AppState {
pub fn new(config: AppConfig) -> Self {
Self {
config,
start_time: Instant::now(),
model_cache_ready: Arc::new(Mutex::new(false)),
}
}
}
/// 创建路由
pub fn create_router(state: Arc<AppState>) -> Router {
// CORS 中间件
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
// Body 限制20MB
let body_limit = RequestBodyLimitLayer::new(20 * 1024 * 1024);
// 创建 Swagger UI无状态路由
let swagger = SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi());
// 创建 API 路由(有状态)
Router::new()
.merge(swagger)
.route("/health", get(handle_health))
.route("/api/embeddings", post(handle_embed))
.route("/api/models/available", get(handle_list_models))
.layer(cors)
.layer(body_limit)
.layer(TraceLayer::new_for_http())
.with_state(state)
}
/// 启动服务器
pub async fn start_server(config: AppConfig) -> Result<()> {
let host = config.server.host.clone();
let port = config.server.port;
let addr = format!("{}:{}", host, port);
let state = Arc::new(AppState::new(config.clone()));
// 预热模型(异步执行)
let warmup_state = state.clone();
let warmup_config = config.clone();
tokio::spawn(async move {
if let Err(e) = warmup_model(warmup_state, warmup_config).await {
tracing::warn!("Model warm-up failed: {}", e);
}
});
let app = create_router(state);
tracing::info!("FastEmbed service is starting...");
tracing::info!("Listening address: {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("✅ FastEmbed service has been started: http://{}", addr);
tracing::info!("Health check: http://{}/health", addr);
tracing::info!("Text embedding: POST http://{}/api/embeddings", addr);
tracing::info!("Available models: GET http://{}/api/models/available", addr);
tracing::info!("📚 Swagger UI: http://{}/swagger-ui/", addr);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
tracing::info!("✅ FastEmbed service has been gracefully closed");
Ok(())
}
/// 模型预热
async fn warmup_model(state: Arc<AppState>, config: AppConfig) -> Result<()> {
use crate::models::{get_or_init_model, parse_model};
tracing::info!("Start preheating model: {}", config.fastembed.default_model);
let start = Instant::now();
let model = parse_model(&config.fastembed.default_model)?;
let model_arc = get_or_init_model(
model,
Some(config.fastembed.cache_dir.clone()),
None, // 使用模型默认的 max_length
)?;
// 执行一次微型嵌入
let warmup_text = vec!["passage: warmup".to_string()];
let mut model_guard = model_arc.lock().unwrap();
model_guard.embed(warmup_text, Some(1))?;
let elapsed = start.elapsed();
// 标记预热完成
*state.model_cache_ready.lock().unwrap() = true;
tracing::info!("✅ Model preheating completed, time consuming: {:?}", elapsed);
Ok(())
}
/// 优雅关闭信号
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c().await.expect("无法安装 Ctrl+C 信号处理器");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("无法安装 SIGTERM 信号处理器")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {
tracing::info!("Receive Ctrl+C signal and start graceful shutdown...");
},
_ = terminate => {
tracing::info!("Receive SIGTERM signal and start graceful shutdown...");
},
}
}