feat(settings): auto-set default provider after successful OAuth (#254)
This commit is contained in:
committed by
GitHub
Unverified
parent
19406757f1
commit
29ef9591cf
@@ -9,10 +9,10 @@ import { EventEmitter } from 'events';
|
|||||||
import { existsSync, writeFileSync } from 'fs';
|
import { existsSync, writeFileSync } from 'fs';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import { PORTS } from '../utils/config';
|
import { PORTS } from '../utils/config';
|
||||||
import {
|
import {
|
||||||
getOpenClawDir,
|
getOpenClawDir,
|
||||||
getOpenClawEntryPath,
|
getOpenClawEntryPath,
|
||||||
isOpenClawBuilt,
|
isOpenClawBuilt,
|
||||||
isOpenClawPresent,
|
isOpenClawPresent,
|
||||||
appendNodeRequireToNodeOptions,
|
appendNodeRequireToNodeOptions,
|
||||||
quoteForCmd,
|
quoteForCmd,
|
||||||
@@ -185,7 +185,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
}> = new Map();
|
}> = new Map();
|
||||||
private deviceIdentity: DeviceIdentity | null = null;
|
private deviceIdentity: DeviceIdentity | null = null;
|
||||||
private restartDebounceTimer: NodeJS.Timeout | null = null;
|
private restartDebounceTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(config?: Partial<ReconnectConfig>) {
|
constructor(config?: Partial<ReconnectConfig>) {
|
||||||
super();
|
super();
|
||||||
this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...config };
|
this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...config };
|
||||||
@@ -244,21 +244,21 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.recentStartupStderrLines.splice(0, this.recentStartupStderrLines.length - MAX_STDERR_LINES);
|
this.recentStartupStderrLines.splice(0, this.recentStartupStderrLines.length - MAX_STDERR_LINES);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current Gateway status
|
* Get current Gateway status
|
||||||
*/
|
*/
|
||||||
getStatus(): GatewayStatus {
|
getStatus(): GatewayStatus {
|
||||||
return { ...this.status };
|
return { ...this.status };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if Gateway is connected and ready
|
* Check if Gateway is connected and ready
|
||||||
*/
|
*/
|
||||||
isConnected(): boolean {
|
isConnected(): boolean {
|
||||||
return this.status.state === 'running' && this.ws?.readyState === WebSocket.OPEN;
|
return this.status.state === 'running' && this.ws?.readyState === WebSocket.OPEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start Gateway process
|
* Start Gateway process
|
||||||
*/
|
*/
|
||||||
@@ -272,7 +272,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
logger.debug('Gateway already running, skipping start');
|
logger.debug('Gateway already running, skipping start');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startLock = true;
|
this.startLock = true;
|
||||||
logger.info(`Gateway start requested (port=${this.status.port})`);
|
logger.info(`Gateway start requested (port=${this.status.port})`);
|
||||||
this.lastSpawnSummary = null;
|
this.lastSpawnSummary = null;
|
||||||
@@ -305,7 +305,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
logger.error('Failed to check Python environment:', err);
|
logger.error('Failed to check Python environment:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
this.recentStartupStderrLines = [];
|
this.recentStartupStderrLines = [];
|
||||||
@@ -321,18 +321,18 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.startHealthCheck();
|
this.startHealthCheck();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('No existing Gateway found, starting new process...');
|
logger.debug('No existing Gateway found, starting new process...');
|
||||||
|
|
||||||
// Start new Gateway process
|
// Start new Gateway process
|
||||||
await this.startProcess();
|
await this.startProcess();
|
||||||
|
|
||||||
// Wait for Gateway to be ready
|
// Wait for Gateway to be ready
|
||||||
await this.waitForReady();
|
await this.waitForReady();
|
||||||
|
|
||||||
// Connect WebSocket
|
// Connect WebSocket
|
||||||
await this.connect(this.status.port);
|
await this.connect(this.status.port);
|
||||||
|
|
||||||
// Start health monitoring
|
// Start health monitoring
|
||||||
this.startHealthCheck();
|
this.startHealthCheck();
|
||||||
logger.debug('Gateway started successfully');
|
logger.debug('Gateway started successfully');
|
||||||
@@ -354,7 +354,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Gateway start failed (port=${this.status.port}, reconnectAttempts=${this.reconnectAttempts}, spawn=${this.lastSpawnSummary ?? 'n/a'})`,
|
`Gateway start failed (port=${this.status.port}, reconnectAttempts=${this.reconnectAttempts}, spawn=${this.lastSpawnSummary ?? 'n/a'})`,
|
||||||
@@ -366,7 +366,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.startLock = false;
|
this.startLock = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop Gateway process
|
* Stop Gateway process
|
||||||
*/
|
*/
|
||||||
@@ -374,10 +374,10 @@ export class GatewayManager extends EventEmitter {
|
|||||||
logger.info('Gateway stop requested');
|
logger.info('Gateway stop requested');
|
||||||
// Disable auto-reconnect
|
// Disable auto-reconnect
|
||||||
this.shouldReconnect = false;
|
this.shouldReconnect = false;
|
||||||
|
|
||||||
// Clear all timers
|
// Clear all timers
|
||||||
this.clearAllTimers();
|
this.clearAllTimers();
|
||||||
|
|
||||||
// If this manager is attached to an external gateway process, ask it to shut down
|
// If this manager is attached to an external gateway process, ask it to shut down
|
||||||
// over protocol before closing the socket.
|
// over protocol before closing the socket.
|
||||||
if (!this.ownsProcess && this.ws?.readyState === WebSocket.OPEN) {
|
if (!this.ownsProcess && this.ws?.readyState === WebSocket.OPEN) {
|
||||||
@@ -393,17 +393,17 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.ws.close(1000, 'Gateway stopped by user');
|
this.ws.close(1000, 'Gateway stopped by user');
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kill process
|
// Kill process
|
||||||
if (this.process && this.ownsProcess) {
|
if (this.process && this.ownsProcess) {
|
||||||
const child = this.process;
|
const child = this.process;
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
// If process already exited, resolve immediately
|
// If process already exited, resolve immediately
|
||||||
if (child.exitCode !== null || child.signalCode !== null) {
|
if (child.exitCode !== null || child.signalCode !== null) {
|
||||||
return resolve();
|
return resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kill the entire process group so respawned children are also terminated.
|
// Kill the entire process group so respawned children are also terminated.
|
||||||
// The gateway entry script may respawn itself; killing only the parent PID
|
// The gateway entry script may respawn itself; killing only the parent PID
|
||||||
// leaves the child orphaned (PPID=1) and still holding the port.
|
// leaves the child orphaned (PPID=1) and still holding the port.
|
||||||
@@ -413,7 +413,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
try { process.kill(-pid, 'SIGTERM'); } catch { /* group kill failed, fall back */ }
|
try { process.kill(-pid, 'SIGTERM'); } catch { /* group kill failed, fall back */ }
|
||||||
}
|
}
|
||||||
child.kill('SIGTERM');
|
child.kill('SIGTERM');
|
||||||
|
|
||||||
// Force kill after timeout
|
// Force kill after timeout
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (child.exitCode === null && child.signalCode === null) {
|
if (child.exitCode === null && child.signalCode === null) {
|
||||||
@@ -430,29 +430,29 @@ export class GatewayManager extends EventEmitter {
|
|||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.once('error', () => {
|
child.once('error', () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.process === child) {
|
if (this.process === child) {
|
||||||
this.process = null;
|
this.process = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.ownsProcess = false;
|
this.ownsProcess = false;
|
||||||
|
|
||||||
// Reject all pending requests
|
// Reject all pending requests
|
||||||
for (const [, request] of this.pendingRequests) {
|
for (const [, request] of this.pendingRequests) {
|
||||||
clearTimeout(request.timeout);
|
clearTimeout(request.timeout);
|
||||||
request.reject(new Error('Gateway stopped'));
|
request.reject(new Error('Gateway stopped'));
|
||||||
}
|
}
|
||||||
this.pendingRequests.clear();
|
this.pendingRequests.clear();
|
||||||
|
|
||||||
this.setStatus({ state: 'stopped', error: undefined, pid: undefined, connectedAt: undefined, uptime: undefined });
|
this.setStatus({ state: 'stopped', error: undefined, pid: undefined, connectedAt: undefined, uptime: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restart Gateway process
|
* Restart Gateway process
|
||||||
*/
|
*/
|
||||||
@@ -481,7 +481,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}, delayMs);
|
}, delayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all active timers
|
* Clear all active timers
|
||||||
*/
|
*/
|
||||||
@@ -503,7 +503,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.restartDebounceTimer = null;
|
this.restartDebounceTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an RPC call to the Gateway
|
* Make an RPC call to the Gateway
|
||||||
* Uses OpenClaw protocol format: { type: "req", id: "...", method: "...", params: {...} }
|
* Uses OpenClaw protocol format: { type: "req", id: "...", method: "...", params: {...} }
|
||||||
@@ -514,22 +514,22 @@ export class GatewayManager extends EventEmitter {
|
|||||||
reject(new Error('Gateway not connected'));
|
reject(new Error('Gateway not connected'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
// Set timeout for request
|
// Set timeout for request
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
this.pendingRequests.delete(id);
|
this.pendingRequests.delete(id);
|
||||||
reject(new Error(`RPC timeout: ${method}`));
|
reject(new Error(`RPC timeout: ${method}`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
// Store pending request
|
// Store pending request
|
||||||
this.pendingRequests.set(id, {
|
this.pendingRequests.set(id, {
|
||||||
resolve: resolve as (value: unknown) => void,
|
resolve: resolve as (value: unknown) => void,
|
||||||
reject,
|
reject,
|
||||||
timeout,
|
timeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send request using OpenClaw protocol format
|
// Send request using OpenClaw protocol format
|
||||||
const request = {
|
const request = {
|
||||||
type: 'req',
|
type: 'req',
|
||||||
@@ -537,7 +537,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
method,
|
method,
|
||||||
params,
|
params,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.ws.send(JSON.stringify(request));
|
this.ws.send(JSON.stringify(request));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -547,7 +547,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start health check monitoring
|
* Start health check monitoring
|
||||||
*/
|
*/
|
||||||
@@ -555,12 +555,12 @@ export class GatewayManager extends EventEmitter {
|
|||||||
if (this.healthCheckInterval) {
|
if (this.healthCheckInterval) {
|
||||||
clearInterval(this.healthCheckInterval);
|
clearInterval(this.healthCheckInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.healthCheckInterval = setInterval(async () => {
|
this.healthCheckInterval = setInterval(async () => {
|
||||||
if (this.status.state !== 'running') {
|
if (this.status.state !== 'running') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const health = await this.checkHealth();
|
const health = await this.checkHealth();
|
||||||
if (!health.ok) {
|
if (!health.ok) {
|
||||||
@@ -572,7 +572,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}, 30000); // Check every 30 seconds
|
}, 30000); // Check every 30 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check Gateway health via WebSocket ping
|
* Check Gateway health via WebSocket ping
|
||||||
* OpenClaw Gateway doesn't have an HTTP /health endpoint
|
* OpenClaw Gateway doesn't have an HTTP /health endpoint
|
||||||
@@ -580,7 +580,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
async checkHealth(): Promise<{ ok: boolean; error?: string; uptime?: number }> {
|
async checkHealth(): Promise<{ ok: boolean; error?: string; uptime?: number }> {
|
||||||
try {
|
try {
|
||||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
const uptime = this.status.connectedAt
|
const uptime = this.status.connectedAt
|
||||||
? Math.floor((Date.now() - this.status.connectedAt) / 1000)
|
? Math.floor((Date.now() - this.status.connectedAt) / 1000)
|
||||||
: undefined;
|
: undefined;
|
||||||
return { ok: true, uptime };
|
return { ok: true, uptime };
|
||||||
@@ -590,7 +590,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
return { ok: false, error: String(error) };
|
return { ok: false, error: String(error) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unload the system-managed openclaw gateway launchctl service if it is
|
* Unload the system-managed openclaw gateway launchctl service if it is
|
||||||
* loaded. Without this, killing the process only causes launchctl to
|
* loaded. Without this, killing the process only causes launchctl to
|
||||||
@@ -654,7 +654,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
private async findExistingGateway(): Promise<{ port: number, externalToken?: string } | null> {
|
private async findExistingGateway(): Promise<{ port: number, externalToken?: string } | null> {
|
||||||
try {
|
try {
|
||||||
const port = PORTS.OPENCLAW_GATEWAY;
|
const port = PORTS.OPENCLAW_GATEWAY;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Platform-specific command to find processes listening on the gateway port.
|
// Platform-specific command to find processes listening on the gateway port.
|
||||||
// On Windows, lsof doesn't exist; use PowerShell's Get-NetTCPConnection instead.
|
// On Windows, lsof doesn't exist; use PowerShell's Get-NetTCPConnection instead.
|
||||||
@@ -670,46 +670,46 @@ export class GatewayManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}).catch(reject);
|
}).catch(reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (stdout.trim()) {
|
if (stdout.trim()) {
|
||||||
const pids = stdout.trim().split(/\r?\n/)
|
const pids = stdout.trim().split(/\r?\n/)
|
||||||
.map(s => s.trim())
|
.map(s => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
if (pids.length > 0) {
|
if (pids.length > 0) {
|
||||||
if (!this.process || !pids.includes(String(this.process.pid))) {
|
if (!this.process || !pids.includes(String(this.process.pid))) {
|
||||||
logger.info(`Found orphaned process listening on port ${port} (PIDs: ${pids.join(', ')}), attempting to kill...`);
|
logger.info(`Found orphaned process listening on port ${port} (PIDs: ${pids.join(', ')}), attempting to kill...`);
|
||||||
|
|
||||||
// Unload the launchctl service first so macOS doesn't auto-
|
// Unload the launchctl service first so macOS doesn't auto-
|
||||||
// respawn the process we're about to kill.
|
// respawn the process we're about to kill.
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
await this.unloadLaunchctlService();
|
await this.unloadLaunchctlService();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminate orphaned processes
|
// Terminate orphaned processes
|
||||||
for (const pid of pids) {
|
for (const pid of pids) {
|
||||||
try {
|
try {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
// On Windows, use taskkill for reliable process group termination
|
// On Windows, use taskkill for reliable process group termination
|
||||||
import('child_process').then(cp => {
|
import('child_process').then(cp => {
|
||||||
cp.exec(`taskkill /PID ${pid} /T /F`, { timeout: 5000 }, () => {});
|
cp.exec(`taskkill /PID ${pid} /T /F`, { timeout: 5000 }, () => { });
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
} else {
|
} else {
|
||||||
// SIGTERM first so the gateway can clean up its lock file.
|
// SIGTERM first so the gateway can clean up its lock file.
|
||||||
process.kill(parseInt(pid), 'SIGTERM');
|
process.kill(parseInt(pid), 'SIGTERM');
|
||||||
}
|
}
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
await new Promise(r => setTimeout(r, process.platform === 'win32' ? 2000 : 3000));
|
await new Promise(r => setTimeout(r, process.platform === 'win32' ? 2000 : 3000));
|
||||||
|
|
||||||
// SIGKILL any survivors (Unix only — Windows taskkill /F is already forceful)
|
// SIGKILL any survivors (Unix only — Windows taskkill /F is already forceful)
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
for (const pid of pids) {
|
for (const pid of pids) {
|
||||||
try { process.kill(parseInt(pid), 0); process.kill(parseInt(pid), 'SIGKILL'); } catch { /* already exited */ }
|
try { process.kill(parseInt(pid), 0); process.kill(parseInt(pid), 'SIGKILL'); } catch { /* already exited */ }
|
||||||
}
|
}
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -724,13 +724,13 @@ export class GatewayManager extends EventEmitter {
|
|||||||
testWs.close();
|
testWs.close();
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
testWs.on('open', () => {
|
testWs.on('open', () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
testWs.close();
|
testWs.close();
|
||||||
resolve({ port });
|
resolve({ port });
|
||||||
});
|
});
|
||||||
|
|
||||||
testWs.on('error', () => {
|
testWs.on('error', () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
@@ -739,7 +739,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
} catch {
|
} catch {
|
||||||
// Gateway not running
|
// Gateway not running
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,7 +785,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
spawnEnv['OPENCLAW_NO_RESPAWN'] = '1';
|
spawnEnv['OPENCLAW_NO_RESPAWN'] = '1';
|
||||||
const existingNodeOpts = spawnEnv['NODE_OPTIONS'] ?? '';
|
const existingNodeOpts = spawnEnv['NODE_OPTIONS'] ?? '';
|
||||||
if (!existingNodeOpts.includes('--disable-warning=ExperimentalWarning') &&
|
if (!existingNodeOpts.includes('--disable-warning=ExperimentalWarning') &&
|
||||||
!existingNodeOpts.includes('--no-warnings')) {
|
!existingNodeOpts.includes('--no-warnings')) {
|
||||||
spawnEnv['NODE_OPTIONS'] = `${existingNodeOpts} --disable-warning=ExperimentalWarning`.trim();
|
spawnEnv['NODE_OPTIONS'] = `${existingNodeOpts} --disable-warning=ExperimentalWarning`.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -851,7 +851,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start Gateway process
|
* Start Gateway process
|
||||||
* Uses OpenClaw npm package from node_modules (dev) or resources (production)
|
* Uses OpenClaw npm package from node_modules (dev) or resources (production)
|
||||||
@@ -862,14 +862,14 @@ export class GatewayManager extends EventEmitter {
|
|||||||
|
|
||||||
const openclawDir = getOpenClawDir();
|
const openclawDir = getOpenClawDir();
|
||||||
const entryScript = getOpenClawEntryPath();
|
const entryScript = getOpenClawEntryPath();
|
||||||
|
|
||||||
// Verify OpenClaw package exists
|
// Verify OpenClaw package exists
|
||||||
if (!isOpenClawPresent()) {
|
if (!isOpenClawPresent()) {
|
||||||
const errMsg = `OpenClaw package not found at: ${openclawDir}`;
|
const errMsg = `OpenClaw package not found at: ${openclawDir}`;
|
||||||
logger.error(errMsg);
|
logger.error(errMsg);
|
||||||
throw new Error(errMsg);
|
throw new Error(errMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get or generate gateway token
|
// Get or generate gateway token
|
||||||
const gatewayToken = await getSetting('gatewayToken');
|
const gatewayToken = await getSetting('gatewayToken');
|
||||||
|
|
||||||
@@ -900,17 +900,17 @@ export class GatewayManager extends EventEmitter {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('Failed to sync browser config to openclaw.json:', err);
|
logger.warn('Failed to sync browser config to openclaw.json:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
let command: string;
|
let command: string;
|
||||||
let args: string[];
|
let args: string[];
|
||||||
let mode: 'packaged' | 'dev-built' | 'dev-pnpm';
|
let mode: 'packaged' | 'dev-built' | 'dev-pnpm';
|
||||||
|
|
||||||
// Determine the Node.js executable
|
// Determine the Node.js executable
|
||||||
// In packaged Electron app, use process.execPath with ELECTRON_RUN_AS_NODE=1
|
// In packaged Electron app, use process.execPath with ELECTRON_RUN_AS_NODE=1
|
||||||
// which makes the Electron binary behave as plain Node.js.
|
// which makes the Electron binary behave as plain Node.js.
|
||||||
// In development, use system 'node'.
|
// In development, use system 'node'.
|
||||||
const gatewayArgs = ['gateway', '--port', String(this.status.port), '--token', gatewayToken, '--allow-unconfigured'];
|
const gatewayArgs = ['gateway', '--port', String(this.status.port), '--token', gatewayToken, '--allow-unconfigured'];
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
// Production: use Electron binary as Node.js via ELECTRON_RUN_AS_NODE
|
// Production: use Electron binary as Node.js via ELECTRON_RUN_AS_NODE
|
||||||
// On macOS, use the Electron Helper binary to avoid extra dock icons
|
// On macOS, use the Electron Helper binary to avoid extra dock icons
|
||||||
@@ -948,7 +948,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
const finalPath = binPathExists
|
const finalPath = binPathExists
|
||||||
? `${binPath}${path.delimiter}${process.env.PATH || ''}`
|
? `${binPath}${path.delimiter}${process.env.PATH || ''}`
|
||||||
: process.env.PATH || '';
|
: process.env.PATH || '';
|
||||||
|
|
||||||
// Load provider API keys from storage to pass as environment variables
|
// Load provider API keys from storage to pass as environment variables
|
||||||
const providerEnv: Record<string, string> = {};
|
const providerEnv: Record<string, string> = {};
|
||||||
const providerTypes = getKeyableProviderTypes();
|
const providerTypes = getKeyableProviderTypes();
|
||||||
@@ -993,7 +993,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
`Starting Gateway process (mode=${mode}, port=${this.status.port}, command="${command}", args="${this.sanitizeSpawnArgs(args).join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'}, providerKeys=${loadedProviderKeyCount})`
|
`Starting Gateway process (mode=${mode}, port=${this.status.port}, command="${command}", args="${this.sanitizeSpawnArgs(args).join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'}, providerKeys=${loadedProviderKeyCount})`
|
||||||
);
|
);
|
||||||
this.lastSpawnSummary = `mode=${mode}, command="${command}", args="${this.sanitizeSpawnArgs(args).join(' ')}", cwd="${openclawDir}"`;
|
this.lastSpawnSummary = `mode=${mode}, command="${command}", args="${this.sanitizeSpawnArgs(args).join(' ')}", cwd="${openclawDir}"`;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const spawnEnv: Record<string, string | undefined> = {
|
const spawnEnv: Record<string, string | undefined> = {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -1014,7 +1014,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
// Pre-set the NODE_OPTIONS that entry.ts would have added via respawn
|
// Pre-set the NODE_OPTIONS that entry.ts would have added via respawn
|
||||||
const existingNodeOpts = spawnEnv['NODE_OPTIONS'] ?? '';
|
const existingNodeOpts = spawnEnv['NODE_OPTIONS'] ?? '';
|
||||||
if (!existingNodeOpts.includes('--disable-warning=ExperimentalWarning') &&
|
if (!existingNodeOpts.includes('--disable-warning=ExperimentalWarning') &&
|
||||||
!existingNodeOpts.includes('--no-warnings')) {
|
!existingNodeOpts.includes('--no-warnings')) {
|
||||||
spawnEnv['NODE_OPTIONS'] = `${existingNodeOpts} --disable-warning=ExperimentalWarning`.trim();
|
spawnEnv['NODE_OPTIONS'] = `${existingNodeOpts} --disable-warning=ExperimentalWarning`.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1046,13 +1046,13 @@ export class GatewayManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
const child = this.process;
|
const child = this.process;
|
||||||
this.ownsProcess = true;
|
this.ownsProcess = true;
|
||||||
|
|
||||||
child.on('error', (error) => {
|
child.on('error', (error) => {
|
||||||
this.ownsProcess = false;
|
this.ownsProcess = false;
|
||||||
logger.error('Gateway process spawn error:', error);
|
logger.error('Gateway process spawn error:', error);
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('exit', (code, signal) => {
|
child.on('exit', (code, signal) => {
|
||||||
const expectedExit = !this.shouldReconnect || this.status.state === 'stopped';
|
const expectedExit = !this.shouldReconnect || this.status.state === 'stopped';
|
||||||
const level = expectedExit ? logger.info : logger.warn;
|
const level = expectedExit ? logger.info : logger.warn;
|
||||||
@@ -1062,7 +1062,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.process = null;
|
this.process = null;
|
||||||
}
|
}
|
||||||
this.emit('exit', code);
|
this.emit('exit', code);
|
||||||
|
|
||||||
if (this.status.state === 'running') {
|
if (this.status.state === 'running') {
|
||||||
this.setStatus({ state: 'stopped' });
|
this.setStatus({ state: 'stopped' });
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
@@ -1072,7 +1072,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
child.on('close', (code, signal) => {
|
child.on('close', (code, signal) => {
|
||||||
logger.debug(`Gateway process stdio closed (${this.formatExit(code, signal)})`);
|
logger.debug(`Gateway process stdio closed (${this.formatExit(code, signal)})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log stderr
|
// Log stderr
|
||||||
child.stderr?.on('data', (data) => {
|
child.stderr?.on('data', (data) => {
|
||||||
const raw = data.toString();
|
const raw = data.toString();
|
||||||
@@ -1087,7 +1087,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
logger.warn(`[Gateway stderr] ${classified.normalized}`);
|
logger.warn(`[Gateway stderr] ${classified.normalized}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store PID
|
// Store PID
|
||||||
if (child.pid) {
|
if (child.pid) {
|
||||||
logger.info(`Gateway process started (pid=${child.pid})`);
|
logger.info(`Gateway process started (pid=${child.pid})`);
|
||||||
@@ -1095,11 +1095,11 @@ export class GatewayManager extends EventEmitter {
|
|||||||
} else {
|
} else {
|
||||||
logger.warn('Gateway process spawned but PID is undefined');
|
logger.warn('Gateway process spawned but PID is undefined');
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for Gateway to be ready by checking if the port is accepting connections
|
* Wait for Gateway to be ready by checking if the port is accepting connections
|
||||||
*/
|
*/
|
||||||
@@ -1113,7 +1113,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
logger.error(`Gateway process exited before ready (${this.formatExit(code, signal)})`);
|
logger.error(`Gateway process exited before ready (${this.formatExit(code, signal)})`);
|
||||||
throw new Error(`Gateway process exited before becoming ready (${this.formatExit(code, signal)})`);
|
throw new Error(`Gateway process exited before becoming ready (${this.formatExit(code, signal)})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ready = await new Promise<boolean>((resolve) => {
|
const ready = await new Promise<boolean>((resolve) => {
|
||||||
const testWs = new WebSocket(`ws://localhost:${this.status.port}/ws`);
|
const testWs = new WebSocket(`ws://localhost:${this.status.port}/ws`);
|
||||||
@@ -1121,19 +1121,19 @@ export class GatewayManager extends EventEmitter {
|
|||||||
testWs.close();
|
testWs.close();
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
testWs.on('open', () => {
|
testWs.on('open', () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
testWs.close();
|
testWs.close();
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWs.on('error', () => {
|
testWs.on('error', () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ready) {
|
if (ready) {
|
||||||
logger.debug(`Gateway ready after ${i + 1} attempt(s)`);
|
logger.debug(`Gateway ready after ${i + 1} attempt(s)`);
|
||||||
return;
|
return;
|
||||||
@@ -1141,28 +1141,28 @@ export class GatewayManager extends EventEmitter {
|
|||||||
} catch {
|
} catch {
|
||||||
// Gateway not ready yet
|
// Gateway not ready yet
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i > 0 && i % 10 === 0) {
|
if (i > 0 && i % 10 === 0) {
|
||||||
logger.debug(`Still waiting for Gateway... (attempt ${i + 1}/${retries})`);
|
logger.debug(`Still waiting for Gateway... (attempt ${i + 1}/${retries})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(`Gateway failed to become ready after ${retries} attempts on port ${this.status.port}`);
|
logger.error(`Gateway failed to become ready after ${retries} attempts on port ${this.status.port}`);
|
||||||
throw new Error(`Gateway failed to start after ${retries} retries (port ${this.status.port})`);
|
throw new Error(`Gateway failed to start after ${retries} retries (port ${this.status.port})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect WebSocket to Gateway
|
* Connect WebSocket to Gateway
|
||||||
*/
|
*/
|
||||||
private async connect(port: number, _externalToken?: string): Promise<void> {
|
private async connect(port: number, _externalToken?: string): Promise<void> {
|
||||||
logger.debug(`Connecting Gateway WebSocket (ws://localhost:${port}/ws)`);
|
logger.debug(`Connecting Gateway WebSocket (ws://localhost:${port}/ws)`);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// WebSocket URL (token will be sent in connect handshake, not URL)
|
// WebSocket URL (token will be sent in connect handshake, not URL)
|
||||||
const wsUrl = `ws://localhost:${port}/ws`;
|
const wsUrl = `ws://localhost:${port}/ws`;
|
||||||
|
|
||||||
this.ws = new WebSocket(wsUrl);
|
this.ws = new WebSocket(wsUrl);
|
||||||
let handshakeComplete = false;
|
let handshakeComplete = false;
|
||||||
let connectId: string | null = null;
|
let connectId: string | null = null;
|
||||||
@@ -1203,7 +1203,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
const err = error instanceof Error ? error : new Error(String(error));
|
const err = error instanceof Error ? error : new Error(String(error));
|
||||||
reject(err);
|
reject(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sends the connect frame using the server-issued challenge nonce.
|
// Sends the connect frame using the server-issued challenge nonce.
|
||||||
const sendConnectHandshake = async (challengeNonce: string) => {
|
const sendConnectHandshake = async (challengeNonce: string) => {
|
||||||
logger.debug('Sending connect handshake with challenge nonce');
|
logger.debug('Sending connect handshake with challenge nonce');
|
||||||
@@ -1274,7 +1274,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
handshakeTimeout = requestTimeout;
|
handshakeTimeout = requestTimeout;
|
||||||
|
|
||||||
this.pendingRequests.set(connectId, {
|
this.pendingRequests.set(connectId, {
|
||||||
resolve: (_result) => {
|
resolve: (_result) => {
|
||||||
handshakeComplete = true;
|
handshakeComplete = true;
|
||||||
@@ -1309,7 +1309,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.ws.on('open', () => {
|
this.ws.on('open', () => {
|
||||||
logger.debug('Gateway WebSocket opened, waiting for connect.challenge...');
|
logger.debug('Gateway WebSocket opened, waiting for connect.challenge...');
|
||||||
});
|
});
|
||||||
|
|
||||||
let challengeReceived = false;
|
let challengeReceived = false;
|
||||||
|
|
||||||
this.ws.on('message', (data) => {
|
this.ws.on('message', (data) => {
|
||||||
@@ -1342,11 +1342,13 @@ export class GatewayManager extends EventEmitter {
|
|||||||
logger.debug('Failed to parse Gateway WebSocket message:', error);
|
logger.debug('Failed to parse Gateway WebSocket message:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ws.on('close', (code, reason) => {
|
this.ws.on('close', (code, reason) => {
|
||||||
const reasonStr = reason?.toString() || 'unknown';
|
const reasonStr = reason?.toString() || 'unknown';
|
||||||
logger.warn(`Gateway WebSocket closed (code=${code}, reason=${reasonStr}, handshake=${handshakeComplete ? 'ok' : 'pending'})`);
|
logger.warn(`Gateway WebSocket closed (code=${code}, reason=${reasonStr}, handshake=${handshakeComplete ? 'ok' : 'pending'})`);
|
||||||
if (!handshakeComplete) {
|
if (!handshakeComplete) {
|
||||||
|
// If the socket closes before the handshake completes, it usually means the server is still starting or restarting.
|
||||||
|
// Rejecting this promise will cause the caller (startProcess/reconnect logic) to retry cleanly.
|
||||||
rejectOnce(new Error(`WebSocket closed before handshake: ${reasonStr}`));
|
rejectOnce(new Error(`WebSocket closed before handshake: ${reasonStr}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1356,16 +1358,21 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ws.on('error', (error) => {
|
this.ws.on('error', (error) => {
|
||||||
logger.error('Gateway WebSocket error:', error);
|
// Suppress noisy ECONNREFUSED/WebSocket handshake errors that happen during expected Gateway restarts.
|
||||||
|
if (error.message?.includes('closed before handshake') || (error as NodeJS.ErrnoException).code === 'ECONNREFUSED') {
|
||||||
|
logger.debug(`Gateway WebSocket connection error (transient): ${error.message}`);
|
||||||
|
} else {
|
||||||
|
logger.error('Gateway WebSocket error:', error);
|
||||||
|
}
|
||||||
if (!handshakeComplete) {
|
if (!handshakeComplete) {
|
||||||
rejectOnce(error);
|
rejectOnce(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle incoming WebSocket message
|
* Handle incoming WebSocket message
|
||||||
*/
|
*/
|
||||||
@@ -1374,16 +1381,16 @@ export class GatewayManager extends EventEmitter {
|
|||||||
logger.debug('Received non-object Gateway message');
|
logger.debug('Received non-object Gateway message');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = message as Record<string, unknown>;
|
const msg = message as Record<string, unknown>;
|
||||||
|
|
||||||
// Handle OpenClaw protocol response format: { type: "res", id: "...", ok: true/false, ... }
|
// Handle OpenClaw protocol response format: { type: "res", id: "...", ok: true/false, ... }
|
||||||
if (msg.type === 'res' && typeof msg.id === 'string') {
|
if (msg.type === 'res' && typeof msg.id === 'string') {
|
||||||
if (this.pendingRequests.has(msg.id)) {
|
if (this.pendingRequests.has(msg.id)) {
|
||||||
const request = this.pendingRequests.get(msg.id)!;
|
const request = this.pendingRequests.get(msg.id)!;
|
||||||
clearTimeout(request.timeout);
|
clearTimeout(request.timeout);
|
||||||
this.pendingRequests.delete(msg.id);
|
this.pendingRequests.delete(msg.id);
|
||||||
|
|
||||||
if (msg.ok === false || msg.error) {
|
if (msg.ok === false || msg.error) {
|
||||||
const errorObj = msg.error as { message?: string; code?: number } | undefined;
|
const errorObj = msg.error as { message?: string; code?: number } | undefined;
|
||||||
const errorMsg = errorObj?.message || JSON.stringify(msg.error) || 'Unknown error';
|
const errorMsg = errorObj?.message || JSON.stringify(msg.error) || 'Unknown error';
|
||||||
@@ -1394,21 +1401,21 @@ export class GatewayManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle OpenClaw protocol event format: { type: "event", event: "...", payload: {...} }
|
// Handle OpenClaw protocol event format: { type: "event", event: "...", payload: {...} }
|
||||||
if (msg.type === 'event' && typeof msg.event === 'string') {
|
if (msg.type === 'event' && typeof msg.event === 'string') {
|
||||||
this.handleProtocolEvent(msg.event, msg.payload);
|
this.handleProtocolEvent(msg.event, msg.payload);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Check if this is a JSON-RPC 2.0 response (legacy support)
|
// Fallback: Check if this is a JSON-RPC 2.0 response (legacy support)
|
||||||
if (isResponse(message) && message.id && this.pendingRequests.has(String(message.id))) {
|
if (isResponse(message) && message.id && this.pendingRequests.has(String(message.id))) {
|
||||||
const request = this.pendingRequests.get(String(message.id))!;
|
const request = this.pendingRequests.get(String(message.id))!;
|
||||||
clearTimeout(request.timeout);
|
clearTimeout(request.timeout);
|
||||||
this.pendingRequests.delete(String(message.id));
|
this.pendingRequests.delete(String(message.id));
|
||||||
|
|
||||||
if (message.error) {
|
if (message.error) {
|
||||||
const errorMsg = typeof message.error === 'object'
|
const errorMsg = typeof message.error === 'object'
|
||||||
? (message.error as { message?: string }).message || JSON.stringify(message.error)
|
? (message.error as { message?: string }).message || JSON.stringify(message.error)
|
||||||
: String(message.error);
|
: String(message.error);
|
||||||
request.reject(new Error(errorMsg));
|
request.reject(new Error(errorMsg));
|
||||||
@@ -1417,16 +1424,16 @@ export class GatewayManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a JSON-RPC notification (server-initiated event)
|
// Check if this is a JSON-RPC notification (server-initiated event)
|
||||||
if (isNotification(message)) {
|
if (isNotification(message)) {
|
||||||
this.handleNotification(message);
|
this.handleNotification(message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('message', message);
|
this.emit('message', message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle OpenClaw protocol events
|
* Handle OpenClaw protocol events
|
||||||
*/
|
*/
|
||||||
@@ -1462,35 +1469,35 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.emit('notification', { method: event, params: payload });
|
this.emit('notification', { method: event, params: payload });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle server-initiated notifications
|
* Handle server-initiated notifications
|
||||||
*/
|
*/
|
||||||
private handleNotification(notification: JsonRpcNotification): void {
|
private handleNotification(notification: JsonRpcNotification): void {
|
||||||
this.emit('notification', notification);
|
this.emit('notification', notification);
|
||||||
|
|
||||||
// Route specific events
|
// Route specific events
|
||||||
switch (notification.method) {
|
switch (notification.method) {
|
||||||
case GatewayEventType.CHANNEL_STATUS_CHANGED:
|
case GatewayEventType.CHANNEL_STATUS_CHANGED:
|
||||||
this.emit('channel:status', notification.params as { channelId: string; status: string });
|
this.emit('channel:status', notification.params as { channelId: string; status: string });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GatewayEventType.MESSAGE_RECEIVED:
|
case GatewayEventType.MESSAGE_RECEIVED:
|
||||||
this.emit('chat:message', notification.params as { message: unknown });
|
this.emit('chat:message', notification.params as { message: unknown });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GatewayEventType.ERROR: {
|
case GatewayEventType.ERROR: {
|
||||||
const errorData = notification.params as { message?: string };
|
const errorData = notification.params as { message?: string };
|
||||||
this.emit('error', new Error(errorData.message || 'Gateway error'));
|
this.emit('error', new Error(errorData.message || 'Gateway error'));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Unknown notification type, just log it
|
// Unknown notification type, just log it
|
||||||
logger.debug(`Unknown Gateway notification: ${notification.method}`);
|
logger.debug(`Unknown Gateway notification: ${notification.method}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start ping interval to keep connection alive
|
* Start ping interval to keep connection alive
|
||||||
*/
|
*/
|
||||||
@@ -1498,14 +1505,14 @@ export class GatewayManager extends EventEmitter {
|
|||||||
if (this.pingInterval) {
|
if (this.pingInterval) {
|
||||||
clearInterval(this.pingInterval);
|
clearInterval(this.pingInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pingInterval = setInterval(() => {
|
this.pingInterval = setInterval(() => {
|
||||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||||
this.ws.ping();
|
this.ws.ping();
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule reconnection attempt with exponential backoff
|
* Schedule reconnection attempt with exponential backoff
|
||||||
*/
|
*/
|
||||||
@@ -1514,35 +1521,35 @@ export class GatewayManager extends EventEmitter {
|
|||||||
logger.debug('Gateway reconnect skipped (auto-reconnect disabled)');
|
logger.debug('Gateway reconnect skipped (auto-reconnect disabled)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.reconnectTimer) {
|
if (this.reconnectTimer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.reconnectAttempts >= this.reconnectConfig.maxAttempts) {
|
if (this.reconnectAttempts >= this.reconnectConfig.maxAttempts) {
|
||||||
logger.error(`Gateway reconnect failed: max attempts reached (${this.reconnectConfig.maxAttempts})`);
|
logger.error(`Gateway reconnect failed: max attempts reached (${this.reconnectConfig.maxAttempts})`);
|
||||||
this.setStatus({
|
this.setStatus({
|
||||||
state: 'error',
|
state: 'error',
|
||||||
error: 'Failed to reconnect after maximum attempts',
|
error: 'Failed to reconnect after maximum attempts',
|
||||||
reconnectAttempts: this.reconnectAttempts
|
reconnectAttempts: this.reconnectAttempts
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate delay with exponential backoff
|
// Calculate delay with exponential backoff
|
||||||
const delay = Math.min(
|
const delay = Math.min(
|
||||||
this.reconnectConfig.baseDelay * Math.pow(2, this.reconnectAttempts),
|
this.reconnectConfig.baseDelay * Math.pow(2, this.reconnectAttempts),
|
||||||
this.reconnectConfig.maxDelay
|
this.reconnectConfig.maxDelay
|
||||||
);
|
);
|
||||||
|
|
||||||
this.reconnectAttempts++;
|
this.reconnectAttempts++;
|
||||||
logger.warn(`Scheduling Gateway reconnect attempt ${this.reconnectAttempts}/${this.reconnectConfig.maxAttempts} in ${delay}ms`);
|
logger.warn(`Scheduling Gateway reconnect attempt ${this.reconnectAttempts}/${this.reconnectConfig.maxAttempts} in ${delay}ms`);
|
||||||
|
|
||||||
this.setStatus({
|
this.setStatus({
|
||||||
state: 'reconnecting',
|
state: 'reconnecting',
|
||||||
reconnectAttempts: this.reconnectAttempts
|
reconnectAttempts: this.reconnectAttempts
|
||||||
});
|
});
|
||||||
|
|
||||||
this.reconnectTimer = setTimeout(async () => {
|
this.reconnectTimer = setTimeout(async () => {
|
||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null;
|
||||||
try {
|
try {
|
||||||
@@ -1556,7 +1563,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
this.startHealthCheck();
|
this.startHealthCheck();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise restart the process
|
// Otherwise restart the process
|
||||||
await this.startProcess();
|
await this.startProcess();
|
||||||
await this.waitForReady();
|
await this.waitForReady();
|
||||||
@@ -1569,21 +1576,21 @@ export class GatewayManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update status and emit event
|
* Update status and emit event
|
||||||
*/
|
*/
|
||||||
private setStatus(update: Partial<GatewayStatus>): void {
|
private setStatus(update: Partial<GatewayStatus>): void {
|
||||||
const previousState = this.status.state;
|
const previousState = this.status.state;
|
||||||
this.status = { ...this.status, ...update };
|
this.status = { ...this.status, ...update };
|
||||||
|
|
||||||
// Calculate uptime if connected
|
// Calculate uptime if connected
|
||||||
if (this.status.state === 'running' && this.status.connectedAt) {
|
if (this.status.state === 'running' && this.status.connectedAt) {
|
||||||
this.status.uptime = Date.now() - this.status.connectedAt;
|
this.status.uptime = Date.now() - this.status.connectedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('status', this.status);
|
this.emit('status', this.status);
|
||||||
|
|
||||||
// Log state transitions
|
// Log state transitions
|
||||||
if (previousState !== this.status.state) {
|
if (previousState !== this.status.state) {
|
||||||
logger.debug(`Gateway state changed: ${previousState} -> ${this.status.state}`);
|
logger.debug(`Gateway state changed: ${previousState} -> ${this.status.state}`);
|
||||||
|
|||||||
@@ -516,7 +516,14 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
|
|||||||
// If we call add() here with undefined baseUrl, it will overwrite and erase it!
|
// If we call add() here with undefined baseUrl, it will overwrite and erase it!
|
||||||
// So we just fetch the latest list from the backend to update the UI.
|
// So we just fetch the latest list from the backend to update the UI.
|
||||||
try {
|
try {
|
||||||
await useProviderStore.getState().fetchProviders();
|
const store = useProviderStore.getState();
|
||||||
|
await store.fetchProviders();
|
||||||
|
|
||||||
|
// Auto-set as default if no default is currently configured
|
||||||
|
if (!store.defaultProviderId && latestRef.current.selectedType) {
|
||||||
|
// Provider type is expected to match provider ID for built-in OAuth providers
|
||||||
|
await store.setDefaultProvider(latestRef.current.selectedType);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to refresh providers after OAuth:', err);
|
console.error('Failed to refresh providers after OAuth:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user