From 5825ae7742b619e93e053da91d9e433e9db3aa3d Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Thu, 16 Apr 2026 09:29:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=B0=E5=BF=86=E6=A8=A1=E6=80=81?= =?UTF-8?q?=E6=A1=86=E6=94=AF=E6=8C=81=E5=AF=BC=E5=85=A5=20Markdown=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 工具栏新增「导入」按钮,仅限 .md 文件 - 按 ## 二级标题自动分块解析为记忆条目 - 从标题自动推断记忆类型(事实/偏好/规则) - 支持从标题提取重要性标记(importance:N) - 回退策略:无标题分块时按段落拆分 --- src/renderer/components/memory-modal.ts | 111 ++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/src/renderer/components/memory-modal.ts b/src/renderer/components/memory-modal.ts index fee5a7d..73757b5 100644 --- a/src/renderer/components/memory-modal.ts +++ b/src/renderer/components/memory-modal.ts @@ -91,6 +91,7 @@ export function initMemoryModal(): void { 关键词模式 + @@ -134,6 +135,9 @@ export function initMemoryModal(): void { // 添加记忆 overlay.querySelector('#btnAddMemoryLg')!.addEventListener('click', openAddDialog); + // 导入 Markdown 记忆文档 + overlay.querySelector('#btnImportMemoryMd')!.addEventListener('click', importMemoryFromMd); + // 清空 overlay.querySelector('#btnClearMemoryLg')!.addEventListener('click', async () => { if (await showConfirm('确定清空所有记忆?此操作不可恢复!', '清空记忆')) { @@ -278,6 +282,113 @@ function renderList(): void { }); } +// ── 导入 Markdown 记忆文档 ── + +async function importMemoryFromMd(): Promise { + const bridge = (window as any).metonaDesktop; + if (!bridge || !bridge.isDesktop) { + showToast('导入功能仅在桌面端可用', 'warning'); + return; + } + + const paths = await bridge.dialog.openFile({ + filters: [{ name: 'Markdown 文档', extensions: ['md'] }] + }); + if (!paths || paths.length === 0) return; + + const filePath = paths[0]; + const fileName = filePath.split(/[/\\]/).pop() || filePath; + + try { + const result = await bridge.fs.readFile(filePath); + if (!result.success) { + showToast(`读取失败: ${result.error}`, 'error'); + return; + } + + const content = result.content as string; + const sections = parseMdToMemories(content); + + if (sections.length === 0) { + showToast('未从文档中解析到有效记忆内容', 'warning'); + return; + } + + let imported = 0; + let skipped = 0; + for (const sec of sections) { + if (!sec.content || sec.content.length < 5) { skipped++; continue; } + await addMemory({ + type: sec.type || 'fact', + content: sec.content, + importance: sec.importance || 5, + tags: sec.tags || [], + source: fileName + }); + imported++; + } + + renderList(); + updateStats(); + showToast(`已从 ${fileName} 导入 ${imported} 条记忆${skipped > 0 ? `(跳过 ${skipped} 条)` : ''}`, 'success', 3000); + } catch (err) { + showToast(`导入失败: ${(err as Error).message}`, 'error'); + } +} + +function parseMdToMemories(md: string): Array<{ type: MemoryType; content: string; importance: number; tags: string[] }> { + const results: Array<{ type: MemoryType; content: string; importance: number; tags: string[] }> = []; + + // 按 ## 二级标题分块 + const blocks = md.split(/^## /m).filter(b => b.trim()); + + for (const block of blocks) { + const lines = block.trim().split('\n'); + const heading = lines[0]?.trim() || ''; + const body = lines.slice(1).join('\n').trim(); + if (!body && !heading) continue; + + // 从标题推断类型 + let type: MemoryType = 'fact'; + const headingLower = heading.toLowerCase(); + if (headingLower.includes('偏好') || headingLower.includes('preference') || headingLower.includes('风格') || headingLower.includes('习惯')) { + type = 'preference'; + } else if (headingLower.includes('规则') || headingLower.includes('rule') || headingLower.includes('规范') || headingLower.includes('约束')) { + type = 'rule'; + } + + // 提取标签:标题中的关键词 + 正文前几个有意义的词 + const tags: string[] = []; + if (heading) { + const headingTags = heading.replace(/[^\w\u4e00-\u9fff\s]/g, ' ').split(/\s+/).filter(w => w.length > 1); + tags.push(...headingTags.slice(0, 3)); + } + + // 内容:标题 + 正文(如果正文为空用标题作内容) + const content = body ? `${heading ? heading + ':' : ''}${body}` : heading; + if (content.length < 5) continue; + + // 从标题提取重要性(如 "重要性:8" 或 "importance: 7") + let importance = 5; + const impMatch = heading.match(/(?:重要性|importance)\s*[::]\s*(\d+)/i); + if (impMatch) importance = Math.min(10, Math.max(1, parseInt(impMatch[1]) || 5)); + + results.push({ type, content: content.slice(0, 500), importance, tags }); + } + + // 如果按标题分块无结果,按段落分块 + if (results.length === 0) { + const paragraphs = md.split(/\n{2,}/).filter(p => p.trim()); + for (const para of paragraphs) { + const text = para.trim().replace(/^[-*]\s+/gm, '').replace(/^#+\s+/gm, ''); + if (text.length < 10) continue; + results.push({ type: 'fact', content: text.slice(0, 500), importance: 5, tags: [] }); + } + } + + return results; +} + // ── 添加对话框 ── async function openAddDialog(): Promise {