feat: 升级至 v0.3.0 — 安全增强、死循环检测、六轮全面审计修复
大版本迭代,新增安全增强、Agent Loop 增强、UI/UX 增强,经六轮全面审计修复所有问题。
新增功能:
- OutputValidator 事实一致性检查 + 幻觉检测
- PromptInjectionDefender 语义级检测(指令性动词密度、角色边界、分隔符嵌套)
- DeadLoopError 死循环检测(连续3轮相同工具调用自动终止)
- PolicyEngine maxFrequency 滑动窗口频率限制
- MemoryManager TF-IDF 语义检索 + IDF 缓存原子替换
- 斜杠命令(/tool /memory /clear /export)+ 快捷键(Ctrl+B/J/Shift+F/N/[/])
- 专注模式(Ctrl+Shift+F)带面板状态快照保存/恢复
六轮审计修复(共修复 3 CRITICAL + 8 HIGH + 13 MEDIUM + 11 LOW):
CRITICAL:
- main.ts 传入 createAdapter 而非 reloadAdapter,导致 Provider 切换完全失效
- Ctrl+N/Ctrl+[/Ctrl+] 不同步 agent-store,导致消息发到错误会话
- engine.ts emit('error') 无监听器导致 DONE 事件丢失、前端卡死
HIGH:
- 死循环检测在工具执行之后(移入 PARSING 后 EXECUTING 前)
- deadLoop 事件前端未处理
- ConfirmationHook.clearPending 从未调用导致定时器泄漏
- engine.ts retry abort listener 未移除导致监听器堆积
- MCPManager JSON.parse 无 try-catch 导致初始化崩溃
- sendMessage 自动创建会话不同步 session-store
- setCurrentSession 竞态导致旧请求覆盖新会话数据
MEDIUM:
- toggleFocusMode 覆盖用户原有面板状态
- 正则检测可被常见词绕过
- 频率限制内存泄漏 + customPolicies 覆盖
- LIKE 回退转义未包含反斜杠
- OutputValidator 新功能未传入 toolResults/context
- MemoryConsolidator LLM 调用无超时保护
- AuditService 每次 log 都查询数据库
- browser-window-manager 超时后未停止页面加载
- output-validator URL 比较大小写敏感
- FileReader 无 onerror 导致 Promise 永久挂起
- /clear /export 不关闭斜杠菜单
- 专注模式下 toggleSidebar/toggleDetail 未恢复另一面板快照
LOW:
- abortPromise 事件监听器堆积
- RateLimitHook Map 内存泄漏
- memory.ts await 同步方法
- PolicyEngine 死代码清理
- Orchestrator sessionDepth 会话结束不清理
- workspace.service.ts 元数据插入边界问题
This commit is contained in:
@@ -130,6 +130,10 @@ export class MemoryManager {
|
||||
|
||||
/**
|
||||
* 更新 IDF 缓存
|
||||
*
|
||||
* v0.3.0 增强:
|
||||
* - 原子替换缓存(先构建新数据再替换,避免中间不一致状态)
|
||||
* - 错误处理(数据库查询失败时保留旧缓存,不更新时间戳)
|
||||
*/
|
||||
private updateIdfCache(): void {
|
||||
const now = Date.now();
|
||||
@@ -138,35 +142,46 @@ export class MemoryManager {
|
||||
}
|
||||
|
||||
const db = this.getDB();
|
||||
this.idfCache.clear();
|
||||
|
||||
// 获取所有记忆内容(episodic + semantic + working)
|
||||
const episodicRows = db.prepare('SELECT content, summary FROM episodic_memories').all() as Array<{ content: string; summary: string | null }>;
|
||||
const semanticRows = db.prepare('SELECT value FROM semantic_memories').all() as Array<{ value: string }>;
|
||||
const workingRows = db.prepare('SELECT value FROM working_memories').all() as Array<{ value: string }>;
|
||||
try {
|
||||
// v0.3.0: 先构建新缓存数据,再原子替换
|
||||
const newIdfCache = new Map<string, number>();
|
||||
|
||||
const allDocs = [
|
||||
...episodicRows.map((r) => r.content + ' ' + (r.summary ?? '')),
|
||||
...semanticRows.map((r) => r.value),
|
||||
...workingRows.map((r) => r.value),
|
||||
];
|
||||
// 获取所有记忆内容(episodic + semantic + working)
|
||||
const episodicRows = db.prepare('SELECT content, summary FROM episodic_memories').all() as Array<{ content: string; summary: string | null }>;
|
||||
const semanticRows = db.prepare('SELECT value FROM semantic_memories').all() as Array<{ value: string }>;
|
||||
const workingRows = db.prepare('SELECT value FROM working_memories').all() as Array<{ value: string }>;
|
||||
|
||||
this.cachedDocCount = allDocs.length;
|
||||
const docFreq = new Map<string, number>();
|
||||
const allDocs = [
|
||||
...episodicRows.map((r) => r.content + ' ' + (r.summary ?? '')),
|
||||
...semanticRows.map((r) => r.value),
|
||||
...workingRows.map((r) => r.value),
|
||||
];
|
||||
|
||||
for (const doc of allDocs) {
|
||||
const tokens = new Set(tokenize(doc));
|
||||
for (const token of tokens) {
|
||||
docFreq.set(token, (docFreq.get(token) ?? 0) + 1);
|
||||
const newDocCount = allDocs.length;
|
||||
const docFreq = new Map<string, number>();
|
||||
|
||||
for (const doc of allDocs) {
|
||||
const tokens = new Set(tokenize(doc));
|
||||
for (const token of tokens) {
|
||||
docFreq.set(token, (docFreq.get(token) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IDF = log((N+1)/(df+1)) + 1(Sklearn 风格平滑),确保非负
|
||||
for (const [term, df] of docFreq) {
|
||||
this.idfCache.set(term, Math.log((this.cachedDocCount + 1) / (df + 1)) + 1);
|
||||
}
|
||||
// IDF = log((N+1)/(df+1)) + 1(Sklearn 风格平滑),确保非负
|
||||
for (const [term, df] of docFreq) {
|
||||
newIdfCache.set(term, Math.log((newDocCount + 1) / (df + 1)) + 1);
|
||||
}
|
||||
|
||||
this.cacheUpdatedAt = now;
|
||||
// v0.3.0: 原子替换 — 只有新数据完全准备好后才替换旧缓存
|
||||
this.idfCache = newIdfCache;
|
||||
this.cachedDocCount = newDocCount;
|
||||
this.cacheUpdatedAt = now;
|
||||
} catch (error) {
|
||||
// v0.3.0: 数据库查询失败时保留旧缓存,不更新 cacheUpdatedAt
|
||||
// 这样下次 search() 会再次尝试更新
|
||||
log.error('MemoryManager: Failed to update IDF cache, keeping stale cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,10 +314,15 @@ export class MemoryManager {
|
||||
|
||||
/**
|
||||
* 存储记忆
|
||||
*
|
||||
* v0.3.0 修复:
|
||||
* - switch 添加 default 分支,未知 type 抛错而非静默失败
|
||||
* - working 类型使用 item.id(若提供)或生成唯一 key,避免同 session 多次存储互相覆盖
|
||||
* - semantic 类型使用 item.summary 作为 key(若提供),支持更新已有记忆
|
||||
*/
|
||||
store(item: Omit<MemoryItem, 'id' | 'createdAt'>): string {
|
||||
store(item: Omit<MemoryItem, 'id' | 'createdAt'> & { id?: string }): string {
|
||||
const db = this.getDB();
|
||||
const id = `mem_${nanoid(12)}`;
|
||||
const id = item.id ?? `mem_${nanoid(12)}`;
|
||||
const now = Date.now();
|
||||
const importance = item.importance ?? this.calculateImportance(item);
|
||||
|
||||
@@ -314,17 +334,22 @@ export class MemoryManager {
|
||||
`).run(id, item.sessionId ?? null, item.content, item.summary ?? null, item.source, importance, now);
|
||||
break;
|
||||
case 'semantic':
|
||||
// v0.3.0 修复:使用 summary 作为 key(若提供),支持更新已有语义记忆
|
||||
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);
|
||||
`).run(id, item.summary ?? id, item.content, 'general', importance, item.sessionId ?? null, now, now);
|
||||
break;
|
||||
case 'working':
|
||||
// v0.3.0 修复:使用 summary 作为 key(若提供),避免硬编码 'default' 导致覆盖
|
||||
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);
|
||||
`).run(id, item.sessionId ?? 'default', 'default', item.summary ?? id, item.content, now);
|
||||
break;
|
||||
default:
|
||||
// v0.3.0 修复:未知 type 抛错而非静默失败
|
||||
throw new Error(`Unknown memory type: ${(item as { type: string }).type}`);
|
||||
}
|
||||
|
||||
// 使 IDF 缓存失效
|
||||
@@ -346,7 +371,8 @@ export class MemoryManager {
|
||||
search(query: string, options: MemorySearchOptions = {}): SearchResult[] {
|
||||
const db = this.getDB();
|
||||
const { topK = 5, type, minImportance = 0 } = options;
|
||||
if (!query) return [];
|
||||
// v0.3.0 修复:拦截空 query 和纯空格 query
|
||||
if (!query || !query.trim()) return [];
|
||||
|
||||
// v0.2.0: 优先使用 TF-IDF 语义搜索
|
||||
const tfidfResults = this.tfidfSearch(query, options);
|
||||
@@ -356,7 +382,8 @@ export class MemoryManager {
|
||||
|
||||
// 回退:如果 TF-IDF 没有结果(如 IDF 缓存为空),使用 LIKE 关键词搜索
|
||||
// 转义 LIKE 通配符,避免用户输入的 % 和 _ 影响匹配
|
||||
const escapedQuery = query.replace(/[%_]/g, '\\$&');
|
||||
// v0.3.0 修复: 反斜杠也需转义,否则含 \ 的搜索(如 Windows 路径)会导致 SQLite LIKE 报错
|
||||
const escapedQuery = query.replace(/[%_\\]/g, '\\$&');
|
||||
const pattern = `%${escapedQuery}%`;
|
||||
const results: SearchResult[] = [];
|
||||
|
||||
@@ -402,7 +429,8 @@ export class MemoryManager {
|
||||
}
|
||||
|
||||
// 搜索工作记忆
|
||||
if (type === 'working') {
|
||||
// v0.3.0 修复:LIKE 回退路径也需添加 !type 分支(与 tfidfSearch 保持一致)
|
||||
if (!type || type === 'working') {
|
||||
const rows = db.prepare(`
|
||||
SELECT * FROM working_memories
|
||||
WHERE (key LIKE ? ESCAPE '\\' OR value LIKE ? ESCAPE '\\')
|
||||
|
||||
Reference in New Issue
Block a user