From 52bec30c900d26e0d924df975b4502560a36cb60 Mon Sep 17 00:00:00 2001 From: Accusys Date: Tue, 21 Jul 2026 03:19:17 +0800 Subject: [PATCH] feat: unify TKG face_track external_id to trace_N, add trace restore endpoint - tkg.rs: use trace_N as primary external_id, delete duplicate face_track_N nodes - trace_agent_api.rs: add POST /trace/:id/restore to undo soft delete - ensure 1:1 mapping: each trace has exactly one TKG node with full profile --- src/api/trace_agent_api.rs | 80 ++++++++++++++++++++++++++++++++++++++ src/core/processor/tkg.rs | 14 ++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/api/trace_agent_api.rs b/src/api/trace_agent_api.rs index 2677355..6f9b9c5 100644 --- a/src/api/trace_agent_api.rs +++ b/src/api/trace_agent_api.rs @@ -57,6 +57,10 @@ pub fn trace_agent_routes() -> Router { "/api/v1/file/:file_uuid/trace/:trace_id", delete(delete_trace), ) + .route( + "/api/v1/file/:file_uuid/trace/:trace_id/restore", + post(restore_trace), + ) .route( "/api/v1/file/:file_uuid/trace/:trace_id/merge/:target_trace_id", post(merge_trace), @@ -1898,3 +1902,79 @@ async fn merge_trace( "tkg_nodes_deleted": tkg_deleted })) } + +async fn restore_trace( + State(state): State, + Path((file_uuid, trace_id)): Path<(String, i32)>, +) -> Json { + let qdrant = crate::core::db::qdrant_db::QdrantDb::new(); + + // Restore Qdrant points: clear deleted status + let filter = serde_json::json!({ + "must": [ + {"key": "file_uuid", "match": {"value": file_uuid}}, + {"key": "trace_id", "match": {"value": trace_id}}, + {"key": "status", "match": {"value": "deleted"}} + ] + }); + + // Get count of points to restore + let points_to_restore = qdrant.scroll_all_points("_faces", filter.clone(), 1).await + .map(|pts| pts.len()).unwrap_or(0); + + if points_to_restore == 0 { + return Json(serde_json::json!({ + "success": false, + "error": "Trace not found or not deleted" + })); + } + + // Clear the deleted status + let clear_payload = serde_json::json!({ + "status": serde_json::Value::Null + }); + + let qdrant_restored = match qdrant.update_payload_by_filter("_faces", filter, clear_payload).await { + Ok(_) => { + tracing::info!("[restore_trace] Restored {} Qdrant points for trace {} in {}", points_to_restore, trace_id, file_uuid); + true + } + Err(e) => { + tracing::error!("[restore_trace] Failed to restore Qdrant points: {}", e); + return Json(serde_json::json!({ + "success": false, + "error": format!("Failed to restore Qdrant points: {}", e) + })); + } + }; + + // Restore TKG node: clear deleted status + let nodes_table = crate::core::db::schema::table_name("tkg_nodes"); + let ext_id_trace = format!("trace_{}", trace_id); + + let tkg_result = sqlx::query(&format!( + "UPDATE {} SET properties = properties - 'status' \ + WHERE file_uuid = $1 AND node_type = 'face_track' AND external_id = $2", + nodes_table + )) + .bind(&file_uuid) + .bind(&ext_id_trace) + .execute(state.db.pool()) + .await; + + let tkg_restored = match tkg_result { + Ok(r) => r.rows_affected(), + Err(e) => { + tracing::error!("[restore_trace] Failed to restore TKG node: {}", e); + 0 + } + }; + + Json(serde_json::json!({ + "success": true, + "file_uuid": file_uuid, + "trace_id": trace_id, + "qdrant_restored": qdrant_restored, + "tkg_nodes_restored": tkg_restored + })) +} diff --git a/src/core/processor/tkg.rs b/src/core/processor/tkg.rs index a47de7b..007ad2d 100644 --- a/src/core/processor/tkg.rs +++ b/src/core/processor/tkg.rs @@ -1153,7 +1153,8 @@ async fn build_face_track_nodes_from_qdrant( let mut count = 0; for (tid, agg) in &traces { - let external_id = format!("face_track_{}", tid); + // Use trace_N as primary external_id (consistent with cluster_agent) + let external_id = format!("trace_{}", tid); let label = format!("Face Trace {}", tid); // Compute average pose for this trace @@ -1217,6 +1218,17 @@ async fn build_face_track_nodes_from_qdrant( .execute(pool) .await?; + // Delete old face_track_N format to avoid duplicates + let old_external_id = format!("face_track_{}", tid); + let _ = sqlx::query(&format!( + "DELETE FROM {} WHERE file_uuid = $1 AND node_type = 'face_track' AND external_id = $2", + nodes_table + )) + .bind(file_uuid) + .bind(&old_external_id) + .execute(pool) + .await; + count += 1; }