feat: 升级至 v0.3.1 — 全量代码审计修复 + 安全增强
本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题, 并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。 Critical (10/10 完成): - C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配 可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过 High (11/11 完成): - 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等 Medium (55/55 完成): - 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、 React 组件 cancelled 标志、类型收窄等 Low (20/20 完成): - 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等) - nanoid 统一替代 Date.now()+Math.random() - confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等 返工审计修复 (10/10 完成): - L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert - M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死 - M-42: 脱敏短值(length <= 4)泄露修复 - M-47: tasks:update 补全 title/description 类型校验 - L-9: ollama.adapter 非流式路径 nanoid 统一 - M-45: audit:query limit 策略与 memory:listAll 一致化 - SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup - L-15: CommandPalette useMemo 补全 sessions 响应式依赖 - useAgentStream 事件类型补全 seq/timestamp 字段 新增依赖: shell-quote + @types/shell-quote 版本号: 0.3.0 -> 0.3.1
This commit is contained in:
@@ -219,11 +219,18 @@ export class MemoryConsolidator {
|
||||
|
||||
try {
|
||||
// v0.3.0 修复: 添加 30 秒超时保护,防止 LLM 响应缓慢导致 consolidator 任务挂起
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('LLM extraction timeout')), 30_000),
|
||||
);
|
||||
const response = await Promise.race([this.adapter.chat(request), timeoutPromise]);
|
||||
return response.content.trim();
|
||||
// M-17 修复: 使用 try/finally 清理 setTimeout,防止每次会话结束时 timer 堆积
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error('LLM extraction timeout')), 30_000);
|
||||
});
|
||||
const response = await Promise.race([this.adapter.send(request), timeoutPromise]);
|
||||
return response.content.trim();
|
||||
} finally {
|
||||
// M-17 修复: LLM 调用正常完成时清理未触发的 30 秒 timer
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn('[MemoryConsolidator] LLM call failed:', (error as Error).message);
|
||||
return null;
|
||||
|
||||
@@ -184,6 +184,61 @@ export class MemoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* L-5 修复: 提取 scoreAndPushMemory 辅助函数
|
||||
*
|
||||
* 计算 TF-IDF 余弦相似度并应用时间衰减和重要度权重,
|
||||
* 将分数 > 0 的记忆 push 到 results 数组。
|
||||
*
|
||||
* 三种记忆类型(episodic/semantic/working)的评分逻辑统一调用此函数,
|
||||
* 仅在调用前构造 docText/createdAt/importance 等参数。
|
||||
*
|
||||
* @param params - 评分参数
|
||||
* @param results - 结果数组(push 到此数组)
|
||||
*/
|
||||
private scoreAndPushMemory(
|
||||
params: {
|
||||
docText: string;
|
||||
createdAt: number;
|
||||
importance: number;
|
||||
id: string;
|
||||
type: MemoryType;
|
||||
content: string;
|
||||
summary?: string;
|
||||
source: MemorySource;
|
||||
sessionId?: string;
|
||||
expiresAt?: number;
|
||||
},
|
||||
queryTF: Map<string, number>,
|
||||
queryNorm: number,
|
||||
now: number,
|
||||
results: SearchResult[],
|
||||
): void {
|
||||
const docTokens = tokenize(params.docText);
|
||||
const docTF = computeTF(docTokens);
|
||||
const docNorm = vectorNorm(docTF, this.idfCache);
|
||||
|
||||
if (docNorm === 0) return;
|
||||
|
||||
const dotProd = dotProduct(queryTF, docTF, this.idfCache);
|
||||
const cosineSim = dotProd / (queryNorm * docNorm);
|
||||
|
||||
// 时间衰减
|
||||
const decayWeight = timeDecayWeight(params.createdAt, now);
|
||||
// 最终分数 = 余弦相似度 * 时间衰减 * 重要度权重
|
||||
const finalScore = cosineSim * decayWeight * (0.5 + params.importance * 0.5);
|
||||
|
||||
if (finalScore > 0) {
|
||||
results.push({
|
||||
id: params.id, type: params.type, content: params.content,
|
||||
summary: params.summary, source: params.source,
|
||||
importance: params.importance, sessionId: params.sessionId,
|
||||
createdAt: params.createdAt, expiresAt: params.expiresAt,
|
||||
score: finalScore,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TF-IDF 相似度搜索
|
||||
*/
|
||||
@@ -202,6 +257,8 @@ export class MemoryManager {
|
||||
const results: SearchResult[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
// L-5 修复: 三段搜索统一调用 scoreAndPushMemory,消除重复的 tokenize/computeTF/vectorNorm/dotProduct 逻辑
|
||||
|
||||
// 搜索 episodic 记忆
|
||||
if (!type || type === 'episodic') {
|
||||
const rows = db.prepare(`
|
||||
@@ -213,30 +270,16 @@ export class MemoryManager {
|
||||
}>;
|
||||
|
||||
for (const row of rows) {
|
||||
const docText = row.content + ' ' + (row.summary ?? '');
|
||||
const docTokens = tokenize(docText);
|
||||
const docTF = computeTF(docTokens);
|
||||
const docNorm = vectorNorm(docTF, this.idfCache);
|
||||
|
||||
if (docNorm === 0) continue;
|
||||
|
||||
const dotProd = dotProduct(queryTF, docTF, this.idfCache);
|
||||
const cosineSim = dotProd / (queryNorm * docNorm);
|
||||
|
||||
// 时间衰减
|
||||
const decayWeight = timeDecayWeight(row.created_at, now);
|
||||
// 最终分数 = 余弦相似度 * 时间衰减 * 重要度权重
|
||||
const finalScore = cosineSim * decayWeight * (0.5 + row.importance * 0.5);
|
||||
|
||||
if (finalScore > 0) {
|
||||
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: finalScore,
|
||||
});
|
||||
}
|
||||
this.scoreAndPushMemory({
|
||||
docText: row.content + ' ' + (row.summary ?? ''),
|
||||
createdAt: row.created_at,
|
||||
importance: row.importance,
|
||||
id: row.id, type: 'episodic', content: row.content,
|
||||
summary: row.summary ?? undefined,
|
||||
source: row.source as MemorySource,
|
||||
sessionId: row.session_id ?? undefined,
|
||||
expiresAt: row.expires_at ?? undefined,
|
||||
}, queryTF, queryNorm, now, results);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,27 +294,14 @@ export class MemoryManager {
|
||||
}>;
|
||||
|
||||
for (const row of rows) {
|
||||
const docText = row.key + ' ' + row.value;
|
||||
const docTokens = tokenize(docText);
|
||||
const docTF = computeTF(docTokens);
|
||||
const docNorm = vectorNorm(docTF, this.idfCache);
|
||||
|
||||
if (docNorm === 0) continue;
|
||||
|
||||
const dotProd = dotProduct(queryTF, docTF, this.idfCache);
|
||||
const cosineSim = dotProd / (queryNorm * docNorm);
|
||||
|
||||
const decayWeight = timeDecayWeight(row.created_at, now);
|
||||
const finalScore = cosineSim * decayWeight * (0.5 + row.confidence * 0.5);
|
||||
|
||||
if (finalScore > 0) {
|
||||
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: finalScore,
|
||||
});
|
||||
}
|
||||
this.scoreAndPushMemory({
|
||||
docText: row.key + ' ' + row.value,
|
||||
createdAt: row.created_at,
|
||||
importance: row.confidence,
|
||||
id: row.id, type: 'semantic', content: row.value,
|
||||
source: 'imported',
|
||||
sessionId: row.source_session ?? undefined,
|
||||
}, queryTF, queryNorm, now, results);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,27 +315,14 @@ export class MemoryManager {
|
||||
}>;
|
||||
|
||||
for (const row of rows) {
|
||||
const docText = row.key + ' ' + row.value;
|
||||
const docTokens = tokenize(docText);
|
||||
const docTF = computeTF(docTokens);
|
||||
const docNorm = vectorNorm(docTF, this.idfCache);
|
||||
|
||||
if (docNorm === 0) continue;
|
||||
|
||||
const dotProd = dotProduct(queryTF, docTF, this.idfCache);
|
||||
const cosineSim = dotProd / (queryNorm * docNorm);
|
||||
|
||||
const decayWeight = timeDecayWeight(row.updated_at, now);
|
||||
const finalScore = cosineSim * decayWeight * 0.5;
|
||||
|
||||
if (finalScore > 0) {
|
||||
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: finalScore,
|
||||
});
|
||||
}
|
||||
this.scoreAndPushMemory({
|
||||
docText: row.key + ' ' + row.value,
|
||||
createdAt: row.updated_at,
|
||||
importance: 0.5,
|
||||
id: row.id, type: 'working', content: row.value,
|
||||
source: 'agent_thought',
|
||||
sessionId: row.session_id,
|
||||
}, queryTF, queryNorm, now, results);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user