import { useCallback, useEffect, useState } from "react"; import { adminMemoryTidyTuning, adminMemoryTidyTuningSave, type AdminMemoryTidyTuningView, } from "../../lib/api/adminMemories"; import { Button, Surface, Tag } from "../../components/primitives"; import { Notice } from "../components"; import { useAuditStore } from "../audit"; import type { TuningDraft } from "./memoryTidyTuning"; import { TUNING_LABELS, buildTuningPatch, draftFromView, fallbackLabel, patchSummary, secondsHint, unitLabel, } from "./memoryTidyTuning"; /** * 记忆整理「调优」卡(切片 MEMPANEL / M4)。真源: * internal/services/gateway/admin_memory_tuning.go。 * * 它回答的是主人的两句原话:「`.env` 只做兜底用 …… 后端自动创建独立的面板配置文件 * 之后就走面板配置文件,这些键优先从面板配置上取」与「频率必须调,可以调到 1 小时 * 或者三小时,或者动态分配,比如白天间隔短一点、晚上静默期稍微长一点」。 * * 三条口径落在文案里,因为它们是这张卡的产品承诺: * 1. **面板优先、`.env` 兜底**:留空 = 不写这个键 ⇒ 回落 `.env` → 代码默认。 * 每个键都写清「跟随谁」,不做含糊的「未设置」; * 2. **坏值就地报错**:校验规则来自后端的 specs(类型/单位/上下界),非法草稿 * **不发请求**(后端那道 400 仍在,只是不让主人白跑); * 3. **热生效与否如实说**:后端回 `hot:false` 时写「重启后端后生效」,绝不写成 * 「已生效」。 * * 呈现与取数分层:`MemoryTidyTuningBody` 只吃状态(渲染测试直接喂 fixture),卡组件 * 负责取数 + 表单草稿 + 保存。 */ /** 这张卡的核心承诺(渲染测试钉住它必须出现)。 */ export const TIDY_TUNING_DISCLAIMER = "面板配置文件优先,.env 只做兜底"; /** 热生效与「要重启」两种文案(测试分别钉住)。 */ export const TIDY_TUNING_HOT_LABEL = "已热生效(不用重启后端)"; export const TIDY_TUNING_COLD_LABEL = "已保存到面板配置文件,重启后端后生效"; /** BodyProps 是展示半边的输入(全部状态由外面给,便于直接渲染断言)。 */ export interface TuningBodyProps { view: AdminMemoryTidyTuningView; draft: TuningDraft; errors: Record; busy: boolean; /** 保存成功的回执文案(含热生效与否);null = 还没保存过。 */ receipt: string | null; /** 保存后回显的真值(后端**重新读盘**的结果)。 */ savedView: AdminMemoryTidyTuningView | null; onDraftChange: (key: string, value: string) => void; onSave: () => void; } /** hotLabel 返回「这次到底生没生效」的诚实文案。 */ export function hotLabel(view: AdminMemoryTidyTuningView): string { return view.hot ? TIDY_TUNING_HOT_LABEL : TIDY_TUNING_COLD_LABEL; } /** fallbackHint 返回「留空时跟随谁 + 兜底那一档当前是多少」(有就报数)。 */ function fallbackHint(view: AdminMemoryTidyTuningView, key: string, fallback: string): string { const envValue = view.envValues?.[key]; const base = fallbackLabel({ key, kind: "int", fallback }); return envValue === undefined ? base : `${base}(当前 ${envValue})`; } /** FieldRow 渲染一个键:中文标签 + 控件 + 兜底说明 + 就地错误。 */ function FieldRow({ spec, value, error, fallback, valueHint, onChange, }: { spec: AdminMemoryTidyTuningView["specs"][number]; value: string; error: string; fallback: string; valueHint: string; onChange: (next: string) => void; }): React.ReactNode { const label = TUNING_LABELS[spec.key] ?? spec.key; const inputCls = "w-40 rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)]"; return (
{label} {spec.kind === "bool" ? ( ) : ( onChange(e.target.value)} /> )} {valueHint ? {valueHint} : null} {spec.key}
{value.trim() === "" ? `留空 ⇒ ${fallback}` : `写进面板配置文件(覆盖 ${fallback})`} {spec.kind === "int" && unitLabel(spec) ? ` · 单位:${unitLabel(spec)}` : ""}
{error ?
⚠ {error}
: null}
); } /** MemoryTidyTuningBody 是展示半边:吃状态、不取数(渲染测试直接喂它)。 */ export function MemoryTidyTuningBody({ view, draft, errors, busy, receipt, savedView, onDraftChange, onSave, }: TuningBodyProps): React.ReactNode { const hasErrors = Object.keys(errors).length > 0; return (
{TIDY_TUNING_DISCLAIMER} {view.path} {view.present ? "面板配置已存在" : "面板配置还没创建(后端下次启动会自动创建)"}
{view.specs.map((spec) => ( onDraftChange(spec.key, next)} /> ))}
{hasErrors ? ( 有坏值,未提交(后端一个字都不会改) ) : null}
{receipt ? {receipt} : null} {savedView ? (
保存后回显(从磁盘重新读回的真值): {savedView.specs.map((spec) => { const v = savedView.values[spec.key]; return (
{spec.key} = {v === undefined ? "(未写 ⇒ 跟随兜底)" : String(v)}
); })}
{hotLabel(savedView)}
{savedView.hot ? null : (
{savedView.hotError ?? "运行期未接线可热生效的实现"}
)}
) : null}

只写这张卡的 7 个键;文件里其它旋钮(T1 判定 / T1 限速 / 每轮自动落地上限)原样保留。 清空一个键 = 从面板文件里删掉它(回到下一档兜底),不是写 0。

); } /** MemoryTidyTuningCard 负责取数、表单草稿与保存。 */ export function MemoryTidyTuningCard(): React.ReactNode { const [view, setView] = useState(null); const [draft, setDraft] = useState({}); const [errors, setErrors] = useState>({}); const [busy, setBusy] = useState(false); const [fail, setFail] = useState(null); const [receipt, setReceipt] = useState(null); const [savedView, setSavedView] = useState(null); const log = useAuditStore((s) => s.log); const reload = useCallback(() => { setFail(null); return adminMemoryTidyTuning() .then((next) => { setView(next); setDraft(draftFromView(next)); }) .catch((e: unknown) => setFail(e instanceof Error ? e.message : "读取面板配置失败")); }, []); useEffect(() => { void reload(); }, [reload]); const onDraftChange = useCallback((key: string, value: string) => { setDraft((prev) => ({ ...prev, [key]: value })); setReceipt(null); }, []); const onSave = useCallback(() => { if (!view) return; const check = buildTuningPatch(view, draft); setErrors(check.errors); if (Object.keys(check.errors).length > 0) return; setBusy(true); setFail(null); adminMemoryTidyTuningSave(check.patch) .then((next) => { // 响应里的值是后端**重新读盘**的结果:据此重铺草稿(回显真值)。 setView(next); setDraft(draftFromView(next)); setSavedView(next); setErrors({}); setReceipt(`已保存:${patchSummary(check.patch)}。${hotLabel(next)}`); log("memory.tidy.tuning", "ok"); }) .catch((e: unknown) => { setFail(e instanceof Error ? e.message : "保存面板配置失败"); log("memory.tidy.tuning", "error"); }) .finally(() => setBusy(false)); }, [view, draft, log]); return (

记忆整理调优(面板配置)

GET / PUT /api/v1/admin/memories/tidy/tuning
{fail ? {fail} : null} {view === null ? ( {fail ? "读取失败" : "读取中…"} ) : ( )}
); }