feat: memory工具新增remove_batch批量删除action,一次读+一次写高效删除多条记忆

This commit is contained in:
thzxx
2026-07-10 21:58:24 +08:00
parent 0145f0cfe1
commit 9bcde7f78d
3 changed files with 97 additions and 3 deletions
+15
View File
@@ -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 });
}
// 其他 actionadd/replace/remove)→ 保留完整 JSON,走 default 逻辑
return formatDefaultToolResult(toolName, result);
}
+69
View File
@@ -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<string>();
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<void> {
+13 -3
View File
@@ -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<string, unknown
try {
// 内存工具(不走 IPC,直接处理 MEMORY.md
if (toolName === 'memory') {
const { search, addEntry, replaceEntry, removeEntry, loadAllEntries, DuplicateEntryError } = await import('./memory-service.js');
const { search, addEntry, replaceEntry, removeEntry, removeEntries, loadAllEntries, DuplicateEntryError } = await import('./memory-service.js');
const action = args.action as string;
switch (action) {
case 'search': {
@@ -842,6 +845,13 @@ const results = await search(query, limit);
logToolResult('memory', result.success, result.message);
return { success: result.success, action, ...(result.success ? {} : { error: result.message }), message: result.message };
}
case 'remove_batch': {
const oldTexts = Array.isArray(args.old_texts) ? args.old_texts.map(String) : [];
if (oldTexts.length === 0) return { success: false, error: '缺少 old_texts 参数(字符串数组)' };
const result = await removeEntries(oldTexts);
logToolResult('memory', result.success, result.message);
return { success: result.success, action, results: result.results, deleted: result.deleted, failed: result.failed, ...(result.success ? {} : { error: result.message }), message: result.message };
}
case 'read_all': {
const entries = await loadAllEntries();
logToolResult('memory', true, `${entries.length} 条记忆`);