feat: v0.4.0 四阶段迭代 — 安全加固 + 工程基线 + 架构重构 + 双 Provider 扩展
P0 安全修复: - API Key 加密存储(safeStorage 密钥链,版本化前缀,历史明文平滑兼容) - 间接提示注入防护(SecurityScanHook 工具结果深扫描,网络工具脱敏/本地工具警示分级) - error:report IPC 断链修复(渲染进程错误上报落 electron-log + 审计) - abort 信号贯通工具层(run_command/dev-tools 子进程随会话中断终止) - run_command 沙箱加固(cd 系统目录/敏感文件读取拦截 + chcp 前缀剥离防解析退化) - .env 真实生效(dotenv 回退加载,应用内配置优先) P1 工程基础: - ESLint 9 flat config + 全部 34 条存量 warnings 清零(零容忍基线) - 测试基线 118 用例 11 文件(token/文件防护/权限/沙箱/注入/命令/引擎/注册表/审计链/摘要分层) - test:electron 双模式(ELECTRON_RUN_AS_NODE 跑 Electron ABI,SQLite 套件全执行) - SessionRecorder 多会话隔离 + 9 种 TRACE 事件补全(含最终轮 iteration_end) - Provider 故障转移(重试耗尽/不可重试一次性切换 fallback + 前端通知) - MCP 真就绪(等待全部连接完成再广播 tools:ready) - SLO/HealthChecker 真实接入(60s 巡检 + 托盘状态) - CONFIG_DEFAULTS 单一来源(消除 SEED 双源漂移) P2 架构升级: - handlers.ts 1940 行拆分为 13 个 IPC 域模块(防重入注册 + 多窗口广播) - AgentEngineManager 每会话独立引擎(LRU 30 + adapter 工厂隔离 abort 信号) - TaskOrchestrator EngineProvider 改造 + abortByParent 联动中断 SubAgent - 会话摘要分层上下文(session_summaries 滚动摘要 + 截断游标清理防因果污染) - 消息编辑重发/重新生成(truncateAfter IPC + store 动作 + UI) - Markdown 导出 / WebSearch 并行抓取(并发 3)/ 记忆 TF 缓存 / 版本构建期注入 P3 能力扩展: - OpenAI Adapter(o 系列推理模型 reasoning_effort/max_completion_tokens) - Anthropic Adapter(原生 Messages API:tool_use 块/角色合并/thinking budget/图片 base64/SSE 事件机) - 设置页/Onboarding 六 Provider 全链路接入
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* IPC Config Handlers — 配置管理域(P2-9 从 handlers.ts 拆分)
|
||||
*
|
||||
* config:get / config:set / config:setBatch。
|
||||
* P2-9: set 与 setBatch 共享 applyConfigSideEffects(消除原 300 行重复逻辑)。
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
import type { IPCContext } from './context';
|
||||
import { LLM_CONFIG_KEYS, applyConfigSideEffects, clearApiKeyOnProviderChange, maskSensitive } from './shared';
|
||||
import log from 'electron-log';
|
||||
|
||||
export function registerConfigHandlers(ctx: IPCContext): void {
|
||||
const { configService, auditService } = ctx;
|
||||
|
||||
ipcMain.handle('config:get', async (_event, key) => {
|
||||
return configService.get(key);
|
||||
});
|
||||
|
||||
ipcMain.handle('config:set', async (_event, key: unknown, value: unknown) => {
|
||||
// M-42 修复: 参数校验
|
||||
if (typeof key !== 'string' || !key.trim()) {
|
||||
return { success: false, error: 'Invalid config key' };
|
||||
}
|
||||
// value 必须是可序列化的基本类型(string|number|boolean|null)
|
||||
if (value !== null && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
||||
return { success: false, error: 'Invalid config value: must be string, number, boolean, or null' };
|
||||
}
|
||||
|
||||
// C-1 修复: Provider 切换时清空 API key(P2-9: 统一走共享函数)
|
||||
clearApiKeyOnProviderChange(ctx, [{ key, value }]);
|
||||
|
||||
configService.set(key, value);
|
||||
|
||||
// M-42 修复: 敏感配置项脱敏后再写入审计日志
|
||||
auditService.log({
|
||||
sessionId: '',
|
||||
eventType: 'config_change',
|
||||
actor: 'user',
|
||||
target: key,
|
||||
details: { value: maskSensitive(key, value) },
|
||||
outcome: 'success',
|
||||
});
|
||||
|
||||
// P2-9: 统一副作用(reloadAdapter + Engine 同步 + 广播 + 工作空间路径)
|
||||
const sideEffectError = await applyConfigSideEffects(ctx, [{ key, value }]);
|
||||
if (sideEffectError) {
|
||||
return { success: false, error: sideEffectError };
|
||||
}
|
||||
if (LLM_CONFIG_KEYS.includes(key)) {
|
||||
log.info(`[CONFIG] LLM config changed (${key}), adapter reloaded`);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// ===== v0.3.9: 批量保存配置(P2-9: 复用共享副作用逻辑) =====
|
||||
// 设计原因:前端设置页一次保存多个字段,串行 config:set 会在中间态(如 provider
|
||||
// 已改但 apiKey 还没保存)触发 reloadAdapter 失败。批量保存:先写入所有字段,
|
||||
// 最后统一触发一次 reloadAdapter 和 Engine/Orchestrator 同步。
|
||||
ipcMain.handle('config:setBatch', async (_event, entries: unknown) => {
|
||||
// 参数校验:必须是 {key, value}[] 非空数组
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
return { success: false, error: 'Invalid entries: must be non-empty array of {key, value}' };
|
||||
}
|
||||
// 逐条校验每个元素的结构和类型
|
||||
for (const entry of entries) {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return { success: false, error: 'Invalid entry: must be {key, value} object' };
|
||||
}
|
||||
const e = entry as { key?: unknown; value?: unknown };
|
||||
if (typeof e.key !== 'string' || !e.key.trim()) {
|
||||
return { success: false, error: 'Invalid config key in batch' };
|
||||
}
|
||||
const v = e.value;
|
||||
if (v !== null && typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean') {
|
||||
return { success: false, error: `Invalid config value for key "${e.key}": must be string, number, boolean, or null` };
|
||||
}
|
||||
}
|
||||
|
||||
// v0.3.10 防御性修复: Provider 切换清空 apiKey 必须在循环前执行(共享函数内含说明)
|
||||
clearApiKeyOnProviderChange(ctx, entries as Array<{ key: string; value: unknown }>);
|
||||
|
||||
// 第一步:逐条写入 configService + 审计日志(脱敏)
|
||||
for (const entry of entries) {
|
||||
const { key, value } = entry as { key: string; value: unknown };
|
||||
configService.set(key, value);
|
||||
auditService.log({
|
||||
sessionId: '',
|
||||
eventType: 'config_change',
|
||||
actor: 'user',
|
||||
target: key,
|
||||
details: { value: maskSensitive(key, value) },
|
||||
outcome: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
// 第二步:统一副作用(reloadAdapter + Engine 同步 + 日志级别 + 广播 + 工作空间路径)
|
||||
const sideEffectError = await applyConfigSideEffects(ctx, entries as Array<{ key: string; value: unknown }>);
|
||||
if (sideEffectError) {
|
||||
log.warn('[CONFIG] Config batch save side effects failed');
|
||||
return { success: false, error: sideEffectError };
|
||||
}
|
||||
log.info(`[CONFIG] Config batch saved (${entries.length} keys)`);
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user