feat: Library 批量注册 + PeopleView 修复 + 国际化

Library:
- 添加 checkbox 和鼠标框选功能
- Unregistered 卡片添加快速注册按钮
- 状态标签移至左下方,人脸图标移至左侧中间
- Deep Scan 改为 unregister -> register -> process
- 完整国际化(日语、繁体中文、英语)

PeopleView:
- 修复卡片不显示问题(使用 get_file_traces API)
- 移除对 identity_id 字段的依赖

其他:
- 添加日语语言包
- 添加 useDragSelect composable
- 新增文档:Face Bbox Null Bug Report
This commit is contained in:
2026-10-01 00:42:13 +08:00
parent 7b60ca3d2d
commit f3d75ebce8
35 changed files with 8054 additions and 961 deletions

View File

@@ -4,6 +4,7 @@ use dirs;
use markbase_core::vfs::VfsBackend;
mod proxy;
mod db;
mod validate;
use serde::Serialize;
use base64::{Engine as _, engine::general_purpose::STANDARD};
@@ -12,7 +13,6 @@ use std::sync::{Arc, Mutex};
use std::num::NonZeroUsize;
use lru::LruCache;
use image::GenericImageView;
use image::DynamicImage;
use tokio::sync::Semaphore;
static THUMB_CACHE: Mutex<Option<LruCache<String, String>>> = Mutex::new(None);
@@ -242,7 +242,7 @@ async fn get_files(args: GetFilesArgs) -> Result<Vec<FileInfo>, String> {
modified_time: f["modified_time"].as_str().unwrap_or("").to_string(),
is_registered: is_reg,
registration_time: f["registration_time"].as_str().map(|s| s.to_string()),
ingested: status == "completed",
ingested: matches!(status.as_str(), "completed" | "keyword_ready" | "semantic_ready" | "face_mgmt_ready" | "agent_ready"),
status,
})
})
@@ -999,14 +999,17 @@ struct CreateIdentityFromFaceResult {
}
#[tauri::command(rename_all = "camelCase")]
async fn create_identity_from_face(name: String, _face_id: Option<String>, _face_row_id: Option<i64>, file_uuid: String) -> Result<CreateIdentityFromFaceResult, String> {
async fn create_identity_from_face(name: String, _face_id: Option<String>, _face_row_id: Option<i64>, file_uuid: String, trace_ids: Option<Vec<i64>>) -> Result<CreateIdentityFromFaceResult, String> {
let client = get_client();
// Use POST /api/v1/file/:file_uuid/pending-person to create pending identity
let url = format!("{}/api/v1/file/{}/pending-person?api_key={}", CORE_API, file_uuid, API_KEY);
let body = serde_json::json!({
let mut body = serde_json::json!({
"name": name
});
if let Some(tids) = &trace_ids {
body["trace_ids"] = serde_json::json!(tids);
}
let resp = client.post(&url)
.json(&body)
@@ -1293,6 +1296,16 @@ async fn merge_history(source_uuid: Option<String>, target_uuid: Option<String>,
#[tauri::command(rename_all = "camelCase")]
async fn stop_service(name: String) -> Result<String, String> {
if name == "Worker" || name == "worker" {
// Worker has no port — stop via pidfile / pkill
let output = std::process::Command::new("sh")
.arg("-c")
.arg("sleep 0.5; WPID=$(cat /Users/accusys/momentry_core/logs/worker_3002.pid 2>/dev/null); if [ -n \"$WPID\" ]; then pkill -P $WPID 2>/dev/null; kill -9 $WPID 2>/dev/null; fi; pkill -9 -f 'target/release/momentry worker' 2>/dev/null; rm -f /Users/accusys/momentry_core/logs/worker_3002.pid; echo 'stopped'")
.output()
.map_err(|e| format!("Failed to stop worker: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
return Ok(stdout.trim().to_string());
}
let port = match name.as_str() {
"Core API" | "core" => 3002,
"MarkBase" | "markbase" => 11438,
@@ -1313,9 +1326,31 @@ async fn stop_service(name: String) -> Result<String, String> {
#[tauri::command(rename_all = "camelCase")]
async fn start_service(name: String) -> Result<String, String> {
if name == "Worker" || name == "worker" {
// Worker has no port — verify via pidfile instead of lsof
let output = std::process::Command::new("sh")
.arg("-c")
.arg("cd /Users/accusys/momentry_core && nohup bash run-worker-3002.sh > /tmp/momentry_worker.log 2>&1 &")
.output()
.map_err(|e| format!("Failed to start worker: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Worker start failed: {}", stderr));
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let check = std::process::Command::new("sh")
.arg("-c")
.arg("WPID=$(cat /Users/accusys/momentry_core/logs/worker_3002.pid 2>/dev/null); [ -n \"$WPID\" ] && kill -0 $WPID 2>/dev/null && echo alive")
.output()
.map_err(|e| format!("Worker check failed: {}", e))?;
if String::from_utf8_lossy(&check.stdout).contains("alive") {
return Ok("started".to_string());
}
return Err("Worker failed to start".to_string());
}
let cmd = match name.as_str() {
"MarkBase" | "markbase" => {
"cd /Users/accusys/markbase && nohup cargo run --bin markbase-core -- web-start --port 11438 > /tmp/markbase_web.log 2>&1 &"
"mkdir -p /Users/accusys/markbase/data && cd /Users/accusys/markbase && nohup ./target/release/markbase-core web-start --port 11438 > /tmp/markbase_web.log 2>&1 &"
}
"Core API" | "core" => {
"cd /Users/accusys/momentry_core && nohup bash run-server-3002.sh > /tmp/momentry_start.log 2>&1 &"
@@ -1405,48 +1440,6 @@ async fn get_current_user(username: String) -> Result<Option<db::User>, String>
db::get_user_by_username(&username)
}
#[tauri::command(rename_all = "camelCase")]
async fn list_admin_users() -> Result<serde_json::Value, String> {
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/Users/accusys"));
let db_path = home.join("momentry/data/auth.sqlite");
let conn = rusqlite::Connection::open(&db_path)
.map_err(|e| format!("Failed to open database: {}", e))?;
let mut stmt = conn.prepare("SELECT username, home_dir, status FROM sftpgo_users")
.map_err(|e| format!("Failed to prepare query: {}", e))?;
let users = stmt.query_map([], |row| {
Ok(serde_json::json!({
"username": row.get::<_, String>(0)?,
"homeDir": row.get::<_, String>(1)?,
"status": row.get::<_, i32>(2)?
}))
}).map_err(|e| format!("Failed to query: {}", e))?;
let result: Vec<serde_json::Value> = users.filter_map(|u| u.ok()).collect();
Ok(serde_json::Value::Array(result))
}
#[tauri::command(rename_all = "camelCase")]
async fn get_system_stats() -> Result<serde_json::Value, String> {
let mut stats = serde_json::Map::new();
stats.insert("cpu".to_string(), serde_json::Value::Number(25.into()));
stats.insert("memory".to_string(), serde_json::Value::Number(60.into()));
stats.insert("disk".to_string(), serde_json::Value::Number(45.into()));
Ok(serde_json::Value::Object(stats))
}
#[tauri::command(rename_all = "camelCase")]
async fn list_shares() -> Result<serde_json::Value, String> {
let shares = vec![
serde_json::json!({"name": "MarkBase Web", "port": 11438, "status": "running", "protocol": "HTTP/WebDAV", "url": "http://localhost:11438"}),
serde_json::json!({"name": "SFTP", "port": 2024, "status": "running", "protocol": "SSH/SFTP"}),
serde_json::json!({"name": "SMB", "port": 4445, "status": "running", "protocol": "SMB"}),
];
Ok(serde_json::Value::Array(shares))
}
// === Client Commands ===
#[tauri::command(rename_all = "camelCase")]
@@ -1509,6 +1502,29 @@ fn main() {
rt.block_on(proxy::start_proxy_server());
});
fn ensure_markbase_running() {
let check = std::process::Command::new("sh")
.arg("-c")
.arg("lsof -ti :11438")
.output();
if let Ok(output) = check {
if output.stdout.is_empty() {
let cmd = "mkdir -p /Users/accusys/markbase/data && \
cd /Users/accusys/markbase && \
nohup ./target/release/markbase-core web-start --port 11438 \
> /tmp/markbase_web.log 2>&1 &";
let _ = std::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.spawn();
eprintln!("[startup] MarkBase started on port 11438");
} else {
eprintln!("[startup] MarkBase already running on port 11438");
}
}
}
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.setup(|app| {
@@ -1517,6 +1533,7 @@ fn main() {
if let Err(e) = db::init_db() {
eprintln!("Failed to init database: {}", e);
}
ensure_markbase_running();
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -1568,11 +1585,11 @@ fn main() {
db::get_bookmarks,
db::save_bookmark,
db::delete_bookmark,
db::get_video_marks,
db::save_video_mark,
db::delete_video_mark,
login_user,
get_current_user,
list_admin_users,
get_system_stats,
list_shares,
list_client_files,
mkdir_client,
rm_client,