feat: v0.6.0 全量审计修复 — 安全加固 + 死代码激活 + 依赖治理
基于全量源码审计(electron/ 70+ 文件、src/ 45+ 文件完整读取)的 四阶段迭代,修复 1 项安全缺陷、8 项功能缺陷、依赖与文档系统性脱节。 【P0 安全与数据正确性】 - S-1 数据导出泄露明文密钥:data:export 全量导出直接透传 configService.getAll()(敏感 key 解密返回),导出文件含明文 API Key。 新增 sanitizeExportConfig(shared.ts)逐 key 脱敏 + 5 项回归测试 - F-3 session_summaries 无级联删除:删会话后摘要永久残留。建表语句补 FOREIGN KEY ON DELETE CASCADE + 迁移 8 重建存量表(幂等检测)+ 2 项测试 - F-4 macOS activate 重建窗口后确认弹框失效:窗口创建收敛为 createMainWindow 单一入口(beforeLoad 补 setMainWindow/IPC 注册),TrayManager 补 rebindWindow,app:selectFolder 改 event.sender 动态解析窗口 【P1 死代码激活与功能补全】 - F-1 会话右键菜单挂载:ContextMenu session 分支(重命名/置顶/归档/ 导出/删除)约 200 行此前无任何触发点,Sidebar SessionItem 挂载 onContextMenu;置顶/归档 label 随状态切换显示 - F-2 归档会话不可找回:Sidebar 新增「已归档」折叠面板(恢复入口), 归档功能形成完整闭环 - F-7 Provider 白名单校验:createAdapter/buildFallbackAdapter 未知 provider 显式拒绝(此前静默落入 DeepSeekAdapter 以空配置失败) - F-8 死配置治理:接通 5 项(llm.temperature/llm.maxTokens 注入引擎、 security.promptInjectionDefense 控制 SecurityScanHook+消息检测、 logging.auditEnabled 控制 AuditLogHook、logging.traceEnabled 控制 SessionRecorder,均 fail-secure 仅显式 false 关闭);删除 4 项 无消费者配置(requireWriteConfirmation/maxFileWriteSizeKB/fontSize/ animationMode) 【P2 依赖治理与 UX 修缮】 - D-1 移除僵尸依赖 electron-store/zod/rehype-raw(源码零引用, 共裁 24 包);README 技术栈表同步删除虚假宣称 - metona-toast 升级 0.2.1 → 0.5.0(API 全兼容:107 种图标类型、 配置项超集,default/configure/use 接口不变) - F-5 工作空间手输路径实时落库中间态:改 pendingPath 草稿 + 显式 「校验」按钮(选择文件夹与手输共用 validatePath 流程) - F-6 确认弹框超时滞留:倒计时归零时主动 refreshPending(后端超时 已删条目,拉取后弹框自然消解) - D-3 ChatInput accept 移除 .pdf(分类器不识别,误导性入口); Onboarding 切换 Provider 自动填充默认 URL(与 LLMSettings 一致) 【P3 文档口径收敛】 - README:工具数 30+→28、版本 0.6.0、测试数 252、配置表补 F-8 接通项 - built-in/index.ts 计数注释 30→28 - docs/网络工具 v2 存储键名统一为点号口径(searxng.enabled) - docs/完整设计指南修正 db.handlers.ts 失效路径引用为 data.ts 【验证】 - lint 0 error / 0 warning - typecheck 双工程(node+web)0 错误 - test:electron 24 文件 252 用例全通过(+7 新增:导出脱敏 ×5、 级联删除 ×2) - electron-vite build 成功(metona-toast 0.5.0 chunk 正常) - 系统 Node 模式 npm test 225 通过 + 27 ABI skip(符合预期)
This commit is contained in:
+61
-11
@@ -13,11 +13,20 @@ import { isSensitiveConfigKey } from '../utils/secure-config';
|
||||
|
||||
/** LLM 相关配置 key(变更时触发热重载 Adapter) */
|
||||
export const LLM_CONFIG_KEYS = [
|
||||
'llm.provider', 'llm.model', 'llm.apiKey', 'llm.baseURL',
|
||||
'llm.fallbackProvider', 'llm.fallbackModel', 'llm.fallbackApiKey', 'llm.fallbackBaseURL',
|
||||
'llm.provider',
|
||||
'llm.model',
|
||||
'llm.apiKey',
|
||||
'llm.baseURL',
|
||||
'llm.fallbackProvider',
|
||||
'llm.fallbackModel',
|
||||
'llm.fallbackApiKey',
|
||||
'llm.fallbackBaseURL',
|
||||
'ollama.numCtx',
|
||||
'deepseek.contextWindow', 'agnes.contextWindow', 'mimo.contextWindow',
|
||||
'openai.contextWindow', 'anthropic.contextWindow',
|
||||
'deepseek.contextWindow',
|
||||
'agnes.contextWindow',
|
||||
'mimo.contextWindow',
|
||||
'openai.contextWindow',
|
||||
'anthropic.contextWindow',
|
||||
];
|
||||
|
||||
/** 敏感配置值脱敏(审计日志用:长值保留后 4 位,短值完全掩码) */
|
||||
@@ -28,20 +37,43 @@ export function maskSensitive(key: string, value: unknown): unknown {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出配置脱敏(S-1 修复):对 configService.getAll() 的结果逐 key 脱敏。
|
||||
*
|
||||
* 背景:getAll() 对敏感 key 解密后返回明文(ConfigService 的读取契约),
|
||||
* data:export 直接透传会把明文 API Key / 认证密钥写入用户下载的 JSON 文件,
|
||||
* 绕过 safeStorage 密钥链加密。此函数确保任何导出路径不泄露明文密钥。
|
||||
*
|
||||
* @param config configService.getAll() 的完整配置快照
|
||||
* @returns 脱敏后的副本(敏感值替换为掩码,原对象不修改)
|
||||
*/
|
||||
export function sanitizeExportConfig(config: Record<string, unknown>): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
out[key] = maskSensitive(key, value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider 切换时清空 API key(C-1 修复,供 set/setBatch 共用)
|
||||
*
|
||||
* 必须在写入 entries 之前执行:若前端把 llm.apiKey 放在 llm.provider 之前,
|
||||
* 先 set apiKey 再处理 provider 会把用户刚填的 key 清空。
|
||||
*/
|
||||
export function clearApiKeyOnProviderChange(ctx: IPCContext, entries: Array<{ key: string; value: unknown }>): void {
|
||||
export function clearApiKeyOnProviderChange(
|
||||
ctx: IPCContext,
|
||||
entries: Array<{ key: string; value: unknown }>,
|
||||
): void {
|
||||
const providerEntry = entries.find((e) => e.key === 'llm.provider');
|
||||
if (!providerEntry) return;
|
||||
const oldProvider = ctx.configService.get<string>('llm.provider') ?? '';
|
||||
const newProvider = (providerEntry.value as string) ?? '';
|
||||
if (oldProvider && newProvider && oldProvider !== newProvider) {
|
||||
ctx.configService.set('llm.apiKey', '');
|
||||
log.info(`[CONFIG] Provider changed (${oldProvider} → ${newProvider}), API key cleared to prevent incompatible key usage`);
|
||||
log.info(
|
||||
`[CONFIG] Provider changed (${oldProvider} → ${newProvider}), API key cleared to prevent incompatible key usage`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +94,12 @@ export function applyEngineConfigKey(ctx: IPCContext, key: string, value: unknow
|
||||
orchestrator.updateDefaultConfig({ thinkingEnabled: value as boolean });
|
||||
break;
|
||||
case 'agent.thinkingEffort':
|
||||
agentEngineManager.updateConfigAll({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
|
||||
orchestrator.updateDefaultConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
|
||||
agentEngineManager.updateConfigAll({
|
||||
thinkingEffort: value as 'low' | 'medium' | 'high' | 'max',
|
||||
});
|
||||
orchestrator.updateDefaultConfig({
|
||||
thinkingEffort: value as 'low' | 'medium' | 'high' | 'max',
|
||||
});
|
||||
break;
|
||||
case 'agent.toolExecutionTimeoutMs':
|
||||
agentEngineManager.updateConfigAll({ toolExecutionTimeoutMs: value as number });
|
||||
@@ -121,8 +157,20 @@ export async function applyConfigSideEffects(
|
||||
const logLevelEntry = entries.find((e) => e.key === 'logging.level');
|
||||
if (logLevelEntry && typeof logLevelEntry.value === 'string') {
|
||||
const { transports } = await import('electron-log');
|
||||
transports.file.level = logLevelEntry.value as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly';
|
||||
transports.console.level = logLevelEntry.value as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly';
|
||||
transports.file.level = logLevelEntry.value as
|
||||
| 'error'
|
||||
| 'warn'
|
||||
| 'info'
|
||||
| 'debug'
|
||||
| 'verbose'
|
||||
| 'silly';
|
||||
transports.console.level = logLevelEntry.value as
|
||||
| 'error'
|
||||
| 'warn'
|
||||
| 'info'
|
||||
| 'debug'
|
||||
| 'verbose'
|
||||
| 'silly';
|
||||
log.info(`[CONFIG] Log level updated to ${logLevelEntry.value}`);
|
||||
}
|
||||
|
||||
@@ -132,7 +180,9 @@ export async function applyConfigSideEffects(
|
||||
}
|
||||
|
||||
// 5. 工作空间路径写入独立文件(下次启动生效)
|
||||
const workspaceEntry = entries.find((e) => e.key === 'workspace.path' && typeof e.value === 'string');
|
||||
const workspaceEntry = entries.find(
|
||||
(e) => e.key === 'workspace.path' && typeof e.value === 'string',
|
||||
);
|
||||
if (workspaceEntry) {
|
||||
try {
|
||||
const { writeWorkspacePathToFile } = await import('../main');
|
||||
|
||||
Reference in New Issue
Block a user