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

@@ -11,6 +11,21 @@ function detectBaseUrl(): string {
return localStorage.getItem('markbase_url') || 'http://localhost:11438'
}
// Poll command status until completed or failed
export async function pollCommandStatus(commandId: string, intervalMs: number = 2000, maxAttempts: number = 150): Promise<any> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const status = await apiCall('get_command_status', { commandId })
if (status?.status === 'completed') {
return status
}
if (status?.status === 'failed') {
throw new Error(status.last_error || 'Command failed')
}
await new Promise(resolve => setTimeout(resolve, intervalMs))
}
throw new Error('Command polling timeout')
}
// API proxy: Tauri IPC or HTTP fetch
export async function apiCall(cmd: string, args: Record<string, any>): Promise<any> {
if (cmd === 'get_video_stream') {
@@ -71,6 +86,31 @@ export async function apiCall(cmd: string, args: Record<string, any>): Promise<a
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
// get_trace_profile / get_trace_profile_file 也沒有 Tauri command
if (cmd === 'get_trace_profile' || cmd === 'get_trace_profile_file') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
// get_file_traces 也沒有 Tauri command
if (cmd === 'get_file_traces') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data, args)
}
// search_file_faces 也沒有 Tauri command
if (cmd === 'search_file_faces') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data, args)
}
// get_face_landmarks 也沒有 Tauri command
if (cmd === 'get_face_landmarks') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data, args)
}
// Admin data comes from MarkBase API (/api/v2/admin/*) in all modes — no Tauri command
if (cmd === 'get_system_stats' || cmd === 'list_admin_users' || cmd === 'list_shares' || cmd === 'validate_system_stats') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
if (isTauri) {
try {
const data = await invoke(cmd, args)
@@ -282,6 +322,9 @@ case 'search_keyword': {
case 'get_file_thumbnail_by_path': {
return { url: `/api/v1/file/thumbnail?path=${encodeURIComponent(a.path)}&frame=${a.frame || 30}`, method: 'GET' }
}
case 'regenerate_trace_thumbnail': {
return { url: `/api/v1/file/${a.fileUuid}/trace/${a.traceId}/regenerate-thumbnail`, method: 'POST' }
}
// --- Video API ---
case 'get_video_stream': {
@@ -340,6 +383,20 @@ case 'search_keyword': {
case 'list_traces': {
return { url: `/api/v1/file/${a.file_uuid}/traces`, method: 'POST' }
}
case 'get_file_traces': {
const fu = a.fileUuid ?? a.file_uuid
return { url: `/api/v1/file/${fu}/traces`, method: 'POST', body: { page: a.page || 1, page_size: a.page_size || 3000, limit: a.limit || 3000 } }
}
case 'search_file_faces': {
const fu = a.fileUuid ?? a.file_uuid
const params = new URLSearchParams()
if (a.q) params.set('q', a.q)
params.set('page', String(a.page || 1))
params.set('page_size', String(a.perPage || 20))
if (a.binding) params.set('binding', a.binding)
if (a.minConfidence != null) params.set('min_confidence', String(a.minConfidence))
return { url: `/api/v1/file/${fu}/faces/search?${params.toString()}`, method: 'GET' }
}
case 'get_trace_profile': {
const params = new URLSearchParams()
const fu = a.file_uuid ?? a.fileUuid
@@ -348,6 +405,14 @@ case 'search_keyword': {
if (tid != null) params.set('trace_id', String(tid))
return { url: `/api/v1/trace-profile?${params.toString()}`, method: 'GET' }
}
case 'get_trace_profile_file': {
const params = new URLSearchParams()
const fu = a.file_uuid ?? a.fileUuid
const tid = a.trace_id ?? a.traceId
if (fu) params.set('file_uuid', fu)
if (tid != null) params.set('trace_id', String(tid))
return { url: `/api/v1/trace-profile-file?${params.toString()}`, method: 'GET' }
}
case 'update_trace_profile': {
const body = a.body ?? {
file_uuid: a.file_uuid ?? a.fileUuid,
@@ -357,7 +422,12 @@ case 'search_keyword': {
return { url: '/api/v1/trace-profile', method: 'PUT', body }
}
case 'delete_trace': {
const body: any = { hard_delete: a.hard_delete ?? true }
const body: any = { mode: a.mode ?? 'ungroup' }
if (a.mode === 'delete') {
if (a.hard_delete !== undefined) body.hard_delete = a.hard_delete
} else {
body.hard_delete = a.hard_delete ?? true
}
return { url: `/api/v1/file/${a.file_uuid}/trace/${a.trace_id}`, method: 'DELETE', body }
}
case 'restore_trace': {
@@ -460,6 +530,9 @@ case 'search_keyword': {
case 'register_file': {
return { url: '/api/v1/files/register', method: 'POST', body: { file_path: a.filePath } }
}
case 'get_command_status': {
return { url: `/api/v1/command/${a.commandId}`, method: 'GET' }
}
case 'process_file': {
return { url: `/api/v1/file/${a.fileUuid}/process`, method: 'POST', body: { processors: a.processors } }
}
@@ -467,10 +540,10 @@ case 'search_keyword': {
return { url: `/api/v1/identity-matches?file_hash=${a.fileHash}`, method: 'GET' }
}
case 'run_cluster_agent': {
return { url: `/api/v1/file/${a.fileUuid}/cluster-agent`, method: 'POST', body: { confirm: true } }
return { url: `/api/v1/file/${a.fileUuid}/cluster-agent`, method: 'POST', body: { confirm: true }, timeout: 600000 }
}
case 'get_cluster_results': {
return { url: `/api/v1/cluster-results?file_hash=${a.fileHash}`, method: 'GET' }
return { url: `/api/v1/cluster-results?file_hash=${a.fileHash}&include_trace_ids=false`, method: 'GET' }
}
case 'get_similar_face_groups': {
const params = new URLSearchParams()
@@ -486,7 +559,9 @@ case 'search_keyword': {
return { url: `/api/v1/file-profile?file_uuid=${a.fileUuid}`, method: 'GET' }
}
case 'update_file_profile': {
return { url: '/api/v1/file-profile', method: 'PUT', body: { file_uuid: a.fileUuid, file_path: a.filePath, file_name: a.fileName } }
const body: any = { file_uuid: a.fileUuid, file_path: a.filePath, file_name: a.fileName }
if (a.faceFilter != null) body.face_filter = a.faceFilter
return { url: '/api/v1/file-profile', method: 'PUT', body }
}
case 'get_processor_json': {
return { url: `/api/v1/processor-json?file_hash=${a.fileHash}&processor=${a.processor}`, method: 'GET' }
@@ -555,6 +630,17 @@ case 'search_keyword': {
return { url: `/api/v1/bookmarks/${a.id}`, method: 'DELETE' }
}
// --- Video Marks ---
case 'get_video_marks': {
return { url: `/api/v1/video-marks?file_uuid=${encodeURIComponent(a.fileUuid)}`, method: 'GET' }
}
case 'save_video_mark': {
return { url: '/api/v1/video-marks', method: 'POST', body: { file_uuid: a.fileUuid, tag: a.tag, start_frame: a.startFrame, end_frame: a.endFrame, note: a.note } }
}
case 'delete_video_mark': {
return { url: `/api/v1/video-mark/${a.id}`, method: 'DELETE' }
}
// --- Service Management ---
case 'stop_service': {
const mbUrl = detectBaseUrl()
@@ -568,6 +654,11 @@ case 'search_keyword': {
// --- Admin Panel ---
case 'get_system_stats':
return { url: '/api/v2/admin/system-stats', method: 'GET' }
case 'validate_system_stats': {
let url = '/api/v1/system-stats/validate'
if (a.layers) url += `?layers=${a.layers}`
return { url, method: 'GET' }
}
case 'list_admin_users':
return { url: '/api/v2/admin/users', method: 'GET' }
case 'list_shares':
@@ -590,6 +681,14 @@ case 'search_keyword': {
if (a.bboxH != null) url += `&bbox_h=${a.bboxH}`
return { url, method: 'GET' }
}
case 'get_face_landmarks': {
let url = `/api/v1/file/${a.fileUuid}/face-landmarks?frame=${a.frame}`
if (a.bboxX != null) url += `&bbox_x=${a.bboxX}`
if (a.bboxY != null) url += `&bbox_y=${a.bboxY}`
if (a.bboxW != null) url += `&bbox_w=${a.bboxW}`
if (a.bboxH != null) url += `&bbox_h=${a.bboxH}`
return { url, method: 'GET' }
}
case 'sync_file_status': {
return { url: `/api/v1/file/${a.fileUuid}/sync-status`, method: 'POST' }
@@ -602,7 +701,7 @@ case 'search_keyword': {
// Transform HTTP response to match Tauri invoke format
// Some endpoints need data reshaping to match what the Rust commands return
export function transformResponse(cmd: string, data: any): any {
export function transformResponse(cmd: string, data: any, args?: Record<string, any>): any {
switch (cmd) {
case 'get_files': {
const files = data.files || data.data || data || []
@@ -614,6 +713,7 @@ export function transformResponse(cmd: string, data: any): any {
modified_time: f.modified_time || '',
isRegistered: f.isRegistered ?? f.is_registered ?? false,
status: f.status || '',
status_updated_at: f.status_updated_at || null,
registrationTime: f.registration_time || null,
ingested: f.ingested ?? ['completed','keyword_ready','semantic_ready','face_mgmt_ready','agent_ready'].includes(f.status),
}))
@@ -674,7 +774,7 @@ export function transformResponse(cmd: string, data: any): any {
bbox: c.bbox ? { x: c.bbox.x, y: c.bbox.y, width: c.bbox.width, height: c.bbox.height } : null,
}))
}
case 'get_unassigned_traces': {
case 'get_unassigned_traces': {
const traces = data.traces || data.data || []
return {
total: data.total || traces.length || 0,
@@ -688,6 +788,10 @@ case 'get_unassigned_traces': {
best_face_frame: t.best_face_frame || 1,
best_face_confidence: t.best_face_confidence || 1,
best_face_bbox: t.best_face_bbox ? { x: t.best_face_bbox.x, y: t.best_face_bbox.y, width: t.best_face_bbox.width, height: t.best_face_bbox.height } : null,
best_face_capture_quality: t.best_capture_quality ?? null,
best_face_eye_ratio: t.best_face_eye_ratio ?? null,
best_face_nose_ratio: t.best_face_nose_ratio ?? null,
best_face_pose_score: t.best_face_pose_score ?? null,
})),
}
}
@@ -700,6 +804,52 @@ case 'get_unassigned_traces': {
})),
}
}
case 'get_file_traces': {
const traces = data.traces || data.data || []
const fu = args?.fileUuid ?? args?.file_uuid ?? ''
return {
total: data.total_traces || traces.length || 0,
traces: traces.map((t: any) => ({
trace_id: t.trace_id,
file_uuid: t.file_uuid || fu,
face_count: t.face_count || 0,
frame_count: t.face_count || 0,
start_frame: t.start_frame || 0,
end_frame: t.end_frame || 1,
start_time: t.start_time || 0,
end_time: t.end_time || 0,
duration_sec: t.duration_sec || 0,
avg_confidence: t.avg_confidence || 0,
best_face_id: t.best_face_id ?? null,
best_face_frame: t.best_face_frame || 0,
best_face_confidence: t.best_face_confidence || 0,
best_face_bbox: t.best_face_bbox || null,
best_face_capture_quality: t.best_face_capture_quality ?? null,
avg_capture_quality: t.avg_capture_quality ?? null,
thumbnail_url: t.thumbnail_url || '',
})),
}
}
case 'search_file_faces': {
const items = data.data || data.traces || []
const fu = args?.fileUuid ?? args?.file_uuid ?? ''
return {
total: data.total || items.length || 0,
page: data.page || 1,
page_size: data.page_size || 20,
traces: items.map((t: any) => ({
trace_id: t.trace_id ?? 0,
file_uuid: t.file_uuid || fu,
name: t.name ?? null,
start_frame: t.start_frame || 0,
end_frame: t.end_frame || 1,
frame_count: t.frame_count || 0,
avg_confidence: t.avg_confidence || 0,
best_face_bbox: t.best_face_bbox ?? null,
thumbnail_url: t.thumbnail_url || '',
})),
}
}
case 'search_llm_smart': {
const results = data.results || data.data || data || []
return results.map((r: any) => {
@@ -789,11 +939,16 @@ case 'get_unassigned_traces': {
case 'register_file': {
return {
success: data.success ?? false,
command_id: data.command_id ?? '',
status: data.status ?? '',
file_uuid: data.file_uuid ?? '',
file_name: data.file_name ?? '',
message: data.message ?? '',
}
}
case 'get_command_status': {
return data || null
}
case 'process_file': {
return {
success: data.success ?? false,
@@ -817,6 +972,7 @@ case 'get_unassigned_traces': {
return data || null
}
case 'get_trace_profile': { return data || null }
case 'get_trace_profile_file': { return data || null }
case 'update_trace_profile': { return data || { success: true } }
case 'update_trace_profile_group': { return data || { success: true } }
case 'get_file_profile': { return data || null }
@@ -839,7 +995,14 @@ case 'get_unassigned_traces': {
message: data.message ?? '',
}
}
case 'unregister_file':
case 'unregister_file': {
return {
success: data.success ?? false,
command_id: data.command_id ?? '',
file_uuid: data.file_uuid ?? '',
message: data.message ?? '',
}
}
case 'ingest_file':
case 'checkout_file': {
return {
@@ -885,7 +1048,19 @@ case 'get_unassigned_traces': {
}
case 'get_pose':
case 'get_appearance':
case 'get_face_landmarks':
return data
case 'get_system_stats':
case 'list_shares':
return data
case 'list_admin_users': {
const users = data.users || data.data || data || []
return users.map((u: any) => ({
username: u.username || '',
homeDir: u.home_dir ?? u.homeDir ?? '',
status: u.status ?? 1,
}))
}
default:
return data
}