feat: v0.8.3 工作空间切换链路根治 · 思考强度扩档 — 启动TDZ连锁/继承丢数/回显滞后三修复 · xhigh+true档位单源 · 安全防线ready后注册 · 2217 用例全量回归 + 生产模式E2E
This commit is contained in:
+88
-83
@@ -17,7 +17,6 @@
|
||||
import 'dotenv/config';
|
||||
import { app, shell, Menu, BrowserWindow, dialog, session } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils';
|
||||
import log from 'electron-log';
|
||||
import { DatabaseService } from './services/database.service';
|
||||
@@ -47,6 +46,8 @@ import { OllamaAdapter } from './harness/adapters/ollama.adapter';
|
||||
import { OpenAIAdapter } from './harness/adapters/openai.adapter';
|
||||
import { AnthropicAdapter } from './harness/adapters/anthropic.adapter';
|
||||
import type { IMetonaProviderAdapter } from './harness/types/metona-adapter';
|
||||
// v0.8.3: 思考强度档位单源(新增 xhigh / true 档)
|
||||
import type { MetonaThinkingEffort } from './harness/types';
|
||||
import {
|
||||
ReadFileTool,
|
||||
WriteFileTool,
|
||||
@@ -95,6 +96,10 @@ import { HealthChecker, SLOMonitor } from './utils/slo';
|
||||
// v0.8.1 P0-4: 主进程 toast/通知文案双语(ui.locale 驱动)
|
||||
import { setMainLocale, mt } from './utils/main-locale';
|
||||
// v0.6.4 P4-5: session 级网络代理应用工具(default + agent-browser 分区)
|
||||
// v0.8.3: 工作空间路径持久化独立成模块(消除 main ↔ ipc/shared 循环依赖 ——
|
||||
// 此前 shared.ts 动态 import('../main') 取 writeWorkspacePathToFile,入口模块
|
||||
// 求值一旦中断即抛 TDZ,工作空间切换保存随之报错)
|
||||
import { readWorkspacePathFromFile, writeWorkspacePathToFile } from './utils/workspace-path';
|
||||
|
||||
// ===== 步骤 1: 初始化日志系统(SYS 层)=====
|
||||
log.transports.file.level = 'info';
|
||||
@@ -130,31 +135,6 @@ if (process.env['METONA_USER_DATA_DIR']) {
|
||||
app.setPath('userData', process.env['METONA_USER_DATA_DIR']);
|
||||
}
|
||||
|
||||
// ===== 工作空间路径独立存储(解决 DB 在 workspace 内的鸡生蛋问题)=====
|
||||
const WORKSPACE_CONFIG_FILE = join(app.getPath('userData'), 'workspace-config.json');
|
||||
|
||||
function readWorkspacePathFromFile(): string | null {
|
||||
try {
|
||||
if (existsSync(WORKSPACE_CONFIG_FILE)) {
|
||||
const data = JSON.parse(readFileSync(WORKSPACE_CONFIG_FILE, 'utf-8'));
|
||||
return data.workspacePath ?? null;
|
||||
}
|
||||
} catch {
|
||||
// 忽略读取错误
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeWorkspacePathToFile(workspacePath: string): void {
|
||||
try {
|
||||
writeFileSync(WORKSPACE_CONFIG_FILE, JSON.stringify({ workspacePath }, null, 2), 'utf-8');
|
||||
} catch (err) {
|
||||
log.error('Failed to write workspace config file:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export { readWorkspacePathFromFile, writeWorkspacePathToFile };
|
||||
|
||||
let databaseService: DatabaseService | null = null;
|
||||
let trayManager: TrayManager | null = null;
|
||||
let windowManager: WindowManager | null = null;
|
||||
@@ -196,8 +176,14 @@ async function initialize(): Promise<void> {
|
||||
const workspaceService = new WorkspaceService(savedWorkspacePath ?? undefined);
|
||||
const workspaceInfo = workspaceService.initialize();
|
||||
// 持久化工作空间路径(供下次启动读取)
|
||||
// v0.8.3: writeWorkspacePathToFile 改为向上抛错(供切换工作空间场景 toast 告知),
|
||||
// 启动期回写失败非致命 —— 仅日志留痕,不中断初始化。
|
||||
if (savedWorkspacePath !== workspaceInfo.path) {
|
||||
writeWorkspacePathToFile(workspaceInfo.path);
|
||||
try {
|
||||
writeWorkspacePathToFile(workspaceInfo.path);
|
||||
} catch {
|
||||
// 错误已在 writeWorkspacePathToFile 内记录
|
||||
}
|
||||
}
|
||||
log.info(
|
||||
`Workspace: ${workspaceInfo.path} (missing: ${workspaceInfo.missingFiles.join(', ') || 'none'})`,
|
||||
@@ -220,6 +206,19 @@ async function initialize(): Promise<void> {
|
||||
// v0.8.1 P0-4: 主进程语言随 ui.locale 注入(变更经 applyConfigSideEffects 热切换)
|
||||
setMainLocale(configService.get<string>('ui.locale'));
|
||||
|
||||
// v0.8.3: workspace.path 权威源对齐(设置页回显修复)
|
||||
// 切换工作空间时,新路径经 config:set 写入的是旧空间的 DB,而"继承数据库"复制
|
||||
// 发生在该写入之前 —— 新空间的 DB 携带的是滞后一位的旧路径;未继承 DB 的全新
|
||||
// 空间则根本没有该键。重启后设置页从当前空间 DB 读回显,显示的便不是当前空间。
|
||||
// workspace-config.json 是启动权威源(readWorkspacePathFromFile 决定本次加载哪个
|
||||
// 空间),这里把它回写进当前空间 DB,自愈一切来源的漂移(继承复制滞后 / 全新
|
||||
// 空间缺键 / 引导向导写入),保证 config:get('workspace.path') 与实际加载的空间
|
||||
// 永远一致。
|
||||
if (configService.get<string>('workspace.path') !== workspaceInfo.path) {
|
||||
configService.set('workspace.path', workspaceInfo.path);
|
||||
log.info(`[CONFIG] workspace.path synced to loaded workspace: ${workspaceInfo.path}`);
|
||||
}
|
||||
|
||||
// v0.8.1 P2-5: E2E 引导种子(见文件顶部说明;仅 METONA_E2E_SEED_CONFIG 时生效)
|
||||
if (process.env['METONA_E2E_SEED_CONFIG'] === '1') {
|
||||
configService.set('onboarding.completed', true);
|
||||
@@ -476,12 +475,8 @@ async function initialize(): Promise<void> {
|
||||
contextWindow: getContextWindowSetting(),
|
||||
thinkingEnabled: configService.get<boolean>('agent.enableThinking') ?? true,
|
||||
thinkingEffort:
|
||||
(configService.get<string>('agent.thinkingEffort') as
|
||||
| 'low'
|
||||
| 'medium'
|
||||
| 'high'
|
||||
| 'max'
|
||||
| null) ?? 'high',
|
||||
(configService.get<string>('agent.thinkingEffort') as MetonaThinkingEffort | null) ??
|
||||
'high',
|
||||
toolExecutionTimeoutMs: configService.get<number>('agent.toolExecutionTimeoutMs') ?? 120_000,
|
||||
// v0.7.3 P3-1 接线: agent.enableReflection 此前为死配置(引擎读取但全链路
|
||||
// 无置 true 路径)。现注入引擎基线配置,REFLECTING 状态由此开关真实驱动。
|
||||
@@ -627,12 +622,8 @@ async function initialize(): Promise<void> {
|
||||
{
|
||||
thinkingEnabled: configService.get<boolean>('agent.enableThinking') ?? true,
|
||||
thinkingEffort:
|
||||
(configService.get<string>('agent.thinkingEffort') as
|
||||
| 'low'
|
||||
| 'medium'
|
||||
| 'high'
|
||||
| 'max'
|
||||
| null) ?? 'high',
|
||||
(configService.get<string>('agent.thinkingEffort') as MetonaThinkingEffort | null) ??
|
||||
'high',
|
||||
contextLength: getOllamaContextLength(),
|
||||
contextWindow: getContextWindowSetting(),
|
||||
// v0.7.3 P3-1: SubAgent 与主引擎同源消费 enableReflection
|
||||
@@ -1048,8 +1039,11 @@ if (!gotSingleInstanceLock) {
|
||||
// v0.5.0: 启动链路异常兜底 — DB 损坏/工作空间不可写等初始化失败时,
|
||||
// 原实现会静默挂起(渲染进程白屏且无任何用户可见错误)。
|
||||
// 兜底策略:记录日志 + 弹出系统错误对话框 + 退出(exit 1)
|
||||
// v0.8.3: 安全防线在同一 ready 链上先于 initialize 注册(回调按挂载顺序执行)
|
||||
// —— CSP 拦截必须在主窗口发起首个 mainFrame 请求前就位,否则首页响应漏注入。
|
||||
app
|
||||
.whenReady()
|
||||
.then(registerSecurityHardening)
|
||||
.then(initialize)
|
||||
.catch((err) => {
|
||||
log.error('[Startup] Initialization failed:', err);
|
||||
@@ -1072,57 +1066,68 @@ if (!gotSingleInstanceLock) {
|
||||
// 此前主窗口无 CSP、无 permission handler —— notifications/geo/media/clipboard
|
||||
// 等请求全部走 Chromium 默认放行,且渲染层一旦被注入可静默触达敏感能力。
|
||||
// 两条防线均为 deny-by-default 白名单制,任何一条失败都不放大攻击面。
|
||||
{
|
||||
//
|
||||
// v0.8.1 根治: session.defaultSession 必须在 app ready 后才能访问 —— 此前在
|
||||
// 模块加载期(ready 前)直接调用,每次启动都抛 "Session can only be received
|
||||
// when app is ready",权限白名单静默失效(FATAL 日志为证)。防线一延迟到 ready
|
||||
// 后注册,防线真正生效。
|
||||
//
|
||||
// v0.8.3 根治收尾: 防线二(CSP)当时漏改,仍留在模块顶层 —— 生产环境每次启动
|
||||
// 都在此抛 "Session can only be received when app is ready",模块求值中止:
|
||||
// 权限白名单与 CSP 实际从未注册,且打包产物末尾的模块命名空间永不初始化,
|
||||
// 连锁导致 ipc/shared.ts 保存 workspace.path 的动态 import 抛 TDZ
|
||||
// (工作空间切换保存失败,utils/workspace-path.ts 头注有完整因果链)。
|
||||
// 现两条防线统一收敛为 registerSecurityHardening,挂载在 initialize 之前执行;
|
||||
// 单防线失败仅记日志,不阻断启动链。
|
||||
function registerSecurityHardening(): void {
|
||||
// 防线一:权限请求白名单(deny-by-default)
|
||||
// v0.8.1 根治: session.defaultSession 必须在 app ready 后才能访问 —— 此前在
|
||||
// 模块加载期(ready 前)直接调用,每次启动都抛 "Session can only be received
|
||||
// when app is ready",权限白名单静默失效(FATAL 日志为证)。现延迟到 ready 后
|
||||
// 注册,防线真正生效。
|
||||
const ALLOWED_PERMISSIONS = new Set<string>(['clipboard-sanitized-write', 'fullscreen']);
|
||||
void app
|
||||
.whenReady()
|
||||
.then(() => {
|
||||
session.defaultSession.setPermissionRequestHandler((_wc, permission, callback) => {
|
||||
callback(ALLOWED_PERMISSIONS.has(permission));
|
||||
});
|
||||
session.defaultSession.setPermissionCheckHandler((_wc, permission) =>
|
||||
ALLOWED_PERMISSIONS.has(permission),
|
||||
);
|
||||
log.info('[Security] Permission whitelist registered (app ready)');
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error('[Security] Failed to register permission whitelist:', err);
|
||||
try {
|
||||
const ALLOWED_PERMISSIONS = new Set<string>(['clipboard-sanitized-write', 'fullscreen']);
|
||||
session.defaultSession.setPermissionRequestHandler((_wc, permission, callback) => {
|
||||
callback(ALLOWED_PERMISSIONS.has(permission));
|
||||
});
|
||||
session.defaultSession.setPermissionCheckHandler((_wc, permission) =>
|
||||
ALLOWED_PERMISSIONS.has(permission),
|
||||
);
|
||||
log.info('[Security] Permission whitelist registered (app ready)');
|
||||
} catch (err) {
|
||||
log.error('[Security] Failed to register permission whitelist:', err);
|
||||
}
|
||||
|
||||
// 防线二:生产环境 CSP 注入(仅 mainFrame,不触碰 dev server 的 HMR)。
|
||||
// MUI/emotion 需要 style-src 'unsafe-inline'(运行时注入 <style> 标签与 style 属性);
|
||||
// 附件预览/截图使用 data:/blob: 图片;渲染进程本身不直接外联(所有 fetch 在主进程)。
|
||||
if (!process.env['ELECTRON_RENDERER_URL']) {
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
if (details.resourceType === 'mainFrame') {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': [
|
||||
[
|
||||
"default-src 'self'",
|
||||
"script-src 'self'",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self' data:",
|
||||
"connect-src 'self'",
|
||||
"object-src 'none'",
|
||||
"frame-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'none'",
|
||||
].join('; '),
|
||||
],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
callback({ responseHeaders: details.responseHeaders });
|
||||
}
|
||||
});
|
||||
try {
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
if (details.resourceType === 'mainFrame') {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': [
|
||||
[
|
||||
"default-src 'self'",
|
||||
"script-src 'self'",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self' data:",
|
||||
"connect-src 'self'",
|
||||
"object-src 'none'",
|
||||
"frame-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'none'",
|
||||
].join('; '),
|
||||
],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
callback({ responseHeaders: details.responseHeaders });
|
||||
}
|
||||
});
|
||||
log.info('[Security] CSP header injection registered (app ready)');
|
||||
} catch (err) {
|
||||
log.error('[Security] Failed to register CSP header injection:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user