//! 管理 API 路由处理器 —— 登录/设置密码、账号池状态、请求统计、模型列表、配置查看 use axum::{ body::Body, extract::{Query, State}, http::{StatusCode, header}, response::Response, }; use serde::{Deserialize, Serialize}; use super::handlers::AppState; use crate::config::Config; // ── 请求/响应类型 ────────────────────────────────────────────────────────── #[derive(Deserialize)] pub struct SetupRequest { pub password: String, } #[derive(Deserialize)] pub struct LoginRequest { pub password: String, } #[derive(Serialize)] pub struct LoginResponse { pub token: String, } #[derive(Serialize)] pub struct AdminStatusResponse { pub accounts: Vec, pub total: usize, pub idle: usize, pub busy: usize, pub error: usize, pub invalid: usize, } #[derive(Serialize)] pub struct AdminStatsResponse { #[serde(flatten)] pub stats: super::stats::StatsSnapshot, } #[derive(Serialize)] pub struct AdminConfigResponse { pub server: ServerConfigView, pub ds_core: DsCoreView, pub proxy: ProxyConfigView, pub admin: AdminConfigView, pub api_keys: Vec, } #[derive(Serialize)] pub struct DsCoreView { pub accounts: Vec, pub api_base: String, pub wasm_url: String, pub user_agent: String, pub client_version: String, pub client_platform: String, pub client_locale: String, pub model_types: Vec, pub max_input_tokens: Vec, pub max_output_tokens: Vec, pub input_character_limits: Vec, pub model_aliases: Vec, pub tool_call: ToolCallTagConfigView, } #[derive(Serialize)] pub struct ServerConfigView { pub host: String, pub port: u16, pub cors_origins: Vec, } #[derive(Serialize)] pub struct ToolCallTagConfigView { pub extra_starts: Vec, pub extra_ends: Vec, } #[derive(Serialize)] pub struct ProxyConfigView { pub url: Option, } #[derive(Serialize)] pub struct AdminConfigView { pub password_set: bool, pub jwt_issued_at: u64, } #[derive(Serialize)] pub struct ApiKeyEntryView { pub key: String, pub description: String, } #[derive(Serialize)] pub struct AccountView { pub email: String, pub mobile: String, pub area_code: String, pub password: String, } // ── 脱敏 ───────────────────────────────────────────────────────────────── fn mask_config(config: &Config) -> AdminConfigResponse { AdminConfigResponse { server: ServerConfigView { host: config.server.host.clone(), port: config.server.port, cors_origins: config.server.cors_origins.clone(), }, ds_core: DsCoreView { accounts: config .ds_core .accounts .iter() .map(|a| AccountView { email: a.email.clone(), mobile: a.mobile.clone(), area_code: a.area_code.clone(), password: a.password.clone(), }) .collect(), api_base: config.ds_core.api_base.clone(), wasm_url: config.ds_core.wasm_url.clone(), user_agent: config.ds_core.user_agent.clone(), client_version: config.ds_core.client_version.clone(), client_platform: config.ds_core.client_platform.clone(), client_locale: config.ds_core.client_locale.clone(), model_types: config.ds_core.model_types.clone(), max_input_tokens: config.ds_core.max_input_tokens.clone(), max_output_tokens: config.ds_core.max_output_tokens.clone(), input_character_limits: config.ds_core.input_character_limits.clone(), model_aliases: config.ds_core.model_aliases.clone(), tool_call: ToolCallTagConfigView { extra_starts: config.ds_core.tool_call.extra_starts.clone(), extra_ends: config.ds_core.tool_call.extra_ends.clone(), }, }, proxy: ProxyConfigView { url: config.proxy.url.clone(), }, admin: AdminConfigView { password_set: !config.admin.password_hash.is_empty(), jwt_issued_at: config.admin.jwt_issued_at, }, api_keys: config .api_keys .iter() .map(|k| ApiKeyEntryView { key: k.key.clone(), description: k.description.clone(), }) .collect(), } } // ── Handlers ────────────────────────────────────────────────────────────── /// POST /admin/api/setup — 首次设置密码 pub(crate) async fn admin_setup( State(state): State, body: axum::body::Bytes, ) -> Response { let req: SetupRequest = match serde_json::from_slice(&body) { Ok(r) => r, Err(e) => return error_response(StatusCode::BAD_REQUEST, &format!("请求格式错误: {}", e)), }; match super::auth::setup_admin(&state.store, &state.login_limiter, &req.password).await { Ok(token) => json_response(&LoginResponse { token }), Err(msg) => { let status = if msg.contains("已设置") { StatusCode::FORBIDDEN } else if msg.contains("次数过多") { StatusCode::TOO_MANY_REQUESTS } else if msg.contains("至少 6 位") { StatusCode::BAD_REQUEST } else { StatusCode::INTERNAL_SERVER_ERROR }; error_response(status, &msg) } } } pub(crate) async fn admin_login( State(state): State, body: axum::body::Bytes, ) -> Response { let req: LoginRequest = match serde_json::from_slice(&body) { Ok(r) => r, Err(e) => return error_response(StatusCode::BAD_REQUEST, &format!("请求格式错误: {}", e)), }; match super::auth::login_admin(&state.store, &state.login_limiter, &req.password).await { Ok(token) => json_response(&LoginResponse { token }), Err(msg) => { let status = if msg.contains("次数过多") { StatusCode::TOO_MANY_REQUESTS } else if msg.contains("未设置密码") { StatusCode::FORBIDDEN } else { StatusCode::UNAUTHORIZED }; error_response(status, &msg) } } } /// GET /admin/api/status pub(crate) async fn admin_status(State(state): State) -> Response { let statuses = state.adapter.account_statuses(); let total = statuses.len(); let busy = statuses.iter().filter(|a| a.state == "busy").count(); let idle = statuses.iter().filter(|a| a.state == "idle").count(); let error = statuses.iter().filter(|a| a.state == "error").count(); let invalid = statuses.iter().filter(|a| a.state == "invalid").count(); let resp = AdminStatusResponse { accounts: statuses, total, idle, busy, error, invalid, }; json_response(&resp) } /// GET /admin/api/stats pub(crate) async fn admin_stats(State(state): State) -> Response { let snapshot = state.stats.snapshot(); let resp = AdminStatsResponse { stats: snapshot }; json_response(&resp) } /// GET /admin/api/models pub(crate) async fn admin_models(State(state): State) -> Response { let models = state.adapter.list_models().await; json_response(&models) } /// GET /admin/api/config pub(crate) async fn admin_config(State(state): State) -> Response { let config = state.config.read().await; let config_view = mask_config(&config); json_response(&config_view) } /// PUT /admin/api/config — 更新并热重载配置 pub(crate) async fn admin_put_config( State(state): State, body: axum::body::Bytes, ) -> Response { let mut new_config: Config = match serde_json::from_slice(&body) { Ok(c) => c, Err(e) => return error_response(StatusCode::BAD_REQUEST, &format!("JSON 解析失败: {}", e)), }; // Validate if let Err(e) = new_config.validate() { return error_response(StatusCode::BAD_REQUEST, &e.to_string()); } // Merge: empty/"***" passwords keep existing values from current config; // API keys match by `id` (stable identifier), falling back to description for old-format migration. { let current = state.config.read().await; for a in &mut new_config.ds_core.accounts { if (a.password.is_empty() || a.password == "***") && let Some(existing) = current .ds_core .accounts .iter() .find(|e| e.email == a.email && e.mobile == a.mobile) { a.password.clone_from(&existing.password); } } // Admin 配置:空的 password_hash/jwt_secret 保留现有值(前端不返回这些字段) if new_config.admin.password_hash.is_empty() { new_config .admin .password_hash .clone_from(¤t.admin.password_hash); } if new_config.admin.jwt_secret.is_empty() { new_config .admin .jwt_secret .clone_from(¤t.admin.jwt_secret); } // 密码修改:前端发 old_password + new_password if !new_config.admin.old_password.is_empty() || !new_config.admin.new_password.is_empty() { if new_config.admin.old_password.is_empty() || new_config.admin.new_password.is_empty() { return error_response( StatusCode::BAD_REQUEST, "修改密码需要同时提供旧密码和新密码", ); } if !bcrypt::verify(&new_config.admin.old_password, ¤t.admin.password_hash) .unwrap_or(false) { return error_response(StatusCode::BAD_REQUEST, "旧密码不正确"); } new_config.admin.password_hash = super::store::hash_password(&new_config.admin.new_password); new_config.admin.jwt_secret = super::store::generate_hex_secret(); new_config.admin.jwt_issued_at += 1; } } // Persist { let mut guard = state.config.write().await; *guard = new_config.clone(); if let Err(e) = guard.save(&state.config_path) { return error_response( StatusCode::INTERNAL_SERVER_ERROR, &format!("保存失败: {}", e), ); } } // Hot-reload: sync accounts from the new config state .adapter .sync_accounts(&new_config.ds_core.accounts) .await; json_response(&serde_json::json!({"ok": true})) } #[derive(Deserialize)] pub struct LogsQuery { #[serde(default = "default_limit")] pub limit: usize, } fn default_limit() -> usize { 50 } /// GET /admin/api/logs — 获取最近的请求日志 pub(crate) async fn admin_logs( Query(query): Query, State(state): State, ) -> Response { let logs = state.stats.recent_logs(query.limit); json_response(&logs) } #[derive(Deserialize)] pub struct RuntimeLogsQuery { #[serde(default)] pub offset: usize, #[serde(default = "default_runtime_limit")] pub limit: usize, } fn default_runtime_limit() -> usize { 100 } /// GET /admin/api/runtime-logs — 分页查询运行日志 pub(crate) async fn admin_runtime_logs(Query(query): Query) -> Response { let (total, logs) = super::runtime_log::query_logs(query.offset, query.limit).await; json_response(&serde_json::json!({ "total": total, "offset": query.offset, "limit": query.limit, "logs": logs, })) } // ── Helpers ────────────────────────────────────────────────────────────── fn json_response(data: &T) -> Response { let bytes = serde_json::to_vec(data).unwrap(); Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/json") .body(Body::from(bytes)) .unwrap() } fn error_response(status: StatusCode, message: &str) -> Response { let body = serde_json::json!({"error": message}); let bytes = serde_json::to_vec(&body).unwrap(); Response::builder() .status(status) .header(header::CONTENT_TYPE, "application/json") .body(Body::from(bytes)) .unwrap() }