feat: v0.8.2 安全纵深补全 · 协议保真 · 断链修复 — 图片SSRF/根MEMORY.md保护根治 · Anthropic thinking回传+pause_turn续传 · 2523 用例全量回归 + E2E 扩充
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取窗口
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user