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:
189
src/api/index.ts
189
src/api/index.ts
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user