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
This commit is contained in:
Accusys
2026-07-21 03:19:17 +08:00
parent 0ad6927d25
commit 52bec30c90
2 changed files with 93 additions and 1 deletions
+80
View File
@@ -57,6 +57,10 @@ pub fn trace_agent_routes() -> Router<crate::api::types::AppState> {
"/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<crate::api::types::AppState>,
Path((file_uuid, trace_id)): Path<(String, i32)>,
) -> Json<serde_json::Value> {
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
}))
}
+13 -1
View File
@@ -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;
}