import path from 'path'; import { BrowserWindow, screen } from 'electron'; import { getBaseDir, isDev } from 'ee-core/ps'; import { logger } from 'ee-core/log'; import { getMainWindow } from 'ee-core/electron'; import { getSmartWindowOptions, smartWindowSize } from '../../config/windowOptions'; import { localSettingsService } from '../setting/localSettings'; /** 窗口 bounds 在本机设置里的键名 */ const BOUNDS_KEY = 'smartWindowBounds'; /** 智能窗口只承载工作台,非该路径一律改写 */ const WORKBENCH_PATH = '/workbench'; /** 主进程 → 主窗口的窗口开关状态通道 */ export const SMART_WINDOW_STATE_CHANNEL = 'smart-window:state'; export interface OpenSmartWindowOptions { /** 完整路由(含 query),如 /workbench?newChat=1 */ fullPath?: string; /** 窗口已存在时是否整窗重载(换案后需要重新读案件上下文) */ reloadIfOpen?: boolean; /** 窗口已存在且目标带 query 时,是否导航到该地址(?newChat=1 / ?view=agent 这类明确意图) */ navigateIfOpen?: boolean; } interface SavedBounds { x: number; y: number; width: number; height: number; } /** 单例:应用内最多一个智能工作台窗口 */ let smartWindow: BrowserWindow | null = null; export function getSmartWindow(): BrowserWindow | null { return smartWindow && !smartWindow.isDestroyed() ? smartWindow : null; } export function isSmartWindowOpen(): boolean { return !!getSmartWindow(); } /** 判断某个窗口是不是智能工作台窗口(IPC 解析窗口角色用) */ export function isSmartWindow(win: BrowserWindow | null | undefined): boolean { const current = getSmartWindow(); return !!win && !!current && win.id === current.id; } /** * 打开/聚焦智能工作台窗口。 * * 已存在时只做 restore/show/focus(reloadIfOpen 时再 reload),保证单例。 */ export function openSmartWindow(options: OpenSmartWindowOptions = {}): BrowserWindow { const fullPath = normalizeFullPath(options.fullPath); const existing = getSmartWindow(); if (existing) { focusWindow(existing); if (options.reloadIfOpen) { // 刚由本次导航创建、还在加载时不用再刷一次;已加载完成的才需要按新案件重建 if (!existing.webContents.isLoading()) { existing.webContents.reload(); } } else if (options.navigateIfOpen && fullPath.includes('?')) { // 显式带 query(?newChat=1 / ?view=agent)说明是明确的导航意图; // 普通打开/聚焦不动它当前所在的视图 loadRenderer(existing, fullPath); } return existing; } const win = new BrowserWindow({ ...getSmartWindowOptions(), ...restoreBounds(), }); smartWindow = win; win.once('ready-to-show', () => { if (!win.isDestroyed()) { win.show(); win.focus(); } }); win.on('close', () => saveBounds(win)); win.on('closed', () => { if (smartWindow === win) smartWindow = null; notifyState(false); }); // 渲染进程崩溃后留一个空壳没有意义:销毁它,下次点「智能」重新创建 win.webContents.on('render-process-gone', (_event, details) => { logger.error('[smartWindow] render-process-gone:', details?.reason); if (!win.isDestroyed()) { saveBounds(win); win.destroy(); } }); loadRenderer(win, fullPath); notifyState(true); return win; } export function closeSmartWindow(): void { const win = getSmartWindow(); if (!win) return; win.close(); } /** 把已打开的智能窗口拉到前台;没打开返回 false */ export function focusSmartWindow(): boolean { const win = getSmartWindow(); if (!win) return false; focusWindow(win); return true; } /** 把主窗口拉到前台(智能窗口遇到登录/授权失效时,由主窗口负责提示) */ export function focusMainWindow(): void { try { focusWindow(getMainWindow()); } catch (e) { logger.warn('[smartWindow] focusMainWindow failed:', e instanceof Error ? e.message : String(e)); } } function focusWindow(win: BrowserWindow): void { if (win.isMinimized()) win.restore(); win.show(); win.focus(); } function normalizeFullPath(fullPath?: string): string { const raw = typeof fullPath === 'string' ? fullPath.trim() : ''; if (!raw) return WORKBENCH_PATH; // 只接受工作台地址,避免把业务页塞进智能窗口(如 /workbenchfoo 不算) return /^\/workbench(\/|\?|$)/.test(raw) ? raw : WORKBENCH_PATH; } function loadRenderer(win: BrowserWindow, fullPath: string): void { const devServerUrl = resolveDevServerUrl(); if (devServerUrl) { win.loadURL(`${devServerUrl}${fullPath}`).catch((err) => { logger.error('[smartWindow] loadURL failed:', err); }); return; } // 生产是 file:// + hash 路由:hash 带上完整路径与 query const indexPath = path.join(getBaseDir(), 'public', 'dist', 'index.html'); win.loadFile(indexPath, { hash: fullPath }).catch((err) => { logger.error('[smartWindow] loadFile failed:', err); }); } /** * 开发环境的前端地址:与 ee-core 加载主窗口同口径,端口来自 cmd/bin.js 的 dev.frontend.port。 * 生产环境(或读不到配置)返回 null,走 loadFile。 */ function resolveDevServerUrl(): string | null { if (!isDev()) return null; try { // eslint-disable-next-line @typescript-eslint/no-var-requires const binConfig = require(path.join(getBaseDir(), 'cmd', 'bin.js')); const frontend = binConfig?.dev?.frontend || {}; const protocol = typeof frontend.protocol === 'string' ? frontend.protocol : 'http://'; if (protocol === 'file://') return null; const hostname = typeof frontend.hostname === 'string' ? frontend.hostname : 'localhost'; const port = Number(frontend.port) || 3100; return `${protocol}${hostname}:${port}`; } catch (e) { logger.warn('[smartWindow] 读取 cmd/bin.js 失败,回退 http://localhost:3100'); return 'http://localhost:3100'; } } function saveBounds(win: BrowserWindow): void { try { const { x, y, width, height } = win.getBounds(); localSettingsService.set(BOUNDS_KEY, JSON.stringify({ x, y, width, height })); } catch (e) { logger.warn('[smartWindow] 保存窗口位置失败:', e instanceof Error ? e.message : String(e)); } } /** 恢复上次的位置与大小;越界或尺寸不合法时交给 Electron 默认放置 */ function restoreBounds(): Partial { try { const raw = localSettingsService.get(BOUNDS_KEY); if (!raw) return {}; const parsed = JSON.parse(raw) as Partial; const { x, y, width, height } = parsed || {}; if (![x, y, width, height].every((value) => typeof value === 'number' && Number.isFinite(value))) { return {}; } if ((width as number) < smartWindowSize.minWidth || (height as number) < smartWindowSize.minHeight) { return {}; } // 显示器拔掉/分辨率变化后,旧坐标可能落在屏幕外 const visible = screen.getAllDisplays().some((display) => { const area = display.workArea; return ( (x as number) < area.x + area.width && (x as number) + (width as number) > area.x && (y as number) < area.y + area.height && (y as number) + (height as number) > area.y ); }); return visible ? { x, y, width, height } : {}; } catch (e) { logger.warn('[smartWindow] 恢复窗口位置失败:', e instanceof Error ? e.message : String(e)); return {}; } } function notifyState(open: boolean): void { try { const main = getMainWindow(); if (!main.isDestroyed() && !main.webContents.isDestroyed()) { main.webContents.send(SMART_WINDOW_STATE_CHANNEL, { open }); } } catch { // 主窗口尚未创建或应用正在退出:忽略 } }