Files
Cyrene_v2/frontend/web/src/lib/api/adminMemoryTidyApply.ts
T
AskaEth c512609c63
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(admin): 记忆整理候选的应用/回滚操作面(切片 TIDY-FE)
主人报障「面板上点不到『应用』」→ 后端 TIDY-1 的五条端点(契约 v0.83 B20–B24)
此前只能 curl 驱动。本片补齐界面:候选表(动作/状态/摘要/理由/时间 + 行内
应用/回滚,回滚仅在 status=applied 时可点)、status 筛选、批量应用全部待定、
手动真跑一轮(两个写动作都走二次确认)。

三条契约硬口径落到界面:
- 幂等不是错误:skipped 用中性色,绝不渲染成失败;
- 超限必须如实:truncated=true 时黄牌「还有 N 条待定未处理」,且该分支绝不
  渲染「已全部处理」;未处理量只由后端权威字段算得(total - 已处理三项);
- 权威键只读:状态与计数全部取自响应,不做「应该成功了」的推断。

一个刻意的偏离(有测试钉住):截断提示不用共用 Notice tone=warn —— 那个组件
的固定前缀是「待实现」,贴在「还有 N 条未处理」上会被读成「功能还没做」。

验收(主会话复跑 + 亲自变异):tsc --noEmit 0 错;vite build 通过;
渲染测试 43 checks、传输层测试 28 checks 全绿;我把 truncated 判断取反 ⇒
渲染测试红在「截断时必须显式点明命中数大于本轮处理数」,还原后 md5 一致且绿。

⚠️ 未跑真机浏览器交互(需 owner 凭据 + 真后端),证据止于渲染层与传输层。
2026-10-07 09:58:21 +08:00

109 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AdminTidyCandidatesResponse> {
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<AdminTidyCandidatesResponse>(`${CANDIDATES_PATH}${q ? `?${q}` : ""}`);
}
/** POST …/candidates/{id}/apply —— 应用**一条**候选。
*
* 幂等:已决定过的候选回 **200 + `status:"skipped"`**(不是错误,界面不许报红);
* 候选不存在回 404 not_found。 */
export function adminTidyCandidateApply(id: string): Promise<AdminTidyApplyResult> {
return request<AdminTidyApplyResult>(`${CANDIDATES_PATH}/${encodeURIComponent(id)}/apply`, {
method: "POST",
});
}
/** POST …/candidates/{id}/revert —— 回滚一条**已应用**候选(记忆行复原 + 候选退回
* pending;行永不物理删除,因此无损可逆)。候选不在已应用状态时回
* `status:"skipped"` + `reason:"not_applied"`。 */
export function adminTidyCandidateRevert(id: string): Promise<AdminTidyApplyResult> {
return request<AdminTidyApplyResult>(`${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<AdminTidyApplyBatch> {
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<AdminTidyApplyBatch>(`${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<AdminTidyRunResult> {
return request<AdminTidyRunResult>("/admin/memories/tidy/run", { method: "POST" });
}