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
+78 -39
View File
@@ -14,6 +14,13 @@ import { app } from 'electron';
import { existsSync, mkdirSync } from 'fs';
import log from 'electron-log';
/**
* L-8 修复: 提取 toErrorMessage 工具函数,消除 5 处重复的 error instanceof Error 三元表达式
*/
function toErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
export class DatabaseService {
private db: Database.Database | null = null;
private dbPath: string;
@@ -95,11 +102,13 @@ export class DatabaseService {
);
-- ===== 消息表 =====
-- C-6 修复: content 允许 NULL — assistant 消息仅有 tool_calls 时 content 必须为 null
-- @see project_memory.md — Assistant messages with tool_calls must set content to null
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
content TEXT NOT NULL,
content TEXT,
reasoning_content TEXT,
tool_calls TEXT,
tool_result TEXT,
@@ -246,50 +255,76 @@ export class DatabaseService {
private runMigrations(): void {
const db = this.db!;
// L-6 修复: 提取 tryAddColumn 辅助方法,消除 4 处重复的 try/catch 模式
// L-8 修复: 使用 toErrorMessage 替代重复的 error instanceof Error 三元表达式
const tryAddColumn = (table: string, column: string, type: string, migrationName: string) => {
try {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
log.info(`[DB] Migration: added ${column} column to ${table}`);
} catch (error) {
// 只忽略 "duplicate column" 错误(列已存在),其他错误必须抛出
const msg = toErrorMessage(error);
if (!msg.includes('duplicate column')) {
throw error;
}
}
};
// 迁移 1: messages 表添加 attachments 列
try {
db.exec('ALTER TABLE messages ADD COLUMN attachments TEXT');
log.info('[DB] Migration: added attachments column to messages');
} catch (error) {
// 只忽略 "duplicate column" 错误(列已存在),其他错误必须抛出
const msg = error instanceof Error ? error.message : String(error);
if (!msg.includes('duplicate column')) {
throw error;
}
}
tryAddColumn('messages', 'attachments', 'TEXT', 'attachments');
// 迁移 2: audit_logs 表添加 iteration 列
try {
db.exec('ALTER TABLE audit_logs ADD COLUMN iteration INTEGER');
log.info('[DB] Migration: added iteration column to audit_logs');
} catch (error) {
// 只忽略 "duplicate column" 错误(列已存在),其他错误必须抛出
const msg = error instanceof Error ? error.message : String(error);
if (!msg.includes('duplicate column')) {
throw error;
}
}
tryAddColumn('audit_logs', 'iteration', 'INTEGER', 'iteration');
// v0.2.0 迁移 3: audit_logs 表添加 prev_hash 列(链式哈希)
try {
db.exec('ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT');
log.info('[DB] Migration: added prev_hash column to audit_logs');
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (!msg.includes('duplicate column')) {
throw error;
}
}
tryAddColumn('audit_logs', 'prev_hash', 'TEXT', 'prev_hash');
// v0.2.0 迁移 4: audit_logs 表添加 current_hash 列(链式哈希)
tryAddColumn('audit_logs', 'current_hash', 'TEXT', 'current_hash');
// C-6 修复 迁移 5: 重建 messages 表,将 content 列从 NOT NULL 改为允许 NULL
// @see project_memory.md — Assistant messages with tool_calls must set content to null
// SQLite 不支持 ALTER COLUMN,需要重建表
try {
db.exec('ALTER TABLE audit_logs ADD COLUMN current_hash TEXT');
log.info('[DB] Migration: added current_hash column to audit_logs');
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (!msg.includes('duplicate column')) {
throw error;
// 检测 content 列是否有 NOT NULL 约束
const columns = db.prepare('PRAGMA table_info(messages)').all() as Array<{ name: string; notnull: number }>;
const contentCol = columns.find((c) => c.name === 'content');
if (contentCol && contentCol.notnull === 1) {
log.info('[DB] Migration: rebuilding messages table to allow NULL content');
db.exec(`
CREATE TABLE IF NOT EXISTS messages_new (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
content TEXT,
reasoning_content TEXT,
tool_calls TEXT,
tool_result TEXT,
attachments TEXT,
iteration INTEGER,
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
INSERT INTO messages_new (id, session_id, role, content, reasoning_content, tool_calls, tool_result, attachments, iteration, created_at)
SELECT id, session_id, role, content, reasoning_content, tool_calls, tool_result, attachments, iteration, created_at
FROM messages;
DROP TABLE messages;
ALTER TABLE messages_new RENAME TO messages;
`);
// 重建索引
db.exec(`
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, created_at);
CREATE INDEX IF NOT EXISTS idx_messages_role ON messages(role);
`);
log.info('[DB] Migration: messages table rebuilt successfully (content now allows NULL)');
}
} catch (error) {
// L-8 修复: 使用 toErrorMessage 替代重复的三元表达式
const msg = toErrorMessage(error);
log.warn(`[DB] Migration 5 (messages content NULL) skipped: ${msg}`);
// 非致命错误 — 如果迁移失败,NOT NULL 约束仍生效,saveMessage 会保存空字符串
}
}
@@ -316,6 +351,10 @@ export class DatabaseService {
{ key: 'agent.enableThinking', value: 'true', category: 'agent' },
{ key: 'agent.thinkingEffort', value: '"high"', category: 'agent' },
{ key: 'agent.enableReflection', value: 'false', category: 'agent' },
// C-10 修复: 补充缺失的 agent 配置默认值
// @see project_memory.md — Tool confirmation timeout is configurable via agent.confirmationTimeoutMs (30s~600s, default 120s)
{ key: 'agent.confirmationTimeoutMs', value: '120000', category: 'agent' },
{ key: 'agent.toolExecutionTimeoutMs', value: '120000', category: 'agent' },
// 安全配置
{ key: 'security.requireWriteConfirmation', value: 'true', category: 'security' },
+11 -5
View File
@@ -29,7 +29,8 @@ export interface MessageRow {
id: string;
session_id: string;
role: string;
content: string;
// C-6 修复: content 允许 null — assistant 消息仅有 tool_calls 时为 null
content: string | null;
reasoning_content: string | null;
tool_calls: string | null;
tool_result: string | null;
@@ -52,7 +53,8 @@ export interface SessionInfo {
export interface MessageInfo {
id: string;
role: string;
content: string;
// C-6 修复: content 允许 null — assistant 消息仅有 tool_calls 时为 null
content: string | null;
reasoningContent?: string;
toolCalls?: unknown[];
toolResult?: unknown;
@@ -192,7 +194,8 @@ export class SessionService {
saveMessage(params: {
sessionId: string;
role: string;
content: string;
// C-6 修复: content 允许 null — assistant 消息仅有 tool_calls 时为 null
content: string | null;
reasoningContent?: string;
toolCalls?: unknown[];
toolResult?: unknown;
@@ -311,14 +314,17 @@ export class SessionService {
}
private toMessageInfo(row: MessageRow): MessageInfo {
// M-55 修复: 运行时收窄,防止数据库被篡改或老版本数据格式不一致导致下游 .map/.length 崩溃
const parsedToolCalls = row.tool_calls ? this.safeJsonParse(row.tool_calls) : undefined;
const parsedAttachments = row.attachments ? this.safeJsonParse(row.attachments) : undefined;
return {
id: row.id,
role: row.role,
content: row.content,
reasoningContent: row.reasoning_content ?? undefined,
toolCalls: row.tool_calls ? this.safeJsonParse(row.tool_calls) as unknown[] : undefined,
toolCalls: Array.isArray(parsedToolCalls) ? parsedToolCalls : undefined,
toolResult: row.tool_result ? this.safeJsonParse(row.tool_result) : undefined,
attachments: row.attachments ? this.safeJsonParse(row.attachments) as unknown[] : undefined,
attachments: Array.isArray(parsedAttachments) ? parsedAttachments : undefined,
iteration: row.iteration ?? undefined,
timestamp: row.created_at,
};
+11 -1
View File
@@ -82,7 +82,17 @@ export class WindowManager {
});
win.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
// C-8 修复: 校验 URL 协议,只允许 http/https,防止 javascript:/file: 等协议执行代码
try {
const parsed = new URL(url);
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
shell.openExternal(url);
} else {
log.warn(`[WindowManager] Blocked window.open with unsafe protocol: ${parsed.protocol}`);
}
} catch {
log.warn(`[WindowManager] Blocked window.open with invalid URL: ${url.slice(0, 100)}`);
}
return { action: 'deny' };
});