forked from boos/BoosAPI
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:
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user