| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- 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<SavedBounds> {
- try {
- const raw = localSettingsService.get(BOUNDS_KEY);
- if (!raw) return {};
- const parsed = JSON.parse(raw) as Partial<SavedBounds>;
- 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 {
- // 主窗口尚未创建或应用正在退出:忽略
- }
- }
|