Files
metona-ai-desktop/electron/harness/memory/manager.ts
T
thzxx 97a3d3d53b fix: 全面修复审计问题并优化系统提示词
**崩溃/挂死修复 (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 (状态由右键菜单展示)
2026-06-30 23:17:38 +08:00

214 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Memory Manager — 记忆管理器
*
* 基于 SQLitebetter-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));
}
}