import type { AdminTidyApplyBatch, AdminTidyApplyResult, AdminTidyCandidatesResponse, AdminTidyRunResult, } from "../types"; import { request } from "./core"; /* ---------------------------------------------------------------------------- * 记忆整理**应用路径**(切片 TIDY-1)。真源: * internal/services/gateway/admin_memory_tidy_apply.go * internal/services/gateway/admin_memory_tidy_run.go * * GET /admin/memories/candidates?status=&limit= 候选列表(含 id/runId) * POST /admin/memories/candidates/{id}/apply 应用一条(真的改记忆) * POST /admin/memories/candidates/apply?runId=&limit= 批量应用(省略 runId = 全部待定) * POST /admin/memories/candidates/{id}/revert 回滚一条(行永不物理删除) * POST /admin/memories/tidy/run 手动**真跑**一轮(写候选,不自动应用) * * 五个都是 `writeAuth`(owner|admin):候选正文含隐私,读侧也不给 viewer。 * ⚠️ 失败走 `error.code`(tidy_unavailable / tidy_disabled / tidy_run_unavailable / * tidy_timeout / tidy_run_failed / not_found / tidy_run_not_found),**绝不**回一份 * 空结果假装「没有候选」。`status:"skipped"` 是 200 + 幂等命中,不是错误。 * -------------------------------------------------------------------------- */ /** 候选列表的路径(五条里有三条挂在它下面)。 */ const CANDIDATES_PATH = "/admin/memories/candidates"; /** 候选列表只认这两个状态(真源:tidy_store.go / tidy_apply_store.go 的字面量)。 * 因此面板只提供这两个筛选项 —— 加一个「全部」会撒谎,因为空 `status` 在**后端** * 是「按 pending 筛」,不是「不过滤」(见 listTidyCandidates 的默认化分支)。 */ export const TIDY_STATUS_PENDING = "pending"; export const TIDY_STATUS_APPLIED = "applied"; /** 应用/回滚**回执**的状态闭集(真源 internal/runtime/memory/tidy_apply.go 的 * `TidyApplyStatus`:applied = 真的改了/撤销了;skipped = 没改,且**不是错误**; * failed = 尝试过并失败)。 */ export const TIDY_APPLY_STATUS_APPLIED = "applied"; export const TIDY_APPLY_STATUS_SKIPPED = "skipped"; export const TIDY_APPLY_STATUS_FAILED = "failed"; /** GET /api/v1/admin/memories/candidates?status=&limit= * * `limit` 缺省 100、>500 钳到 500、非整数回 400 bad_request —— **钳位与默认值都在 * 后端**,本函数只转发调用方显式给出的值(否则 URL 里不出现 `limit`)。 * * ⚠️ 响应里的 `count` 是**返回条数**,后端没有给「待定总数」;列表被 `limit` 截断时 * 没有 `truncated` 标记。因此界面不许把这一页读成「全部待定」——「到底有多少待定」 * 的权威数字在**批量应用回执**的 `total` 里。 */ export function adminTidyCandidates( params: { status?: string; limit?: number } = {} ): Promise { const qs = new URLSearchParams(); if (params.status) qs.set("status", params.status); if (params.limit && params.limit > 0) qs.set("limit", String(params.limit)); const q = qs.toString(); return request(`${CANDIDATES_PATH}${q ? `?${q}` : ""}`); } /** POST …/candidates/{id}/apply —— 应用**一条**候选。 * * 幂等:已决定过的候选回 **200 + `status:"skipped"`**(不是错误,界面不许报红); * 候选不存在回 404 not_found。 */ export function adminTidyCandidateApply(id: string): Promise { return request(`${CANDIDATES_PATH}/${encodeURIComponent(id)}/apply`, { method: "POST", }); } /** POST …/candidates/{id}/revert —— 回滚一条**已应用**候选(记忆行复原 + 候选退回 * pending;行永不物理删除,因此无损可逆)。候选不在已应用状态时回 * `status:"skipped"` + `reason:"not_applied"`。 */ export function adminTidyCandidateRevert(id: string): Promise { return request(`${CANDIDATES_PATH}/${encodeURIComponent(id)}/revert`, { method: "POST", }); } /** POST …/candidates/apply?runId=&limit= —— **批量**应用。 * * `runId` 省略 = 应用**全部待定**(面板那一档);给出时只应用该轮产出的候选 * (查不到该轮回 404 tidy_run_not_found)。`limit` 省略 ⇒ 由后端领域侧取默认值 * (100,钳到 500)。 * * ⚠️ 回执里的 `total` 与 `applied+skipped+failed` **可以不等**:后者只覆盖 `limit` * 之内,前者是命中的真总数 —— 不等时 `truncated=true`,界面必须如实显示「本轮未 * 处理完 + 还剩多少」,不许画成「全部完成」。 */ export function adminTidyCandidatesApplyBatch( opts: { runId?: string; limit?: number } = {} ): Promise { const qs = new URLSearchParams(); if (opts.runId) qs.set("runId", opts.runId); if (opts.limit && opts.limit > 0) qs.set("limit", String(opts.limit)); const q = qs.toString(); return request(`${CANDIDATES_PATH}/apply${q ? `?${q}` : ""}`, { method: "POST", }); } /** POST /api/v1/admin/memories/tidy/run —— 手动**真跑**一轮(写候选 + 写运行留痕, * **不自动应用**)。它是「不必等到低峰时段」的那个按钮。 * * 失败是**诚实错误**,不是空回执:配置关闭 ⇒ 409 tidy_disabled;超时 ⇒ 503 * tidy_timeout;未接线(只接了试算)⇒ 500 tidy_run_unavailable;其余 ⇒ 500 * tidy_run_failed。审计由领域侧记 `memory.tidy.run`,本函数不重复。 */ export function adminTidyRun(): Promise { return request("/admin/memories/tidy/run", { method: "POST" }); }