Merge remote-tracking branch 'origin/master' into ji

This commit is contained in:
吉鑫升 committed 2026-05-19 16:28:39 +08:00
commit 4c38055d38
54 files changed
+9509 -6830

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
"use client";
import { BrainIcon, PaletteIcon, UserIcon, WrenchIcon } from "lucide-react";
import { BellIcon, PaletteIcon, SparklesIcon, UserIcon, WrenchIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import {
@@ -50,11 +50,19 @@ export function SettingsDialog(props: SettingsDialogProps) {
label: t.settings.sections.appearance,
icon: PaletteIcon,
},
{
id: "memory",
label: t.settings.sections.memory,
icon: BrainIcon,
},
// {
// id: "notification",
// 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 },
// 记忆已移至侧边栏入口 (/page/workspace/memory)
// {
// id: "memory",
// label: t.settings.sections.memory,
// icon: BrainIcon,
// },
{ id: "tools", label: t.settings.sections.tools, icon: WrenchIcon },
{
id: "account",
@@ -177,6 +177,9 @@ function StatusCard() {
<span>
{tt.archived}: {status.agent_created_skills.archived}
</span>
<span>
{tt.pinned}: {status.agent_created_skills.pinned}
</span>
</span>
</StatRow>
<StatRow label={tt.lastActions} className="sm:col-span-2">
@@ -12,7 +12,8 @@ import {
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { useI18n } from "@/core/i18n/hooks";
import { BotIcon, ListChecks, MessageSquarePlus, MessagesSquare, SparklesIcon } from "lucide-react";
// import { BotIcon, BrainIcon, ListChecks, MessageSquarePlus, MessagesSquare, SparklesIcon, UsersIcon } from "lucide-react";
import { BotIcon, BrainIcon, CalendarClock, ListChecks, MessageSquarePlus, MessagesSquare, SparklesIcon, UsersIcon } from "lucide-react";
export function WorkspaceNavChatList() {
const { t } = useI18n();
@@ -61,6 +62,17 @@ export function WorkspaceNavChatList() {
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
isActive={pathname.startsWith("/page/workspace/memory")}
asChild
>
<Link className="text-sidebar-foreground" href="/page/workspace/memory">
<BrainIcon />
<span>记忆</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
isActive={pathname === "/page/workspace/scheduled/tasks"}
+2
View File
@@ -47,6 +47,8 @@ export interface CuratorStatus {
active: number;
stale: number;
archived: number;
/** 被 pin 的数量;与 active/stale/archived 正交。 */
pinned: number;
};
/** 上次运行 LLM 自述的建议(它"说"要做什么)。 */
last_consolidation_suggestions: CuratorSuggestions;
+63 -81
View File
@@ -365,81 +365,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: "The master switch for memory. When on, the assistant draws on what it remembers about you and past conversations; when off, it sees no memory at all and treats every chat as the first time. The options below only take effect when this is on.",
includeBuiltin: "Inject local memory",
includeBuiltinDesc: "Give the assistant locally stored memory as background knowledge: the user profile (your identity, habits and preferences) and work notes (key points the assistant saved while working with you). Kept in local server files — no external service required.",
includeHindsight: "Inject Hindsight recall",
includeHindsightDesc: "Before each reply, automatically retrieve memories relevant to the current topic from the Hindsight long-term memory store and add them as reference. Requires a deployed, reachable Hindsight service.",
maxTokens: "Injection token limit",
maxTokensDesc: "How much room injected memory may take up per turn (in tokens). Higher lets the assistant see more memory but leaves less space for the conversation itself. Allowed range 100–8000.",
memoryMode: "Hindsight tool mode",
memoryModeDesc: "How the assistant uses the Hindsight store: automatic background retrieval, an on-demand search tool the assistant calls itself, or both.",
memoryModeContext: "context — background auto recall only, no action needed",
memoryModeTools: "tools — search tool only, the assistant queries on demand",
memoryModeHybrid: "hybrid — background auto recall + search tool, both enabled",
recallBudget: "Recall depth",
recallBudgetDesc: "How thorough each Hindsight search is. Higher finds more but is slower and more costly; lower is faster but may miss relevant memories.",
recallBudgetLow: "Low",
recallBudgetMid: "Mid",
recallBudgetHigh: "High",
bankMission: "Hindsight synthesis settings",
bankMissionDesc: "For 'reflect' (synthesis) searches, tells Hindsight what role and perspective to take when summarizing memories. 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",
@@ -594,6 +575,7 @@ export const enUS: Translations = {
stale: "Stale",
archived: "Archived",
total: "Total",
pinned: "Pinned",
runNow: "Run now",
runStarted: "Background run started",
runBlockedEvolutionDisabled:
@@ -613,22 +595,22 @@ export const enUS: Translations = {
"Saves to skill_evolution.json and hot-reloads — no restart required.",
enabledLabel: "Enable skill evolution",
enabledHelper:
"When off, the agent cannot create or modify skills under custom/.",
"The master switch for skill evolution. When on, the agent gets the skill_manage tool — it can create, modify and delete skills under custom/, and is nudged to offer saving a workflow as a skill after a complex task. When off, those skill-writing abilities are removed and the agent can only view and use existing skills. All curator settings below require this to be on.",
curatorEnabledLabel: "Curator runs automatically",
curatorEnabledHelper:
"When off, the curator only runs when triggered from this page.",
"The curator is a background skill librarian that periodically scans your skill library: it flags long-unused skills as stale, then archives them, and uses an LLM to review whether duplicate skills should be merged. When on, it runs automatically on the interval below; when off, it only runs when you click \"Run now\" on this page.",
intervalHoursLabel: "Run interval (hours)",
intervalHoursHelper:
"Minimum hours between automatic curator runs.",
"Minimum hours between automatic curator runs. Default 168 (one week). Manual runs are not subject to this limit.",
staleAfterDaysLabel: "Stale threshold (days)",
staleAfterDaysHelper:
"Skills inactive for this many days are flagged stale.",
"A skill not used for this many days is flagged stale by the curator. Stale skills are no longer injected into the agent's prompt, but the files are kept — using one again restores it to active.",
archiveAfterDaysLabel: "Archive threshold (days)",
archiveAfterDaysHelper:
"Stale skills inactive for this many additional days are archived.",
"A skill idle for this many days since its last use is archived by the curator (counted as total idle days, not added on top of the stale threshold). Archiving is a soft delete — files stay under custom/ and can be restored anytime. Must be greater than the stale threshold.",
curatorModelLabel: "Curator review model",
curatorModelHelper:
"Model for the curator's LLM review and skill security moderation. Defaults to the primary model.",
"Model used for the curator's skill review (merge/prune decisions) and skill security moderation. Leave empty to use the primary model.",
curatorModelDefault: "Primary model (default)",
save: "Save changes",
saveSuccess: "Configuration updated",
+55 -68
View File
@@ -303,75 +303,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;
@@ -514,6 +500,7 @@ export interface Translations {
stale: string;
archived: string;
total: string;
pinned: string;
runNow: string;
runStarted: string;
runBlockedEvolutionDisabled: string;
+69 -79
View File
@@ -348,79 +348,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 长期记忆库中检索与当前话题相关的历史记忆,临时补充给助手参考。需要已部署并连通 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 综合推理设定",
bankMissionDesc: "使用「综合」模式检索时,告诉 Hindsight 以什么身份、从什么角度来归纳整理记忆。留空则使用默认设定。",
bankMissionPlaceholder: "例如:你是用户的长期助手,关注其工作偏好。",
settingsSaved: "设置已保存",
searchTitle: "Hindsight 记忆检索",
searchDesc: "在 Hindsight 长期记忆库里检索历史记忆。",
searchPlaceholder: "搜索历史记忆…",
searchModeRecall: "检索",
searchModeReflect: "综合",
searchButton: "搜索",
searchUnavailable: "Hindsight 未配置或不可用。",
searchNoResults: "没有找到相关记忆。",
searchResultsTitle: "检索结果",
searchEmpty: "输入关键词检索 Hindsight 记忆。",
},
appearance: {
themeTitle: "主题",
@@ -566,6 +549,7 @@ export const zhCN: Translations = {
stale: "陈旧",
archived: "已归档",
total: "合计",
pinned: "被钉住",
runNow: "立即执行",
runStarted: "已触发后台执行",
runBlockedEvolutionDisabled: "自我进化未开启 —— 请先在下方打开开关。",
@@ -582,17 +566,23 @@ export const zhCN: Translations = {
description:
"保存后会写入 skill_evolution.json 并热更新,无需重启。",
enabledLabel: "启用自我进化",
enabledHelper: "关闭后 agent 不会创建或修改 custom/ 下的技能。",
enabledHelper:
"自我进化的总开关。开启后会给 agent 注册 skill_manage 工具 —— 它就能在对话中创建、修改、删除 custom/ 目录下的技能,并在完成复杂任务后主动提议把流程沉淀为技能。关闭后这些写技能的能力全部收起,agent 只能查看和使用已有技能。下方 curator 的所有设置都需要此开关开启才生效。",
curatorEnabledLabel: "curator 自动运行",
curatorEnabledHelper: "关闭后只能在此页面手动触发。",
curatorEnabledHelper:
"curator 是后台的技能策展程序,会定期巡检技能库:把长期不用的技能标记为陈旧、再归档,并用 LLM 评审是否该合并重复技能。开启后按下面的运行间隔自动运行;关闭后只能在本页面手动点「立即执行」触发。",
intervalHoursLabel: "运行间隔(小时)",
intervalHoursHelper: "两次自动运行之间至少经过的小时数。",
intervalHoursHelper:
"两次 curator 自动运行之间至少间隔的小时数,默认 168 小时(一周)。手动触发不受此限制。",
staleAfterDaysLabel: "陈旧阈值(天)",
staleAfterDaysHelper: "技能长时间不活跃后被标记为陈旧。",
staleAfterDaysHelper:
"一个技能连续这么多天没被使用,就会被 curator 标记为「陈旧」。陈旧技能不再注入 agent 的提示词,但文件仍保留;被重新用到时会自动恢复为活跃。",
archiveAfterDaysLabel: "归档阈值(天)",
archiveAfterDaysHelper: "标记陈旧后再经过这么多天会被自动归档。",
archiveAfterDaysHelper:
"一个技能从最后一次使用算起连续闲置这么多天,就会被 curator 自动「归档」(按总闲置天数计算,不是在标记陈旧之后再单独累加)。归档是软删除,文件仍留在 custom/ 目录、可随时手动恢复;此值需大于陈旧阈值。",
curatorModelLabel: "技能优化模型",
curatorModelHelper: "用于 curator 技能评审和技能安全审核的模型,为空则使用主模型。",
curatorModelHelper:
"curator 做技能评审(判断能否合并、精简)和技能安全审核时使用的模型。留空则使用主模型。",
curatorModelDefault: "主模型(默认)",
save: "保存修改",
saveSuccess: "配置已更新",
+143 -149
View File
@@ -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");
}
+92 -84
View File
@@ -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);
},
});
}
+57 -54
View File
@@ -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;
}
+9
View File
@@ -0,0 +1,9 @@
import { MemorySettingsPage } from "@/components/workspace/settings/memory-settings-page";
export default function MemoryPage() {
return (
<div className="flex flex-col h-full overflow-auto p-6">
<MemorySettingsPage />
</div>
);
}
@@ -22,6 +22,7 @@ import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
import ScheduledTaskRunDetailPage from "./ScheduledTaskRunDetailPage";
import ScheduledTasksPage from "./ScheduledTasksPage";
import LlmMetricsPage from "./LlmMetricsPage";
import MemoryPage from "./MemoryPage";
import PromptPrefixSettingsPage from "./PromptPrefixSettingsPage";
import SkillEvolutionPage from "./SkillEvolutionPage";
import SkillsPage from "./SkillsPage";
@@ -158,6 +159,14 @@ export default function WorkspaceRoutes({
</WorkspaceLayout>
}
/>
<Route
path="memory"
element={
<WorkspaceLayout {...layoutProps}>
<MemoryPage />
</WorkspaceLayout>
}
/>
<Route
path="scheduled"
element={<Navigate to="/page/workspace/scheduled/tasks" replace />}