Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,18 @@
|
||||
export let isQuitting = false;
|
||||
/**
|
||||
* Application quit state.
|
||||
*
|
||||
* Exposed as a function accessor (not a bare `export let`) so that every
|
||||
* import site reads the *live* value. With `export let`, bundlers that
|
||||
* compile to CJS may snapshot the variable at import time, causing
|
||||
* `isQuitting` to stay `false` forever and preventing the window from
|
||||
* closing on Windows/Linux.
|
||||
*/
|
||||
let _isQuitting = false;
|
||||
|
||||
export function isQuitting(): boolean {
|
||||
return _isQuitting;
|
||||
}
|
||||
|
||||
export function setQuitting(value = true): void {
|
||||
isQuitting = value;
|
||||
_isQuitting = value;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,13 @@ import { ClawHubService } from '../gateway/clawhub';
|
||||
import { ensureClawXContext, repairClawXOnlyBootstrapFiles } from '../utils/openclaw-workspace';
|
||||
import { isQuitting, setQuitting } from './app-state';
|
||||
|
||||
// Disable GPU acceleration for better compatibility
|
||||
app.disableHardwareAcceleration();
|
||||
// Disable GPU acceleration only on Linux where GPU driver issues are common.
|
||||
// On Windows and macOS, hardware acceleration is essential for responsive UI;
|
||||
// forcing CPU rendering makes the main thread compete with sync I/O and
|
||||
// contributes to "Not Responding" hangs.
|
||||
if (process.platform === 'linux') {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
// Global references
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
@@ -122,30 +127,28 @@ async function initialize(): Promise<void> {
|
||||
// Create system tray
|
||||
createTray(mainWindow);
|
||||
|
||||
// Override security headers ONLY for the OpenClaw Gateway Control UI
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
const isGatewayUrl = details.url.includes('127.0.0.1:18789') || details.url.includes('localhost:18789');
|
||||
|
||||
if (!isGatewayUrl) {
|
||||
callback({ responseHeaders: details.responseHeaders });
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = { ...details.responseHeaders };
|
||||
delete headers['X-Frame-Options'];
|
||||
delete headers['x-frame-options'];
|
||||
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 });
|
||||
});
|
||||
// Override security headers ONLY for the OpenClaw Gateway Control UI.
|
||||
// The URL filter ensures this callback only fires for gateway requests,
|
||||
// avoiding unnecessary overhead on every other HTTP response.
|
||||
session.defaultSession.webRequest.onHeadersReceived(
|
||||
{ urls: ['http://127.0.0.1:18789/*', 'http://localhost:18789/*'] },
|
||||
(details, callback) => {
|
||||
const headers = { ...details.responseHeaders };
|
||||
delete headers['X-Frame-Options'];
|
||||
delete headers['x-frame-options'];
|
||||
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, clawHubService, mainWindow);
|
||||
@@ -158,7 +161,7 @@ async function initialize(): Promise<void> {
|
||||
|
||||
// Minimize to tray on close instead of quitting (macOS & Windows)
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!isQuitting) {
|
||||
if (!isQuitting()) {
|
||||
event.preventDefault();
|
||||
mainWindow?.hide();
|
||||
}
|
||||
@@ -171,11 +174,9 @@ async function initialize(): Promise<void> {
|
||||
// Repair any bootstrap files that only contain ClawX markers (no OpenClaw
|
||||
// template content). This fixes a race condition where ensureClawXContext()
|
||||
// previously created the file before the gateway could seed the full template.
|
||||
try {
|
||||
repairClawXOnlyBootstrapFiles();
|
||||
} catch (error) {
|
||||
void repairClawXOnlyBootstrapFiles().catch((error) => {
|
||||
logger.warn('Failed to repair bootstrap files:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Start Gateway automatically (this seeds missing bootstrap files with full templates)
|
||||
try {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Registers all IPC handlers for main-renderer communication
|
||||
*/
|
||||
import { ipcMain, BrowserWindow, shell, dialog, app, nativeImage } from 'electron';
|
||||
import { existsSync, copyFileSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join, extname, basename } from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
@@ -85,7 +85,7 @@ export function registerIpcHandlers(
|
||||
registerClawHubHandlers(clawHubService);
|
||||
|
||||
// OpenClaw handlers
|
||||
registerOpenClawHandlers();
|
||||
registerOpenClawHandlers(gatewayManager);
|
||||
|
||||
// Provider handlers
|
||||
registerProviderHandlers(gatewayManager);
|
||||
@@ -135,7 +135,7 @@ function registerSkillConfigHandlers(): void {
|
||||
apiKey?: string;
|
||||
env?: Record<string, string>;
|
||||
}) => {
|
||||
return updateSkillConfig(params.skillKey, {
|
||||
return await updateSkillConfig(params.skillKey, {
|
||||
apiKey: params.apiKey,
|
||||
env: params.env,
|
||||
});
|
||||
@@ -143,12 +143,12 @@ function registerSkillConfigHandlers(): void {
|
||||
|
||||
// Get skill config
|
||||
ipcMain.handle('skill:getConfig', async (_, skillKey: string) => {
|
||||
return getSkillConfig(skillKey);
|
||||
return await getSkillConfig(skillKey);
|
||||
});
|
||||
|
||||
// Get all skill configs
|
||||
ipcMain.handle('skill:getAllConfigs', async () => {
|
||||
return getAllSkillConfigs();
|
||||
return await getAllSkillConfigs();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ function registerLogHandlers(): void {
|
||||
|
||||
// Read log file content (last N lines)
|
||||
ipcMain.handle('log:readFile', async (_, tailLines?: number) => {
|
||||
return logger.readLogFile(tailLines);
|
||||
return await logger.readLogFile(tailLines);
|
||||
});
|
||||
|
||||
// Get log file path (so user can open in file explorer)
|
||||
@@ -389,7 +389,7 @@ function registerLogHandlers(): void {
|
||||
|
||||
// List all log files
|
||||
ipcMain.handle('log:listFiles', async () => {
|
||||
return logger.listLogFiles();
|
||||
return await logger.listLogFiles();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -479,8 +479,10 @@ function registerGatewayHandlers(
|
||||
const fileReferences: string[] = [];
|
||||
|
||||
if (params.media && params.media.length > 0) {
|
||||
const fsP = await import('fs/promises');
|
||||
for (const m of params.media) {
|
||||
logger.info(`[chat:sendWithMedia] Processing file: ${m.fileName} (${m.mimeType}), path: ${m.filePath}, exists: ${existsSync(m.filePath)}, isVision: ${VISION_MIME_TYPES.has(m.mimeType)}`);
|
||||
const exists = await fsP.access(m.filePath).then(() => true, () => false);
|
||||
logger.info(`[chat:sendWithMedia] Processing file: ${m.fileName} (${m.mimeType}), path: ${m.filePath}, exists: ${exists}, isVision: ${VISION_MIME_TYPES.has(m.mimeType)}`);
|
||||
|
||||
// Always add file path reference so the model can access it via tools
|
||||
fileReferences.push(
|
||||
@@ -491,7 +493,7 @@ function registerGatewayHandlers(
|
||||
// Send as base64 attachment in the format the Gateway expects:
|
||||
// { content: base64String, mimeType: string, fileName?: string }
|
||||
// The Gateway normalizer looks for `a.content` (NOT `a.source.data`).
|
||||
const fileBuffer = readFileSync(m.filePath);
|
||||
const fileBuffer = await fsP.readFile(m.filePath);
|
||||
const base64Data = fileBuffer.toString('base64');
|
||||
logger.info(`[chat:sendWithMedia] Read ${fileBuffer.length} bytes, base64 length: ${base64Data.length}`);
|
||||
imageAttachments.push({
|
||||
@@ -605,7 +607,7 @@ function registerGatewayHandlers(
|
||||
* OpenClaw-related IPC handlers
|
||||
* For checking package status and channel configuration
|
||||
*/
|
||||
function registerOpenClawHandlers(): void {
|
||||
function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
|
||||
// Get OpenClaw package status
|
||||
ipcMain.handle('openclaw:status', () => {
|
||||
@@ -664,7 +666,13 @@ function registerOpenClawHandlers(): void {
|
||||
ipcMain.handle('channel:saveConfig', async (_, channelType: string, config: Record<string, unknown>) => {
|
||||
try {
|
||||
logger.info('channel:saveConfig', { channelType, keys: Object.keys(config || {}) });
|
||||
saveChannelConfig(channelType, config);
|
||||
await saveChannelConfig(channelType, config);
|
||||
// Debounced restart so the gateway picks up the new channel config.
|
||||
// The gateway watches openclaw.json, but a restart ensures a clean
|
||||
// start for newly-added channels. Using debouncedRestart() here
|
||||
// instead of an explicit restart on the frontend side means that
|
||||
// rapid config changes (e.g. setup wizard) coalesce into one restart.
|
||||
gatewayManager.debouncedRestart();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to save channel config:', error);
|
||||
@@ -675,7 +683,7 @@ function registerOpenClawHandlers(): void {
|
||||
// Get channel configuration
|
||||
ipcMain.handle('channel:getConfig', async (_, channelType: string) => {
|
||||
try {
|
||||
const config = getChannelConfig(channelType);
|
||||
const config = await getChannelConfig(channelType);
|
||||
return { success: true, config };
|
||||
} catch (error) {
|
||||
console.error('Failed to get channel config:', error);
|
||||
@@ -686,7 +694,7 @@ function registerOpenClawHandlers(): void {
|
||||
// Get channel form values (reverse-transformed for UI pre-fill)
|
||||
ipcMain.handle('channel:getFormValues', async (_, channelType: string) => {
|
||||
try {
|
||||
const values = getChannelFormValues(channelType);
|
||||
const values = await getChannelFormValues(channelType);
|
||||
return { success: true, values };
|
||||
} catch (error) {
|
||||
console.error('Failed to get channel form values:', error);
|
||||
@@ -697,7 +705,7 @@ function registerOpenClawHandlers(): void {
|
||||
// Delete channel configuration
|
||||
ipcMain.handle('channel:deleteConfig', async (_, channelType: string) => {
|
||||
try {
|
||||
deleteChannelConfig(channelType);
|
||||
await deleteChannelConfig(channelType);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to delete channel config:', error);
|
||||
@@ -708,7 +716,7 @@ function registerOpenClawHandlers(): void {
|
||||
// List configured channels
|
||||
ipcMain.handle('channel:listConfigured', async () => {
|
||||
try {
|
||||
const channels = listConfiguredChannels();
|
||||
const channels = await listConfiguredChannels();
|
||||
return { success: true, channels };
|
||||
} catch (error) {
|
||||
console.error('Failed to list channels:', error);
|
||||
@@ -719,7 +727,7 @@ function registerOpenClawHandlers(): void {
|
||||
// Enable or disable a channel
|
||||
ipcMain.handle('channel:setEnabled', async (_, channelType: string, enabled: boolean) => {
|
||||
try {
|
||||
setChannelEnabled(channelType, enabled);
|
||||
await setChannelEnabled(channelType, enabled);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to set channel enabled:', error);
|
||||
@@ -838,10 +846,8 @@ function registerDeviceOAuthHandlers(mainWindow: BrowserWindow): void {
|
||||
function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
// Listen for OAuth success to automatically restart the Gateway with new tokens/configs
|
||||
deviceOAuthManager.on('oauth:success', (providerType) => {
|
||||
logger.info(`[IPC] Restarting Gateway after ${providerType} OAuth success...`);
|
||||
void gatewayManager.restart().catch(err => {
|
||||
logger.error('Failed to restart Gateway after OAuth success:', err);
|
||||
});
|
||||
logger.info(`[IPC] Scheduling Gateway restart after ${providerType} OAuth success...`);
|
||||
gatewayManager.debouncedRestart();
|
||||
});
|
||||
|
||||
// Get all providers with key info
|
||||
@@ -871,7 +877,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
|
||||
// Also write to OpenClaw auth-profiles.json so the gateway can use it
|
||||
try {
|
||||
saveProviderKeyToOpenClaw(ock, trimmedKey);
|
||||
await saveProviderKeyToOpenClaw(ock, trimmedKey);
|
||||
} catch (err) {
|
||||
console.warn('Failed to save key to OpenClaw auth-profiles:', err);
|
||||
}
|
||||
@@ -884,7 +890,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
const api = config.type === 'custom' || config.type === 'ollama' ? 'openai-completions' : meta?.api;
|
||||
|
||||
if (api) {
|
||||
syncProviderConfigToOpenClaw(ock, config.model, {
|
||||
await syncProviderConfigToOpenClaw(ock, config.model, {
|
||||
baseUrl: config.baseUrl || meta?.baseUrl,
|
||||
api,
|
||||
apiKeyEnv: meta?.apiKeyEnv,
|
||||
@@ -897,7 +903,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
: await getApiKey(config.id);
|
||||
if (resolvedKey && config.baseUrl) {
|
||||
const modelId = config.model;
|
||||
updateAgentModelProvider(ock, {
|
||||
await updateAgentModelProvider(ock, {
|
||||
baseUrl: config.baseUrl,
|
||||
api: 'openai-completions',
|
||||
models: modelId ? [{ id: modelId, name: modelId }] : [],
|
||||
@@ -906,11 +912,10 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Restart Gateway so it picks up the new config and env vars
|
||||
logger.info(`Restarting Gateway after saving provider "${ock}" config`);
|
||||
void gatewayManager.restart().catch((err) => {
|
||||
logger.warn('Gateway restart after provider save failed:', err);
|
||||
});
|
||||
// Debounced restart so the gateway picks up new config/env vars.
|
||||
// Multiple rapid provider saves (e.g. during setup) are coalesced.
|
||||
logger.info(`Scheduling Gateway restart after saving provider "${ock}" config`);
|
||||
gatewayManager.debouncedRestart();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to sync openclaw provider config:', err);
|
||||
@@ -932,13 +937,11 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
if (existing?.type) {
|
||||
try {
|
||||
const ock = getOpenClawProviderKey(existing.type, providerId);
|
||||
removeProviderFromOpenClaw(ock);
|
||||
await removeProviderFromOpenClaw(ock);
|
||||
|
||||
// Restart Gateway so it no longer loads the deleted provider's plugin/config
|
||||
logger.info(`Restarting Gateway after deleting provider "${ock}"`);
|
||||
void gatewayManager.restart().catch((err) => {
|
||||
logger.warn('Gateway restart after provider delete failed:', err);
|
||||
});
|
||||
// Debounced restart so the gateway stops loading the deleted provider.
|
||||
logger.info(`Scheduling Gateway restart after deleting provider "${ock}"`);
|
||||
gatewayManager.debouncedRestart();
|
||||
} catch (err) {
|
||||
console.warn('Failed to completely remove provider from OpenClaw:', err);
|
||||
}
|
||||
@@ -960,7 +963,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
const providerType = provider?.type || providerId;
|
||||
const ock = getOpenClawProviderKey(providerType, providerId);
|
||||
try {
|
||||
saveProviderKeyToOpenClaw(ock, apiKey);
|
||||
await saveProviderKeyToOpenClaw(ock, apiKey);
|
||||
} catch (err) {
|
||||
console.warn('Failed to save key to OpenClaw auth-profiles:', err);
|
||||
}
|
||||
@@ -1003,10 +1006,10 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
const trimmedKey = apiKey.trim();
|
||||
if (trimmedKey) {
|
||||
await storeApiKey(providerId, trimmedKey);
|
||||
saveProviderKeyToOpenClaw(ock, trimmedKey);
|
||||
await saveProviderKeyToOpenClaw(ock, trimmedKey);
|
||||
} else {
|
||||
await deleteApiKey(providerId);
|
||||
removeProviderFromOpenClaw(ock);
|
||||
await removeProviderFromOpenClaw(ock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1016,7 +1019,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
const api = nextConfig.type === 'custom' || nextConfig.type === 'ollama' ? 'openai-completions' : meta?.api;
|
||||
|
||||
if (api) {
|
||||
syncProviderConfigToOpenClaw(ock, nextConfig.model, {
|
||||
await syncProviderConfigToOpenClaw(ock, nextConfig.model, {
|
||||
baseUrl: nextConfig.baseUrl || meta?.baseUrl,
|
||||
api,
|
||||
apiKeyEnv: meta?.apiKeyEnv,
|
||||
@@ -1029,7 +1032,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
: await getApiKey(providerId);
|
||||
if (resolvedKey && nextConfig.baseUrl) {
|
||||
const modelId = nextConfig.model;
|
||||
updateAgentModelProvider(ock, {
|
||||
await updateAgentModelProvider(ock, {
|
||||
baseUrl: nextConfig.baseUrl,
|
||||
api: 'openai-completions',
|
||||
models: modelId ? [{ id: modelId, name: modelId }] : [],
|
||||
@@ -1046,20 +1049,18 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
? `${ock}/${nextConfig.model}`
|
||||
: undefined;
|
||||
if (nextConfig.type !== 'custom' && nextConfig.type !== 'ollama') {
|
||||
setOpenClawDefaultModel(nextConfig.type, modelOverride);
|
||||
await setOpenClawDefaultModel(nextConfig.type, modelOverride);
|
||||
} else {
|
||||
setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||
baseUrl: nextConfig.baseUrl,
|
||||
api: 'openai-completions',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Restart Gateway so it picks up the new config and env vars
|
||||
logger.info(`Restarting Gateway after updating provider "${ock}" config`);
|
||||
void gatewayManager.restart().catch((err) => {
|
||||
logger.warn('Gateway restart after provider update failed:', err);
|
||||
});
|
||||
// Debounced restart so the gateway picks up updated config/env vars.
|
||||
logger.info(`Scheduling Gateway restart after updating provider "${ock}" config`);
|
||||
gatewayManager.debouncedRestart();
|
||||
} catch (err) {
|
||||
console.warn('Failed to sync openclaw config after provider update:', err);
|
||||
}
|
||||
@@ -1071,10 +1072,10 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
await saveProvider(existing);
|
||||
if (previousKey) {
|
||||
await storeApiKey(providerId, previousKey);
|
||||
saveProviderKeyToOpenClaw(previousOck, previousKey);
|
||||
await saveProviderKeyToOpenClaw(previousOck, previousKey);
|
||||
} else {
|
||||
await deleteApiKey(providerId);
|
||||
removeProviderFromOpenClaw(previousOck);
|
||||
await removeProviderFromOpenClaw(previousOck);
|
||||
}
|
||||
} catch (rollbackError) {
|
||||
console.warn('Failed to rollback provider updateWithKey:', rollbackError);
|
||||
@@ -1096,7 +1097,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
const ock = getOpenClawProviderKey(providerType, providerId);
|
||||
try {
|
||||
if (ock) {
|
||||
removeProviderFromOpenClaw(ock);
|
||||
await removeProviderFromOpenClaw(ock);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to completely remove provider from OpenClaw:', err);
|
||||
@@ -1144,17 +1145,17 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
: undefined;
|
||||
|
||||
if (provider.type === 'custom' || provider.type === 'ollama') {
|
||||
setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||
baseUrl: provider.baseUrl,
|
||||
api: 'openai-completions',
|
||||
});
|
||||
} else {
|
||||
setOpenClawDefaultModel(provider.type, modelOverride);
|
||||
await setOpenClawDefaultModel(provider.type, modelOverride);
|
||||
}
|
||||
|
||||
// Keep auth-profiles in sync with the default provider instance.
|
||||
if (providerKey) {
|
||||
saveProviderKeyToOpenClaw(ock, providerKey);
|
||||
await saveProviderKeyToOpenClaw(ock, providerKey);
|
||||
}
|
||||
} else {
|
||||
// OAuth providers (minimax-portal, minimax-portal-cn, qwen-portal)
|
||||
@@ -1177,7 +1178,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
? 'minimax-portal'
|
||||
: provider.type;
|
||||
|
||||
setOpenClawDefaultModelWithOverride(targetProviderKey, undefined, {
|
||||
await setOpenClawDefaultModelWithOverride(targetProviderKey, undefined, {
|
||||
baseUrl,
|
||||
api,
|
||||
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
|
||||
@@ -1191,7 +1192,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
// authHeader immediately, without waiting for Gateway to sync openclaw.json.
|
||||
try {
|
||||
const defaultModelId = provider.model?.split('/').pop();
|
||||
updateAgentModelProvider(targetProviderKey, {
|
||||
await updateAgentModelProvider(targetProviderKey, {
|
||||
baseUrl,
|
||||
api,
|
||||
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
|
||||
@@ -1210,7 +1211,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
provider.baseUrl
|
||||
) {
|
||||
const modelId = provider.model;
|
||||
updateAgentModelProvider(ock, {
|
||||
await updateAgentModelProvider(ock, {
|
||||
baseUrl: provider.baseUrl,
|
||||
api: 'openai-completions',
|
||||
models: modelId ? [{ id: modelId, name: modelId }] : [],
|
||||
@@ -1218,12 +1219,10 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
});
|
||||
}
|
||||
|
||||
// Restart Gateway so it picks up the new config and env vars.
|
||||
// Debounced restart so the gateway picks up the new default provider.
|
||||
if (gatewayManager.isConnected()) {
|
||||
logger.info(`Restarting Gateway after provider switch to "${ock}"`);
|
||||
void gatewayManager.restart().catch((err) => {
|
||||
logger.warn('Gateway restart after provider switch failed:', err);
|
||||
});
|
||||
logger.info(`Scheduling Gateway restart after provider switch to "${ock}"`);
|
||||
gatewayManager.debouncedRestart();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to set OpenClaw default model:', err);
|
||||
@@ -1757,7 +1756,7 @@ const OUTBOUND_DIR = join(homedir(), '.openclaw', 'media', 'outbound');
|
||||
* longer side so the image is never squished). The frontend handles
|
||||
* square cropping via CSS object-fit: cover.
|
||||
*/
|
||||
function generateImagePreview(filePath: string, mimeType: string): string | null {
|
||||
async function generateImagePreview(filePath: string, mimeType: string): Promise<string | null> {
|
||||
try {
|
||||
const img = nativeImage.createFromPath(filePath);
|
||||
if (img.isEmpty()) return null;
|
||||
@@ -1770,8 +1769,9 @@ function generateImagePreview(filePath: string, mimeType: string): string | null
|
||||
: img.resize({ height: maxDim }); // portrait → constrain height
|
||||
return `data:image/png;base64,${resized.toPNG().toString('base64')}`;
|
||||
}
|
||||
// Small image — use original
|
||||
const buf = readFileSync(filePath);
|
||||
// Small image — use original (async read to avoid blocking)
|
||||
const { readFile: readFileAsync } = await import('fs/promises');
|
||||
const buf = await readFileAsync(filePath);
|
||||
return `data:${mimeType};base64,${buf.toString('base64')}`;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -1785,26 +1785,27 @@ function generateImagePreview(filePath: string, mimeType: string): string | null
|
||||
function registerFileHandlers(): void {
|
||||
// Stage files from real disk paths (used with dialog:open)
|
||||
ipcMain.handle('file:stage', async (_, filePaths: string[]) => {
|
||||
mkdirSync(OUTBOUND_DIR, { recursive: true });
|
||||
const fsP = await import('fs/promises');
|
||||
await fsP.mkdir(OUTBOUND_DIR, { recursive: true });
|
||||
|
||||
const results = [];
|
||||
for (const filePath of filePaths) {
|
||||
const id = crypto.randomUUID();
|
||||
const ext = extname(filePath);
|
||||
const stagedPath = join(OUTBOUND_DIR, `${id}${ext}`);
|
||||
copyFileSync(filePath, stagedPath);
|
||||
await fsP.copyFile(filePath, stagedPath);
|
||||
|
||||
const stat = statSync(stagedPath);
|
||||
const s = await fsP.stat(stagedPath);
|
||||
const mimeType = getMimeType(ext);
|
||||
const fileName = basename(filePath);
|
||||
|
||||
// Generate preview for images
|
||||
let preview: string | null = null;
|
||||
if (mimeType.startsWith('image/')) {
|
||||
preview = generateImagePreview(stagedPath, mimeType);
|
||||
preview = await generateImagePreview(stagedPath, mimeType);
|
||||
}
|
||||
|
||||
results.push({ id, fileName, mimeType, fileSize: stat.size, stagedPath, preview });
|
||||
results.push({ id, fileName, mimeType, fileSize: s.size, stagedPath, preview });
|
||||
}
|
||||
return results;
|
||||
});
|
||||
@@ -1815,13 +1816,14 @@ function registerFileHandlers(): void {
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
}) => {
|
||||
mkdirSync(OUTBOUND_DIR, { recursive: true });
|
||||
const fsP = await import('fs/promises');
|
||||
await fsP.mkdir(OUTBOUND_DIR, { recursive: true });
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const ext = extname(payload.fileName) || mimeToExt(payload.mimeType);
|
||||
const stagedPath = join(OUTBOUND_DIR, `${id}${ext}`);
|
||||
const buffer = Buffer.from(payload.base64, 'base64');
|
||||
writeFileSync(stagedPath, buffer);
|
||||
await fsP.writeFile(stagedPath, buffer);
|
||||
|
||||
const mimeType = payload.mimeType || getMimeType(ext);
|
||||
const fileSize = buffer.length;
|
||||
@@ -1829,7 +1831,7 @@ function registerFileHandlers(): void {
|
||||
// Generate preview for images
|
||||
let preview: string | null = null;
|
||||
if (mimeType.startsWith('image/')) {
|
||||
preview = generateImagePreview(stagedPath, mimeType);
|
||||
preview = await generateImagePreview(stagedPath, mimeType);
|
||||
}
|
||||
|
||||
return { id, fileName: payload.fileName, mimeType, fileSize, stagedPath, preview };
|
||||
@@ -1856,11 +1858,17 @@ function registerFileHandlers(): void {
|
||||
});
|
||||
if (result.canceled || !result.filePath) return { success: false };
|
||||
|
||||
if (params.filePath && existsSync(params.filePath)) {
|
||||
copyFileSync(params.filePath, result.filePath);
|
||||
const fsP = await import('fs/promises');
|
||||
if (params.filePath) {
|
||||
try {
|
||||
await fsP.access(params.filePath);
|
||||
await fsP.copyFile(params.filePath, result.filePath);
|
||||
} catch {
|
||||
return { success: false, error: 'Source file not found' };
|
||||
}
|
||||
} else if (params.base64) {
|
||||
const buffer = Buffer.from(params.base64, 'base64');
|
||||
writeFileSync(result.filePath, buffer);
|
||||
await fsP.writeFile(result.filePath, buffer);
|
||||
} else {
|
||||
return { success: false, error: 'No image data provided' };
|
||||
}
|
||||
@@ -1871,19 +1879,16 @@ function registerFileHandlers(): void {
|
||||
});
|
||||
|
||||
ipcMain.handle('media:getThumbnails', async (_, paths: Array<{ filePath: string; mimeType: string }>) => {
|
||||
const fsP = await import('fs/promises');
|
||||
const results: Record<string, { preview: string | null; fileSize: number }> = {};
|
||||
for (const { filePath, mimeType } of paths) {
|
||||
try {
|
||||
if (!existsSync(filePath)) {
|
||||
results[filePath] = { preview: null, fileSize: 0 };
|
||||
continue;
|
||||
}
|
||||
const stat = statSync(filePath);
|
||||
const s = await fsP.stat(filePath);
|
||||
let preview: string | null = null;
|
||||
if (mimeType.startsWith('image/')) {
|
||||
preview = generateImagePreview(filePath, mimeType);
|
||||
preview = await generateImagePreview(filePath, mimeType);
|
||||
}
|
||||
results[filePath] = { preview, fileSize: stat.size };
|
||||
results[filePath] = { preview, fileSize: s.size };
|
||||
} catch {
|
||||
results[filePath] = { preview: null, fileSize: 0 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user