package gateway import ( "context" "errors" "net/http" apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" ) // 本文件是记忆整理**手动真跑**入口(切片 TIDY-1): // POST /api/v1/admin/memories/tidy/run —— 立刻按同一套规则跑一轮,写候选 + 写运行 // 留痕,并把「新增/合并/软删各多少 + 耗时」如实回执。审计由领域侧 TidyService 记 // `memory.tidy.run`(一次点击一条,本文件不重复)。 // adminMemoryTidyRun 处理 POST /api/v1/admin/memories/tidy/run:手动**真跑**一轮 // (与心跳、试算走同一套规则与配置;写候选 + 运行留痕)。它是「panic button」: // 主人不必等到 23:00 就能让整理真的产出一批待定候选,再决定应用。 // // 审计不在这里写 —— 领域侧 TidyService 已按 design §8 记 `memory.tidy.run` // (result 里带 trigger=manual / run_id / 计数)。一次点击一条审计。 func (s *Server) adminMemoryTidyRun(w http.ResponseWriter, r *http.Request) { if s.memoryTidy == nil { writeError(w, apperr.Wrap(apperr.ErrInternal, tidyUnavailableCode, "记忆整理服务未接线")) return } runner, ok := s.memoryTidy.(MemoryTidyRunner) if !ok { writeError(w, &apperr.AppError{ Code: "tidy_run_unavailable", Message: "记忆整理真跑入口未接线(只有只读试算可用)", HTTPCode: http.StatusInternalServerError, }) return } ctx, cancel := context.WithTimeout(r.Context(), tidyRunTimeout) defer cancel() stats, err := runner.ManualRun(ctx) if err != nil { writeError(w, tidyRunAppError(err)) return } s.log.Info("memory tidy manual run finished", "run_id", stats.RunID, "scanned", stats.Scanned, "candidates", stats.Candidates, "duplicates", stats.Duplicates, "duration_ms", stats.DurationMS) writeJSON(w, http.StatusOK, adminTidyRunResponse(stats)) } // tidyRunAppError 把「真跑」失败映射成诚实的错误码。映射表与试算那张 // (tidyDryRunAppError)**分开**:试算的 500/409 文案讲的是「没产出候选」, // 真跑讲的是「没跑成」—— 同一类别、不同事实,混用会让排障读到错的那句。 func tidyRunAppError(err error) *apperr.AppError { switch { case errors.Is(err, memory.ErrTidyDisabled): return &apperr.AppError{ Code: "tidy_disabled", Message: "记忆整理已被配置关闭(MEMORY_TIDY_ENABLED=false)", HTTPCode: http.StatusConflict, } case errors.Is(err, context.DeadlineExceeded): return &apperr.AppError{ Code: "tidy_timeout", Message: "记忆整理真跑超时(本轮未完整跑完,已跑部分仍写入了留痕)", HTTPCode: http.StatusServiceUnavailable, } default: return apperr.Wrap(apperr.ErrInternal, "tidy_run_failed", "记忆整理真跑失败") } } // adminTidyRunResponse 投影真跑回执。 func adminTidyRunResponse(stats memory.TidyStats) AdminTidyRunResponse { byAction := stats.ByAction if byAction == nil { byAction = map[string]int{} } reasons := stats.VetoReasons if reasons == nil { reasons = map[string]int{} } return AdminTidyRunResponse{ RunID: stats.RunID, Trigger: "manual", RuleVersion: memory.TidyRuleVersion, DurationMs: stats.DurationMS, Partitions: stats.Partitions, Scanned: stats.Scanned, Compared: stats.Compared, Pairs: stats.Pairs, Vetoed: stats.Vetoed, Errors: stats.Errors, Truncated: stats.Truncated, VetoReasons: reasons, CandidatesCreated: stats.Candidates, CandidatesDuplicate: stats.Duplicates, CandidatesByAction: byAction, DedupeGroups: stats.DedupeGroups, DedupeRowsReducible: stats.DedupeRows, DedupeTruncated: stats.DedupeTruncated, } }