feat: integrate new merge_groups API from Core Team

Replace merge_trace implementation with merge_groups API

Changes:
1. src/api/index.ts:
   - Add merge_groups API definition
   - POST /api/v1/file/:file_uuid/groups/merge
   - Parameters: file_uuid, source_groups[], target_group_name

2. src/views/PeopleView.vue:
   - Rewrite executeMerge() function
   - Use group names instead of trace IDs
   - Single API call for all groups
   - Remove trace-level loop

3. src/views/PeopleView.vue:
   - Delete updateClusterAfterMerge() function
   - No longer needed with new API

Benefits:
-  Atomic operation (single API call)
-  Correct merge behavior (groups, not traces)
-  Better performance (1 call vs N calls)
-  Data consistency guaranteed

API behavior:
- Merge N groups into 1 target group
- All traces from source groups merged to target
- Source groups deleted after merge
- Returns traces_merged count

Core API: Implemented by Core Team (2026-07-25)
Status: Ready for testing
This commit is contained in:
2026-07-25 14:25:35 +08:00
parent 042bb45574
commit 8af2dc6128
2 changed files with 48 additions and 68 deletions

View File

@@ -359,6 +359,17 @@ case 'search_keyword': {
case 'merge_trace': {
return { url: `/api/v1/file/${a.file_uuid}/trace/${a.source_id}/merge/${a.target_id}`, method: 'POST' }
}
case 'merge_groups': {
return {
url: `/api/v1/file/${a.fileUuid}/groups/merge`,
method: 'POST',
body: {
file_uuid: a.fileUuid,
source_groups: a.sourceGroups,
target_group_name: a.targetGroupName
}
}
}
case 'bind_face': {
const bindBody: any = { file_uuid: a.fileUuid }
if (a.faceId) bindBody.face_id = a.faceId

View File

@@ -1725,18 +1725,10 @@ async function executeMerge(targetGroupId: string) {
console.log('[executeMerge] targetGroup:', targetGroup)
if (!targetGroup) return
const targetTraceId = targetGroup.trace_ids?.[0]
console.log('[executeMerge] targetTraceId:', targetTraceId, typeof targetTraceId)
if (!targetTraceId) {
alert('Target group has no traces')
return
}
const targetTrace = clusterTracesMap.value[String(targetTraceId)]
console.log('[executeMerge] targetTrace:', targetTrace)
const targetFileUuid = targetTrace?.file_uuid
if (!targetFileUuid) {
alert('Target trace has no file_uuid')
const targetGroupName = targetGroup.name
console.log('[executeMerge] targetGroupName:', targetGroupName)
if (!targetGroupName) {
alert('Target group has no name')
return
}
@@ -1745,71 +1737,48 @@ async function executeMerge(targetGroupId: string) {
const sourceGroups = clusterAsPending.value.filter((c: any) => sourceGroupIds.includes(c.identity_uuid))
console.log('[executeMerge] sourceGroups:', sourceGroups.length)
let totalTraces = 0
let validTraces = 0
for (const g of sourceGroups) {
for (const tid of g.trace_ids || []) {
totalTraces++
const t = clusterTracesMap.value[String(tid)]
if (t?.file_uuid) validTraces++
}
}
const sourceGroupNames = sourceGroups.map(g => g.name).filter(Boolean)
console.log('[executeMerge] sourceGroupNames:', sourceGroupNames)
console.log('[executeMerge] totalTraces:', totalTraces, 'validTraces:', validTraces)
if (validTraces === 0) {
alert('No valid traces to merge (all missing file_uuid)')
if (sourceGroupNames.length === 0) {
alert('No valid source groups to merge')
return
}
if (!confirm(`Merge ${validTraces}/${totalTraces} trace(s) from ${sourceGroups.length} group(s)?`)) return
if (!confirm(`Merge ${sourceGroupNames.length} group(s) into "${targetGroupName}"?`)) return
operationLoading.value = true
operationMessage.value = `Merging ${validTraces} trace(s)...`
operationMessage.value = `Merging ${sourceGroupNames.length} group(s)...`
let successCount = 0
let failCount = 0
for (const g of sourceGroups) {
for (const srcTraceId of g.trace_ids || []) {
const srcTrace = clusterTracesMap.value[String(srcTraceId)]
const srcFileUuid = srcTrace?.file_uuid
if (!srcFileUuid) {
console.warn('[merge] skipping trace without file_uuid:', srcTraceId)
failCount++
continue
}
if (srcFileUuid !== targetFileUuid) {
console.warn('[merge] skipping cross-file trace:', srcTraceId, srcFileUuid, '!=', targetFileUuid)
failCount++
continue
}
try {
console.log('[executeMerge] calling merge_trace:', {
file_uuid: srcFileUuid,
source_id: srcTraceId,
target_id: targetTraceId
})
await apiCall('merge_trace', {
file_uuid: srcFileUuid,
source_id: srcTraceId,
target_id: targetTraceId
})
successCount++
} catch (e) {
console.error('Merge trace failed:', srcTraceId, e)
failCount++
}
try {
console.log('[executeMerge] calling merge_groups API:', {
fileUuid: selectedFileUuid.value,
sourceGroups: sourceGroupNames,
targetGroupName: targetGroupName
})
const result = await apiCall('merge_groups', {
fileUuid: selectedFileUuid.value,
sourceGroups: sourceGroupNames,
targetGroupName: targetGroupName
})
console.log('[executeMerge] result:', result)
if (result?.success) {
console.log('[executeMerge] success, traces_merged:', result.traces_merged)
await loadClusterResults()
clearSelection()
} else {
alert(`Merge failed: ${result?.error || 'Unknown error'}`)
}
} catch (e) {
console.error('[executeMerge] error:', e)
alert(`Merge failed: ${e}`)
} finally {
operationLoading.value = false
operationMessage.value = ''
}
console.log('[executeMerge] completed:', successCount, 'success,', failCount, 'failed')
operationLoading.value = false
operationMessage.value = ''
clearSelection()
await loadClusterResults()
}
async function batchMoveFaces() {