import type { Server } from 'node:http'; import { app, BrowserWindow, ipcMain } from 'electron' import { CONFIG_KEYS, IPC_EVENTS } from '@runtime/lib/constants' import { setupMainWindow } from './wins'; import started from 'electron-squirrel-startup' import configManager from '@electron/service/config-service' import themeManager from '@electron/service/theme-service' import { runTaskOperationService } from '@electron/process/runTaskOperationService' import { initScriptStoreService } from '@electron/service/script-store-service' import log from 'electron-log'; import 'bytenode'; // Ensure bytenode is bundled/externalized correctly import { appUpdater } from '@electron/service/updater'; import axios from 'axios'; import { onProviderChange } from '@electron/service/provider-api-service'; import { gatewayManager } from '@electron/gateway/manager'; import { createHostApiContext, dispatchLocalHostApi } from '@electron/api/router'; import { hostEventBus } from '@electron/api/event-bus'; import { getHostApiBase, getHostApiToken, startHostApiServer } from '@electron/api/server'; import { syncProviderRuntimeSnapshot } from '@electron/service/provider-runtime-sync'; import { applyLaunchAtStartupSetting, syncLaunchAtStartupSettingFromConfig } from '@electron/service/launch-at-startup'; import { ensureBuiltinSkillsInstalled, ensurePreinstalledSkillsInstalled } from '@electron/utils/skill-config'; import { initTelemetry, shutdownTelemetry } from '@electron/utils/telemetry'; import { syncGatewayConfigBeforeLaunch } from '@electron/gateway/config-sync'; // 初始化 updater,确保在 app ready 之前或者之中注册好 IPC appUpdater.init(); // 注册 hostapi:fetch IPC 代理 // 模型管理相关接口在本地处理(对齐 ClawX),其余接口代理到远端后端 const HOST_API_BASE_URL = process.env['ZN_AI_HOST_API_BASE_URL'] || process.env['VITE_SERVICE_URL'] || 'http://8.138.234.141/ingress'; const GATEWAY_QUIT_TIMEOUT_MS = 5_000; let gatewayEventBridgeBound = false; let gatewayQuitCleanupInProgress = false; let gatewayQuitCleanupCompleted = false; let hostApiServer: Server | null = null; type HostApiProxyRequest = { path: string; method?: string; headers?: Record; body?: unknown; }; function refreshProviderRuntime(): { warnings: string[] } { try { return syncProviderRuntimeSnapshot(); } catch (error) { log.error('provider runtime sync failed', error); return { warnings: [error instanceof Error ? error.message : String(error)], }; } } async function requestUpstreamHostApi(path: string, method: string, headers: Record | undefined, body: unknown) { const url = `${HOST_API_BASE_URL}${path}`; try { const response = await axios({ url, method, headers: { 'Content-Type': 'application/json', ...headers, }, data: body ?? undefined, timeout: 30000, }); return { success: true, ok: true, status: response.status, json: response.data, data: response.data, }; } catch (error: any) { if (error.response) { return { success: false, ok: false, status: error.response.status, error: error.response.data?.message || error.message, text: error.response.statusText, data: error.response.data, }; } return { success: false, ok: false, error: error.message || 'Unknown error', }; } } async function closeHostApiServer(): Promise { if (!hostApiServer) { return; } const server = hostApiServer; hostApiServer = null; await new Promise((resolve) => { server.close(() => resolve()); }); } function normalizeProxyBody(body: unknown): string | undefined { if (body == null) { return undefined; } if (typeof body === 'string') { return body; } return JSON.stringify(body); } async function proxyHostApiRequest(request: HostApiProxyRequest) { const path = typeof request.path === 'string' ? request.path : ''; if (!path || !path.startsWith('/')) { return { success: false, ok: false, status: 400, error: `Invalid host API path: ${String(request.path)}`, }; } const hostApiToken = getHostApiToken(); if (!hostApiServer || !hostApiToken) { const localResult = await dispatchLocalHostApi(request); if (localResult) { return localResult; } return requestUpstreamHostApi( path, request.method || 'GET', request.headers, request.body, ); } const method = (request.method || 'GET').toUpperCase(); const headers: Record = { ...(request.headers || {}), 'X-Host-Api-Token': hostApiToken, }; const body = normalizeProxyBody(request.body); if (body !== undefined && !headers['Content-Type'] && !headers['content-type']) { headers['Content-Type'] = 'application/json'; } try { const response = await fetch(`${getHostApiBase()}${path}`, { method, headers, body, }); const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { return await response.json(); } const text = await response.text(); return { success: response.ok, ok: response.ok, status: response.status, text, ...(response.ok ? {} : { error: text || response.statusText }), }; } catch (error) { return { success: false, ok: false, error: error instanceof Error ? error.message : String(error), }; } } function emitGatewayRendererEvent(channel: string, payload: unknown): void { BrowserWindow.getAllWindows().forEach((window) => { if (!window.isDestroyed()) { window.webContents.send(channel, payload); } }); } function bindGatewayLifecycleEvents(): void { if (gatewayEventBridgeBound) { return; } gatewayEventBridgeBound = true; gatewayManager.on('status', (status) => { hostEventBus.emit('gateway:status', status); emitGatewayRendererEvent('gateway:status-changed', status); }); gatewayManager.on('message', (message) => { hostEventBus.emit('gateway:message', message); emitGatewayRendererEvent('gateway:message', message); }); gatewayManager.on('notification', (notification) => { hostEventBus.emit('gateway:notification', notification); emitGatewayRendererEvent('gateway:notification', notification); }); gatewayManager.on('channel:status', (data) => { hostEventBus.emit('gateway:channel-status', data); emitGatewayRendererEvent('gateway:channel-status', data); }); gatewayManager.on('chat:message', (data) => { hostEventBus.emit('gateway:chat-message', data); emitGatewayRendererEvent('gateway:chat-message', data); }); gatewayManager.on('exit', (code) => { hostEventBus.emit('gateway:exit', { code }); emitGatewayRendererEvent('gateway:exit', code); }); gatewayManager.on('error', (error) => { hostEventBus.emit('gateway:error', { message: error.message }); emitGatewayRendererEvent('gateway:error', error.message); }); } function requestQuitOnSignal(signal: NodeJS.Signals): void { log.info(`Received ${signal}; requesting app quit`); app.quit(); } function emergencyGatewayCleanup(reason: string, error: unknown): void { log.error(`${reason}:`, error); hostEventBus.closeAll(); void closeHostApiServer().catch(() => { // ignore host API server close failures during emergency cleanup }); try { void gatewayManager.stop().catch(() => { // ignore stop failures during emergency cleanup }); } catch { // ignore stop invocation failures if state is corrupted } setTimeout(() => { void shutdownTelemetry().catch(() => { // ignore telemetry flush failures during crash shutdown }); void gatewayManager.forceTerminateOwnedProcessForQuit().catch(() => { // ignore forced termination failures during crash shutdown }).finally(() => { process.exit(1); }); }, 3_000).unref(); } ipcMain.handle(IPC_EVENTS.HOST_API_TOKEN, async () => getHostApiToken()); ipcMain.handle(IPC_EVENTS.HOST_API_FETCH, async (_event, request: HostApiProxyRequest) => { return proxyHostApiRequest({ ...request, method: request.method || 'GET' }); // 1. 优先本地处理 Host API 路由(逐步对齐 ClawX) // 2. 其余接口代理到远端后端 }); // Gateway RPC IPC handler ipcMain.handle(IPC_EVENTS.GATEWAY_RPC, async (_event, method: string, params: any) => { return gatewayManager.rpc(method, params); }); // import logManager from '@electron/service/logger' // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) { app.quit(); } // process.on('uncaughtException', (err) => { // logManager.error('uncaughtException', err); // }); // process.on('unhandledRejection', (reason, promise) => { // logManager.error('unhandledRejection', reason, promise); // }); process.once('SIGINT', () => requestQuitOnSignal('SIGINT')); process.once('SIGTERM', () => requestQuitOnSignal('SIGTERM')); process.on('uncaughtException', (error) => { emergencyGatewayCleanup('Uncaught exception in main process', error); }); process.on('unhandledRejection', (reason) => { emergencyGatewayCleanup('Unhandled promise rejection in main process', reason); }); app.on('before-quit', (event) => { if (gatewayQuitCleanupCompleted) { return; } event.preventDefault(); if (gatewayQuitCleanupInProgress) { return; } gatewayQuitCleanupInProgress = true; hostEventBus.closeAll(); const closeServerPromise = closeHostApiServer().catch((error) => { log.warn('Host API server close failed during quit:', error); }); const stopPromise = Promise.all([ closeServerPromise, gatewayManager.stop(), ]).catch((error) => { log.warn('gatewayManager.stop() error during quit:', error); }); const timeoutPromise = new Promise<'timeout'>((resolve) => { setTimeout(() => resolve('timeout'), GATEWAY_QUIT_TIMEOUT_MS); }); void Promise.race([ stopPromise.then(() => 'stopped' as const), timeoutPromise, ]).then(async (result) => { if (result === 'timeout') { log.warn('Gateway shutdown timed out during app quit; proceeding with forced quit'); try { const terminated = await gatewayManager.forceTerminateOwnedProcessForQuit(); if (terminated) { log.warn('Forced gateway process termination completed after quit timeout'); } } catch (error) { log.warn('Forced gateway termination failed after quit timeout:', error); } } try { await shutdownTelemetry(); } catch (error) { log.warn('Telemetry shutdown failed during app quit:', error); } gatewayQuitCleanupCompleted = true; app.quit(); }).catch((error) => { gatewayQuitCleanupInProgress = false; log.warn('Gateway quit cleanup failed:', error); gatewayQuitCleanupCompleted = true; app.quit(); }); }); app.whenReady().then(async () => { await configManager.init(); await syncLaunchAtStartupSettingFromConfig(); await themeManager.init(); await initTelemetry(); bindGatewayLifecycleEvents(); try { hostApiServer = startHostApiServer({ ctx: createHostApiContext(), dispatchRequest: dispatchLocalHostApi, fallbackRequest: async (request) => { return requestUpstreamHostApi( request.path, request.method || 'GET', request.headers, request.body, ); }, }); } catch (error) { log.error('Failed to start Host API server:', error); } let launchAtStartup = Boolean(configManager.get(CONFIG_KEYS.LAUNCH_AT_STARTUP)); const stopLaunchAtStartupSync = configManager.onConfigChange((config) => { const nextLaunchAtStartup = Boolean(config[CONFIG_KEYS.LAUNCH_AT_STARTUP]); if (nextLaunchAtStartup === launchAtStartup) { return; } launchAtStartup = nextLaunchAtStartup; void applyLaunchAtStartupSetting(nextLaunchAtStartup); }); app.once('will-quit', () => { stopLaunchAtStartupSync(); void closeHostApiServer().catch(() => { // ignore host API server close failures during final teardown }); }); void ensureBuiltinSkillsInstalled().catch((error) => { log.warn('Failed to install built-in skills:', error); }); void ensurePreinstalledSkillsInstalled().catch((error) => { log.warn('Failed to install preinstalled skills:', error); }); try { await syncGatewayConfigBeforeLaunch(); } catch (error) { log.warn('Failed to sync OpenClaw config before launch:', error); } refreshProviderRuntime(); void gatewayManager.init().catch((error) => { log.warn('Failed to initialize GatewayManager:', error); }); onProviderChange(() => { const runtimeSync = refreshProviderRuntime(); gatewayManager.reloadProviders({ topics: ['providers', 'models', 'agents'], reason: 'providers:changed', warnings: runtimeSync.warnings, }); }); setupMainWindow(); // 初始化脚本存储服务 initScriptStoreService() // 开启任务操作子进程 runTaskOperationService() // 开启subagent子进程 }); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin' && !configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY)) { log.info('app closing due to all windows being closed'); app.quit(); } }); // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { setupMainWindow(); } });