package gateway import ( "errors" "net/http" "strconv" apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" ) // 本文件承载整理候选的**批量拒绝**(按动作)管理面: // // POST /api/v1/admin/memories/candidates/reject?action=&limit= // // 动因(2026-10-07 主人:「那么多 link 是干什么的?该删就删」):活库里积压了 501 条 // `link` 候选,而 link **没有执行语义**(design/23 §14 把「link 关系如何存储与呈现」 // 列为待细化 ⇒ 无落点、无数据模型,详见 memory 包的 tidy_rules.go 与 tidy_apply.go 的 // applyable())—— 它们永远不会被应用,只会一直占着待办列表。规则层已改成不产它们, // 存量则需要一条**批量**的出路:一条条点不现实(这正是主人这次投诉的形状)。 // // 口径: // // 1. **它是「决定」不是「删除」**:只写候选表的状态列(`rejected`),**不碰 // memory_items 任何一行**。记忆的删除/归档语义仍只有两条路(archive 候选 + §8 的 // 软删除口径),候选拒绝与它无关。 // 2. **幂等**:只动 `status='pending'` 的行,重复调用第二次就是 0 条,不是错误。 // 3. **门同族**:走 `writeAuth`(owner|admin),与 `…/candidates/apply`、 // `…/candidates/{id}/revert` 同档。 // 4. **动作名必须合法**:不在 design/23 §10 的动作闭集里 ⇒ 400 `bad_request`, // 而不是静默改 0 条(拼错动作与「没有这类候选」必须能分辨)。 // 5. **写审计**:动作名 `memory.tidy.reject`(design/23 §8 的动作名族),只带计数, // 不带候选正文。 func (s *Server) adminTidyCandidatesReject(w http.ResponseWriter, r *http.Request) { store := s.tidyCandidates() if store == nil { s.auditClaims(r, tidyRejectAction, "error") writeError(w, apperr.Wrap(apperr.ErrInternal, tidyUnavailableCode, "记忆整理候选未接线")) return } action := memory.TidyAction(r.URL.Query().Get("action")) if action == "" { s.auditClaims(r, tidyRejectAction, "error") writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "action is required")) return } limit, ok := parseTidyApplyLimit(r) if !ok { s.auditClaims(r, tidyRejectAction, "error") writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid limit")) return } batch, err := store.RejectTidyCandidatesByAction(r.Context(), action, memory.TidyApplyOptions{ Actor: s.tidyActor(r), Limit: limit, }) if err != nil { s.auditClaims(r, tidyRejectAction, "error") if errors.Is(err, memory.ErrTidyUnknownAction) { writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "unknown action %q", string(action))) return } s.log.Error("reject tidy candidates", "err", err, "action", string(action)) writeError(w, apperr.Wrap(apperr.ErrInternal, "tidy_reject_failed", "批量拒绝整理候选失败")) return } s.auditClaims(r, tidyRejectAction, tidyRejectAuditResult(batch)) writeJSON(w, http.StatusOK, adminTidyRejectBatch(batch)) } // tidyRejectAuditResult 渲染批量拒绝的审计 result(以 ok 开头,与同族一致): // 带真总数与超限标记,因此审计能回答「这次到底决定掉多少、有没有被上限截住」。 func tidyRejectAuditResult(batch memory.TidyRejectBatch) string { return "ok action=" + string(batch.Action) + " limit=" + strconv.Itoa(batch.Limit) + " total=" + strconv.Itoa(batch.Total) + " rejected=" + strconv.Itoa(batch.Rejected) + " truncated=" + strconv.FormatBool(batch.Truncated) } // adminTidyRejectBatch 投影批量拒绝回执。 func adminTidyRejectBatch(batch memory.TidyRejectBatch) AdminTidyRejectResponse { return AdminTidyRejectResponse{ Action: string(batch.Action), Limit: batch.Limit, Total: batch.Total, Rejected: batch.Rejected, Truncated: batch.Truncated, } }