feat: memory工具新增remove_batch批量删除action,一次读+一次写高效删除多条记忆
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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} 条记忆`);
|
||||
|
||||
Reference in New Issue
Block a user