fix: 完成 B-1, B-2, B-4(proxy 硬编码移除 + re-scan 端点)
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)
This commit is contained in:
@@ -18,16 +18,41 @@ use std::num::NonZeroUsize;
|
|||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use tokio::sync::Semaphore;
|
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 CORE_API: &str = "http://127.0.0.1:3002";
|
||||||
const MARKBASE_API: &str = "http://127.0.0.1:11438";
|
const MARKBASE_API: &str = "http://127.0.0.1:11438";
|
||||||
const API_KEY: &str = "muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69";
|
|
||||||
const DIST_DIR: &str = "../dist";
|
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<Option<LruCache<String, String>>> = Mutex::new(None);
|
static FACE_THUMB_CACHE: Mutex<Option<LruCache<String, String>>> = Mutex::new(None);
|
||||||
static PROFILE_PROXY_CACHE: Mutex<Option<LruCache<String, String>>> = Mutex::new(None);
|
static PROFILE_PROXY_CACHE: Mutex<Option<LruCache<String, String>>> = 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 query_str = req.uri().query().unwrap_or("").to_string();
|
||||||
let mut url = if inject_api_key {
|
let mut url = if inject_api_key {
|
||||||
format!("{}{}?api_key={}", base, path, API_KEY)
|
format!("{}{}?api_key={}", base, path, get_api_key())
|
||||||
} else {
|
} else {
|
||||||
format!("{}{}", base, path)
|
format!("{}{}", base, path)
|
||||||
};
|
};
|
||||||
@@ -620,14 +645,14 @@ async fn get_identity_profile_handler_inner(uuid: String) -> Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let result = (|| -> Option<String> {
|
let result = (|| -> Option<String> {
|
||||||
for dir in &PROFILE_DIRS {
|
let output_dir = get_output_dir();
|
||||||
for candidate in [&no_dash, &uuid] {
|
let identities_dir = output_dir.join("identities");
|
||||||
let path = format!("{}/{}/profile.jpg", dir, candidate);
|
for candidate in [&no_dash, &uuid] {
|
||||||
if std::path::Path::new(&path).exists() {
|
let path = identities_dir.join(candidate).join("profile.jpg");
|
||||||
eprintln!("[proxy] PROFILE FOUND: {}", path);
|
if path.exists() {
|
||||||
let bytes = std::fs::read(&path).ok()?;
|
eprintln!("[proxy] PROFILE FOUND: {:?}", path);
|
||||||
return Some(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes)));
|
let bytes = std::fs::read(&path).ok()?;
|
||||||
}
|
return Some(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -689,7 +714,7 @@ async fn get_face_thumbnail_handler(State(state): State<ProxyState>, 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 {
|
let bytes = match state.client.get(&url).send().await {
|
||||||
Ok(resp) => match resp.bytes().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(),
|
Some(v) => v.clone(),
|
||||||
None => return (StatusCode::BAD_REQUEST, "Missing file_hash").into_response(),
|
None => return (StatusCode::BAD_REQUEST, "Missing file_hash").into_response(),
|
||||||
};
|
};
|
||||||
let output_bases = [
|
let output_base = get_output_dir();
|
||||||
"/Users/accusys/momentry/output",
|
let match_path = format!("{}/{}.identity_match_round1.json", output_base.display(), file_hash);
|
||||||
"/Users/accusys/momentry/output_dev",
|
let match_path = if std::path::Path::new(&match_path).exists() {
|
||||||
"/Volumes/external/momentry/output",
|
Some(match_path)
|
||||||
"/Volumes/external/momentry/output_dev",
|
} else {
|
||||||
];
|
None
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match match_path {
|
match match_path {
|
||||||
Some(path) => match std::fs::read_to_string(&path) {
|
Some(path) => match std::fs::read_to_string(&path) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
@@ -780,20 +798,13 @@ async fn get_trace_profile_file_handler(axum::extract::Query(params): axum::extr
|
|||||||
Some(v) => v.clone(),
|
Some(v) => v.clone(),
|
||||||
None => return (StatusCode::BAD_REQUEST, "Missing trace_id").into_response(),
|
None => return (StatusCode::BAD_REQUEST, "Missing trace_id").into_response(),
|
||||||
};
|
};
|
||||||
let output_bases = [
|
let output_base = get_output_dir();
|
||||||
"/Users/accusys/momentry/output",
|
let profile_path = format!("{}/trace_{}/trace_profile.json", output_base.display(), trace_id);
|
||||||
"/Users/accusys/momentry/output_dev",
|
let profile_path = if std::path::Path::new(&profile_path).exists() {
|
||||||
"/Volumes/external/momentry/output",
|
Some(profile_path)
|
||||||
"/Volumes/external/momentry/output_dev",
|
} else {
|
||||||
];
|
None
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match profile_path {
|
match profile_path {
|
||||||
Some(path) => match std::fs::read_to_string(&path) {
|
Some(path) => match std::fs::read_to_string(&path) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
@@ -820,7 +831,7 @@ async fn get_cluster_results_handler(axum::extract::Query(params): axum::extract
|
|||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
// Try new cluster-results endpoint first, fallback to face-groups
|
// Try new cluster-results endpoint first, fallback to face-groups
|
||||||
let url = format!("{}/api/v1/cluster-results?file_hash={}", CORE_API, file_hash);
|
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 {
|
match resp {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
@@ -841,7 +852,7 @@ async fn get_cluster_results_handler(axum::extract::Query(params): axum::extract
|
|||||||
|
|
||||||
// Fallback to face-groups
|
// Fallback to face-groups
|
||||||
let fg_url = format!("{}/api/v1/file/{}/face-groups", CORE_API, file_hash);
|
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,
|
Ok(r) => r,
|
||||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Face groups request failed: {}", e)).into_response(),
|
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_url = format!("{}/api/v1/file/{}/json/{}", CORE_API, file_hash, processor);
|
||||||
let json_resp = match client
|
let json_resp = match client
|
||||||
.post(&json_url)
|
.post(&json_url)
|
||||||
.header("X-API-Key", API_KEY)
|
.header("X-API-Key", get_api_key())
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -971,7 +982,7 @@ async fn get_frame_handler(State(state): State<ProxyState>, axum::extract::Query
|
|||||||
let frame: u32 = params.get("frame").and_then(|v| v.parse().ok()).unwrap_or(0);
|
let frame: u32 = params.get("frame").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||||
|
|
||||||
// Get file path from Core API
|
// 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 {
|
let file_info = match state.client.get(&url).send().await {
|
||||||
Ok(resp) => match resp.json::<serde_json::Value>().await {
|
Ok(resp) => match resp.json::<serde_json::Value>().await {
|
||||||
Ok(json) => json,
|
Ok(json) => json,
|
||||||
@@ -1055,7 +1066,7 @@ async fn get_pose_handler(
|
|||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let resp = match client
|
let resp = match client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.header("X-API-Key", API_KEY)
|
.header("X-API-Key", get_api_key())
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -1098,7 +1109,7 @@ async fn get_appearance_handler(
|
|||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let resp = match client
|
let resp = match client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.header("X-API-Key", API_KEY)
|
.header("X-API-Key", get_api_key())
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -1114,10 +1125,10 @@ async fn get_appearance_handler(
|
|||||||
|
|
||||||
(status, body).into_response()
|
(status, body).into_response()
|
||||||
}
|
}
|
||||||
const ADMIN_CONFIG_PATH: &str = "/Users/accusys/momentry/config/admin_config.json";
|
|
||||||
|
|
||||||
async fn get_admin_config_handler() -> Response {
|
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) => {
|
Ok(content) => {
|
||||||
match serde_json::from_str::<serde_json::Value>(&content) {
|
match serde_json::from_str::<serde_json::Value>(&content) {
|
||||||
Ok(v) => axum::Json(v).into_response(),
|
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<serde_json::Value>) -> Response {
|
async fn save_admin_config_handler(axum::extract::Json(payload): axum::extract::Json<serde_json::Value>) -> Response {
|
||||||
|
let config_path = get_admin_config_path();
|
||||||
match serde_json::to_string_pretty(&payload) {
|
match serde_json::to_string_pretty(&payload) {
|
||||||
Ok(formatted) => {
|
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(),
|
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(),
|
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 client = reqwest::Client::new();
|
||||||
let url = format!("{}/health/detailed", CORE_API);
|
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) => {
|
Ok(resp) => {
|
||||||
match resp.json::<serde_json::Value>().await {
|
match resp.json::<serde_json::Value>().await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
|
|||||||
@@ -536,6 +536,9 @@ case 'search_keyword': {
|
|||||||
case 'process_file': {
|
case 'process_file': {
|
||||||
return { url: `/api/v1/file/${a.fileUuid}/process`, method: 'POST', body: { processors: a.processors } }
|
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': {
|
case 'get_identity_matches': {
|
||||||
return { url: `/api/v1/identity-matches?file_hash=${a.fileHash}`, method: 'GET' }
|
return { url: `/api/v1/identity-matches?file_hash=${a.fileHash}`, method: 'GET' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1962,93 +1962,34 @@ async function doReprocess(f: any) {
|
|||||||
if (reprocessing.value) return
|
if (reprocessing.value) return
|
||||||
if (!f.isRegistered) { actionMsg.value = 'File not registered'; return }
|
if (!f.isRegistered) { actionMsg.value = 'File not registered'; return }
|
||||||
reprocessing.value = true
|
reprocessing.value = true
|
||||||
const oldUuid = f.file_uuid
|
|
||||||
actionMsg.value = '🔄 Unregistering...'
|
|
||||||
try {
|
try {
|
||||||
const unregResult: any = await apiCall('unregister_file', { fileUuid: oldUuid, deleteOutputFiles: true })
|
actionMsg.value = '🔄 Re-scanning...'
|
||||||
if (!unregResult.success) {
|
|
||||||
actionMsg.value = `Unregister failed: ${unregResult.message}`
|
|
||||||
reprocessing.value = false
|
|
||||||
setTimeout(() => { actionMsg.value = '' }, 8000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unregResult.command_id) {
|
// Use single re-scan API with auto_pipeline: true
|
||||||
actionMsg.value = '🔄 Waiting for unregister...'
|
const result = await apiCall('re_scan_file', {
|
||||||
await pollCommandStatus(unregResult.command_id)
|
fileUuid: f.file_uuid,
|
||||||
}
|
autoPipeline: true
|
||||||
|
|
||||||
// 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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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 (result.success) {
|
||||||
if (regResult.command_id && !newUuid) {
|
actionMsg.value = '✅ Re-scan started'
|
||||||
actionMsg.value = '📥 Waiting for registration...'
|
// Update status to scanning
|
||||||
const finalStatus = await pollCommandStatus(regResult.command_id)
|
f.status = 'scanning'
|
||||||
newUuid = finalStatus.file_uuid
|
fileStatusCache.value[f.file_uuid] = 'scanning'
|
||||||
}
|
// Sync status from Core API
|
||||||
|
await syncFileStatus(f.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)
|
|
||||||
} else {
|
} else {
|
||||||
actionMsg.value = `Scan failed: ${procResult.message}`
|
actionMsg.value = `Failed: ${result.message || result.error}`
|
||||||
// Revert status on failure
|
|
||||||
f.status = 'registered'
|
|
||||||
fileStatusCache.value[f.file_uuid] = 'registered'
|
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
actionMsg.value = `Reprocess error: ${e}`
|
actionMsg.value = `Error: ${e.message}`
|
||||||
console.error('[reprocess]', e)
|
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) {
|
async function ctxUnregister(f: any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user