Files
DeskClaw/electron/main/index.ts
Haze 284861a0f5 feat(chat): write API keys to OpenClaw and embed Control UI for chat
Part 1: API Key Integration
- Create electron/utils/openclaw-auth.ts to write keys to
  ~/.openclaw/agents/main/agent/auth-profiles.json
- Update provider:save and provider:setApiKey IPC handlers to
  persist keys to OpenClaw auth-profiles alongside ClawX storage
- Save API key to OpenClaw on successful validation in Setup wizard
- Pass provider API keys as environment variables when starting
  the Gateway process (ANTHROPIC_API_KEY, OPENROUTER_API_KEY, etc.)

Part 2: Embed OpenClaw Control UI for Chat
- Replace custom Chat UI with <webview> embedding the Gateway's
  built-in Control UI at http://127.0.0.1:{port}/?token={token}
- Add gateway:getControlUiUrl IPC handler to provide tokenized URL
- Enable webviewTag in Electron BrowserWindow preferences
- Override X-Frame-Options/CSP headers to allow webview embedding
- Suppress noisy control-ui token_mismatch stderr messages
- Add loading/error states for the embedded webview

This fixes the "No API key found for provider" error and replaces
the buggy custom chat implementation with OpenClaw's battle-tested
Control UI.
2026-02-06 03:12:17 +08:00

155 lines
4.4 KiB
TypeScript

/**
* Electron Main Process Entry
* Manages window creation, system tray, and IPC handlers
*/
import { app, BrowserWindow, ipcMain, session, shell } from 'electron';
import { join } from 'path';
import { GatewayManager } from '../gateway/manager';
import { registerIpcHandlers } from './ipc-handlers';
import { createTray } from './tray';
import { createMenu } from './menu';
import { PORTS } from '../utils/config';
import { appUpdater, registerUpdateHandlers } from './updater';
// Disable GPU acceleration for better compatibility
app.disableHardwareAcceleration();
// Global references
let mainWindow: BrowserWindow | null = null;
const gatewayManager = new GatewayManager();
/**
* Create the main application window
*/
function createWindow(): BrowserWindow {
const win = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 960,
minHeight: 600,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
nodeIntegration: false,
contextIsolation: true,
sandbox: false,
webviewTag: true, // Enable <webview> for embedding OpenClaw Control UI
},
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
trafficLightPosition: { x: 16, y: 16 },
show: false,
});
// Show window when ready to prevent visual flash
win.once('ready-to-show', () => {
win.show();
});
// Handle external links
win.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
// Load the app
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL);
// Open DevTools in development
win.webContents.openDevTools();
} else {
win.loadFile(join(__dirname, '../../dist/index.html'));
}
return win;
}
/**
* Initialize the application
*/
async function initialize(): Promise<void> {
// Set application menu
createMenu();
// Create the main window
mainWindow = createWindow();
// Create system tray
createTray(mainWindow);
// Override security headers for the OpenClaw Control UI webview
// The Control UI sets X-Frame-Options: DENY and CSP frame-ancestors 'none'
// which prevents embedding in an Electron webview
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
const headers = { ...details.responseHeaders };
// Remove X-Frame-Options to allow embedding in webview
delete headers['X-Frame-Options'];
delete headers['x-frame-options'];
// Remove restrictive CSP frame-ancestors
if (headers['Content-Security-Policy']) {
headers['Content-Security-Policy'] = headers['Content-Security-Policy'].map(
(csp) => csp.replace(/frame-ancestors\s+'none'/g, "frame-ancestors 'self' *")
);
}
if (headers['content-security-policy']) {
headers['content-security-policy'] = headers['content-security-policy'].map(
(csp) => csp.replace(/frame-ancestors\s+'none'/g, "frame-ancestors 'self' *")
);
}
callback({ responseHeaders: headers });
});
// Register IPC handlers
registerIpcHandlers(gatewayManager, mainWindow);
// Register update handlers
registerUpdateHandlers(appUpdater, mainWindow);
// Check for updates after a delay (only in production)
if (!process.env.VITE_DEV_SERVER_URL) {
setTimeout(() => {
appUpdater.checkForUpdates().catch((err) => {
console.error('Failed to check for updates:', err);
});
}, 10000); // Check after 10 seconds
}
// Handle window close
mainWindow.on('closed', () => {
mainWindow = null;
});
// Start Gateway automatically (optional based on settings)
try {
await gatewayManager.start();
console.log('Gateway started successfully');
} catch (error) {
console.error('Failed to start Gateway:', error);
// Notify renderer about the error
mainWindow?.webContents.send('gateway:error', String(error));
}
}
// Application lifecycle
app.whenReady().then(initialize);
app.on('window-all-closed', () => {
// On macOS, keep the app running in the menu bar
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On macOS, re-create window when dock icon is clicked
if (BrowserWindow.getAllWindows().length === 0) {
mainWindow = createWindow();
}
});
app.on('before-quit', async () => {
// Clean up Gateway process
await gatewayManager.stop();
});
// Export for testing
export { mainWindow, gatewayManager };