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 全链路接入
150 lines
6.4 KiB
TypeScript
150 lines
6.4 KiB
TypeScript
/**
|
||
* IPC Shared — 配置写入的共享副作用逻辑(P2-9)
|
||
*
|
||
* 提取原 handlers.ts 中 config:set 与 config:setBatch 两处重复的:
|
||
* 敏感值脱敏、Provider 切换清空 apiKey、Engine/Orchestrator 配置同步、
|
||
* 日志级别应用、配置变更广播、工作空间路径持久化。
|
||
*/
|
||
|
||
import log from 'electron-log';
|
||
import type { IPCContext } from './context';
|
||
import { broadcast } from './context';
|
||
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',
|
||
'ollama.numCtx',
|
||
'deepseek.contextWindow', 'agnes.contextWindow', 'mimo.contextWindow',
|
||
'openai.contextWindow', 'anthropic.contextWindow',
|
||
];
|
||
|
||
/** 敏感配置值脱敏(审计日志用:长值保留后 4 位,短值完全掩码) */
|
||
export function maskSensitive(key: string, value: unknown): unknown {
|
||
if (isSensitiveConfigKey(key) && typeof value === 'string' && value.length > 0) {
|
||
return value.length > 4 ? '***' + value.slice(-4) : '***';
|
||
}
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* 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 {
|
||
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`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 应用单条配置的引擎/编排器副作用(Engine/Orchestrator/ConfirmationHook 同步)
|
||
*/
|
||
export function applyEngineConfigKey(ctx: IPCContext, key: string, value: unknown): void {
|
||
const { agentEngineManager, orchestrator, confirmationHook } = ctx;
|
||
switch (key) {
|
||
case 'agent.maxIterations':
|
||
agentEngineManager.updateConfigAll({ maxIterations: value as number });
|
||
break;
|
||
case 'agent.totalTimeoutMs':
|
||
agentEngineManager.updateConfigAll({ totalTimeoutMs: value as number });
|
||
break;
|
||
case 'agent.enableThinking':
|
||
agentEngineManager.updateConfigAll({ thinkingEnabled: value as boolean });
|
||
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' });
|
||
break;
|
||
case 'agent.toolExecutionTimeoutMs':
|
||
agentEngineManager.updateConfigAll({ toolExecutionTimeoutMs: value as number });
|
||
break;
|
||
case 'agent.confirmationTimeoutMs':
|
||
confirmationHook.setConfirmationTimeout(value as number);
|
||
break;
|
||
case 'ollama.numCtx':
|
||
agentEngineManager.updateConfigAll({ contextLength: (value as number) || undefined });
|
||
orchestrator.updateDefaultConfig({ contextLength: (value as number) || undefined });
|
||
break;
|
||
case 'deepseek.contextWindow':
|
||
case 'agnes.contextWindow':
|
||
case 'mimo.contextWindow':
|
||
case 'openai.contextWindow':
|
||
case 'anthropic.contextWindow':
|
||
// reloadAdapter 已重建 adapter 并同步 contextWindow,此处确保 Engine 配置同步(兜底)
|
||
agentEngineManager.updateConfigAll({ contextWindow: (value as number) || undefined });
|
||
orchestrator.updateDefaultConfig({ contextWindow: (value as number) || undefined });
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 配置写入后的统一副作用(供 config:set / config:setBatch 共用)
|
||
*
|
||
* 1. LLM key 变更 → 统一 reloadAdapter 一次(避免中间态失败)
|
||
* 2. Engine/Orchestrator 配置同步
|
||
* 3. 日志级别即时应用
|
||
* 4. 广播配置变更(前端 store 实时更新)
|
||
* 5. 工作空间路径写独立文件(下次启动生效)
|
||
*
|
||
* @returns 错误信息(成功为 null)
|
||
*/
|
||
export async function applyConfigSideEffects(
|
||
ctx: IPCContext,
|
||
entries: Array<{ key: string; value: unknown }>,
|
||
): Promise<string | null> {
|
||
// 1. LLM 配置变更 → 统一热重载 Adapter(一次)
|
||
if (entries.some((e) => LLM_CONFIG_KEYS.includes(e.key))) {
|
||
if (!ctx.reloadAdapter()) {
|
||
log.warn('[CONFIG] Adapter reload failed after config save');
|
||
return 'LLM 配置不完整,请检查 Provider、API Key、Base URL 和 Model 是否都已填写';
|
||
}
|
||
}
|
||
|
||
// 2. Engine/Orchestrator/ConfirmationHook 配置同步
|
||
for (const { key, value } of entries) {
|
||
applyEngineConfigKey(ctx, key, value);
|
||
}
|
||
|
||
// 3. 日志级别即时应用
|
||
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';
|
||
log.info(`[CONFIG] Log level updated to ${logLevelEntry.value}`);
|
||
}
|
||
|
||
// 4. 广播所有配置变更事件(前端监听后更新 store)
|
||
for (const { key, value } of entries) {
|
||
broadcast('config:changed', { key, value });
|
||
}
|
||
|
||
// 5. 工作空间路径写入独立文件(下次启动生效)
|
||
const workspaceEntry = entries.find((e) => e.key === 'workspace.path' && typeof e.value === 'string');
|
||
if (workspaceEntry) {
|
||
try {
|
||
const { writeWorkspacePathToFile } = await import('../main');
|
||
writeWorkspacePathToFile(workspaceEntry.value as string);
|
||
log.info(`[CONFIG] Workspace path saved (restart required): ${workspaceEntry.value}`);
|
||
} catch (err) {
|
||
log.error('[CONFIG] Failed to save workspace path:', err);
|
||
// v0.3.10: 写入失败必须告知用户,否则下次启动仍使用旧路径
|
||
return `工作空间路径保存失败:${(err as Error).message}`;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|