fix: 记忆去重时明确告知 AI「已存在,不要再重复添加」
之前 addEntry 检测到重复后静默返回已有条目(success),AI 误以为又成功保存了
新条目,于是继续循环调用。现在改为抛出 DuplicateEntryError,tool handler
捕获后返回 {success:true, duplicate:true, message:'已跳过...不要再重复添加'},
明确告诉 AI 任务已完成。
也处理了内存面板的手动添加场景,友好提示用户。
This commit is contained in:
@@ -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');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 || '');
|
||||||
|
|||||||
Reference in New Issue
Block a user