feat(t207): audit local procurement results

This commit is contained in:
QiuSW
2026-07-27 12:23:05 +08:00
parent a00c1e3e17
commit 4533df7abc
37 changed files with 4004 additions and 115 deletions
@@ -24,12 +24,13 @@ const (
)
type AdminServices struct {
Assets *usecase.AssetService
Tasks *usecase.TaskService
Assets *usecase.AssetService
Tasks *usecase.TaskService
Results *usecase.ExecutionResultService
}
func (s AdminServices) validate() error {
if s.Assets == nil || s.Tasks == nil {
if s.Assets == nil || s.Tasks == nil || s.Results == nil {
return errors.New("admin services are required")
}
return nil
@@ -49,10 +50,38 @@ func registerAdminAPI(routes gin.IRoutes, services AdminServices) error {
routes.POST("/api/v1/tasks", handler.createTask)
routes.GET("/api/v1/tasks", handler.listTasks)
routes.GET("/api/v1/tasks/:id", handler.taskDetail)
routes.GET(
"/api/v1/tasks/:id/evidence/:evidence_id/content",
handler.evidenceContent,
)
routes.POST("/api/v1/tasks/:id/cancel", handler.cancelTask)
return nil
}
func (h *adminHandlers) evidenceContent(ctx *gin.Context) {
result, err := h.services.Results.OpenEvidence(
ctx.Request.Context(),
ctx.Param("id"),
ctx.Param("evidence_id"),
)
if err != nil {
writeUsecaseError(ctx, err)
return
}
defer result.Content.Close()
ctx.Header("Cache-Control", "private, no-store")
ctx.Header("Content-Type", result.Evidence.MediaType)
ctx.Header("Content-Length", strconv.FormatInt(result.Evidence.SizeBytes, 10))
ctx.Header("ETag", `"`+result.Evidence.SHA256+`"`)
ctx.Header("X-Content-Type-Options", "nosniff")
ctx.Header(
"Content-Disposition",
`inline; filename="`+result.Evidence.ID+`.jpg"`,
)
ctx.Status(http.StatusOK)
_, _ = io.Copy(ctx.Writer, result.Content)
}
func (h *adminHandlers) uploadAsset(ctx *gin.Context) {
if !hasMediaType(ctx, "multipart/form-data") {
writePublicError(
@@ -317,6 +346,10 @@ func (h *adminHandlers) taskDetail(ctx *gin.Context) {
if detail.Execution != nil {
execution = executionResponse(*detail.Execution)
}
var executionReport any
if detail.Report != nil {
executionReport = executionReportResponse(detail.Report)
}
ctx.Header("Cache-Control", "no-store")
ctx.JSON(http.StatusOK, gin.H{
"id": detail.Task.ID,
@@ -337,6 +370,7 @@ func (h *adminHandlers) taskDetail(ctx *gin.Context) {
"derived_requirement": nil,
"claim": claim,
"execution": execution,
"execution_report": executionReport,
"events": events,
"assets": []gin.H{
assetResponse(detail.Asset),
@@ -344,6 +378,78 @@ func (h *adminHandlers) taskDetail(ctx *gin.Context) {
})
}
func executionReportResponse(report *domain.ExecutionReport) gin.H {
events := make([]gin.H, 0, len(report.Events))
for _, event := range report.Events {
events = append(events, gin.H{
"id": event.ID,
"step": event.Step,
"type": event.Type,
"message": event.Message,
"occurred_at": formatTime(event.OccurredAt),
"received_at": formatTime(event.ReceivedAt),
"received_after_execution_expiry": event.ReceivedAfterExecutionExpiry,
})
}
evidence := make([]gin.H, 0, len(report.EvidenceAssets))
for _, asset := range report.EvidenceAssets {
evidence = append(evidence, gin.H{
"id": asset.ID,
"media_type": asset.MediaType,
"size_bytes": asset.SizeBytes,
"sha256": asset.SHA256,
"created_at": formatTime(asset.CreatedAt),
"received_after_execution_expiry": asset.ReceivedAfterExecutionExpiry,
})
}
response := gin.H{
"events": events,
"evidence": evidence,
}
if batch := report.CandidateBatch; batch != nil {
response["candidate_batch"] = gin.H{
"task_content_sha256": batch.TaskContentSHA256,
"execution_mode": batch.ExecutionMode,
"search_query": batch.SearchQuery,
"provenance": decodedAuditJSON(batch.ProvenanceJSON),
"candidates": decodedAuditJSON(&batch.CandidatesJSON),
"recommendation": decodedAuditJSON(batch.RecommendationJSON),
"received_at": formatTime(batch.ReceivedAt),
"received_after_execution_expiry": batch.ReceivedAfterExecutionExpiry,
}
}
if outcome := report.Outcome; outcome != nil {
response["outcome"] = gin.H{
"result_type": outcome.ResultType,
"execution_mode": outcome.ExecutionMode,
"task_content_sha256": outcome.TaskContentSHA256,
"outcome": outcome.Outcome,
"operator_reason": outcome.OperatorReason,
"selected_candidate": decodedAuditJSON(outcome.SelectedCandidateJSON),
"evidence_asset_ids": decodedAuditJSON(outcome.EvidenceAssetIDsJSON),
"error_code": outcome.ErrorCode,
"error_message": outcome.ErrorMessage,
"error_step": outcome.ErrorStep,
"retryable": outcome.Retryable,
"order_submitted": outcome.OrderSubmitted,
"received_at": formatTime(outcome.ReceivedAt),
"received_after_execution_expiry": outcome.ReceivedAfterExecutionExpiry,
}
}
return response
}
func decodedAuditJSON(value *string) any {
if value == nil {
return nil
}
var decoded any
if err := json.Unmarshal([]byte(*value), &decoded); err != nil {
return nil
}
return decoded
}
func (h *adminHandlers) cancelTask(ctx *gin.Context) {
if !hasMediaType(ctx, "application/json") {
writePublicError(