feat: cache remote picture templates in database

This commit is contained in:
boos
2026-07-14 03:42:34 +08:00
parent bdd26a5e43
commit 379dfd7b11
5 changed files with 142 additions and 52 deletions
+3
View File
@@ -276,6 +276,7 @@ async fn main() -> anyhow::Result<()> {
let state = AppState::new_with_redis(config.clone(), pool, builtin, runtime_auth, redis);
let payment_job_worker = boosapi::services::payment_jobs::spawn(state.clone());
let account_health_worker = boosapi::services::account_health::spawn(state.clone());
let picture_template_worker = boosapi::routes::spawn_template_sync(state.clone());
if let Err(err) = state.comfyui.start().await {
tracing::warn!(%err, "ComfyUI embedded process did not start");
}
@@ -301,6 +302,8 @@ async fn main() -> anyhow::Result<()> {
}
account_health_worker.abort();
let _ = account_health_worker.await;
picture_template_worker.abort();
let _ = picture_template_worker.await;
if let Err(err) = comfyui_process.stop().await {
tracing::warn!(%err, "ComfyUI embedded process did not stop cleanly");
+1
View File
@@ -42,6 +42,7 @@ mod payment;
mod payment_providers;
mod payment_refunds;
mod picture_agent;
pub use picture_agent::spawn_template_sync;
mod profile_avatar;
mod proxy;
mod search;
+88 -14
View File
@@ -25,6 +25,10 @@ const TEMPLATE_GALLERY_PAGE_URL: &str =
"https://weiyyin.github.io/linux-do-gpt-image/prompt_gallery.html";
const TEMPLATE_DATA_URL: &str =
"https://weiyyin.github.io/linux-do-gpt-image/linux_do_2036927_prompts_images.with_tags.json";
const TEMPLATE_CACHE_KEY: &str = "picture_agent.linux_do_templates";
const TEMPLATE_REFRESH_INTERVAL: Duration = Duration::from_secs(6 * 60 * 60);
const TEMPLATE_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
const TEMPLATE_MAX_BYTES: usize = 16 * 1024 * 1024;
const TASK_TYPE_PICTURE_GENERATION: &str = "picture-agent-generation";
const PICTURE_GENERATION_TIMEOUT_SECS: u64 = 300;
const PICTURE_GENERATION_RETRY_SECS: u64 = 180;
@@ -58,6 +62,21 @@ pub fn routes() -> Router<AppState> {
.route("/api/picture-agent/images/edits", post(images_edits))
}
pub fn spawn_template_sync(state: AppState) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
refresh_template_cache(&state).await;
let mut ticker = tokio::time::interval_at(
tokio::time::Instant::now() + TEMPLATE_REFRESH_INTERVAL,
TEMPLATE_REFRESH_INTERVAL,
);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
ticker.tick().await;
refresh_template_cache(&state).await;
}
})
}
#[derive(Debug, Deserialize)]
struct TemplateQuery {
limit: Option<usize>,
@@ -435,38 +454,93 @@ async fn templates_handler(
) -> AppResult<Json<Value>> {
let _auth = resolve_picture_agent_auth(&headers, &state).await?;
let limit = query.limit.unwrap_or(144).clamp(1, 240);
let (templates, remote_status) = match fetch_gallery_templates(&state.http, limit).await {
Ok(templates) => (templates, json!({ "ok": true, "source": "remote-json" })),
Err(err) => {
tracing::warn!(%err, "Picture Agent remote Linux Do template gallery unavailable");
(Vec::new(), json!({ "ok": false, "message": err }))
let (templates, cache_status) = match cached_template_json(&state).await {
Ok(Some(raw)) => match parse_gallery_templates(&raw, limit) {
Ok(templates) => (templates, json!({ "ok": true, "source": "database-cache" })),
Err(err) => (Vec::new(), json!({ "ok": false, "message": err })),
},
Ok(None) => {
refresh_template_cache(&state).await;
match cached_template_json(&state).await {
Ok(Some(raw)) => match parse_gallery_templates(&raw, limit) {
Ok(templates) => (
templates,
json!({ "ok": true, "source": "database-cache" }),
),
Err(err) => (Vec::new(), json!({ "ok": false, "message": err })),
},
Ok(None) => (
Vec::new(),
json!({ "ok": false, "message": "template cache is empty" }),
),
Err(err) => (Vec::new(), json!({ "ok": false, "message": err })),
}
}
Err(err) => (Vec::new(), json!({ "ok": false, "message": err })),
};
Ok(Json(json!({
"success": true,
"sourceUrl": TEMPLATE_GALLERY_PAGE_URL,
"source": remote_status,
"source": cache_status,
"templates": templates,
})))
}
async fn fetch_gallery_templates(
client: &reqwest::Client,
limit: usize,
) -> Result<Vec<Value>, String> {
let raw = client
async fn refresh_template_cache(state: &AppState) {
match fetch_template_json(state).await {
Ok(raw) => {
let result = crate::db::sql::query(
"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",
)
.bind(TEMPLATE_CACHE_KEY)
.bind(raw)
.execute(&*state.db)
.await;
if let Err(err) = result {
tracing::warn!(%err, "failed to persist Picture Agent template cache");
} else {
tracing::info!("Picture Agent template cache refreshed");
}
}
Err(err) => {
tracing::warn!(%err, "Picture Agent template refresh failed; keeping old cache")
}
}
}
async fn fetch_template_json(state: &AppState) -> Result<String, String> {
let response = state
.http
.get(TEMPLATE_DATA_URL)
.timeout(TEMPLATE_REQUEST_TIMEOUT)
.send()
.await
.map_err(|err| format!("failed to fetch remote Linux Do templates: {err}"))?
.error_for_status()
.map_err(|err| format!("remote Linux Do templates returned an error: {err}"))?
.text()
.map_err(|err| format!("remote Linux Do templates returned an error: {err}"))?;
let bytes = response
.bytes()
.await
.map_err(|err| format!("failed to read remote Linux Do templates: {err}"))?;
if bytes.len() > TEMPLATE_MAX_BYTES {
return Err(format!(
"remote Linux Do templates exceed {TEMPLATE_MAX_BYTES} bytes"
));
}
String::from_utf8(bytes.to_vec())
.map_err(|err| format!("remote Linux Do templates are not UTF-8: {err}"))
}
parse_gallery_templates(&raw, limit)
async fn cached_template_json(state: &AppState) -> Result<Option<String>, String> {
crate::db::sql::query_scalar::<_, Option<String>>(
"SELECT value FROM settings WHERE key = ? LIMIT 1",
)
.bind(TEMPLATE_CACHE_KEY)
.fetch_optional(&*state.db)
.await
.map(|value| value.flatten())
.map_err(|err| format!("failed to read Picture Agent template cache: {err}"))
}
fn parse_gallery_templates(raw: &str, limit: usize) -> Result<Vec<Value>, String> {