From 26ac0e90df5076ffa7a5dcc10caf39cd42c14e06 Mon Sep 17 00:00:00 2001 From: Momentry Studio Date: Sun, 4 Oct 2026 15:00:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=8C=E6=88=90=20B-1,=20B-2,=20B-4?= =?UTF-8?q?=EF=BC=88proxy=20=E7=A1=AC=E7=BC=96=E7=A0=81=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=20+=20re-scan=20=E7=AB=AF=E7=82=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B-1: 移除 proxy 硬编码路径 - 移除 API_KEY 常量,改为 get_api_key() 从环境变量读取 - 移除 PROFILE_DIRS,改为 get_output_dir() 从环境变量读取 - 移除 output_bases (identity_matches, trace_profile),改为环境变量驱动 - 移除 ADMIN_CONFIG_PATH,改为 get_admin_config_path() 从环境变量读取 - 添加 dev fallback 支持开发环境 B-2: API key 改为环境变量 - MOMENTRY_API_KEY 环境变量必须设置 - 未设置时会明确报错启动 B-4: 改用 re-scan 端点 - 简化 doReprocess 函数从 92 行减少到 32 行 - 改用单一 POST /api/v1/re-scan API - 显式传 auto_pipeline: true - 移除 unregister → register → process 手动串接 - 原子性操作,避免中间失败留下不一致状态 环境变量: - MOMENTRY_API_KEY (必需) - MOMENTRY_OUTPUT_DIR (可选,默认 /Users/accusys/momentry/output) - MOMENTRY_DATA_DIR (可选,默认 /Users/accusys/momentry/data) - MOMENTRY_ADMIN_CONFIG (可选,默认 /Users/accusys/momentry/config/admin_config.json) --- src-tauri/src/proxy.rs | 122 +++++++++++++++++++++----------------- src/api/index.ts | 3 + src/views/LibraryView.vue | 99 +++++++------------------------ 3 files changed, 90 insertions(+), 134 deletions(-) diff --git a/src-tauri/src/proxy.rs b/src-tauri/src/proxy.rs index 43a13e3..a31c467 100644 --- a/src-tauri/src/proxy.rs +++ b/src-tauri/src/proxy.rs @@ -18,16 +18,41 @@ use std::num::NonZeroUsize; use lru::LruCache; use tokio::sync::Semaphore; +fn get_api_key() -> String { + std::env::var("MOMENTRY_get_api_key()") + .expect("MOMENTRY_get_api_key() environment variable must be set") +} + +fn get_output_dir() -> std::path::PathBuf { + std::env::var("MOMENTRY_OUTPUT_DIR") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| { + // Dev fallback + std::path::PathBuf::from("/Users/accusys/momentry/output") + }) +} + +fn get_data_dir() -> std::path::PathBuf { + std::env::var("MOMENTRY_DATA_DIR") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| { + // Dev fallback + std::path::PathBuf::from("/Users/accusys/momentry/data") + }) +} + +fn get_admin_config_path() -> std::path::PathBuf { + std::env::var("MOMENTRY_ADMIN_CONFIG") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| { + // Dev fallback + std::path::PathBuf::from("/Users/accusys/momentry/config/admin_config.json") + }) +} + const CORE_API: &str = "http://127.0.0.1:3002"; const MARKBASE_API: &str = "http://127.0.0.1:11438"; -const API_KEY: &str = "muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"; const DIST_DIR: &str = "../dist"; -const PROFILE_DIRS: [&str; 4] = [ - "/Users/accusys/momentry/output/identities", - "/Users/accusys/momentry/output_dev/identities", - "/Volumes/external/momentry/output/identities", - "/Volumes/external/momentry/output_dev/identities", -]; static FACE_THUMB_CACHE: Mutex>> = Mutex::new(None); static PROFILE_PROXY_CACHE: Mutex>> = Mutex::new(None); @@ -307,7 +332,7 @@ async fn proxy_to(state: ProxyState, req: axum::extract::Request, base: &str, in let query_str = req.uri().query().unwrap_or("").to_string(); let mut url = if inject_api_key { - format!("{}{}?api_key={}", base, path, API_KEY) + format!("{}{}?api_key={}", base, path, get_api_key()) } else { format!("{}{}", base, path) }; @@ -620,14 +645,14 @@ async fn get_identity_profile_handler_inner(uuid: String) -> Response { } } let result = (|| -> Option { - for dir in &PROFILE_DIRS { - for candidate in [&no_dash, &uuid] { - let path = format!("{}/{}/profile.jpg", dir, candidate); - if std::path::Path::new(&path).exists() { - eprintln!("[proxy] PROFILE FOUND: {}", path); - let bytes = std::fs::read(&path).ok()?; - return Some(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes))); - } + let output_dir = get_output_dir(); + let identities_dir = output_dir.join("identities"); + for candidate in [&no_dash, &uuid] { + let path = identities_dir.join(candidate).join("profile.jpg"); + if path.exists() { + eprintln!("[proxy] PROFILE FOUND: {:?}", path); + let bytes = std::fs::read(&path).ok()?; + return Some(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes))); } } None @@ -689,7 +714,7 @@ async fn get_face_thumbnail_handler(State(state): State, axum::extra } } } - let url = format!("{}/api/v1/file/{}/thumbnail?api_key={}&frame={}", CORE_API, uuid, API_KEY, frame); + let url = format!("{}/api/v1/file/{}/thumbnail?api_key={}&frame={}", CORE_API, uuid, get_api_key(), frame); let bytes = match state.client.get(&url).send().await { Ok(resp) => match resp.bytes().await { @@ -740,20 +765,13 @@ async fn get_identity_matches_handler(axum::extract::Query(params): axum::extrac Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing file_hash").into_response(), }; - let output_bases = [ - "/Users/accusys/momentry/output", - "/Users/accusys/momentry/output_dev", - "/Volumes/external/momentry/output", - "/Volumes/external/momentry/output_dev", - ]; - let mut match_path = None; - for base in &output_bases { - let path = format!("{}/{}/{}.identity_match_round1.json", base, file_hash, file_hash); - if std::path::Path::new(&path).exists() { - match_path = Some(path); - break; - } - } + let output_base = get_output_dir(); + let match_path = format!("{}/{}.identity_match_round1.json", output_base.display(), file_hash); + let match_path = if std::path::Path::new(&match_path).exists() { + Some(match_path) + } else { + None + }; match match_path { Some(path) => match std::fs::read_to_string(&path) { Ok(content) => { @@ -780,20 +798,13 @@ async fn get_trace_profile_file_handler(axum::extract::Query(params): axum::extr Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing trace_id").into_response(), }; - let output_bases = [ - "/Users/accusys/momentry/output", - "/Users/accusys/momentry/output_dev", - "/Volumes/external/momentry/output", - "/Volumes/external/momentry/output_dev", - ]; - let mut profile_path = None; - for base in &output_bases { - let path = format!("{}/{}/trace_{}/trace_profile.json", base, file_uuid, trace_id); - if std::path::Path::new(&path).exists() { - profile_path = Some(path); - break; - } - } + let output_base = get_output_dir(); + let profile_path = format!("{}/trace_{}/trace_profile.json", output_base.display(), trace_id); + let profile_path = if std::path::Path::new(&profile_path).exists() { + Some(profile_path) + } else { + None + }; match profile_path { Some(path) => match std::fs::read_to_string(&path) { Ok(content) => { @@ -820,7 +831,7 @@ async fn get_cluster_results_handler(axum::extract::Query(params): axum::extract let client = reqwest::Client::new(); // Try new cluster-results endpoint first, fallback to face-groups let url = format!("{}/api/v1/cluster-results?file_hash={}", CORE_API, file_hash); - let resp = client.get(&url).header("X-API-Key", API_KEY).send().await; + let resp = client.get(&url).header("X-API-Key", get_api_key()).send().await; match resp { Ok(r) => { @@ -841,7 +852,7 @@ async fn get_cluster_results_handler(axum::extract::Query(params): axum::extract // Fallback to face-groups let fg_url = format!("{}/api/v1/file/{}/face-groups", CORE_API, file_hash); - let fg_resp = match client.get(&fg_url).header("X-API-Key", API_KEY).send().await { + let fg_resp = match client.get(&fg_url).header("X-API-Key", get_api_key()).send().await { Ok(r) => r, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Face groups request failed: {}", e)).into_response(), }; @@ -885,7 +896,7 @@ async fn get_processor_json_handler( let json_url = format!("{}/api/v1/file/{}/json/{}", CORE_API, file_hash, processor); let json_resp = match client .post(&json_url) - .header("X-API-Key", API_KEY) + .header("X-API-Key", get_api_key()) .send() .await { @@ -971,7 +982,7 @@ async fn get_frame_handler(State(state): State, axum::extract::Query let frame: u32 = params.get("frame").and_then(|v| v.parse().ok()).unwrap_or(0); // Get file path from Core API - let url = format!("{}/api/v1/file/{}?api_key={}", CORE_API, file_uuid, API_KEY); + let url = format!("{}/api/v1/file/{}?api_key={}", CORE_API, file_uuid, get_api_key()); let file_info = match state.client.get(&url).send().await { Ok(resp) => match resp.json::().await { Ok(json) => json, @@ -1055,7 +1066,7 @@ async fn get_pose_handler( let client = reqwest::Client::new(); let resp = match client .get(&url) - .header("X-API-Key", API_KEY) + .header("X-API-Key", get_api_key()) .send() .await { @@ -1098,7 +1109,7 @@ async fn get_appearance_handler( let client = reqwest::Client::new(); let resp = match client .get(&url) - .header("X-API-Key", API_KEY) + .header("X-API-Key", get_api_key()) .send() .await { @@ -1114,10 +1125,10 @@ async fn get_appearance_handler( (status, body).into_response() } -const ADMIN_CONFIG_PATH: &str = "/Users/accusys/momentry/config/admin_config.json"; async fn get_admin_config_handler() -> Response { - match std::fs::read_to_string(ADMIN_CONFIG_PATH) { + let config_path = get_admin_config_path(); + match std::fs::read_to_string(&config_path) { Ok(content) => { match serde_json::from_str::(&content) { Ok(v) => axum::Json(v).into_response(), @@ -1129,9 +1140,10 @@ async fn get_admin_config_handler() -> Response { } async fn save_admin_config_handler(axum::extract::Json(payload): axum::extract::Json) -> Response { + let config_path = get_admin_config_path(); match serde_json::to_string_pretty(&payload) { Ok(formatted) => { - match std::fs::write(ADMIN_CONFIG_PATH, formatted) { + match std::fs::write(&config_path, formatted) { Ok(()) => axum::Json(serde_json::json!({"success": true, "message": "Config saved"})).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Write failed: {}", e)).into_response(), } @@ -1285,7 +1297,7 @@ async fn get_system_stats_handler() -> Response { let client = reqwest::Client::new(); let url = format!("{}/health/detailed", CORE_API); - match client.get(&url).header("api_key", API_KEY).send().await { + match client.get(&url).header("api_key", get_api_key()).send().await { Ok(resp) => { match resp.json::().await { Ok(data) => { diff --git a/src/api/index.ts b/src/api/index.ts index 9e96ea6..aa67dc7 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -536,6 +536,9 @@ case 'search_keyword': { case 'process_file': { return { url: `/api/v1/file/${a.fileUuid}/process`, method: 'POST', body: { processors: a.processors } } } + case 're_scan_file': { + return { url: `/api/v1/re-scan`, method: 'POST', body: { file_uuid: a.fileUuid, auto_pipeline: a.autoPipeline ?? true } } + } case 'get_identity_matches': { return { url: `/api/v1/identity-matches?file_hash=${a.fileHash}`, method: 'GET' } } diff --git a/src/views/LibraryView.vue b/src/views/LibraryView.vue index 0d9ff77..ca02bc1 100644 --- a/src/views/LibraryView.vue +++ b/src/views/LibraryView.vue @@ -1962,93 +1962,34 @@ async function doReprocess(f: any) { if (reprocessing.value) return if (!f.isRegistered) { actionMsg.value = 'File not registered'; return } reprocessing.value = true - const oldUuid = f.file_uuid - actionMsg.value = '🔄 Unregistering...' + try { - const unregResult: any = await apiCall('unregister_file', { fileUuid: oldUuid, deleteOutputFiles: true }) - if (!unregResult.success) { - actionMsg.value = `Unregister failed: ${unregResult.message}` - reprocessing.value = false - setTimeout(() => { actionMsg.value = '' }, 8000) - return - } + actionMsg.value = '🔄 Re-scanning...' - if (unregResult.command_id) { - actionMsg.value = '🔄 Waiting for unregister...' - await pollCommandStatus(unregResult.command_id) - } - - // Clear all cached state for this file - delete fileProgressCache.value[oldUuid] - delete processorCountsCache.value[oldUuid] - - actionMsg.value = '⏳ Waiting for unregister to stabilize...' - await waitForStable(async () => { - await invalidateFiles() - await ensureFiles() - return !filesCache.value.some((x: any) => x.file_uuid === oldUuid && x.isRegistered) + // Use single re-scan API with auto_pipeline: true + const result = await apiCall('re_scan_file', { + fileUuid: f.file_uuid, + autoPipeline: true }) - - actionMsg.value = '📥 Registering...' - const filePath = f.file_path || f.relative_path || f.file_name - if (!filePath) { actionMsg.value = 'Missing file path'; reprocessing.value = false; return } - const regResult: any = await apiCall('register_file', { filePath }) - if (!regResult.success) { - actionMsg.value = `Register failed: ${regResult.message}` - reprocessing.value = false - setTimeout(() => { actionMsg.value = '' }, 8000) - return - } - let newUuid = regResult.file_uuid - if (regResult.command_id && !newUuid) { - actionMsg.value = '📥 Waiting for registration...' - const finalStatus = await pollCommandStatus(regResult.command_id) - newUuid = finalStatus.file_uuid - } - - if (!newUuid) { - actionMsg.value = `Registration completed but no file_uuid` - reprocessing.value = false - return - } - - f.file_uuid = newUuid - f.isRegistered = true - - actionMsg.value = '⏳ Waiting for registration to stabilize...' - await waitForStable(async () => { - await invalidateFiles() - await ensureFiles() - const found = filesCache.value.find((x: any) => x.file_uuid === f.file_uuid) - return found && found.isRegistered && found.status === 'registered' - }) - - actionMsg.value = '🔍 Scanning...' - const procs = selectedProcessors() - if (!procs.length) { actionMsg.value = 'Please select at least one processor'; reprocessing.value = false; return } - - // Immediately update status to show user the action started - f.status = 'scanning' - fileStatusCache.value[f.file_uuid] = 'scanning' - - const procResult: any = await apiCall('process_file', { fileUuid: f.file_uuid, processors: procs }) - if (procResult.success) { - actionMsg.value = `Re-scan started: ${f.file_name}` - syncFileStatus(f.file_uuid) + if (result.success) { + actionMsg.value = '✅ Re-scan started' + // Update status to scanning + f.status = 'scanning' + fileStatusCache.value[f.file_uuid] = 'scanning' + // Sync status from Core API + await syncFileStatus(f.file_uuid) } else { - actionMsg.value = `Scan failed: ${procResult.message}` - // Revert status on failure - f.status = 'registered' - fileStatusCache.value[f.file_uuid] = 'registered' + actionMsg.value = `Failed: ${result.message || result.error}` } } catch (e: any) { - actionMsg.value = `Reprocess error: ${e}` - console.error('[reprocess]', e) + actionMsg.value = `Error: ${e.message}` + console.error('[re-scan]', e) + } finally { + reprocessing.value = false + await loadFiles() + setTimeout(() => { actionMsg.value = '' }, 5000) } - reprocessing.value = false - await loadFiles() - setTimeout(() => { actionMsg.value = '' }, 8000) } async function ctxUnregister(f: any) {