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:
@@ -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' },
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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' };
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user