feat: 记忆模态框支持导入 Markdown 文档
- 工具栏新增「导入」按钮,仅限 .md 文件 - 按 ## 二级标题自动分块解析为记忆条目 - 从标题自动推断记忆类型(事实/偏好/规则) - 支持从标题提取重要性标记(importance:N) - 回退策略:无标题分块时按段落拆分
This commit is contained in:
@@ -91,6 +91,7 @@ export function initMemoryModal(): void {
|
|||||||
<input type="text" id="memorySearchLg" class="memory-search-input-lg" placeholder="搜索记忆(支持关键词和语义搜索)...">
|
<input type="text" id="memorySearchLg" class="memory-search-input-lg" placeholder="搜索记忆(支持关键词和语义搜索)...">
|
||||||
</div>
|
</div>
|
||||||
<span id="memoryVectorBadge" class="memory-vector-badge disabled">关键词模式</span>
|
<span id="memoryVectorBadge" class="memory-vector-badge disabled">关键词模式</span>
|
||||||
|
<button class="btn btn-sm btn-outline" id="btnImportMemoryMd">📥 导入</button>
|
||||||
<button class="btn btn-sm btn-outline" id="btnAddMemoryLg">+ 添加</button>
|
<button class="btn btn-sm btn-outline" id="btnAddMemoryLg">+ 添加</button>
|
||||||
<button class="btn btn-sm btn-danger-outline" id="btnClearMemoryLg">清空</button>
|
<button class="btn btn-sm btn-danger-outline" id="btnClearMemoryLg">清空</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,6 +135,9 @@ export function initMemoryModal(): void {
|
|||||||
// 添加记忆
|
// 添加记忆
|
||||||
overlay.querySelector('#btnAddMemoryLg')!.addEventListener('click', openAddDialog);
|
overlay.querySelector('#btnAddMemoryLg')!.addEventListener('click', openAddDialog);
|
||||||
|
|
||||||
|
// 导入 Markdown 记忆文档
|
||||||
|
overlay.querySelector('#btnImportMemoryMd')!.addEventListener('click', importMemoryFromMd);
|
||||||
|
|
||||||
// 清空
|
// 清空
|
||||||
overlay.querySelector('#btnClearMemoryLg')!.addEventListener('click', async () => {
|
overlay.querySelector('#btnClearMemoryLg')!.addEventListener('click', async () => {
|
||||||
if (await showConfirm('确定清空所有记忆?此操作不可恢复!', '清空记忆')) {
|
if (await showConfirm('确定清空所有记忆?此操作不可恢复!', '清空记忆')) {
|
||||||
@@ -278,6 +282,113 @@ function renderList(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 导入 Markdown 记忆文档 ──
|
||||||
|
|
||||||
|
async function importMemoryFromMd(): Promise<void> {
|
||||||
|
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<void> {
|
async function openAddDialog(): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user