feat: v0.8.2 安全纵深补全 · 协议保真 · 断链修复 — 图片SSRF/根MEMORY.md保护根治 · Anthropic thinking回传+pause_turn续传 · 2523 用例全量回归 + E2E 扩充
CI / 类型检查 + Lint + 单元测试 (push) Failing after 9m45s
CI / 全量测试 (Electron ABI) (push) Failing after 6m28s
CI / 产物编译验证 (push) Successful in 11m18s

This commit is contained in:
2026-09-08 14:30:27 +08:00
parent 69776e447f
commit 4cd6e997b5
86 changed files with 4303 additions and 956 deletions
+94 -3
View File
@@ -10,9 +10,9 @@
* @see docs/MetonaAI-Desktop UI UX 设计集成方案.html — 窗口管理
*/
import { BrowserWindow, globalShortcut, shell } from 'electron';
import { BrowserWindow, globalShortcut, shell, app, screen } from 'electron';
import { join } from 'path';
import { existsSync } from 'fs';
import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'fs';
import { is } from '@electron-toolkit/utils';
import log from 'electron-log';
@@ -24,6 +24,9 @@ export interface WindowState {
isMaximized?: boolean;
}
/** v0.8.2 P3-5: 窗口状态持久化文件(userData 下,机器级) */
const WINDOW_STATE_FILE = join(app.getPath('userData'), 'window-state.json');
export class WindowManager {
private windows = new Map<string, BrowserWindow>();
private activeWindowId: string | null = null;
@@ -51,7 +54,8 @@ export class WindowManager {
beforeLoad?: (win: BrowserWindow) => void;
}): BrowserWindow {
const id = options.id ?? `window_${Date.now()}`;
const state = options.state ?? { width: 1440, height: 900 };
// v0.8.2 P3-5: 未显式传 state 时自动读取持久化状态(恢复上次位置/尺寸/最大化)
const state = options.state ?? WindowManager.loadWindowState();
const win = new BrowserWindow({
width: state.width,
@@ -94,8 +98,42 @@ export class WindowManager {
win.show();
});
// v0.8.2 P3-5: 窗口状态持久化 —— move/resize 防抖 800ms 落盘 + close 时兜底
// 捕获一次(maximized 还原依赖 close 时的 isMaximized 标志)。此前状态通道
// state?: WindowState)存在但 main.ts 从未读写,每次启动固定 1440×900 居中。
let saveStateTimer: NodeJS.Timeout | null = null;
const captureState = (): WindowState => {
const bounds = win.getBounds();
return {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
isMaximized: win.isMaximized(),
};
};
const scheduleSaveState = (): void => {
if (saveStateTimer) clearTimeout(saveStateTimer);
saveStateTimer = setTimeout(() => {
saveStateTimer = null;
WindowManager.saveWindowState(captureState());
}, 800);
saveStateTimer.unref?.();
};
win.on('resize', scheduleSaveState);
win.on('move', scheduleSaveState);
win.on('close', () => {
if (saveStateTimer) {
clearTimeout(saveStateTimer);
saveStateTimer = null;
}
WindowManager.saveWindowState(captureState());
});
win.on('closed', () => {
this.windows.delete(id);
// v0.8.2 P3-5: 崩溃自愈退避记录同步清理(窗口销毁后 Map 条目残留属泄漏)
this.crashReloadAttempts.delete(win.id);
if (this.activeWindowId === id) {
this.activeWindowId =
this.windows.size > 0 ? (this.windows.keys().next().value ?? null) : null;
@@ -209,6 +247,59 @@ export class WindowManager {
return win;
}
/**
* v0.8.2 P3-5: 读取持久化的窗口状态。
* 文件缺失/损坏返回默认尺寸;恢复的坐标不在任何显示器可见范围时丢弃坐标
* (防止显示器拔除后窗口"消失")。
*/
static loadWindowState(): WindowState {
const fallback: WindowState = { width: 1440, height: 900 };
try {
if (!existsSync(WINDOW_STATE_FILE)) return fallback;
const raw = JSON.parse(readFileSync(WINDOW_STATE_FILE, 'utf-8')) as WindowState;
if (typeof raw?.width !== 'number' || typeof raw?.height !== 'number') return fallback;
if (raw.width < 400 || raw.height < 300) return fallback;
// 坐标可见性校验:x/y 必须落在某个显示器的可见区域内
if (typeof raw.x === 'number' && typeof raw.y === 'number') {
const visible = screen.getAllDisplays().some((d) => {
const { x, y, width, height } = d.bounds;
return (
raw.x! >= x - 100 && raw.x! < x + width && raw.y! >= y - 100 && raw.y! < y + height
);
});
if (!visible) {
return { width: raw.width, height: raw.height, isMaximized: raw.isMaximized };
}
} else {
delete raw.x;
delete raw.y;
}
return raw;
} catch {
return fallback;
}
}
/** v0.8.2 P3-5: 原子落盘窗口状态(tmp + rename;尽力而为,失败仅告警) */
static saveWindowState(state: WindowState): void {
try {
const tmp = `${WINDOW_STATE_FILE}.tmp`;
writeFileSync(tmp, JSON.stringify(state), 'utf-8');
try {
renameSync(tmp, WINDOW_STATE_FILE);
} catch (err) {
try {
unlinkSync(tmp);
} catch {
/* ignore */
}
throw err;
}
} catch (err) {
log.warn(`[WindowManager] Failed to persist window state: ${(err as Error).message}`);
}
}
/**
* 获取窗口
*/