feat(frontend): 记忆页面迁移到 Memory V2

前端记忆模块整体迁到后端 Memory V2 接口,修复调旧端点 /api/memory
返回 404 的问题。

- core/memory/{types,api,hooks}.ts 重写为 V2 数据模型与端点
  (USER.md / MEMORY.md 条目增删改、配置读写、Hindsight 检索)
- memory-settings-page.tsx 重写:用户画像 + 工作笔记两区的条目
  增删改、记忆设置面板(白名单字段)、Hindsight 检索面板
- 工作笔记按专家隔离,顶部专家下拉(默认助手 + useAgents 列出的
  用户专家),专家列表取自 agents 注册表
- settings-dialog 记忆分区移到技能下方
- i18n(zh-CN/en-US/types)memory 文案整体替换

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xuhdandClaude Opus 4.7 committed 2026-05-15 14:14:08 +08:00
1 parent 21a76f5e7c
commit 013aafde17
8 files changed
+912 -1316

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -63,13 +63,13 @@ export function SettingsDialog(props: SettingsDialogProps) {
label: t.settings.sections.notification, label: t.settings.sections.notification,
icon: BellIcon, icon: BellIcon,
}, },
{ id: "tools", label: t.settings.sections.tools, icon: WrenchIcon },
{ id: "skills", label: t.settings.sections.skills, icon: SparklesIcon },
{ {
id: "memory", id: "memory",
label: t.settings.sections.memory, label: t.settings.sections.memory,
icon: BrainIcon, icon: BrainIcon,
}, },
{ id: "tools", label: t.settings.sections.tools, icon: WrenchIcon },
{ id: "skills", label: t.settings.sections.skills, icon: SparklesIcon },
// { id: "about", label: t.settings.sections.about, icon: InfoIcon }, // { id: "about", label: t.settings.sections.about, icon: InfoIcon },
], ],
[ [
+56 -75
View File
@@ -336,81 +336,62 @@ export const enUS: Translations = {
memory: { memory: {
title: "Memory", title: "Memory",
description: description:
"CM Assistant automatically learns from your conversations in the background. These memories help CM Assistant understand you better and deliver a more personalized experience.", "CM Assistant's memory has two stores: the user profile is shared across all assistants, while working notes are isolated per assistant. You can view and manage both here.",
empty: "No memory data to display.", loadError: "Failed to load memory",
rawJson: "Raw JSON", usageLabel: "Usage",
exportButton: "Export memory", entryEmpty: "No memory entries yet.",
exportSuccess: "Memory exported", userProfileTitle: "User profile",
importButton: "Import memory", userProfileDesc: "Facts about you — name, role, preferences, communication style; shared across all assistants.",
importConfirmTitle: "Import memory?", workNotesTitle: "Working notes",
importConfirmDescription: workNotesDesc: "Per-expert working notes — environment facts, conventions, lessons learned. Pick an expert to view its notes.",
"This will overwrite your current memory with the selected JSON backup.", workNotesAgentLabel: "Expert",
importFileLabel: "Selected file", workNotesDefaultAgent: "Default assistant",
importInvalidFile: addEntry: "Add entry",
"Failed to read the selected memory file. Please choose a valid JSON export.", addEntryTitle: "Add memory entry",
importSuccess: "Memory imported", editEntryTitle: "Edit memory entry",
manualFactSource: "Manual", entryContentLabel: "Content",
addFact: "Add fact", entryContentPlaceholder: "Describe the memory you want to save",
addFactTitle: "Add memory fact", entrySaveSuccess: "Entry saved",
editFactTitle: "Edit memory fact", entryValidationContent: "Entry content cannot be empty.",
addFactSuccess: "Fact created", deleteEntryTitle: "Delete this memory?",
editFactSuccess: "Fact updated", deleteEntryDesc: "This memory will be removed immediately. This action cannot be undone.",
clearAll: "Clear all memory", deleteEntrySuccess: "Entry deleted",
clearAllConfirmTitle: "Clear all memory?", entryToDeleteLabel: "Entry to delete",
clearAllConfirmDescription: save: "Save",
"This will remove all saved summaries and facts. This action cannot be undone.", settingsTitle: "Memory settings",
clearAllSuccess: "All memory cleared", settingsDesc: "Adjust how memory is injected and retrieved. Changes take effect immediately.",
factDeleteConfirmTitle: "Delete this fact?", injectionEnabled: "Inject memory into conversations",
factDeleteConfirmDescription: injectionEnabledDesc: "When off, the assistant sees no memory during conversations.",
"This fact will be removed from memory immediately. This action cannot be undone.", includeBuiltin: "Inject local memory",
factDeleteSuccess: "Fact deleted", includeBuiltinDesc: "Inject the user profile and working notes into the system prompt.",
factContentLabel: "Content", includeHindsight: "Inject Hindsight recall",
factCategoryLabel: "Category", includeHindsightDesc: "Retrieve and inject relevant memories from Hindsight before each turn.",
factConfidenceLabel: "Confidence", maxTokens: "Injection token limit",
factContentPlaceholder: "Describe the memory fact you want to save", maxTokensDesc: "Maximum tokens used for memory injection (100–8000).",
factCategoryPlaceholder: "context", memoryMode: "Hindsight tool mode",
factConfidenceHint: "Use a number between 0 and 1.", memoryModeDesc: "Controls whether Hindsight retrieval tools are exposed to the assistant.",
factSave: "Save fact", memoryModeContext: "context — auto recall only",
factValidationContent: "Fact content cannot be empty.", memoryModeTools: "tools — tools only",
factValidationConfidence: "Confidence must be a number between 0 and 1.", memoryModeHybrid: "hybrid — auto + tools",
noFacts: "No saved facts yet.", recallBudget: "Recall depth",
summaryReadOnly: recallBudgetDesc: "How thorough Hindsight retrieval is — higher is slower but more complete.",
"Summary sections are read-only for now. You can currently add, edit, or delete individual facts, or clear all memory.", recallBudgetLow: "Low",
memoryFullyEmpty: "No memory saved yet.", recallBudgetMid: "Mid",
factPreviewLabel: "Fact to delete", recallBudgetHigh: "High",
searchPlaceholder: "Search memory", bankMission: "Hindsight reflect mission",
filterAll: "All", bankMissionDesc: "Identity/framing for reflect reasoning; leave empty to use the default.",
filterFacts: "Facts", bankMissionPlaceholder: "e.g. You are the user's long-term assistant, focused on their work preferences.",
filterSummaries: "Summaries", settingsSaved: "Settings saved",
noMatches: "No matching memory found.", searchTitle: "Hindsight memory search",
markdown: { searchDesc: "Search historical memories in the Hindsight long-term memory store.",
overview: "Overview", searchPlaceholder: "Search past memories…",
userContext: "User context", searchModeRecall: "Recall",
work: "Work", searchModeReflect: "Reflect",
personal: "Personal", searchButton: "Search",
topOfMind: "Top of mind", searchUnavailable: "Hindsight is not configured or unavailable.",
historyBackground: "History", searchNoResults: "No relevant memories found.",
recentMonths: "Recent months", searchResultsTitle: "Search results",
earlierContext: "Earlier context", searchEmpty: "Enter a keyword to search Hindsight memory.",
longTermBackground: "Long-term background",
updatedAt: "Updated at",
facts: "Facts",
empty: "(empty)",
table: {
category: "Category",
confidence: "Confidence",
confidenceLevel: {
veryHigh: "Very high",
high: "High",
normal: "Normal",
unknown: "Unknown",
},
content: "Content",
source: "Source",
createdAt: "CreatedAt",
view: "View",
},
},
}, },
appearance: { appearance: {
themeTitle: "Theme", themeTitle: "Theme",
+54 -68
View File
@@ -274,75 +274,61 @@ export interface Translations {
memory: { memory: {
title: string; title: string;
description: string; description: string;
empty: string; loadError: string;
rawJson: string; usageLabel: string;
exportButton: string; entryEmpty: string;
exportSuccess: string; userProfileTitle: string;
importButton: string; userProfileDesc: string;
importConfirmTitle: string; workNotesTitle: string;
importConfirmDescription: string; workNotesDesc: string;
importFileLabel: string; workNotesAgentLabel: string;
importInvalidFile: string; workNotesDefaultAgent: string;
importSuccess: string; addEntry: string;
manualFactSource: string; addEntryTitle: string;
addFact: string; editEntryTitle: string;
addFactTitle: string; entryContentLabel: string;
editFactTitle: string; entryContentPlaceholder: string;
addFactSuccess: string; entrySaveSuccess: string;
editFactSuccess: string; entryValidationContent: string;
clearAll: string; deleteEntryTitle: string;
clearAllConfirmTitle: string; deleteEntryDesc: string;
clearAllConfirmDescription: string; deleteEntrySuccess: string;
clearAllSuccess: string; entryToDeleteLabel: string;
factDeleteConfirmTitle: string; save: string;
factDeleteConfirmDescription: string; settingsTitle: string;
factDeleteSuccess: string; settingsDesc: string;
factContentLabel: string; injectionEnabled: string;
factCategoryLabel: string; injectionEnabledDesc: string;
factConfidenceLabel: string; includeBuiltin: string;
factContentPlaceholder: string; includeBuiltinDesc: string;
factCategoryPlaceholder: string; includeHindsight: string;
factConfidenceHint: string; includeHindsightDesc: string;
factSave: string; maxTokens: string;
factValidationContent: string; maxTokensDesc: string;
factValidationConfidence: string; memoryMode: string;
noFacts: string; memoryModeDesc: string;
summaryReadOnly: string; memoryModeContext: string;
memoryFullyEmpty: string; memoryModeTools: string;
factPreviewLabel: string; memoryModeHybrid: string;
recallBudget: string;
recallBudgetDesc: string;
recallBudgetLow: string;
recallBudgetMid: string;
recallBudgetHigh: string;
bankMission: string;
bankMissionDesc: string;
bankMissionPlaceholder: string;
settingsSaved: string;
searchTitle: string;
searchDesc: string;
searchPlaceholder: string; searchPlaceholder: string;
filterAll: string; searchModeRecall: string;
filterFacts: string; searchModeReflect: string;
filterSummaries: string; searchButton: string;
noMatches: string; searchUnavailable: string;
markdown: { searchNoResults: string;
overview: string; searchResultsTitle: string;
userContext: string; searchEmpty: string;
work: string;
personal: string;
topOfMind: string;
historyBackground: string;
recentMonths: string;
earlierContext: string;
longTermBackground: string;
updatedAt: string;
facts: string;
empty: string;
table: {
category: string;
confidence: string;
confidenceLevel: {
veryHigh: string;
high: string;
normal: string;
unknown: string;
};
content: string;
source: string;
createdAt: string;
view: string;
};
};
}; };
appearance: { appearance: {
themeTitle: string; themeTitle: string;
+56 -73
View File
@@ -321,79 +321,62 @@ export const zhCN: Translations = {
memory: { memory: {
title: "记忆", title: "记忆",
description: description:
"CM 助手 会在后台不断从你的对话中自动学习。这些记忆能帮助 CM 助手 更好地理解你,并提供更个性化的体验。", "CM 助手 的记忆分两类:用户画像跨所有助手共享,工作笔记按当前助手隔离。你可以在这里查看和管理。",
empty: "暂无可展示的记忆数据。", loadError: "加载记忆失败",
rawJson: "原始 JSON", usageLabel: "用量",
exportButton: "导出记忆", entryEmpty: "还没有记忆条目。",
exportSuccess: "记忆已导出", userProfileTitle: "用户画像",
importButton: "导入记忆", userProfileDesc: "关于你的事实 —— 姓名、角色、偏好、沟通风格,所有助手共享。",
importConfirmTitle: "导入记忆?", workNotesTitle: "工作笔记",
importConfirmDescription: "这会用选中的 JSON 备份覆盖当前记忆。", workNotesDesc: "按专家隔离的工作笔记 —— 环境事实、约定、踩过的坑。选择专家查看对应笔记。",
importFileLabel: "已选择文件", workNotesAgentLabel: "专家",
importInvalidFile: "读取记忆文件失败,请选择有效的 JSON 导出文件。", workNotesDefaultAgent: "默认助手",
importSuccess: "记忆已导入", addEntry: "添加条目",
manualFactSource: "手动添加", addEntryTitle: "添加记忆条目",
addFact: "添加事实", editEntryTitle: "编辑记忆条目",
addFactTitle: "添加记忆事实", entryContentLabel: "内容",
editFactTitle: "编辑记忆事实", entryContentPlaceholder: "描述你想保存的记忆内容",
addFactSuccess: "事实已创建", entrySaveSuccess: "条目已保存",
editFactSuccess: "事实已更新", entryValidationContent: "条目内容不能为空。",
clearAll: "清空全部记忆", deleteEntryTitle: "要删除这条记忆吗?",
clearAllConfirmTitle: "要清空全部记忆吗?", deleteEntryDesc: "这条记忆会立即删除。此操作无法撤销。",
clearAllConfirmDescription: deleteEntrySuccess: "条目已删除",
"这会删除所有已保存的摘要和事实。此操作无法撤销。", entryToDeleteLabel: "即将删除的条目",
clearAllSuccess: "已清空全部记忆", save: "保存",
factDeleteConfirmTitle: "要删除这条事实吗?", settingsTitle: "记忆设置",
factDeleteConfirmDescription: settingsDesc: "调整记忆的注入与检索行为。修改即时生效。",
"这条事实会立即从记忆中删除。此操作无法撤销。", injectionEnabled: "注入记忆到对话",
factDeleteSuccess: "事实已删除", injectionEnabledDesc: "关闭后,助手在对话中不会看到任何记忆。",
factContentLabel: "内容", includeBuiltin: "注入本地记忆",
factCategoryLabel: "类别", includeBuiltinDesc: "把用户画像和工作笔记注入系统提示词。",
factConfidenceLabel: "置信度", includeHindsight: "注入 Hindsight 召回",
factContentPlaceholder: "描述你想保存的记忆事实", includeHindsightDesc: "每轮对话前从 Hindsight 检索相关记忆并注入。",
factCategoryPlaceholder: "context", maxTokens: "注入 token 上限",
factConfidenceHint: "请输入 0 到 1 之间的数字。", maxTokensDesc: "记忆注入占用的最大 token 数 (100–8000)。",
factSave: "保存事实", memoryMode: "Hindsight 工具模式",
factValidationContent: "事实内容不能为空。", memoryModeDesc: "控制是否把 Hindsight 检索工具暴露给助手。",
factValidationConfidence: "置信度必须是 0 到 1 之间的数字。", memoryModeContext: "context —— 仅自动召回",
noFacts: "还没有保存的事实。", memoryModeTools: "tools —— 仅暴露工具",
summaryReadOnly: memoryModeHybrid: "hybrid —— 自动 + 工具",
"摘要分区当前仍为只读。现在你可以清空全部记忆或删除单条事实。", recallBudget: "召回深度",
memoryFullyEmpty: "还没有保存任何记忆。", recallBudgetDesc: "Hindsight 检索的彻底程度,越高越慢但越全。",
factPreviewLabel: "即将删除的事实", recallBudgetLow: "低",
searchPlaceholder: "搜索记忆", recallBudgetMid: "中",
filterAll: "全部", recallBudgetHigh: "高",
filterFacts: "事实", bankMission: "Hindsight reflect mission",
filterSummaries: "摘要", bankMissionDesc: "综合推理时的身份/框架描述,留空则用默认。",
noMatches: "没有找到匹配的记忆。", bankMissionPlaceholder: "例如:你是用户的长期助手,关注其工作偏好。",
markdown: { settingsSaved: "设置已保存",
overview: "概览", searchTitle: "Hindsight 记忆检索",
userContext: "用户上下文", searchDesc: "在 Hindsight 长期记忆库里检索历史记忆。",
work: "工作", searchPlaceholder: "搜索历史记忆…",
personal: "个人", searchModeRecall: "检索",
topOfMind: "近期关注(Top of mind)", searchModeReflect: "综合",
historyBackground: "历史背景", searchButton: "搜索",
recentMonths: "近几个月", searchUnavailable: "Hindsight 未配置或不可用。",
earlierContext: "更早上下文", searchNoResults: "没有找到相关记忆。",
longTermBackground: "长期背景", searchResultsTitle: "检索结果",
updatedAt: "更新于", searchEmpty: "输入关键词检索 Hindsight 记忆。",
facts: "事实",
empty: "(空)",
table: {
category: "类别",
confidence: "置信度",
confidenceLevel: {
veryHigh: "极高",
high: "较高",
normal: "一般",
unknown: "未知",
},
content: "内容",
source: "来源",
createdAt: "创建时间",
view: "查看",
},
},
}, },
appearance: { appearance: {
themeTitle: "主题", themeTitle: "主题",
+114 -120
View File
@@ -1,149 +1,143 @@
import { getBackendBaseURL } from "../config"; // Memory V2 API 客户端。对应后端 /api/memory/v2 系列端点。
import { apiFetch } from "../api"; import { apiFetch } from "../api";
import { getBackendBaseURL } from "../config";
import type { import type {
MemoryFactInput, MemorySearchMode,
MemoryFactPatchInput, MemorySearchResult,
UserMemory, MemoryTarget,
MemoryV2ConfigResponse,
MemoryV2Data,
} from "./types"; } from "./types";
async function readMemoryResponse( /** 从响应里提取后端的错误详情文本。 */
response: Response, function formatErrorDetail(detail: unknown): string | null {
fallbackMessage: string, if (typeof detail === "string") {
): Promise<UserMemory> { return detail;
function formatErrorDetail(detail: unknown): string | null {
if (typeof detail === "string") {
return detail;
}
if (Array.isArray(detail)) {
const parts = detail
.map((item) => {
if (typeof item === "string") {
return item;
}
if (item && typeof item === "object") {
const record = item as Record<string, unknown>;
if (typeof record.msg === "string") {
return record.msg;
}
try {
return JSON.stringify(record);
} catch {
return null;
}
}
return String(item);
})
.filter(Boolean);
return parts.length > 0 ? parts.join("; ") : null;
}
if (detail && typeof detail === "object") {
try {
return JSON.stringify(detail);
} catch {
return null;
}
}
if (
typeof detail === "string" ||
typeof detail === "number" ||
typeof detail === "boolean" ||
typeof detail === "bigint"
) {
return String(detail);
}
if (typeof detail === "symbol") {
return detail.description ?? null;
}
return null;
} }
if (Array.isArray(detail)) {
const parts = detail
.map((item) => {
if (typeof item === "string") return item;
if (item && typeof item === "object") {
const record = item as Record<string, unknown>;
if (typeof record.msg === "string") return record.msg;
}
return null;
})
.filter(Boolean);
return parts.length > 0 ? parts.join("; ") : null;
}
return null;
}
async function readJson<T>(response: Response, fallbackMessage: string): Promise<T> {
if (!response.ok) { if (!response.ok) {
const errorData = (await response.json().catch(() => ({}))) as { const errorData = (await response.json().catch(() => ({}))) as {
detail?: unknown; detail?: unknown;
}; };
const detailMessage = formatErrorDetail(errorData.detail); const detailMessage = formatErrorDetail(errorData.detail);
throw new Error( throw new Error(detailMessage ?? `${fallbackMessage}: ${response.statusText}`);
detailMessage ?? `${fallbackMessage}: ${response.statusText}`,
);
} }
return response.json() as Promise<T>;
return response.json() as Promise<UserMemory>;
} }
export async function loadMemory(): Promise<UserMemory> { /** 拼接可选的 ?agent= 查询串。 */
const response = await apiFetch(`${getBackendBaseURL()}/api/memory`); function agentQuery(agent?: string): string {
return readMemoryResponse(response, "Failed to fetch memory"); return agent ? `?agent=${encodeURIComponent(agent)}` : "";
} }
export async function clearMemory(): Promise<UserMemory> { // --- 本地记忆数据 (USER.md / MEMORY.md) ---
const response = await apiFetch(`${getBackendBaseURL()}/api/memory`, {
method: "DELETE",
});
return readMemoryResponse(response, "Failed to clear memory");
}
export async function deleteMemoryFact(factId: string): Promise<UserMemory> { export async function loadMemoryV2(agent?: string): Promise<MemoryV2Data> {
const response = await apiFetch( const response = await apiFetch(
`${getBackendBaseURL()}/api/memory/facts/${encodeURIComponent(factId)}`, `${getBackendBaseURL()}/api/memory/v2${agentQuery(agent)}`,
);
return readJson<MemoryV2Data>(response, "Failed to fetch memory");
}
export async function addMemoryEntry(
target: MemoryTarget,
content: string,
agent?: string,
): Promise<MemoryV2Data> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/memory/v2/entries${agentQuery(agent)}`,
{ {
method: "DELETE", method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ target, content }),
}, },
); );
return readMemoryResponse(response, "Failed to delete memory fact"); return readJson<MemoryV2Data>(response, "Failed to add memory entry");
} }
export async function exportMemory(): Promise<UserMemory> { export async function replaceMemoryEntry(
const response = await apiFetch(`${getBackendBaseURL()}/api/memory/export`); target: MemoryTarget,
return readMemoryResponse(response, "Failed to export memory"); oldText: string,
} content: string,
agent?: string,
export async function importMemory(memory: UserMemory): Promise<UserMemory> { ): Promise<MemoryV2Data> {
const response = await apiFetch(`${getBackendBaseURL()}/api/memory/import`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(memory),
});
return readMemoryResponse(response, "Failed to import memory");
}
export async function createMemoryFact(
input: MemoryFactInput,
): Promise<UserMemory> {
const response = await apiFetch(`${getBackendBaseURL()}/api/memory/facts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
});
return readMemoryResponse(response, "Failed to create memory fact");
}
export async function updateMemoryFact(
factId: string,
input: MemoryFactPatchInput,
): Promise<UserMemory> {
const response = await apiFetch( const response = await apiFetch(
`${getBackendBaseURL()}/api/memory/facts/${encodeURIComponent(factId)}`, `${getBackendBaseURL()}/api/memory/v2/entries${agentQuery(agent)}`,
{ {
method: "PATCH", method: "PUT",
headers: { headers: { "Content-Type": "application/json" },
"Content-Type": "application/json", body: JSON.stringify({ target, old_text: oldText, content }),
},
body: JSON.stringify(input),
}, },
); );
return readMemoryResponse(response, "Failed to update memory fact"); return readJson<MemoryV2Data>(response, "Failed to replace memory entry");
}
export async function deleteMemoryEntry(
target: MemoryTarget,
oldText: string,
agent?: string,
): Promise<MemoryV2Data> {
const params = new URLSearchParams({ target, old_text: oldText });
if (agent) {
params.set("agent", agent);
}
const response = await apiFetch(
`${getBackendBaseURL()}/api/memory/v2/entries?${params.toString()}`,
{ method: "DELETE" },
);
return readJson<MemoryV2Data>(response, "Failed to delete memory entry");
}
// --- 配置 ---
export async function loadMemoryConfig(): Promise<MemoryV2ConfigResponse> {
const response = await apiFetch(`${getBackendBaseURL()}/api/memory/v2/config`);
return readJson<MemoryV2ConfigResponse>(response, "Failed to fetch memory config");
}
export async function patchMemoryConfig(
overrides: Record<string, unknown>,
): Promise<MemoryV2ConfigResponse> {
const response = await apiFetch(`${getBackendBaseURL()}/api/memory/v2/config`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ overrides }),
});
return readJson<MemoryV2ConfigResponse>(response, "Failed to update memory config");
}
// --- Hindsight 检索 ---
export async function searchMemory(
query: string,
mode: MemorySearchMode = "recall",
agent?: string,
): Promise<MemorySearchResult> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/memory/v2/search${agentQuery(agent)}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, mode }),
},
);
return readJson<MemorySearchResult>(response, "Failed to search memory");
} }
+67 -59
View File
@@ -1,84 +1,92 @@
// Memory V2 React Query hooks。
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
clearMemory, addMemoryEntry,
createMemoryFact, deleteMemoryEntry,
deleteMemoryFact, loadMemoryConfig,
importMemory, loadMemoryV2,
loadMemory, patchMemoryConfig,
updateMemoryFact, replaceMemoryEntry,
} from "./api"; } from "./api";
import type { import type {
MemoryFactInput, MemoryTarget,
MemoryFactPatchInput, MemoryV2ConfigResponse,
UserMemory, MemoryV2Data,
} from "./types"; } from "./types";
export function useMemory() { const MEMORY_KEY = ["memory", "v2"] as const;
const MEMORY_CONFIG_KEY = ["memory", "v2", "config"] as const;
/** 读取 builtin 本地记忆 (USER.md / MEMORY.md)。 */
export function useMemory(agent?: string) {
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
queryKey: ["memory"], queryKey: [...MEMORY_KEY, agent ?? ""],
queryFn: () => loadMemory(), queryFn: () => loadMemoryV2(agent),
}); });
return { memory: data ?? null, isLoading, error }; return { memory: data ?? null, isLoading, error };
} }
export function useClearMemory() { /** 新增一条记忆条目。 */
export function useAddMemoryEntry(agent?: string) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: () => clearMemory(), mutationFn: ({ target, content }: { target: MemoryTarget; content: string }) =>
onSuccess: (memory) => { addMemoryEntry(target, content, agent),
queryClient.setQueryData<UserMemory>(["memory"], memory); onSuccess: (data) => {
queryClient.setQueryData<MemoryV2Data>([...MEMORY_KEY, agent ?? ""], data);
}, },
}); });
} }
export function useDeleteMemoryFact() { /** 替换一条记忆条目。 */
export function useReplaceMemoryEntry(agent?: string) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({
mutationFn: (factId: string) => deleteMemoryFact(factId),
onSuccess: (memory) => {
queryClient.setQueryData<UserMemory>(["memory"], memory);
},
});
}
export function useImportMemory() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (memory: UserMemory) => importMemory(memory),
onSuccess: (memory) => {
queryClient.setQueryData<UserMemory>(["memory"], memory);
},
});
}
export function useCreateMemoryFact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: MemoryFactInput) => createMemoryFact(input),
onSuccess: (memory) => {
queryClient.setQueryData<UserMemory>(["memory"], memory);
},
});
}
export function useUpdateMemoryFact() {
const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ mutationFn: ({
factId, target,
input, oldText,
content,
}: { }: {
factId: string; target: MemoryTarget;
input: MemoryFactPatchInput; oldText: string;
}) => updateMemoryFact(factId, input), content: string;
onSuccess: (memory) => { }) => replaceMemoryEntry(target, oldText, content, agent),
queryClient.setQueryData<UserMemory>(["memory"], memory); onSuccess: (data) => {
queryClient.setQueryData<MemoryV2Data>([...MEMORY_KEY, agent ?? ""], data);
},
});
}
/** 删除一条记忆条目。 */
export function useDeleteMemoryEntry(agent?: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ target, oldText }: { target: MemoryTarget; oldText: string }) =>
deleteMemoryEntry(target, oldText, agent),
onSuccess: (data) => {
queryClient.setQueryData<MemoryV2Data>([...MEMORY_KEY, agent ?? ""], data);
},
});
}
/** 读取生效记忆配置 + 白名单。 */
export function useMemoryConfig() {
const { data, isLoading, error } = useQuery({
queryKey: MEMORY_CONFIG_KEY,
queryFn: () => loadMemoryConfig(),
});
return { configResponse: data ?? null, isLoading, error };
}
/** 写入 per-user 配置覆盖。 */
export function usePatchMemoryConfig() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (overrides: Record<string, unknown>) => patchMemoryConfig(overrides),
onSuccess: (data) => {
queryClient.setQueryData<MemoryV2ConfigResponse>(MEMORY_CONFIG_KEY, data);
}, },
}); });
} }
+50 -47
View File
@@ -1,54 +1,57 @@
export interface MemoryFact { // Memory V2 数据模型。对应后端 /api/memory/v2 系列端点。
id: string;
content: string; /** builtin 本地记忆数据视图 (USER.md / MEMORY.md)。 */
category: string; export interface MemoryV2Data {
confidence: number; /** USER.md 条目 —— 用户画像,跨 agent 共享。 */
createdAt: string; user_entries: string[];
source: string; /** MEMORY.md 条目 —— 当前 agent 的工作笔记。 */
memory_entries: string[];
/** USER.md 字符用量,如 "12% — 165/1375 字符"。 */
user_usage: string;
/** MEMORY.md 字符用量。 */
memory_usage: string;
} }
export interface MemoryFactInput { /** 记忆条目所属类别。 */
content: string; export type MemoryTarget = "user" | "memory";
category: string;
confidence: number; /** 生效记忆配置 + 用户可改字段白名单。 */
export interface MemoryV2ConfigResponse {
/** 按 per-user 覆盖合并后的生效配置 (嵌套 dict)。 */
config: MemoryV2Config;
/** 用户可在运行时覆盖的字段 (点号路径)。 */
writable_fields: string[];
} }
export interface MemoryFactPatchInput { /** 生效记忆配置的关注字段 (后端 dict 的子集)。 */
content?: string; export interface MemoryV2Config {
category?: string; enabled: boolean;
confidence?: number; provider: string;
} injection: {
enabled: boolean;
export interface UserMemory { max_tokens: number;
version: string; include_builtin: boolean;
lastUpdated: string; include_hindsight: boolean;
user: { context_tag: string;
workContext: {
summary: string;
updatedAt: string;
};
personalContext: {
summary: string;
updatedAt: string;
};
topOfMind: {
summary: string;
updatedAt: string;
};
}; };
history: { hindsight: {
recentMonths: { memory_mode: string;
summary: string; recall_budget: string;
updatedAt: string; bank_mission: string;
}; [key: string]: unknown;
earlierContext: {
summary: string;
updatedAt: string;
};
longTermBackground: {
summary: string;
updatedAt: string;
};
}; };
facts: MemoryFact[]; [key: string]: unknown;
}
/** Hindsight 检索模式。 */
export type MemorySearchMode = "recall" | "reflect";
/** Hindsight 检索结果。 */
export interface MemorySearchResult {
/** Hindsight 是否可用 (未配置/未装包时为 false)。 */
available: boolean;
/** recall 模式的检索结果。 */
results: string[];
/** reflect 模式的综合结论。 */
text: string;
} }