feat(pixelle): 连接凭证 admin 端点(GET/PUT 掩码 + 重启)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
boos
2026-06-18 11:57:23 +08:00
parent 55188e51ff
commit 92d38bb309
3 changed files with 88 additions and 0 deletions
+5
View File
@@ -21,6 +21,7 @@ mod compat;
mod downstream_keys;
mod model_candidates;
pub(crate) mod models_marketplace;
mod pixelle_connection;
mod sites;
mod tokens;
mod users;
@@ -191,6 +192,10 @@ pub fn routes(state: AppState) -> Router<AppState> {
"/api/admin/user-balances",
get(users::list_user_balances),
)
.route(
"/api/admin/pixelle/connection",
get(pixelle_connection::get_connection).put(pixelle_connection::put_connection),
)
.route(
"/api/admin/users/{id}/balance/adjust",
post(users::adjust_user_balance),
+67
View File
@@ -0,0 +1,67 @@
use crate::error::AppResult;
use crate::services::pixelle_connection::{self, PixelleConnection};
use crate::AppState;
use axum::{extract::State, Json};
use serde::Deserialize;
use serde_json::{json, Value};
const MASK_SENTINEL: &str = "••••";
fn mask(v: &Option<String>) -> Value {
match v {
Some(s) if s.chars().count() > 4 => {
let last4: String = s.chars().rev().take(4).collect::<Vec<_>>().into_iter().rev().collect();
json!(format!("{}{}", MASK_SENTINEL, last4))
}
Some(s) if !s.is_empty() => json!(MASK_SENTINEL),
_ => Value::Null,
}
}
pub async fn get_connection(State(state): State<AppState>) -> AppResult<Json<Value>> {
let c = pixelle_connection::read_connection(&state.db).await?;
Ok(Json(json!({
"success": true,
"connection": {
"comfyuiUrl": c.comfyui_url,
"runninghubApiKey": mask(&c.runninghub_api_key),
"comfyuiApiKey": mask(&c.comfyui_api_key),
}
})))
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdatePayload {
runninghub_api_key: Option<String>,
comfyui_url: Option<String>,
comfyui_api_key: Option<String>,
}
fn keep_if_not_mask(incoming: Option<String>) -> Option<String> {
match incoming {
Some(s) if s.starts_with(MASK_SENTINEL) => None,
other => other,
}
}
pub async fn put_connection(
State(state): State<AppState>,
Json(p): Json<UpdatePayload>,
) -> AppResult<Json<Value>> {
let conn = PixelleConnection {
runninghub_api_key: keep_if_not_mask(p.runninghub_api_key),
comfyui_url: p.comfyui_url,
comfyui_api_key: keep_if_not_mask(p.comfyui_api_key),
};
pixelle_connection::save_connection(&state.db, &conn).await?;
let backup = pixelle_connection::sync_to_pixelle(&state.config.pixelle_video.config_path, &conn)?;
let restart = pixelle_connection::restart_pixelle(&state.config.pixelle_video.compose_dir).await;
match restart {
Ok(()) => Ok(Json(json!({ "success": true, "backup": backup, "restarted": true }))),
Err(e) => Ok(Json(json!({
"success": true, "backup": backup, "restarted": false,
"warning": format!("配置已写入,但 Pixelle 重启失败,请手动重启: {e}")
}))),
}
}
+16
View File
@@ -71,6 +71,22 @@ pub fn sync_to_pixelle(config_path: &str, conn: &PixelleConnection) -> AppResult
Ok(backup)
}
pub async fn restart_pixelle(compose_dir: &str) -> AppResult<()> {
let output = tokio::process::Command::new("docker")
.args(["compose", "restart", "api"])
.current_dir(compose_dir)
.output()
.await
.map_err(|e| AppError::ServiceUnavailable(format!("docker compose restart 启动失败: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AppError::ServiceUnavailable(format!(
"Pixelle 重启失败: {}", stderr.trim()
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;