From 9bcde7f78d3be61a7f49d292be0b98388a353ec9 Mon Sep 17 00:00:00 2001 From: thzxx Date: Fri, 10 Jul 2026 21:58:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20memory=E5=B7=A5=E5=85=B7=E6=96=B0?= =?UTF-8?q?=E5=A2=9Eremove=5Fbatch=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4acti?= =?UTF-8?q?on=EF=BC=8C=E4=B8=80=E6=AC=A1=E8=AF=BB+=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=E5=86=99=E9=AB=98=E6=95=88=E5=88=A0=E9=99=A4=E5=A4=9A=E6=9D=A1?= =?UTF-8?q?=E8=AE=B0=E5=BF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/services/agent-engine.ts | 15 ++++++ src/renderer/services/memory-service.ts | 69 +++++++++++++++++++++++++ src/renderer/services/tool-registry.ts | 16 ++++-- 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/src/renderer/services/agent-engine.ts b/src/renderer/services/agent-engine.ts index 343e010..1c61785 100644 --- a/src/renderer/services/agent-engine.ts +++ b/src/renderer/services/agent-engine.ts @@ -605,6 +605,21 @@ function formatToolResultForModel(toolName: string, result: ToolResult): string } return JSON.stringify({ success: true, action: 'search', formatted: lines.join('\n'), total: results.length }); } + // remove_batch 结果:格式化每条匹配情况 + if ((result as any).action === 'remove_batch') { + const items = ((result as any).results || []) as Array<{ old_text: string; matched: boolean; entry_id?: string; error?: string }>; + const deleted = (result as any).deleted || 0; + const failed = (result as any).failed || 0; + const lines = [`[批量删除结果] 成功 ${deleted} 条${failed > 0 ? `, 失败 ${failed} 条` : ''}:`]; + for (const item of items) { + if (item.matched) { + lines.push(` ✅ "${item.old_text}" → 已删除 (${item.entry_id})`); + } else { + lines.push(` ❌ "${item.old_text}" → ${item.error || '失败'}`); + } + } + return JSON.stringify({ success: (result as any).success, action: 'remove_batch', formatted: lines.join('\n'), deleted, failed }); + } // 其他 action(add/replace/remove)→ 保留完整 JSON,走 default 逻辑 return formatDefaultToolResult(toolName, result); } diff --git a/src/renderer/services/memory-service.ts b/src/renderer/services/memory-service.ts index 3d570ba..a9657b5 100644 --- a/src/renderer/services/memory-service.ts +++ b/src/renderer/services/memory-service.ts @@ -612,6 +612,75 @@ export async function removeEntry(oldText: string): Promise<{ success: boolean; }); } +/** 批量删除记忆(一次读+一次写,高效) + * @param oldTexts 要删除的子串数组 + * @returns 每个子串的匹配结果 + 总体统计 + */ +export async function removeEntries(oldTexts: string[]): Promise<{ + success: boolean; + message: string; + results: Array<{ old_text: string; matched: boolean; entry_id?: string; error?: string }>; + deleted: number; + failed: number; +}> { + return withWriteLock(async () => { + const results: Array<{ old_text: string; matched: boolean; entry_id?: string; error?: string }> = []; + let deleted = 0; + let failed = 0; + + // 校验 + const validTexts = oldTexts.filter(t => { + if (!t || t.length < 5) { + results.push({ old_text: t || '(空)', matched: false, error: '至少 5 个字符' }); + failed++; + return false; + } + return true; + }); + + if (validTexts.length === 0) { + return { success: false, message: '没有有效的 old_text(每个至少 5 个字符)', results, deleted, failed }; + } + + const entries = await loadAllEntries(); + const idsToDelete = new Set(); + + for (const oldText of validTexts) { + const oldTextNorm = normalizeForDedup(oldText); + const matches = entries.filter(e => !idsToDelete.has(e.id) && normalizeForDedup(e.content).includes(oldTextNorm)); + + if (matches.length === 0) { + results.push({ old_text: oldText.slice(0, 50), matched: false, error: '未找到匹配' }); + failed++; + } else if (matches.length > 1) { + results.push({ old_text: oldText.slice(0, 50), matched: false, error: `匹配到 ${matches.length} 条,需更精确` }); + failed++; + } else { + idsToDelete.add(matches[0].id); + results.push({ old_text: oldText.slice(0, 50), matched: true, entry_id: matches[0].id }); + deleted++; + } + } + + if (idsToDelete.size === 0) { + return { success: false, message: `批量删除失败: ${failed} 条未匹配`, results, deleted: 0, failed }; + } + + const newEntries = entries.filter(e => !idsToDelete.has(e.id)); + const fileContent = newEntries.length > 0 ? serializeMemoryMd(newEntries) : ''; + await writeMemoryFile(fileContent); + + logMemory('批量删除', `删除 ${deleted} 条, 失败 ${failed} 条`); + return { + success: true, + message: `批量删除完成: 成功 ${deleted} 条${failed > 0 ? `, 失败 ${failed} 条` : ''}`, + results, + deleted, + failed, + }; + }); +} + /** 清空所有记忆 * M8: 使用写入锁串行化 */ export async function clearAll(): Promise { diff --git a/src/renderer/services/tool-registry.ts b/src/renderer/services/tool-registry.ts index c202797..22f77f5 100644 --- a/src/renderer/services/tool-registry.ts +++ b/src/renderer/services/tool-registry.ts @@ -364,14 +364,16 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ - add: {"action":"add","type":"fact","content":"the memory content","importance":8,"tags":["tag1","tag2"]} ← type and content are REQUIRED for add! - replace: {"action":"replace","old_text":"unique substring","new_content":"replacement"} - remove: {"action":"remove","old_text":"unique substring"} +- remove_batch: {"action":"remove_batch","old_texts":["substring1","substring2","substring3"]} ← Batch delete multiple entries in one call - read_all: {"action":"read_all"} -CRITICAL: For add action, you MUST include both "type" (fact/preference/rule) and "content" fields. If you omit them, the call will fail.`, +CRITICAL: For add action, you MUST include both "type" (fact/preference/rule) and "content" fields. If you omit them, the call will fail. +TIP: Use remove_batch when deleting multiple entries — it's much more efficient than calling remove repeatedly.`, parameters: { type: 'object', required: ['action'], properties: { - action: { type: 'string', enum: ['search', 'add', 'replace', 'remove', 'read_all'], description: 'Which operation to perform. Note: "add" requires "type"+"content", "replace" requires "old_text"+"new_content", "remove" requires "old_text", "search" requires "query".' }, + action: { type: 'string', enum: ['search', 'add', 'replace', 'remove', 'remove_batch', 'read_all'], description: 'Which operation to perform. Note: "add" requires "type"+"content", "replace" requires "old_text"+"new_content", "remove" requires "old_text", "remove_batch" requires "old_texts" (array), "search" requires "query".' }, query: { type: 'string', description: '[search REQUIRED] Keywords to search for.' }, limit: { type: 'integer', description: '[search] Max results. Default: 8.' }, type: { type: 'string', enum: ['fact', 'preference', 'rule'], description: '[add REQUIRED] Memory type. fact=user info, preference=user preference, rule=behavior rule.' }, @@ -379,6 +381,7 @@ CRITICAL: For add action, you MUST include both "type" (fact/preference/rule) an importance: { type: 'integer', description: '[add] Priority 1-10. Default: 5. Use 8+ for critical info.' }, tags: { type: 'array', items: { type: 'string' }, description: '[add] 2-5 keywords for search matching.' }, old_text: { type: 'string', description: '[replace, remove REQUIRED] Unique substring to identify the entry. Must be ≥5 chars. Use exact text from read_all for best results.' }, + old_texts: { type: 'array', items: { type: 'string' }, description: '[remove_batch REQUIRED] Array of substrings to identify multiple entries for batch deletion. Each must be ≥5 chars.' }, new_content: { type: 'string', description: '[replace REQUIRED] New text to replace with.' } } } @@ -799,7 +802,7 @@ export async function executeTool(toolName: string, args: Record