**崩溃/挂死修复 (5):** - 统一 TrayManager.isQuitting 变量,修复 Cmd+Q 无法退出 - useAgentStream 闭包过期快照 → 每次 getState() - Agnes chatStream 添加 AbortSignal.timeout - SSE JSON.parse 添加 try-catch 保护 - Orchestrator setTools 污染 → save/restore 模式 **功能修复 (14):** - 上下文压缩实现 (每5轮 COMPRESSING 状态) - 修复 requestId 硬编码空串 - ConfigService.set() 保留已有 category - MemoryManager 新增 working 类型搜索 - PromptInjectionDefender 补全 sanitize() - Ollama: 补全 dynamicReminders + reasoningContent - openai-format: 所有 assistant 消息保留 reasoningContent - SSE: finish_reason 时提前 flush tool_calls - DeepSeek thinking effort 映射注释 - Ollama done_reason load→stop - RateLimitHook >= 边界修复 - WorkspaceService isValid 首次启动修复 - sessions:archive IPC handler - 托盘/窗口图标路径生产环境修复 **系统提示词优化:** - SOUL.md 存在时不显示兜底身份,原文放最前 - 兜底身份改为中文 (MetonaAI 自身描述) - 用户文本在前,附件内容在后 **文件上传:** - 非图片文件不再 base64 编码,保留 JSON 结构 - 用户文本优先于文件内容 **UI 修复:** - 首页 Logo 路径修复 (public/ + 相对路径) - TokenUsage contextWindow 动态计算 (Provider 感知) - 切换 Provider 同步 contextWindow - 托盘图标始终显示 Logo (状态由右键菜单展示)
214 lines
7.0 KiB
TypeScript
214 lines
7.0 KiB
TypeScript
/**
|
||
* Memory Manager — 记忆管理器
|
||
*
|
||
* 基于 SQLite(better-sqlite3)的三层记忆系统。
|
||
* 表结构由 DatabaseService 统一创建,此处不再重复。
|
||
*
|
||
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第六章
|
||
* @see standard/开发规范.md — 使用 better-sqlite3(禁止自写数据库层)
|
||
*/
|
||
|
||
import { nanoid } from 'nanoid';
|
||
import type Database from 'better-sqlite3';
|
||
import log from 'electron-log';
|
||
|
||
export type MemoryType = 'episodic' | 'semantic' | 'working';
|
||
export type MemorySource = 'user_input' | 'tool_result' | 'agent_thought' | 'imported';
|
||
|
||
export interface MemoryItem {
|
||
id: string;
|
||
type: MemoryType;
|
||
content: string;
|
||
summary?: string;
|
||
source: MemorySource;
|
||
importance: number;
|
||
sessionId?: string;
|
||
createdAt: number;
|
||
expiresAt?: number;
|
||
}
|
||
|
||
export interface SearchResult extends MemoryItem {
|
||
score: number;
|
||
}
|
||
|
||
interface MemorySearchOptions {
|
||
topK?: number;
|
||
sessionId?: string;
|
||
type?: MemoryType;
|
||
minImportance?: number;
|
||
}
|
||
|
||
/**
|
||
* 记忆管理器
|
||
*/
|
||
export class MemoryManager {
|
||
constructor(private getDB: () => Database.Database) {}
|
||
|
||
/**
|
||
* 初始化(表结构由 DatabaseService 创建)
|
||
*/
|
||
initialize(): void {
|
||
log.info('MemoryManager initialized');
|
||
}
|
||
|
||
/**
|
||
* 存储记忆
|
||
*/
|
||
store(item: Omit<MemoryItem, 'id' | 'createdAt'>): string {
|
||
const db = this.getDB();
|
||
const id = `mem_${nanoid(12)}`;
|
||
const now = Date.now();
|
||
const importance = item.importance ?? this.calculateImportance(item);
|
||
|
||
switch (item.type) {
|
||
case 'episodic':
|
||
db.prepare(`
|
||
INSERT INTO episodic_memories (id, session_id, content, summary, source, importance, created_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
`).run(id, item.sessionId ?? null, item.content, item.summary ?? null, item.source, importance, now);
|
||
break;
|
||
case 'semantic':
|
||
db.prepare(`
|
||
INSERT OR REPLACE INTO semantic_memories (id, key, value, category, confidence, source_session, created_at, updated_at, access_count)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||
`).run(id, id, item.content, 'general', importance, item.sessionId ?? null, now, now);
|
||
break;
|
||
case 'working':
|
||
db.prepare(`
|
||
INSERT OR REPLACE INTO working_memories (id, session_id, task_id, key, value, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
`).run(id, item.sessionId ?? 'default', 'default', 'default', item.content, now);
|
||
break;
|
||
}
|
||
|
||
log.debug(`Memory stored: ${id} (${item.type})`);
|
||
return id;
|
||
}
|
||
|
||
/**
|
||
* 检索记忆(关键词搜索)
|
||
*/
|
||
search(query: string, options: MemorySearchOptions = {}): SearchResult[] {
|
||
const db = this.getDB();
|
||
const { topK = 5, type, minImportance = 0 } = options;
|
||
if (!query) return [];
|
||
|
||
const pattern = `%${query}%`;
|
||
const results: SearchResult[] = [];
|
||
|
||
// 搜索情节记忆
|
||
if (!type || type === 'episodic') {
|
||
const rows = db.prepare(`
|
||
SELECT * FROM episodic_memories
|
||
WHERE (content LIKE ? OR summary LIKE ?) AND importance >= ?
|
||
ORDER BY importance DESC, created_at DESC LIMIT ?
|
||
`).all(pattern, pattern, minImportance, topK) as Array<{
|
||
id: string; session_id: string | null; content: string; summary: string | null;
|
||
source: string; importance: number; created_at: number; expires_at: number | null;
|
||
}>;
|
||
for (const row of rows) {
|
||
results.push({
|
||
id: row.id, type: 'episodic', content: row.content,
|
||
summary: row.summary ?? undefined, source: row.source as MemorySource,
|
||
importance: row.importance, sessionId: row.session_id ?? undefined,
|
||
createdAt: row.created_at, expiresAt: row.expires_at ?? undefined,
|
||
score: row.importance,
|
||
});
|
||
}
|
||
}
|
||
|
||
// 搜索语义记忆
|
||
if (!type || type === 'semantic') {
|
||
const rows = db.prepare(`
|
||
SELECT * FROM semantic_memories
|
||
WHERE (key LIKE ? OR value LIKE ?) AND confidence >= ?
|
||
ORDER BY confidence DESC, access_count DESC LIMIT ?
|
||
`).all(pattern, pattern, minImportance, Math.ceil(topK / 2)) as Array<{
|
||
id: string; key: string; value: string; category: string | null;
|
||
confidence: number; source_session: string | null; created_at: number;
|
||
}>;
|
||
for (const row of rows) {
|
||
results.push({
|
||
id: row.id, type: 'semantic', content: row.value,
|
||
source: 'imported', importance: row.confidence,
|
||
sessionId: row.source_session ?? undefined,
|
||
createdAt: row.created_at, score: row.confidence,
|
||
});
|
||
}
|
||
}
|
||
|
||
// 搜索工作记忆
|
||
if (type === 'working') {
|
||
const rows = db.prepare(`
|
||
SELECT * FROM working_memories
|
||
WHERE (key LIKE ? OR value LIKE ?)
|
||
ORDER BY updated_at DESC LIMIT ?
|
||
`).all(pattern, pattern, topK) as Array<{
|
||
id: string; session_id: string; task_id: string;
|
||
key: string; value: string; updated_at: number;
|
||
}>;
|
||
for (const row of rows) {
|
||
results.push({
|
||
id: row.id, type: 'working', content: row.value,
|
||
source: 'agent_thought', importance: 0.5,
|
||
sessionId: row.session_id, createdAt: row.updated_at,
|
||
score: 0.3, // 工作记忆权重较低
|
||
});
|
||
}
|
||
}
|
||
|
||
return results.sort((a, b) => b.score - a.score).slice(0, topK);
|
||
}
|
||
|
||
/**
|
||
* 获取工作记忆
|
||
*/
|
||
getWorkingMemory(sessionId: string, taskId: string = 'default'): Map<string, string> {
|
||
const db = this.getDB();
|
||
const rows = db.prepare(`
|
||
SELECT key, value FROM working_memories WHERE session_id = ? AND task_id = ?
|
||
`).all(sessionId, taskId) as Array<{ key: string; value: string }>;
|
||
return new Map(rows.map((r) => [r.key, r.value]));
|
||
}
|
||
|
||
/**
|
||
* 更新工作记忆
|
||
*/
|
||
setWorkingMemory(sessionId: string, taskId: string, key: string, value: string): void {
|
||
const db = this.getDB();
|
||
db.prepare(`
|
||
INSERT OR REPLACE INTO working_memories (id, session_id, task_id, key, value, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
`).run(`wm_${nanoid(8)}`, sessionId, taskId, key, value, Date.now());
|
||
}
|
||
|
||
/**
|
||
* 清除工作记忆
|
||
*/
|
||
clearWorkingMemory(sessionId: string, taskId?: string): void {
|
||
const db = this.getDB();
|
||
if (taskId) {
|
||
db.prepare('DELETE FROM working_memories WHERE session_id = ? AND task_id = ?').run(sessionId, taskId);
|
||
} else {
|
||
db.prepare('DELETE FROM working_memories WHERE session_id = ?').run(sessionId);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理过期记忆
|
||
*/
|
||
cleanupExpired(): number {
|
||
const db = this.getDB();
|
||
const result = db.prepare('DELETE FROM episodic_memories WHERE expires_at IS NOT NULL AND expires_at < ?').run(Date.now());
|
||
return result.changes;
|
||
}
|
||
|
||
private calculateImportance(item: Omit<MemoryItem, 'id' | 'createdAt'>): number {
|
||
let score = 0.5;
|
||
if (item.source === 'user_input') score += 0.2;
|
||
if (item.source === 'tool_result') score += 0.1;
|
||
if (item.content.length > 200) score += 0.1;
|
||
return Math.min(1, Math.max(0, score));
|
||
}
|
||
}
|