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:
thzxx
2026-07-13 22:36:58 +08:00
parent 4f5f570ac8
commit e4d81d8247
47 changed files with 2247 additions and 475 deletions
+12 -5
View File
@@ -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;
+83 -66
View File
@@ -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);
}
}