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