feat(gateway): enhance gateway process management with auto-reconnection
Improve Gateway lifecycle management with the following features: - Add exponential backoff reconnection (1s-30s delay, max 10 attempts) - Add health check monitoring every 30 seconds - Add proper restart method with graceful shutdown - Handle server-initiated notifications (channel status, chat messages) - Add 'reconnecting' state for better UI feedback - Enhance IPC handlers with isConnected and health check endpoints - Update preload script with new event channels - Improve type safety and error handling throughout Also fixes several TypeScript errors and unused variable warnings.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
* Gateway WebSocket Client
|
* Gateway WebSocket Client
|
||||||
* Provides a typed interface for Gateway RPC calls
|
* Provides a typed interface for Gateway RPC calls
|
||||||
*/
|
*/
|
||||||
import { GatewayManager } from './manager';
|
import { GatewayManager, GatewayStatus } from './manager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Channel types supported by OpenClaw
|
* Channel types supported by OpenClaw
|
||||||
@@ -19,6 +19,7 @@ export interface Channel {
|
|||||||
status: 'connected' | 'disconnected' | 'connecting' | 'error';
|
status: 'connected' | 'disconnected' | 'connecting' | 'error';
|
||||||
lastActivity?: string;
|
lastActivity?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
config?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,6 +33,20 @@ export interface Skill {
|
|||||||
category?: string;
|
category?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
configurable?: boolean;
|
configurable?: boolean;
|
||||||
|
version?: string;
|
||||||
|
author?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skill bundle definition
|
||||||
|
*/
|
||||||
|
export interface SkillBundle {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
skills: string[];
|
||||||
|
icon?: string;
|
||||||
|
recommended?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +59,7 @@ export interface ChatMessage {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
toolCalls?: ToolCall[];
|
toolCalls?: ToolCall[];
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,6 +71,35 @@ export interface ToolCall {
|
|||||||
arguments: Record<string, unknown>;
|
arguments: Record<string, unknown>;
|
||||||
result?: unknown;
|
result?: unknown;
|
||||||
status: 'pending' | 'running' | 'completed' | 'error';
|
status: 'pending' | 'running' | 'completed' | 'error';
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cron task definition
|
||||||
|
*/
|
||||||
|
export interface CronTask {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
schedule: string;
|
||||||
|
command: string;
|
||||||
|
enabled: boolean;
|
||||||
|
lastRun?: string;
|
||||||
|
nextRun?: string;
|
||||||
|
status: 'idle' | 'running' | 'error';
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider configuration
|
||||||
|
*/
|
||||||
|
export interface ProviderConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: 'openai' | 'anthropic' | 'ollama' | 'custom';
|
||||||
|
apiKey?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
model?: string;
|
||||||
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,6 +109,20 @@ export interface ToolCall {
|
|||||||
export class GatewayClient {
|
export class GatewayClient {
|
||||||
constructor(private manager: GatewayManager) {}
|
constructor(private manager: GatewayManager) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current gateway status
|
||||||
|
*/
|
||||||
|
getStatus(): GatewayStatus {
|
||||||
|
return this.manager.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if gateway is connected
|
||||||
|
*/
|
||||||
|
isConnected(): boolean {
|
||||||
|
return this.manager.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Channel Methods ====================
|
// ==================== Channel Methods ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,13 +220,80 @@ export class GatewayClient {
|
|||||||
return this.manager.rpc<void>('chat.clear');
|
return this.manager.rpc<void>('chat.clear');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Cron Methods ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all cron tasks
|
||||||
|
*/
|
||||||
|
async listCronTasks(): Promise<CronTask[]> {
|
||||||
|
return this.manager.rpc<CronTask[]>('cron.list');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new cron task
|
||||||
|
*/
|
||||||
|
async createCronTask(task: Omit<CronTask, 'id' | 'status'>): Promise<CronTask> {
|
||||||
|
return this.manager.rpc<CronTask>('cron.create', task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a cron task
|
||||||
|
*/
|
||||||
|
async updateCronTask(taskId: string, updates: Partial<CronTask>): Promise<CronTask> {
|
||||||
|
return this.manager.rpc<CronTask>('cron.update', { taskId, ...updates });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a cron task
|
||||||
|
*/
|
||||||
|
async deleteCronTask(taskId: string): Promise<void> {
|
||||||
|
return this.manager.rpc<void>('cron.delete', { taskId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a cron task immediately
|
||||||
|
*/
|
||||||
|
async runCronTask(taskId: string): Promise<void> {
|
||||||
|
return this.manager.rpc<void>('cron.run', { taskId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Provider Methods ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List configured AI providers
|
||||||
|
*/
|
||||||
|
async listProviders(): Promise<ProviderConfig[]> {
|
||||||
|
return this.manager.rpc<ProviderConfig[]>('providers.list');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or update a provider
|
||||||
|
*/
|
||||||
|
async setProvider(provider: ProviderConfig): Promise<void> {
|
||||||
|
return this.manager.rpc<void>('providers.set', provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a provider
|
||||||
|
*/
|
||||||
|
async removeProvider(providerId: string): Promise<void> {
|
||||||
|
return this.manager.rpc<void>('providers.remove', { providerId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test provider connection
|
||||||
|
*/
|
||||||
|
async testProvider(providerId: string): Promise<{ success: boolean; error?: string }> {
|
||||||
|
return this.manager.rpc<{ success: boolean; error?: string }>('providers.test', { providerId });
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== System Methods ====================
|
// ==================== System Methods ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Gateway health status
|
* Get Gateway health status
|
||||||
*/
|
*/
|
||||||
async getHealth(): Promise<{ status: string; uptime: number }> {
|
async getHealth(): Promise<{ status: string; uptime: number; version?: string }> {
|
||||||
return this.manager.rpc<{ status: string; uptime: number }>('system.health');
|
return this.manager.rpc<{ status: string; uptime: number; version?: string }>('system.health');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,4 +309,25 @@ export class GatewayClient {
|
|||||||
async updateConfig(config: Record<string, unknown>): Promise<void> {
|
async updateConfig(config: Record<string, unknown>): Promise<void> {
|
||||||
return this.manager.rpc<void>('system.updateConfig', config);
|
return this.manager.rpc<void>('system.updateConfig', config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Gateway version info
|
||||||
|
*/
|
||||||
|
async getVersion(): Promise<{ version: string; nodeVersion?: string; platform?: string }> {
|
||||||
|
return this.manager.rpc<{ version: string; nodeVersion?: string; platform?: string }>('system.version');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available skill bundles
|
||||||
|
*/
|
||||||
|
async getSkillBundles(): Promise<SkillBundle[]> {
|
||||||
|
return this.manager.rpc<SkillBundle[]>('skills.bundles');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install a skill bundle
|
||||||
|
*/
|
||||||
|
async installBundle(bundleId: string): Promise<void> {
|
||||||
|
return this.manager.rpc<void>('skills.installBundle', { bundleId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,24 @@
|
|||||||
* Gateway Process Manager
|
* Gateway Process Manager
|
||||||
* Manages the OpenClaw Gateway process lifecycle
|
* Manages the OpenClaw Gateway process lifecycle
|
||||||
*/
|
*/
|
||||||
import { spawn, ChildProcess, exec } from 'child_process';
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import { promisify } from 'util';
|
|
||||||
import { PORTS } from '../utils/config';
|
import { PORTS } from '../utils/config';
|
||||||
|
import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol';
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gateway connection status
|
* Gateway connection status
|
||||||
*/
|
*/
|
||||||
export interface GatewayStatus {
|
export interface GatewayStatus {
|
||||||
state: 'stopped' | 'starting' | 'running' | 'error';
|
state: 'stopped' | 'starting' | 'running' | 'error' | 'reconnecting';
|
||||||
port: number;
|
port: number;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
uptime?: number;
|
uptime?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
connectedAt?: number;
|
connectedAt?: number;
|
||||||
|
version?: string;
|
||||||
|
reconnectAttempts?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,10 +28,28 @@ export interface GatewayStatus {
|
|||||||
export interface GatewayManagerEvents {
|
export interface GatewayManagerEvents {
|
||||||
status: (status: GatewayStatus) => void;
|
status: (status: GatewayStatus) => void;
|
||||||
message: (message: unknown) => void;
|
message: (message: unknown) => void;
|
||||||
|
notification: (notification: JsonRpcNotification) => void;
|
||||||
exit: (code: number | null) => void;
|
exit: (code: number | null) => void;
|
||||||
error: (error: Error) => void;
|
error: (error: Error) => void;
|
||||||
|
'channel:status': (data: { channelId: string; status: string }) => void;
|
||||||
|
'chat:message': (data: { message: unknown }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconnection configuration
|
||||||
|
*/
|
||||||
|
interface ReconnectConfig {
|
||||||
|
maxAttempts: number;
|
||||||
|
baseDelay: number;
|
||||||
|
maxDelay: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_RECONNECT_CONFIG: ReconnectConfig = {
|
||||||
|
maxAttempts: 10,
|
||||||
|
baseDelay: 1000,
|
||||||
|
maxDelay: 30000,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gateway Manager
|
* Gateway Manager
|
||||||
* Handles starting, stopping, and communicating with the OpenClaw Gateway
|
* Handles starting, stopping, and communicating with the OpenClaw Gateway
|
||||||
@@ -42,14 +60,19 @@ export class GatewayManager extends EventEmitter {
|
|||||||
private status: GatewayStatus = { state: 'stopped', port: PORTS.OPENCLAW_GATEWAY };
|
private status: GatewayStatus = { state: 'stopped', port: PORTS.OPENCLAW_GATEWAY };
|
||||||
private reconnectTimer: NodeJS.Timeout | null = null;
|
private reconnectTimer: NodeJS.Timeout | null = null;
|
||||||
private pingInterval: NodeJS.Timeout | null = null;
|
private pingInterval: NodeJS.Timeout | null = null;
|
||||||
|
private healthCheckInterval: NodeJS.Timeout | null = null;
|
||||||
|
private reconnectAttempts = 0;
|
||||||
|
private reconnectConfig: ReconnectConfig;
|
||||||
|
private shouldReconnect = true;
|
||||||
private pendingRequests: Map<string, {
|
private pendingRequests: Map<string, {
|
||||||
resolve: (value: unknown) => void;
|
resolve: (value: unknown) => void;
|
||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
timeout: NodeJS.Timeout;
|
timeout: NodeJS.Timeout;
|
||||||
}> = new Map();
|
}> = new Map();
|
||||||
|
|
||||||
constructor() {
|
constructor(config?: Partial<ReconnectConfig>) {
|
||||||
super();
|
super();
|
||||||
|
this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...config };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,6 +82,13 @@ export class GatewayManager extends EventEmitter {
|
|||||||
return { ...this.status };
|
return { ...this.status };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Gateway is connected and ready
|
||||||
|
*/
|
||||||
|
isConnected(): boolean {
|
||||||
|
return this.status.state === 'running' && this.ws?.readyState === WebSocket.OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start Gateway process
|
* Start Gateway process
|
||||||
*/
|
*/
|
||||||
@@ -67,7 +97,9 @@ export class GatewayManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setStatus({ state: 'starting' });
|
this.shouldReconnect = true;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.setStatus({ state: 'starting', reconnectAttempts: 0 });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if Gateway is already running
|
// Check if Gateway is already running
|
||||||
@@ -75,6 +107,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
console.log('Found existing Gateway on port', existing.port);
|
console.log('Found existing Gateway on port', existing.port);
|
||||||
await this.connect(existing.port);
|
await this.connect(existing.port);
|
||||||
|
this.startHealthCheck();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +120,9 @@ export class GatewayManager extends EventEmitter {
|
|||||||
// Connect WebSocket
|
// Connect WebSocket
|
||||||
await this.connect(this.status.port);
|
await this.connect(this.status.port);
|
||||||
|
|
||||||
|
// Start health monitoring
|
||||||
|
this.startHealthCheck();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setStatus({ state: 'error', error: String(error) });
|
this.setStatus({ state: 'error', error: String(error) });
|
||||||
throw error;
|
throw error;
|
||||||
@@ -97,7 +133,56 @@ export class GatewayManager extends EventEmitter {
|
|||||||
* Stop Gateway process
|
* Stop Gateway process
|
||||||
*/
|
*/
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
// Clear timers
|
// Disable auto-reconnect
|
||||||
|
this.shouldReconnect = false;
|
||||||
|
|
||||||
|
// Clear all timers
|
||||||
|
this.clearAllTimers();
|
||||||
|
|
||||||
|
// Close WebSocket
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close(1000, 'Gateway stopped by user');
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill process
|
||||||
|
if (this.process) {
|
||||||
|
this.process.kill('SIGTERM');
|
||||||
|
// Force kill after timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.process) {
|
||||||
|
this.process.kill('SIGKILL');
|
||||||
|
this.process = null;
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
this.process = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject all pending requests
|
||||||
|
for (const [, request] of this.pendingRequests) {
|
||||||
|
clearTimeout(request.timeout);
|
||||||
|
request.reject(new Error('Gateway stopped'));
|
||||||
|
}
|
||||||
|
this.pendingRequests.clear();
|
||||||
|
|
||||||
|
this.setStatus({ state: 'stopped', error: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart Gateway process
|
||||||
|
*/
|
||||||
|
async restart(): Promise<void> {
|
||||||
|
console.log('Restarting Gateway...');
|
||||||
|
await this.stop();
|
||||||
|
// Brief delay before restart
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
await this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all active timers
|
||||||
|
*/
|
||||||
|
private clearAllTimers(): void {
|
||||||
if (this.reconnectTimer) {
|
if (this.reconnectTimer) {
|
||||||
clearTimeout(this.reconnectTimer);
|
clearTimeout(this.reconnectTimer);
|
||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null;
|
||||||
@@ -106,33 +191,16 @@ export class GatewayManager extends EventEmitter {
|
|||||||
clearInterval(this.pingInterval);
|
clearInterval(this.pingInterval);
|
||||||
this.pingInterval = null;
|
this.pingInterval = null;
|
||||||
}
|
}
|
||||||
|
if (this.healthCheckInterval) {
|
||||||
// Close WebSocket
|
clearInterval(this.healthCheckInterval);
|
||||||
if (this.ws) {
|
this.healthCheckInterval = null;
|
||||||
this.ws.close();
|
|
||||||
this.ws = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kill process
|
|
||||||
if (this.process) {
|
|
||||||
this.process.kill();
|
|
||||||
this.process = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject all pending requests
|
|
||||||
for (const [id, request] of this.pendingRequests) {
|
|
||||||
clearTimeout(request.timeout);
|
|
||||||
request.reject(new Error('Gateway stopped'));
|
|
||||||
}
|
|
||||||
this.pendingRequests.clear();
|
|
||||||
|
|
||||||
this.setStatus({ state: 'stopped' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an RPC call to the Gateway
|
* Make an RPC call to the Gateway
|
||||||
*/
|
*/
|
||||||
async rpc<T>(method: string, params?: unknown): Promise<T> {
|
async rpc<T>(method: string, params?: unknown, timeoutMs = 30000): Promise<T> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
reject(new Error('Gateway not connected'));
|
reject(new Error('Gateway not connected'));
|
||||||
@@ -145,7 +213,7 @@ export class GatewayManager extends EventEmitter {
|
|||||||
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}`));
|
||||||
}, 30000);
|
}, timeoutMs);
|
||||||
|
|
||||||
// Store pending request
|
// Store pending request
|
||||||
this.pendingRequests.set(id, {
|
this.pendingRequests.set(id, {
|
||||||
@@ -162,10 +230,61 @@ export class GatewayManager extends EventEmitter {
|
|||||||
params,
|
params,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.ws.send(JSON.stringify(request));
|
try {
|
||||||
|
this.ws.send(JSON.stringify(request));
|
||||||
|
} catch (error) {
|
||||||
|
this.pendingRequests.delete(id);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(new Error(`Failed to send RPC request: ${error}`));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start health check monitoring
|
||||||
|
*/
|
||||||
|
private startHealthCheck(): void {
|
||||||
|
if (this.healthCheckInterval) {
|
||||||
|
clearInterval(this.healthCheckInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.healthCheckInterval = setInterval(async () => {
|
||||||
|
if (this.status.state !== 'running') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const health = await this.checkHealth();
|
||||||
|
if (!health.ok) {
|
||||||
|
console.warn('Gateway health check failed:', health.error);
|
||||||
|
this.emit('error', new Error(health.error || 'Health check failed'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Health check error:', error);
|
||||||
|
}
|
||||||
|
}, 30000); // Check every 30 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check Gateway health via HTTP endpoint
|
||||||
|
*/
|
||||||
|
async checkHealth(): Promise<{ ok: boolean; error?: string; uptime?: number }> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost:${this.status.port}/health`, {
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json() as { uptime?: number };
|
||||||
|
return { ok: true, uptime: data.uptime };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false, error: `Health check returned ${response.status}` };
|
||||||
|
} catch (error) {
|
||||||
|
return { ok: false, error: String(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find existing Gateway process
|
* Find existing Gateway process
|
||||||
*/
|
*/
|
||||||
@@ -307,25 +426,61 @@ export class GatewayManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Handle incoming WebSocket message
|
* Handle incoming WebSocket message
|
||||||
*/
|
*/
|
||||||
private handleMessage(message: { id?: string; result?: unknown; error?: unknown }): void {
|
private handleMessage(message: unknown): void {
|
||||||
// Check if this is a response to a pending request
|
// Check if this is a JSON-RPC response
|
||||||
if (message.id && this.pendingRequests.has(message.id)) {
|
if (isResponse(message) && message.id && this.pendingRequests.has(String(message.id))) {
|
||||||
const request = this.pendingRequests.get(message.id)!;
|
const request = this.pendingRequests.get(String(message.id))!;
|
||||||
clearTimeout(request.timeout);
|
clearTimeout(request.timeout);
|
||||||
this.pendingRequests.delete(message.id);
|
this.pendingRequests.delete(String(message.id));
|
||||||
|
|
||||||
if (message.error) {
|
if (message.error) {
|
||||||
request.reject(new Error(String(message.error)));
|
const errorMsg = typeof message.error === 'object'
|
||||||
|
? (message.error as { message?: string }).message || JSON.stringify(message.error)
|
||||||
|
: String(message.error);
|
||||||
|
request.reject(new Error(errorMsg));
|
||||||
} else {
|
} else {
|
||||||
request.resolve(message.result);
|
request.resolve(message.result);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit message for other handlers
|
// Check if this is a notification (server-initiated event)
|
||||||
|
if (isNotification(message)) {
|
||||||
|
this.handleNotification(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit generic message for other handlers
|
||||||
this.emit('message', message);
|
this.emit('message', message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle server-initiated notifications
|
||||||
|
*/
|
||||||
|
private handleNotification(notification: JsonRpcNotification): void {
|
||||||
|
this.emit('notification', notification);
|
||||||
|
|
||||||
|
// Route specific events
|
||||||
|
switch (notification.method) {
|
||||||
|
case GatewayEventType.CHANNEL_STATUS_CHANGED:
|
||||||
|
this.emit('channel:status', notification.params as { channelId: string; status: string });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GatewayEventType.MESSAGE_RECEIVED:
|
||||||
|
this.emit('chat:message', notification.params as { message: unknown });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GatewayEventType.ERROR:
|
||||||
|
const errorData = notification.params as { message?: string };
|
||||||
|
this.emit('error', new Error(errorData.message || 'Gateway error'));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown notification type, just log it
|
||||||
|
console.log('Unknown Gateway notification:', notification.method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start ping interval to keep connection alive
|
* Start ping interval to keep connection alive
|
||||||
*/
|
*/
|
||||||
@@ -342,29 +497,84 @@ export class GatewayManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule reconnection attempt
|
* Schedule reconnection attempt with exponential backoff
|
||||||
*/
|
*/
|
||||||
private scheduleReconnect(): void {
|
private scheduleReconnect(): void {
|
||||||
|
if (!this.shouldReconnect) {
|
||||||
|
console.log('Auto-reconnect disabled, not scheduling reconnect');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.reconnectTimer) {
|
if (this.reconnectTimer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.reconnectAttempts >= this.reconnectConfig.maxAttempts) {
|
||||||
|
console.error(`Max reconnection attempts (${this.reconnectConfig.maxAttempts}) reached`);
|
||||||
|
this.setStatus({
|
||||||
|
state: 'error',
|
||||||
|
error: 'Failed to reconnect after maximum attempts',
|
||||||
|
reconnectAttempts: this.reconnectAttempts
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate delay with exponential backoff
|
||||||
|
const delay = Math.min(
|
||||||
|
this.reconnectConfig.baseDelay * Math.pow(2, this.reconnectAttempts),
|
||||||
|
this.reconnectConfig.maxDelay
|
||||||
|
);
|
||||||
|
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
console.log(`Scheduling reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
|
||||||
|
|
||||||
|
this.setStatus({
|
||||||
|
state: 'reconnecting',
|
||||||
|
reconnectAttempts: this.reconnectAttempts
|
||||||
|
});
|
||||||
|
|
||||||
this.reconnectTimer = setTimeout(async () => {
|
this.reconnectTimer = setTimeout(async () => {
|
||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null;
|
||||||
try {
|
try {
|
||||||
await this.start();
|
// Try to find existing Gateway first
|
||||||
|
const existing = await this.findExistingGateway();
|
||||||
|
if (existing) {
|
||||||
|
await this.connect(existing.port);
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.startHealthCheck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise restart the process
|
||||||
|
await this.startProcess();
|
||||||
|
await this.waitForReady();
|
||||||
|
await this.connect(this.status.port);
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.startHealthCheck();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Reconnection failed:', error);
|
console.error('Reconnection failed:', error);
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 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;
|
||||||
this.status = { ...this.status, ...update };
|
this.status = { ...this.status, ...update };
|
||||||
|
|
||||||
|
// Calculate uptime if connected
|
||||||
|
if (this.status.state === 'running' && this.status.connectedAt) {
|
||||||
|
this.status.uptime = Date.now() - this.status.connectedAt;
|
||||||
|
}
|
||||||
|
|
||||||
this.emit('status', this.status);
|
this.emit('status', this.status);
|
||||||
|
|
||||||
|
// Log state transitions
|
||||||
|
if (previousState !== this.status.state) {
|
||||||
|
console.log(`Gateway state: ${previousState} -> ${this.status.state}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ function registerGatewayHandlers(
|
|||||||
return gatewayManager.getStatus();
|
return gatewayManager.getStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if Gateway is connected
|
||||||
|
ipcMain.handle('gateway:isConnected', () => {
|
||||||
|
return gatewayManager.isConnected();
|
||||||
|
});
|
||||||
|
|
||||||
// Start Gateway
|
// Start Gateway
|
||||||
ipcMain.handle('gateway:start', async () => {
|
ipcMain.handle('gateway:start', async () => {
|
||||||
try {
|
try {
|
||||||
@@ -60,8 +65,7 @@ function registerGatewayHandlers(
|
|||||||
// Restart Gateway
|
// Restart Gateway
|
||||||
ipcMain.handle('gateway:restart', async () => {
|
ipcMain.handle('gateway:restart', async () => {
|
||||||
try {
|
try {
|
||||||
await gatewayManager.stop();
|
await gatewayManager.restart();
|
||||||
await gatewayManager.start();
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: String(error) };
|
return { success: false, error: String(error) };
|
||||||
@@ -69,26 +73,66 @@ function registerGatewayHandlers(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Gateway RPC call
|
// Gateway RPC call
|
||||||
ipcMain.handle('gateway:rpc', async (_, method: string, params?: unknown) => {
|
ipcMain.handle('gateway:rpc', async (_, method: string, params?: unknown, timeoutMs?: number) => {
|
||||||
try {
|
try {
|
||||||
const result = await gatewayManager.rpc(method, params);
|
const result = await gatewayManager.rpc(method, params, timeoutMs);
|
||||||
return { success: true, result };
|
return { success: true, result };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: String(error) };
|
return { success: false, error: String(error) };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Forward Gateway status events to renderer
|
// Health check
|
||||||
|
ipcMain.handle('gateway:health', async () => {
|
||||||
|
try {
|
||||||
|
const health = await gatewayManager.checkHealth();
|
||||||
|
return { success: true, ...health };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, ok: false, error: String(error) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Forward Gateway events to renderer
|
||||||
gatewayManager.on('status', (status) => {
|
gatewayManager.on('status', (status) => {
|
||||||
mainWindow.webContents.send('gateway:status-changed', status);
|
if (!mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('gateway:status-changed', status);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
gatewayManager.on('message', (message) => {
|
gatewayManager.on('message', (message) => {
|
||||||
mainWindow.webContents.send('gateway:message', message);
|
if (!mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('gateway:message', message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gatewayManager.on('notification', (notification) => {
|
||||||
|
if (!mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('gateway:notification', notification);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gatewayManager.on('channel:status', (data) => {
|
||||||
|
if (!mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('gateway:channel-status', data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gatewayManager.on('chat:message', (data) => {
|
||||||
|
if (!mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('gateway:chat-message', data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
gatewayManager.on('exit', (code) => {
|
gatewayManager.on('exit', (code) => {
|
||||||
mainWindow.webContents.send('gateway:exit', code);
|
if (!mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('gateway:exit', code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gatewayManager.on('error', (error) => {
|
||||||
|
if (!mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('gateway:error', error.message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ const electronAPI = {
|
|||||||
const validChannels = [
|
const validChannels = [
|
||||||
// Gateway
|
// Gateway
|
||||||
'gateway:status',
|
'gateway:status',
|
||||||
|
'gateway:isConnected',
|
||||||
'gateway:start',
|
'gateway:start',
|
||||||
'gateway:stop',
|
'gateway:stop',
|
||||||
'gateway:restart',
|
'gateway:restart',
|
||||||
'gateway:rpc',
|
'gateway:rpc',
|
||||||
|
'gateway:health',
|
||||||
// Shell
|
// Shell
|
||||||
'shell:openExternal',
|
'shell:openExternal',
|
||||||
'shell:showItemInFolder',
|
'shell:showItemInFolder',
|
||||||
@@ -74,6 +76,9 @@ const electronAPI = {
|
|||||||
const validChannels = [
|
const validChannels = [
|
||||||
'gateway:status-changed',
|
'gateway:status-changed',
|
||||||
'gateway:message',
|
'gateway:message',
|
||||||
|
'gateway:notification',
|
||||||
|
'gateway:channel-status',
|
||||||
|
'gateway:chat-message',
|
||||||
'gateway:exit',
|
'gateway:exit',
|
||||||
'gateway:error',
|
'gateway:error',
|
||||||
'navigate',
|
'navigate',
|
||||||
@@ -106,6 +111,9 @@ const electronAPI = {
|
|||||||
const validChannels = [
|
const validChannels = [
|
||||||
'gateway:status-changed',
|
'gateway:status-changed',
|
||||||
'gateway:message',
|
'gateway:message',
|
||||||
|
'gateway:notification',
|
||||||
|
'gateway:channel-status',
|
||||||
|
'gateway:chat-message',
|
||||||
'gateway:exit',
|
'gateway:exit',
|
||||||
'gateway:error',
|
'gateway:error',
|
||||||
'navigate',
|
'navigate',
|
||||||
|
|||||||
@@ -28,8 +28,11 @@ function App() {
|
|||||||
|
|
||||||
// Listen for navigation events from main process
|
// Listen for navigation events from main process
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleNavigate = (path: string) => {
|
const handleNavigate = (...args: unknown[]) => {
|
||||||
navigate(path);
|
const path = args[0];
|
||||||
|
if (typeof path === 'string') {
|
||||||
|
navigate(path);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const unsubscribe = window.electron.ipcRenderer.on('navigate', handleNavigate);
|
const unsubscribe = window.electron.ipcRenderer.on('navigate', handleNavigate);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
type Status = 'connected' | 'disconnected' | 'connecting' | 'error' | 'running' | 'stopped' | 'starting';
|
export type Status = 'connected' | 'disconnected' | 'connecting' | 'error' | 'running' | 'stopped' | 'starting' | 'reconnecting';
|
||||||
|
|
||||||
interface StatusBadgeProps {
|
interface StatusBadgeProps {
|
||||||
status: Status;
|
status: Status;
|
||||||
@@ -20,6 +20,7 @@ const statusConfig: Record<Status, { label: string; variant: 'success' | 'second
|
|||||||
stopped: { label: 'Stopped', variant: 'secondary' },
|
stopped: { label: 'Stopped', variant: 'secondary' },
|
||||||
connecting: { label: 'Connecting', variant: 'warning' },
|
connecting: { label: 'Connecting', variant: 'warning' },
|
||||||
starting: { label: 'Starting', variant: 'warning' },
|
starting: { label: 'Starting', variant: 'warning' },
|
||||||
|
reconnecting: { label: 'Reconnecting', variant: 'warning' },
|
||||||
error: { label: 'Error', variant: 'destructive' },
|
error: { label: 'Error', variant: 'destructive' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Top navigation bar with search and actions
|
* Top navigation bar with search and actions
|
||||||
*/
|
*/
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { Search, Bell, Moon, Sun, Monitor } from 'lucide-react';
|
import { Search, Bell, Moon, Sun, Monitor } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -21,7 +21,6 @@ const pageTitles: Record<string, string> = {
|
|||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const theme = useSettingsStore((state) => state.theme);
|
const theme = useSettingsStore((state) => state.theme);
|
||||||
const setTheme = useSettingsStore((state) => state.setTheme);
|
const setTheme = useSettingsStore((state) => state.setTheme);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Navigation sidebar with menu items
|
* Navigation sidebar with menu items
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Home,
|
Home,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
@@ -62,7 +62,6 @@ function NavItem({ to, icon, label, badge, collapsed }: NavItemProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const location = useLocation();
|
|
||||||
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
|
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
|
||||||
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
|
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
|
||||||
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
|
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Cron Page
|
* Cron Page
|
||||||
* Manage scheduled tasks
|
* Manage scheduled tasks
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Plus, Clock, Play, Pause, Trash2, Edit, RefreshCw } from 'lucide-react';
|
import { Plus, Clock, Play, Pause, Trash2, Edit, RefreshCw } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Settings,
|
Settings,
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
|
||||||
ExternalLink,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
@@ -22,7 +20,6 @@ import { useGatewayStore } from '@/stores/gateway';
|
|||||||
import { useChannelsStore } from '@/stores/channels';
|
import { useChannelsStore } from '@/stores/channels';
|
||||||
import { useSkillsStore } from '@/stores/skills';
|
import { useSkillsStore } from '@/stores/skills';
|
||||||
import { StatusBadge } from '@/components/common/StatusBadge';
|
import { StatusBadge } from '@/components/common/StatusBadge';
|
||||||
import { formatRelativeTime } from '@/lib/utils';
|
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
const gatewayStatus = useGatewayStore((state) => state.status);
|
const gatewayStatus = useGatewayStore((state) => state.status);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Terminal,
|
Terminal,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Info,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ interface CronState {
|
|||||||
setJobs: (jobs: CronJob[]) => void;
|
setJobs: (jobs: CronJob[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCronStore = create<CronState>((set, get) => ({
|
export const useCronStore = create<CronState>((set) => ({
|
||||||
jobs: [],
|
jobs: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
/**
|
/**
|
||||||
* Gateway State Store
|
* Gateway State Store
|
||||||
* Manages Gateway connection state
|
* Manages Gateway connection state and communication
|
||||||
*/
|
*/
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { GatewayStatus } from '../types/gateway';
|
import type { GatewayStatus } from '../types/gateway';
|
||||||
|
|
||||||
|
interface GatewayHealth {
|
||||||
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
uptime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface GatewayState {
|
interface GatewayState {
|
||||||
status: GatewayStatus;
|
status: GatewayStatus;
|
||||||
|
health: GatewayHealth | null;
|
||||||
isInitialized: boolean;
|
isInitialized: boolean;
|
||||||
|
lastError: string | null;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
init: () => Promise<void>;
|
init: () => Promise<void>;
|
||||||
start: () => Promise<void>;
|
start: () => Promise<void>;
|
||||||
stop: () => Promise<void>;
|
stop: () => Promise<void>;
|
||||||
restart: () => Promise<void>;
|
restart: () => Promise<void>;
|
||||||
|
checkHealth: () => Promise<GatewayHealth>;
|
||||||
|
rpc: <T>(method: string, params?: unknown, timeoutMs?: number) => Promise<T>;
|
||||||
setStatus: (status: GatewayStatus) => void;
|
setStatus: (status: GatewayStatus) => void;
|
||||||
|
clearError: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGatewayStore = create<GatewayState>((set, get) => ({
|
export const useGatewayStore = create<GatewayState>((set, get) => ({
|
||||||
@@ -22,7 +33,9 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
|
|||||||
state: 'stopped',
|
state: 'stopped',
|
||||||
port: 18789,
|
port: 18789,
|
||||||
},
|
},
|
||||||
|
health: null,
|
||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
|
lastError: null,
|
||||||
|
|
||||||
init: async () => {
|
init: async () => {
|
||||||
if (get().isInitialized) return;
|
if (get().isInitialized) return;
|
||||||
@@ -36,38 +49,111 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
|
|||||||
window.electron.ipcRenderer.on('gateway:status-changed', (newStatus) => {
|
window.electron.ipcRenderer.on('gateway:status-changed', (newStatus) => {
|
||||||
set({ status: newStatus as GatewayStatus });
|
set({ status: newStatus as GatewayStatus });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for errors
|
||||||
|
window.electron.ipcRenderer.on('gateway:error', (error) => {
|
||||||
|
set({ lastError: String(error) });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for notifications
|
||||||
|
window.electron.ipcRenderer.on('gateway:notification', (notification) => {
|
||||||
|
console.log('Gateway notification:', notification);
|
||||||
|
// Could dispatch to other stores based on notification type
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize Gateway:', error);
|
console.error('Failed to initialize Gateway:', error);
|
||||||
|
set({ lastError: String(error) });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
start: async () => {
|
start: async () => {
|
||||||
try {
|
try {
|
||||||
set({ status: { ...get().status, state: 'starting' } });
|
set({ status: { ...get().status, state: 'starting' }, lastError: null });
|
||||||
const result = await window.electron.ipcRenderer.invoke('gateway:start') as { success: boolean; error?: string };
|
const result = await window.electron.ipcRenderer.invoke('gateway:start') as { success: boolean; error?: string };
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
set({ status: { ...get().status, state: 'error', error: result.error } });
|
set({
|
||||||
|
status: { ...get().status, state: 'error', error: result.error },
|
||||||
|
lastError: result.error || 'Failed to start Gateway'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ status: { ...get().status, state: 'error', error: String(error) } });
|
set({
|
||||||
|
status: { ...get().status, state: 'error', error: String(error) },
|
||||||
|
lastError: String(error)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
stop: async () => {
|
stop: async () => {
|
||||||
try {
|
try {
|
||||||
await window.electron.ipcRenderer.invoke('gateway:stop');
|
await window.electron.ipcRenderer.invoke('gateway:stop');
|
||||||
set({ status: { ...get().status, state: 'stopped' } });
|
set({ status: { ...get().status, state: 'stopped' }, lastError: null });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to stop Gateway:', error);
|
console.error('Failed to stop Gateway:', error);
|
||||||
|
set({ lastError: String(error) });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
restart: async () => {
|
restart: async () => {
|
||||||
const { stop, start } = get();
|
try {
|
||||||
await stop();
|
set({ status: { ...get().status, state: 'starting' }, lastError: null });
|
||||||
await start();
|
const result = await window.electron.ipcRenderer.invoke('gateway:restart') as { success: boolean; error?: string };
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
set({
|
||||||
|
status: { ...get().status, state: 'error', error: result.error },
|
||||||
|
lastError: result.error || 'Failed to restart Gateway'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
status: { ...get().status, state: 'error', error: String(error) },
|
||||||
|
lastError: String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
checkHealth: async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.electron.ipcRenderer.invoke('gateway:health') as {
|
||||||
|
success: boolean;
|
||||||
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
uptime?: number
|
||||||
|
};
|
||||||
|
|
||||||
|
const health: GatewayHealth = {
|
||||||
|
ok: result.ok,
|
||||||
|
error: result.error,
|
||||||
|
uptime: result.uptime,
|
||||||
|
};
|
||||||
|
|
||||||
|
set({ health });
|
||||||
|
return health;
|
||||||
|
} catch (error) {
|
||||||
|
const health: GatewayHealth = { ok: false, error: String(error) };
|
||||||
|
set({ health });
|
||||||
|
return health;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
rpc: async <T>(method: string, params?: unknown, timeoutMs?: number): Promise<T> => {
|
||||||
|
const result = await window.electron.ipcRenderer.invoke('gateway:rpc', method, params, timeoutMs) as {
|
||||||
|
success: boolean;
|
||||||
|
result?: T;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || `RPC call failed: ${method}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.result as T;
|
||||||
},
|
},
|
||||||
|
|
||||||
setStatus: (status) => set({ status }),
|
setStatus: (status) => set({ status }),
|
||||||
|
|
||||||
|
clearError: () => set({ lastError: null }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -7,12 +7,14 @@
|
|||||||
* Gateway connection status
|
* Gateway connection status
|
||||||
*/
|
*/
|
||||||
export interface GatewayStatus {
|
export interface GatewayStatus {
|
||||||
state: 'stopped' | 'starting' | 'running' | 'error';
|
state: 'stopped' | 'starting' | 'running' | 'error' | 'reconnecting';
|
||||||
port: number;
|
port: number;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
uptime?: number;
|
uptime?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
connectedAt?: number;
|
connectedAt?: number;
|
||||||
|
version?: string;
|
||||||
|
reconnectAttempts?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,3 +25,34 @@ export interface GatewayRpcResponse<T = unknown> {
|
|||||||
result?: T;
|
result?: T;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gateway health check response
|
||||||
|
*/
|
||||||
|
export interface GatewayHealth {
|
||||||
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
uptime?: number;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gateway notification (server-initiated event)
|
||||||
|
*/
|
||||||
|
export interface GatewayNotification {
|
||||||
|
method: string;
|
||||||
|
params?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider configuration
|
||||||
|
*/
|
||||||
|
export interface ProviderConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: 'openai' | 'anthropic' | 'ollama' | 'custom';
|
||||||
|
apiKey?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
model?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
|
|||||||
Reference in New Issue
Block a user