feat: Phase 5-6 - Proxy Conductor, Token Router, Services, Dockerfile

- Proxy Conductor: retry/fallback/auth-refresh orchestration loop
- Token Router: exact/wildcard/regex model routing with channel selection
- Services: Cron scheduler, OAuth token refresh, Update center, Announcements
- User routes: profile + API key management
- DB queries module
- Dockerfile: multi-stage build
- Proxy routes integrated with Conductor + TokenRouter
This commit is contained in:
boos
2026-05-18 03:00:09 +08:00
parent 78d46eeddb
commit 56391f2193
75 changed files with 7428 additions and 464 deletions
+92 -36
View File
@@ -5,15 +5,15 @@
// The /api/comfyui/proxy/* route serves static assets (public).
use crate::{
auth::api_key::{ authorize_downstream_token, DownstreamAuthResult },
error::{ AppError, AppResult },
auth::api_key::{authorize_downstream_token, DownstreamAuthResult},
error::{AppError, AppResult},
AppState,
};
use axum::{
extract::{ Path, Query, State },
http::{ HeaderMap, StatusCode },
response::{ Html, IntoResponse, Response },
routing::{ get, post },
extract::{Path, Query, State},
http::{HeaderMap, StatusCode},
response::{Html, IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use reqwest::Client;
@@ -49,7 +49,8 @@ async fn check_auth(state: &AppState, headers: &HeaderMap) -> Result<(), AppErro
// Downstream API key
if auth.starts_with("sk-") {
let result = authorize_downstream_token(auth, &state.db).await
let result = authorize_downstream_token(auth, &state.db)
.await
.map_err(|e| AppError::Unauthorized(e))?;
match result {
@@ -123,10 +124,7 @@ async fn proxy_get_history_by_id(
}
/// GET /api/comfyui/queue
async fn proxy_get_queue(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Response> {
async fn proxy_get_queue(State(state): State<AppState>, headers: HeaderMap) -> AppResult<Response> {
info!("GET /api/comfyui/queue");
check_auth(&state, &headers).await?;
proxy_to_comfyui("GET", "/queue", None, None).await
@@ -149,9 +147,7 @@ async fn proxy_get_view(
// ── Static proxy (public — assets/JS/CSS for iframe) ─────────────────
/// GET /api/comfyui/proxy/{path} — proxy everything (HTML, assets)
async fn proxy_static(
Path(path): Path<String>,
) -> AppResult<Response> {
async fn proxy_static(Path(path): Path<String>) -> AppResult<Response> {
info!("GET /api/comfyui/proxy/{}", path);
let target_path = if path.is_empty() || path == "/" {
"/".to_string()
@@ -161,18 +157,24 @@ async fn proxy_static(
let client = http_client();
let url = format!("{}{}", comfyui_base(), target_path);
let resp = client.get(&url).send().await
let resp = client
.get(&url)
.send()
.await
.map_err(|e| AppError::UpstreamError(format!("ComfyUI unreachable: {e}")))?;
let status = resp.status();
let content_type = resp.headers()
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream")
.to_string();
if content_type.contains("html") {
let text = resp.text().await
let text = resp
.text()
.await
.map_err(|e| AppError::UpstreamError(e.to_string()))?;
// Rebrand: ComfyUI → BoosAPI
let rebranded = text
@@ -180,21 +182,41 @@ async fn proxy_static(
.replace("comfyui", "BoosAPI");
// Rewrite URLs
let rewritten = rewrite_proxy_urls(&rebranded);
Ok((StatusCode::from_u16(status.as_u16()).unwrap(), [(axum::http::header::CONTENT_TYPE, "text/html; charset=utf-8")], rewritten).into_response())
Ok((
StatusCode::from_u16(status.as_u16()).unwrap(),
[(axum::http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
rewritten,
)
.into_response())
} else if content_type.contains("text") || content_type.contains("json") {
let text = resp.text().await
let text = resp
.text()
.await
.map_err(|e| AppError::UpstreamError(e.to_string()))?;
Ok((StatusCode::from_u16(status.as_u16()).unwrap(), [(axum::http::header::CONTENT_TYPE, content_type)], text).into_response())
Ok((
StatusCode::from_u16(status.as_u16()).unwrap(),
[(axum::http::header::CONTENT_TYPE, content_type)],
text,
)
.into_response())
} else {
let bytes = resp.bytes().await
let bytes = resp
.bytes()
.await
.map_err(|e| AppError::UpstreamError(e.to_string()))?;
Ok((StatusCode::from_u16(status.as_u16()).unwrap(), [(axum::http::header::CONTENT_TYPE, content_type)], bytes).into_response())
Ok((
StatusCode::from_u16(status.as_u16()).unwrap(),
[(axum::http::header::CONTENT_TYPE, content_type)],
bytes,
)
.into_response())
}
}
/// GET /comfyui — HTML page with iframe
async fn comfyui_page() -> Html<&'static str> {
Html(r##"<!DOCTYPE html>
Html(
r##"<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
@@ -209,7 +231,8 @@ async fn comfyui_page() -> Html<&'static str> {
<body>
<iframe src="/api/comfyui/proxy/" title="BoosAPI ComfyUI"></iframe>
</body>
</html>"##)
</html>"##,
)
}
// ── Helpers ──────────────────────────────────────────────────────────
@@ -225,12 +248,23 @@ fn rewrite_proxy_urls(html: &str) -> String {
// Look ahead for rc="/", ref="/", ction="/"
let snippet: String = chars.clone().take(6).map(|(_, c)| c).collect();
let full = format!("{}{}", ch, snippet);
if full.starts_with("src=\"/") || full.starts_with("href=\"/") || full.starts_with("action=\"/") {
let attr_end = if full.starts_with("src=\"") { 4 } else if full.starts_with("href=\"") { 5 } else { 7 };
if full.starts_with("src=\"/")
|| full.starts_with("href=\"/")
|| full.starts_with("action=\"/")
{
let attr_end = if full.starts_with("src=\"") {
4
} else if full.starts_with("href=\"") {
5
} else {
7
};
result.push_str(&full[..attr_end]);
result.push_str("/api/comfyui/proxy/");
// Skip the rest of the attribute
for _ in 0..snippet.len() { chars.next(); }
for _ in 0..snippet.len() {
chars.next();
}
continue;
}
}
@@ -275,22 +309,34 @@ async fn proxy_to_comfyui(
let req = match method {
"POST" => {
let body_str = body.map(|b| b.to_string()).unwrap_or_else(|| "{}".to_string());
client.post(&url)
let body_str = body
.map(|b| b.to_string())
.unwrap_or_else(|| "{}".to_string());
client
.post(&url)
.header("Content-Type", "application/json")
.body(body_str)
}
_ => client.get(&url),
};
let resp = req.send().await
let resp = req
.send()
.await
.map_err(|e| AppError::UpstreamError(format!("ComfyUI unreachable: {e}")))?;
let status = resp.status();
let text = resp.text().await
let text = resp
.text()
.await
.map_err(|e| AppError::UpstreamError(e.to_string()))?;
Ok((StatusCode::from_u16(status.as_u16()).unwrap(), [(axum::http::header::CONTENT_TYPE, "application/json")], text).into_response())
Ok((
StatusCode::from_u16(status.as_u16()).unwrap(),
[(axum::http::header::CONTENT_TYPE, "application/json")],
text,
)
.into_response())
}
/// Proxy to ComfyUI — returns binary response (for images)
@@ -299,17 +345,27 @@ async fn proxy_to_comfyui_binary(_method: &str, path: &str) -> AppResult<Respons
let url = format!("{}{}", comfyui_base(), path);
let req = client.get(&url);
let resp = req.send().await
let resp = req
.send()
.await
.map_err(|e| AppError::UpstreamError(format!("ComfyUI unreachable: {e}")))?;
let status = resp.status();
let content_type = resp.headers()
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream")
.to_string();
let bytes = resp.bytes().await
let bytes = resp
.bytes()
.await
.map_err(|e| AppError::UpstreamError(e.to_string()))?;
Ok((StatusCode::from_u16(status.as_u16()).unwrap(), [(axum::http::header::CONTENT_TYPE, content_type)], bytes).into_response())
Ok((
StatusCode::from_u16(status.as_u16()).unwrap(),
[(axum::http::header::CONTENT_TYPE, content_type)],
bytes,
)
.into_response())
}