import { useState } from "react"; import { TIDY_STATUS_PENDING, adminTidyCandidateApply, adminTidyCandidateRevert, adminTidyCandidates, adminTidyCandidatesApplyBatch, adminTidyRun, } from "../../lib/api"; import type { AdminTidyApplyBatch, AdminTidyApplyResult, AdminTidyCandidatesResponse, AdminTidyRunResult, } from "../../lib/types"; import { useAsyncData } from "../../lib/useAsyncData"; import { useAuditStore } from "../audit"; /** * 记忆整理「候选」卡的**取数与写动作**半边(切片 TIDY-1),与展示半边 * (`MemoryTidyApplyCard.tsx`)分开,只为守 AGENTS §10 的文件/函数长度上限。 * * 三条口径落在这里,而不是散在组件里: * 1. **权威键只读**:本模块只把响应原样交给状态(`batch` / `run` / `last` / 列表), * 从不合成计数、从不把「我点了按钮」当成「它成功了」—— 界面显示的一切都来自后端。 * 2. **筛选即查询条件**:`status` 一变就重取,且**旧数据不冒充新筛选的结果**。 * 3. **写动作四件套**(单条应用 / 单条回滚 / 批量应用 / 真跑)共用一份 busy + 失败 + * 客户端审计收尾:失败文案取后端 `error.message`,不自己编。 * * 后端另有权威审计(`memory.tidy.apply` / `memory.tidy.revert` / `memory.tidy.run`), * 这里记的是面板侧可见的那一份(与既有 `memory.add` / `llm.resetFromEnv` 同惯例)。 */ /** 二次确认展开的是哪个写动作。 */ export type TidyConfirmKind = "batch" | "run"; /** 候选列表的取数态。 */ export type TidyCandidateListState = | { kind: "loading" } | { kind: "ready"; data: AdminTidyCandidatesResponse } | { kind: "error"; message: string }; /** 应用卡的完整视图态(纯数据;真实渲染测试直接喂)。 */ export interface TidyApplyViewState { status: string; list: TidyCandidateListState; /** 正在进行的动作(`apply:` / `revert:` / `batch` / `run`),null = 空闲。 */ busy: string | null; confirming: TidyConfirmKind | null; /** 最近一次批量应用回执(计数的**唯一**来源)。 */ batch: AdminTidyApplyBatch | null; /** 最近一次手动真跑回执。 */ run: AdminTidyRunResult | null; /** 最近一次单条应用/回滚回执。 */ last: AdminTidyApplyResult | null; /** 写动作的失败文案(后端 message)。 */ fail: string | null; } /** 视图态的六个交互出口(展示半边只调它们,不碰 API)。 */ export interface TidyApplyActions { onStatusChange: (status: string) => void; onApply: (id: string) => void; onRevert: (id: string) => void; /** 展开某个写动作的二次确认(此时还不发请求)。 */ onRequest: (kind: TidyConfirmKind) => void; /** 确认落刀(批量应用 / 真跑由 `state.confirming` 决定是哪一个)。 */ onConfirm: () => void; onCancelConfirm: () => void; } /** 写动作那一半的出口(`onStatusChange` 属于筛选的持有者,不在这里)。 */ type TidyWriteActions = Omit; /** 一次写动作的收尾器:busy 标记 + 失败文案 + 客户端审计 + 成功收起确认。 */ type TidyAct = (label: string, audit: string, task: () => Promise) => void; /** useTidyCandidateList 取候选列表,并把「屏幕上这批数据属于哪次请求」记在 * `dataStatus` 里:切筛选时旧数据**不许**冒充新筛选的结果(在途期间只显示加载态)。 * * `dataStatus` 在**成功与失败**两条路上都记 —— 否则新筛选请求失败时它会停在旧值, * 界面会永远卡在「加载中」,把失败也一起藏掉。 */ function useTidyCandidateList(status: string): { list: TidyCandidateListState; reload: () => void; } { const [dataStatus, setDataStatus] = useState(status); const fetchList = (): Promise => { const requested = status; return adminTidyCandidates({ status: requested }).then( (res) => { setDataStatus(requested); return res; }, (e: unknown) => { setDataStatus(requested); throw e; } ); }; const { data, loading, error, reload } = useAsyncData(fetchList, [status]); const inFlight = dataStatus !== status || (loading && data === null); const list: TidyCandidateListState = inFlight ? { kind: "loading" } : error ? { kind: "error", message: error } : data ? { kind: "ready", data } : { kind: "loading" }; return { list, reload }; } /** runConfirmed 落刀二次确认里的那个写动作(批量应用 / 真跑),回执存进状态。 */ function runConfirmed( kind: TidyConfirmKind, act: TidyAct, store: { setBatch: (batch: AdminTidyApplyBatch) => void; setRun: (run: AdminTidyRunResult) => void; reload: () => void; } ): void { if (kind === "run") { act("run", "memory.tidy.run", () => adminTidyRun().then((res) => { store.setRun(res); store.reload(); }) ); return; } act("batch", "memory.tidy.applyBatch", () => adminTidyCandidatesApplyBatch().then((res) => { store.setBatch(res); store.reload(); }) ); } /** useTidyWrites 持有四个写动作的状态(busy / 二次确认 / 三种回执 / 失败)并把它们 * 装配成交互出口。`act` 是四者共用的收尾,四个动作一字排开只差调哪个端点。 */ function useTidyWrites(reload: () => void): { busy: string | null; confirming: TidyConfirmKind | null; batch: AdminTidyApplyBatch | null; run: AdminTidyRunResult | null; last: AdminTidyApplyResult | null; fail: string | null; actions: TidyWriteActions; reset: () => void; } { const [busy, setBusy] = useState(null); const [confirming, setConfirming] = useState(null); const [batch, setBatch] = useState(null); const [run, setRun] = useState(null); const [last, setLast] = useState(null); const [fail, setFail] = useState(null); const log = useAuditStore((s) => s.log); const act: TidyAct = (action, audit, task) => { setBusy(action); setFail(null); task() .then(() => { log(audit, "ok"); setConfirming(null); }) .catch((e: unknown) => { log(audit, "error"); setFail(e instanceof Error ? e.message : "操作失败"); }) .finally(() => setBusy(null)); }; const confirmKind = confirming; const actions: TidyWriteActions = { onApply: (id) => act(`apply:${id}`, "memory.tidy.apply", () => adminTidyCandidateApply(id).then((res) => { setLast(res); reload(); }) ), onRevert: (id) => act(`revert:${id}`, "memory.tidy.revert", () => adminTidyCandidateRevert(id).then((res) => { setLast(res); reload(); }) ), onRequest: (kind) => setConfirming(kind), onCancelConfirm: () => setConfirming(null), onConfirm: () => { if (confirmKind) runConfirmed(confirmKind, act, { setBatch, setRun, reload }); }, }; return { busy, confirming, batch, run, last, fail, actions, reset: () => { setLast(null); setFail(null); }, }; } /** useTidyApplyState 把两半拼成候选卡的状态与出口。 */ export function useTidyApplyState(): { state: TidyApplyViewState; actions: TidyApplyActions; } { const [status, setStatus] = useState(TIDY_STATUS_PENDING); const { list, reload } = useTidyCandidateList(status); const writes = useTidyWrites(reload); return { state: { status, list, busy: writes.busy, confirming: writes.confirming, batch: writes.batch, run: writes.run, last: writes.last, fail: writes.fail, }, actions: { ...writes.actions, onStatusChange: (next) => { setStatus(next); writes.reset(); }, }, }; }