diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index 892128f..2592097 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -204,7 +204,7 @@ export default { kanban: 'Kanban', workflow: 'Workflow', models: 'Models', - profiles: 'Profiles', + profiles: 'Agent', skills: 'Skills', plugins: 'Plugins', mcp: 'MCP', @@ -1195,6 +1195,9 @@ export default { title: 'Memory', refresh: 'Refresh', loadFailed: 'Failed to load memory', + editTitle: 'Edit · {name}', + writeTo: 'Write to', + charCount: '{count} chars', myNotes: 'My Notes', noNotes: 'No notes yet.', notesPlaceholder: 'Write your notes...', diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index 9b3cd3f..2331d60 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -204,7 +204,7 @@ export default { kanban: '看板', workflow: '工作流', models: '模型', - profiles: '用户', + profiles: 'Agent', plugins: '插件', mcp: 'MCP', skills: '技能', @@ -1195,6 +1195,9 @@ export default { title: '记忆', refresh: '刷新', loadFailed: '加载记忆失败', + editTitle: '修改 · {name}', + writeTo: '写入文件', + charCount: '{count} 字符', myNotes: '我的笔记', noNotes: '暂无笔记。', notesPlaceholder: '输入笔记内容...', diff --git a/packages/client/src/views/hermes/MemoryView.vue b/packages/client/src/views/hermes/MemoryView.vue index a64c2e9..87a72e2 100644 --- a/packages/client/src/views/hermes/MemoryView.vue +++ b/packages/client/src/views/hermes/MemoryView.vue @@ -2,19 +2,40 @@ import { ref, onMounted, computed } from 'vue' import { useMessage } from '@/composables/useMessage' import { useI18n } from 'vue-i18n' -import MarkdownRenderer from '@/components/hermes/chat/MarkdownRenderer.vue' import { fetchMemory, saveMemory, type MemoryData } from '@/api/hermes/skills' import { useProfilesStore } from '@/stores/hermes/profiles' +type MemorySection = 'memory' | 'user' | 'soul' + const { t } = useI18n() const message = useMessage() const profilesStore = useProfilesStore() const loading = ref(false) const data = ref(null) -const editingSection = ref<'memory' | 'user' | 'soul' | null>(null) +const editingSection = ref(null) const editContent = ref('') const saving = ref(false) +// 三段记忆固定对应三个文件,落盘路径在服务端决定,前端只做展示。 +const SECTION_FILES: Record = { + memory: 'memories/MEMORY.md', + user: 'memories/USER.md', + soul: 'SOUL.md', +} + +const SECTIONS: { key: MemorySection; labelKey: string }[] = [ + { key: 'memory', labelKey: 'memory.myNotes' }, + { key: 'user', labelKey: 'memory.userProfile' }, + { key: 'soul', labelKey: 'memory.soul' }, +] + +// 各段的修改时间在接口里是平铺字段,key 不可由 section 直接拼出(soul_mtime 等)。 +const MTIME_KEY: Record = { + memory: 'memory_mtime', + user: 'user_mtime', + soul: 'soul_mtime', +} + onMounted(loadMemory) async function loadMemory() { @@ -32,7 +53,7 @@ async function loadMemory() { } } -function startEdit(section: 'memory' | 'user' | 'soul') { +function startEdit(section: MemorySection) { editingSection.value = section editContent.value = data.value?.[section] || '' } @@ -59,7 +80,7 @@ async function handleSave() { } function formatTime(ts: number | null): string { - if (!ts) return '' + if (!ts) return '—' return new Date(ts).toLocaleString([], { month: 'short', day: 'numeric', @@ -68,193 +89,181 @@ function formatTime(ts: number | null): string { }) } -const memoryEmpty = computed(() => !data.value?.memory?.trim()) -const userEmpty = computed(() => !data.value?.user?.trim()) -const soulEmpty = computed(() => !data.value?.soul?.trim()) +// 存储态用 § 分隔段落;展示与摘要都按纯文本处理。 +function plainText(raw: string | undefined): string { + return (raw || '').replace(/§/g, ' ').replace(/\s+/g, ' ').trim() +} -const displayMemory = computed(() => (data.value?.memory || '').replace(/§/g, '\n\n')) -const displayUser = computed(() => (data.value?.user || '').replace(/§/g, '\n\n')) -const displaySoul = computed(() => (data.value?.soul || '').replace(/§/g, '\n\n')) +interface MemoryCard { + key: MemorySection + title: string + file: string + summary: string + mtime: number | null + chars: number + empty: boolean + // 进度条按 28 字符折算 1%,与原型一致。 + percent: number +} + +const cards = computed(() => + SECTIONS.map(({ key, labelKey }) => { + const raw = data.value?.[key] || '' + const plain = plainText(raw) + const chars = raw.length + return { + key, + title: t(labelKey), + file: SECTION_FILES[key], + summary: plain.length > 120 ? `${plain.slice(0, 120)}…` : plain, + mtime: data.value?.[MTIME_KEY[key]] ?? null, + chars, + empty: !raw.trim(), + percent: Math.min(100, Math.max(3, Math.round(chars / 28))), + } + }), +) + +const SECTION_LABEL_KEY: Record = { + memory: 'memory.myNotes', + user: 'memory.userProfile', + soul: 'memory.soul', +} + +const SECTION_PLACEHOLDER_KEY: Record = { + memory: 'memory.notesPlaceholder', + user: 'memory.profilePlaceholder', + soul: 'memory.soulPlaceholder', +} + +const SECTION_EMPTY_KEY: Record = { + memory: 'memory.noNotes', + user: 'memory.noProfile', + soul: 'memory.noSoul', +} + +const editingTitle = computed(() => + editingSection.value + ? t('memory.editTitle', { name: t(SECTION_LABEL_KEY[editingSection.value]) }) + : '', +) + +const editingFile = computed(() => (editingSection.value ? SECTION_FILES[editingSection.value] : '')) + +const editingPlaceholder = computed(() => + editingSection.value ? t(SECTION_PLACEHOLDER_KEY[editingSection.value]) : '', +)