fix: 记忆去重时明确告知 AI「已存在,不要再重复添加」

之前 addEntry 检测到重复后静默返回已有条目(success),AI 误以为又成功保存了
新条目,于是继续循环调用。现在改为抛出 DuplicateEntryError,tool handler
捕获后返回 {success:true, duplicate:true, message:'已跳过...不要再重复添加'},
明确告诉 AI 任务已完成。

也处理了内存面板的手动添加场景,友好提示用户。
This commit is contained in:
紫影233
2026-06-24 15:31:58 +08:00
parent e39820d19e
commit 5f9acebad1
3 changed files with 34 additions and 7 deletions
+5 -1
View File
@@ -3,7 +3,7 @@
* 基于工作空间 MEMORY.md 文件 * 基于工作空间 MEMORY.md 文件
*/ */
import { loadAllEntries, addEntry, removeEntry, type MemoryEntry, type MemoryType } from '../services/memory-service.js'; import { loadAllEntries, addEntry, removeEntry, DuplicateEntryError, type MemoryEntry, type MemoryType } from '../services/memory-service.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';
import { showPrompt, showConfirm } from './prompt-modal.js'; import { showPrompt, showConfirm } from './prompt-modal.js';
import { escapeHtml, formatTime } from '../utils/utils.js'; import { escapeHtml, formatTime } from '../utils/utils.js';
@@ -170,6 +170,10 @@ async function openAddDialog(): Promise<void> {
await renderList(); await renderList();
showToast('记忆已添加', 'success', 1500); showToast('记忆已添加', 'success', 1500);
} catch (err) { } catch (err) {
if (err instanceof DuplicateEntryError) {
showToast('该记忆已存在,无需重复添加', 'info', 2000);
} else {
showToast(`添加失败: ${(err as Error).message}`, 'error'); showToast(`添加失败: ${(err as Error).message}`, 'error');
} }
} }
}
+16 -1
View File
@@ -21,6 +21,18 @@ import { logInfo, logWarn, logDebug, logMemory } from './log-service.js';
// 类型定义 // 类型定义
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
/** 重复条目错误:记忆已存在时抛出,告诉调用方无需重复添加 */
export class DuplicateEntryError extends Error {
existingId: string;
existingPreview: string;
constructor(id: string, preview: string) {
super(`重复条目: ${preview}... (ID: ${id})`);
this.name = 'DuplicateEntryError';
this.existingId = id;
this.existingPreview = preview;
}
}
export type MemoryType = 'fact' | 'preference' | 'rule'; export type MemoryType = 'fact' | 'preference' | 'rule';
export interface MemoryEntry { export interface MemoryEntry {
@@ -435,7 +447,10 @@ export async function addEntry(
if (prefixLen > 20 && content.slice(0, prefixLen) === e.content.slice(0, prefixLen)) return true; if (prefixLen > 20 && content.slice(0, prefixLen) === e.content.slice(0, prefixLen)) return true;
return simpleSimilarity(e.content, content) > 0.8; return simpleSimilarity(e.content, content) > 0.8;
}); });
if (existing) return existing; if (existing) {
logDebug('记忆去重: 已有相同条目', `${type}: ${content.slice(0, 40)}`);
throw new DuplicateEntryError(existing.id, existing.content.slice(0, 60));
}
const entry: MemoryEntry = { const entry: MemoryEntry = {
id: generateMemoryId(), id: generateMemoryId(),
+9 -1
View File
@@ -779,7 +779,7 @@ export async function executeTool(toolName: string, args: Record<string, unknown
try { try {
// 内存工具(不走 IPC,直接处理 MEMORY.md // 内存工具(不走 IPC,直接处理 MEMORY.md
if (toolName === 'memory') { if (toolName === 'memory') {
const { search, addEntry, replaceEntry, removeEntry, loadAllEntries } = await import('./memory-service.js'); const { search, addEntry, replaceEntry, removeEntry, loadAllEntries, DuplicateEntryError } = await import('./memory-service.js');
const action = args.action as string; const action = args.action as string;
switch (action) { switch (action) {
case 'search': { case 'search': {
@@ -796,9 +796,17 @@ export async function executeTool(toolName: string, args: Record<string, unknown
const importance = (args.importance as number) || 5; const importance = (args.importance as number) || 5;
const tags = (args.tags as string[]) || []; const tags = (args.tags as string[]) || [];
if (!content || content.length < 2) return { success: false, error: 'add 操作缺少 content 参数或内容太短。示例: {"action":"add","type":"fact","content":"具体的记忆内容"}' }; if (!content || content.length < 2) return { success: false, error: 'add 操作缺少 content 参数或内容太短。示例: {"action":"add","type":"fact","content":"具体的记忆内容"}' };
try {
const entry = await addEntry(type, content, importance, tags); const entry = await addEntry(type, content, importance, tags);
logToolResult('memory', true, `${type}: ${content.slice(0, 50)}`); logToolResult('memory', true, `${type}: ${content.slice(0, 50)}`);
return { success: true, action, id: entry.id, type: entry.type, content: entry.content }; return { success: true, action, id: entry.id, type: entry.type, content: entry.content };
} catch (err) {
if (err instanceof DuplicateEntryError) {
logToolResult('memory', true, `重复跳过: ${(err as DuplicateEntryError).existingPreview}`);
return { success: true, action, duplicate: true, existing_id: (err as DuplicateEntryError).existingId, message: `已跳过: 相同内容的记忆已存在 (${(err as DuplicateEntryError).existingPreview})。不要再重复添加,任务已完成。` };
}
throw err;
}
} }
case 'replace': { case 'replace': {
const oldText = String(args.old_text || ''); const oldText = String(args.old_text || '');