import { useState } from "react"; import { useAsyncData } from "../../lib/useAsyncData"; import { adminAddMemory, adminListMemories, adminMemorySearch } from "../../lib/api"; import type { MemoryRecord, MemorySearchHit } from "../../lib/types"; import { useAutoReload } from "../hooks"; import { useAuditStore } from "../audit"; import { AdminTable, Notice, PageHeader, type AdminColumn } from "../components"; import { Button, Surface, Tag } from "../../components/primitives"; import { MemoryTidyFactsCard } from "./ConfigFactsCards"; import { MemoryTidyDryRunCard } from "./MemoryTidyDryRunCard"; import { MemoryTidyApplyCard } from "./MemoryTidyApplyCard"; import { MemoryTidyTuningCard } from "./MemoryTidyTuningCard"; const scopeTone: Record = { user: "accent", persona: "ok", project: "warn", }; const SCOPES = ["user", "persona", "project"] as const; function MemoryTable({ rows, loading, error }: { rows: MemoryRecord[] | null; loading: boolean; error: string | null }): React.ReactNode { const columns: AdminColumn[] = [ { key: "scope", title: "作用域", render: (m) => {m.scope} }, { key: "text", title: "内容", render: (m) => {m.text} }, { key: "id", title: "ID", render: (m) => {m.id} }, ]; return ( m.id} /> ); } export function MemoriesModule(): React.ReactNode { const { data, loading, error, reload } = useAsyncData(adminListMemories, []); useAutoReload(reload, 15000); const [query, setQuery] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const [personaId, setPersonaId] = useState("cyrene"); const [hits, setHits] = useState(null); const [busy, setBusy] = useState(false); const [fail, setFail] = useState(null); // Manual "添加记忆" form fields. const [addPersona, setAddPersona] = useState("cyrene"); const [addScope, setAddScope] = useState<(typeof SCOPES)[number]>("persona"); const [addContent, setAddContent] = useState(""); const log = useAuditStore((s) => s.log); const filtered = (data ?? []).filter((m) => { if (!query.trim()) return true; const q = query.toLowerCase(); return m.text.toLowerCase().includes(q) || m.scope.toLowerCase().includes(q) || m.id.toLowerCase().includes(q); }); const search = () => { if (!searchQuery.trim()) { setFail("测试查询不能为空"); return; } setBusy(true); setFail(null); adminMemorySearch({ query: searchQuery.trim(), personaId: personaId.trim() || "cyrene" }) .then((r) => { log("memory.vectorTest", "ok"); setHits(r.hits); }) .catch((e: unknown) => { log("memory.vectorTest", "error"); setFail(e instanceof Error ? e.message : "向量测试失败"); }) .finally(() => setBusy(false)); }; const addMemory = () => { if (!addContent.trim()) { setFail("记忆内容不能为空"); return; } setBusy(true); setFail(null); adminAddMemory({ personaId: addPersona.trim() || "cyrene", scope: addScope, content: addContent.trim(), }) .then(() => { log("memory.add", "ok"); setAddContent(""); // Reflect the new record immediately (also refreshes the table). reload(); }) .catch((e: unknown) => { log("memory.add", "error"); setFail(e instanceof Error ? e.message : "添加记忆失败"); }) .finally(() => setBusy(false)); }; return (
向量测试 } /> {fail ? {fail} : null}

添加记忆(写入后端持久化)

setAddPersona(e.target.value)} placeholder="personaId" className="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)]" /> setAddContent(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && addContent.trim()) void addMemory(); }} placeholder="记忆内容(如:她喜欢喝冰美式)…" className="min-w-0 flex-1 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)]" />
作用域为 persona 时与「记住…」同路径;owner 私聊写入安全(群聊/陌生人隔离在 loop 层)。

语义/关键词检索测试(真实 retrieval)

setSearchQuery(e.target.value)} placeholder="检索查询…" className="min-w-0 flex-1 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)]" /> setPersonaId(e.target.value)} placeholder="personaId" className="w-32 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)]" />
{hits ? (
{hits.length === 0 ? ( 无命中 ) : ( hits.map((h) => (
{h.scope} {h.score.toFixed(3)}
{h.text}
)) )} 当前为 keyword×decay 路径;语义/向量路径需主会话注入 vector index(wire.go 装配)。
) : null}
setQuery(e.target.value)} placeholder="按内容 / 作用域 / ID 过滤…" className="w-full 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)]" /> 编辑 / 删除 / 衰减 / 溯源为后端 M10+ 提供,当前只读 + 手动添加 + 检索测试。群聊 / 陌生人会话查记忆时只读不写、不入主会话上下文、不泄露主人敏感信息。
); }