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:
Haze
2026-02-05 23:15:07 +08:00
Unverified
parent b8ab0208d0
commit 1646536e40
15 changed files with 601 additions and 74 deletions

View File

@@ -2,7 +2,7 @@
* Gateway WebSocket Client
* Provides a typed interface for Gateway RPC calls
*/
import { GatewayManager } from './manager';
import { GatewayManager, GatewayStatus } from './manager';
/**
* Channel types supported by OpenClaw
@@ -19,6 +19,7 @@ export interface Channel {
status: 'connected' | 'disconnected' | 'connecting' | 'error';
lastActivity?: string;
error?: string;
config?: Record<string, unknown>;
}
/**
@@ -32,6 +33,20 @@ export interface Skill {
category?: string;
icon?: string;
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;
channel?: string;
toolCalls?: ToolCall[];
metadata?: Record<string, unknown>;
}
/**
@@ -55,6 +71,35 @@ export interface ToolCall {
arguments: Record<string, unknown>;
result?: unknown;
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 {
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 ====================
/**
@@ -161,13 +220,80 @@ export class GatewayClient {
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 ====================
/**
* Get Gateway health status
*/
async getHealth(): Promise<{ status: string; uptime: number }> {
return this.manager.rpc<{ status: string; uptime: number }>('system.health');
async getHealth(): Promise<{ status: string; uptime: number; version?: string }> {
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> {
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 });
}
}

View File

@@ -2,24 +2,24 @@
* Gateway Process Manager
* Manages the OpenClaw Gateway process lifecycle
*/
import { spawn, ChildProcess, exec } from 'child_process';
import { spawn, ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import WebSocket from 'ws';
import { promisify } from 'util';
import { PORTS } from '../utils/config';
const execAsync = promisify(exec);
import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol';
/**
* Gateway connection status
*/
export interface GatewayStatus {
state: 'stopped' | 'starting' | 'running' | 'error';
state: 'stopped' | 'starting' | 'running' | 'error' | 'reconnecting';
port: number;
pid?: number;
uptime?: number;
error?: string;
connectedAt?: number;
version?: string;
reconnectAttempts?: number;
}
/**
@@ -28,10 +28,28 @@ export interface GatewayStatus {
export interface GatewayManagerEvents {
status: (status: GatewayStatus) => void;
message: (message: unknown) => void;
notification: (notification: JsonRpcNotification) => void;
exit: (code: number | null) => 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
* 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 reconnectTimer: 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, {
resolve: (value: unknown) => void;
reject: (error: Error) => void;
timeout: NodeJS.Timeout;
}> = new Map();
constructor() {
constructor(config?: Partial<ReconnectConfig>) {
super();
this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...config };
}
/**
@@ -59,6 +82,13 @@ export class GatewayManager extends EventEmitter {
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
*/
@@ -67,7 +97,9 @@ export class GatewayManager extends EventEmitter {
return;
}
this.setStatus({ state: 'starting' });
this.shouldReconnect = true;
this.reconnectAttempts = 0;
this.setStatus({ state: 'starting', reconnectAttempts: 0 });
try {
// Check if Gateway is already running
@@ -75,6 +107,7 @@ export class GatewayManager extends EventEmitter {
if (existing) {
console.log('Found existing Gateway on port', existing.port);
await this.connect(existing.port);
this.startHealthCheck();
return;
}
@@ -87,6 +120,9 @@ export class GatewayManager extends EventEmitter {
// Connect WebSocket
await this.connect(this.status.port);
// Start health monitoring
this.startHealthCheck();
} catch (error) {
this.setStatus({ state: 'error', error: String(error) });
throw error;
@@ -97,7 +133,56 @@ export class GatewayManager extends EventEmitter {
* Stop Gateway process
*/
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) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
@@ -106,33 +191,16 @@ export class GatewayManager extends EventEmitter {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
// Close WebSocket
if (this.ws) {
this.ws.close();
this.ws = null;
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = 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
*/
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) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
reject(new Error('Gateway not connected'));
@@ -145,7 +213,7 @@ export class GatewayManager extends EventEmitter {
const timeout = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error(`RPC timeout: ${method}`));
}, 30000);
}, timeoutMs);
// Store pending request
this.pendingRequests.set(id, {
@@ -162,10 +230,61 @@ export class GatewayManager extends EventEmitter {
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
*/
@@ -307,25 +426,61 @@ export class GatewayManager extends EventEmitter {
/**
* Handle incoming WebSocket message
*/
private handleMessage(message: { id?: string; result?: unknown; error?: unknown }): void {
// Check if this is a response to a pending request
if (message.id && this.pendingRequests.has(message.id)) {
const request = this.pendingRequests.get(message.id)!;
private handleMessage(message: unknown): void {
// Check if this is a JSON-RPC response
if (isResponse(message) && message.id && this.pendingRequests.has(String(message.id))) {
const request = this.pendingRequests.get(String(message.id))!;
clearTimeout(request.timeout);
this.pendingRequests.delete(message.id);
this.pendingRequests.delete(String(message.id));
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 {
request.resolve(message.result);
}
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);
}
/**
* 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
*/
@@ -342,29 +497,84 @@ export class GatewayManager extends EventEmitter {
}
/**
* Schedule reconnection attempt
* Schedule reconnection attempt with exponential backoff
*/
private scheduleReconnect(): void {
if (!this.shouldReconnect) {
console.log('Auto-reconnect disabled, not scheduling reconnect');
return;
}
if (this.reconnectTimer) {
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 = null;
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) {
console.error('Reconnection failed:', error);
this.scheduleReconnect();
}
}, 5000);
}, delay);
}
/**
* Update status and emit event
*/
private setStatus(update: Partial<GatewayStatus>): void {
const previousState = this.status.state;
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);
// Log state transitions
if (previousState !== this.status.state) {
console.log(`Gateway state: ${previousState} -> ${this.status.state}`);
}
}
}

View File

@@ -37,6 +37,11 @@ function registerGatewayHandlers(
return gatewayManager.getStatus();
});
// Check if Gateway is connected
ipcMain.handle('gateway:isConnected', () => {
return gatewayManager.isConnected();
});
// Start Gateway
ipcMain.handle('gateway:start', async () => {
try {
@@ -60,8 +65,7 @@ function registerGatewayHandlers(
// Restart Gateway
ipcMain.handle('gateway:restart', async () => {
try {
await gatewayManager.stop();
await gatewayManager.start();
await gatewayManager.restart();
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
@@ -69,26 +73,66 @@ function registerGatewayHandlers(
});
// Gateway RPC call
ipcMain.handle('gateway:rpc', async (_, method: string, params?: unknown) => {
ipcMain.handle('gateway:rpc', async (_, method: string, params?: unknown, timeoutMs?: number) => {
try {
const result = await gatewayManager.rpc(method, params);
const result = await gatewayManager.rpc(method, params, timeoutMs);
return { success: true, result };
} catch (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) => {
mainWindow.webContents.send('gateway:status-changed', status);
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('gateway:status-changed', status);
}
});
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) => {
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);
}
});
}

View File

@@ -16,10 +16,12 @@ const electronAPI = {
const validChannels = [
// Gateway
'gateway:status',
'gateway:isConnected',
'gateway:start',
'gateway:stop',
'gateway:restart',
'gateway:rpc',
'gateway:health',
// Shell
'shell:openExternal',
'shell:showItemInFolder',
@@ -74,6 +76,9 @@ const electronAPI = {
const validChannels = [
'gateway:status-changed',
'gateway:message',
'gateway:notification',
'gateway:channel-status',
'gateway:chat-message',
'gateway:exit',
'gateway:error',
'navigate',
@@ -106,6 +111,9 @@ const electronAPI = {
const validChannels = [
'gateway:status-changed',
'gateway:message',
'gateway:notification',
'gateway:channel-status',
'gateway:chat-message',
'gateway:exit',
'gateway:error',
'navigate',

View File

@@ -28,8 +28,11 @@ function App() {
// Listen for navigation events from main process
useEffect(() => {
const handleNavigate = (path: string) => {
navigate(path);
const handleNavigate = (...args: unknown[]) => {
const path = args[0];
if (typeof path === 'string') {
navigate(path);
}
};
const unsubscribe = window.electron.ipcRenderer.on('navigate', handleNavigate);

View File

@@ -5,7 +5,7 @@
import { cn } from '@/lib/utils';
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 {
status: Status;
@@ -20,6 +20,7 @@ const statusConfig: Record<Status, { label: string; variant: 'success' | 'second
stopped: { label: 'Stopped', variant: 'secondary' },
connecting: { label: 'Connecting', variant: 'warning' },
starting: { label: 'Starting', variant: 'warning' },
reconnecting: { label: 'Reconnecting', variant: 'warning' },
error: { label: 'Error', variant: 'destructive' },
};

View File

@@ -3,7 +3,7 @@
* Top navigation bar with search and actions
*/
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 { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -21,7 +21,6 @@ const pageTitles: Record<string, string> = {
export function Header() {
const location = useLocation();
const navigate = useNavigate();
const theme = useSettingsStore((state) => state.theme);
const setTheme = useSettingsStore((state) => state.setTheme);

View File

@@ -3,7 +3,7 @@
* Navigation sidebar with menu items
*/
import { useState, useEffect } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import { NavLink } from 'react-router-dom';
import {
Home,
MessageSquare,
@@ -62,7 +62,6 @@ function NavItem({ to, icon, label, badge, collapsed }: NavItemProps) {
}
export function Sidebar() {
const location = useLocation();
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);

View File

@@ -2,7 +2,7 @@
* Cron Page
* Manage scheduled tasks
*/
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { Plus, Clock, Play, Pause, Trash2, Edit, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';

View File

@@ -11,8 +11,6 @@ import {
Clock,
Settings,
Plus,
RefreshCw,
ExternalLink,
} from 'lucide-react';
import { Link } from 'react-router-dom';
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 { useSkillsStore } from '@/stores/skills';
import { StatusBadge } from '@/components/common/StatusBadge';
import { formatRelativeTime } from '@/lib/utils';
export function Dashboard() {
const gatewayStatus = useGatewayStore((state) => state.status);

View File

@@ -11,7 +11,6 @@ import {
Loader2,
Terminal,
ExternalLink,
Info,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';

View File

@@ -20,7 +20,7 @@ interface CronState {
setJobs: (jobs: CronJob[]) => void;
}
export const useCronStore = create<CronState>((set, get) => ({
export const useCronStore = create<CronState>((set) => ({
jobs: [],
loading: false,
error: null,

View File

@@ -1,20 +1,31 @@
/**
* Gateway State Store
* Manages Gateway connection state
* Manages Gateway connection state and communication
*/
import { create } from 'zustand';
import type { GatewayStatus } from '../types/gateway';
interface GatewayHealth {
ok: boolean;
error?: string;
uptime?: number;
}
interface GatewayState {
status: GatewayStatus;
health: GatewayHealth | null;
isInitialized: boolean;
lastError: string | null;
// Actions
init: () => Promise<void>;
start: () => Promise<void>;
stop: () => Promise<void>;
restart: () => Promise<void>;
checkHealth: () => Promise<GatewayHealth>;
rpc: <T>(method: string, params?: unknown, timeoutMs?: number) => Promise<T>;
setStatus: (status: GatewayStatus) => void;
clearError: () => void;
}
export const useGatewayStore = create<GatewayState>((set, get) => ({
@@ -22,7 +33,9 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
state: 'stopped',
port: 18789,
},
health: null,
isInitialized: false,
lastError: null,
init: async () => {
if (get().isInitialized) return;
@@ -36,38 +49,111 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
window.electron.ipcRenderer.on('gateway:status-changed', (newStatus) => {
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) {
console.error('Failed to initialize Gateway:', error);
set({ lastError: String(error) });
}
},
start: async () => {
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 };
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) {
set({ status: { ...get().status, state: 'error', error: String(error) } });
set({
status: { ...get().status, state: 'error', error: String(error) },
lastError: String(error)
});
}
},
stop: async () => {
try {
await window.electron.ipcRenderer.invoke('gateway:stop');
set({ status: { ...get().status, state: 'stopped' } });
set({ status: { ...get().status, state: 'stopped' }, lastError: null });
} catch (error) {
console.error('Failed to stop Gateway:', error);
set({ lastError: String(error) });
}
},
restart: async () => {
const { stop, start } = get();
await stop();
await start();
try {
set({ status: { ...get().status, state: 'starting' }, lastError: null });
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 }),
clearError: () => set({ lastError: null }),
}));

View File

@@ -7,12 +7,14 @@
* Gateway connection status
*/
export interface GatewayStatus {
state: 'stopped' | 'starting' | 'running' | 'error';
state: 'stopped' | 'starting' | 'running' | 'error' | 'reconnecting';
port: number;
pid?: number;
uptime?: number;
error?: string;
connectedAt?: number;
version?: string;
reconnectAttempts?: number;
}
/**
@@ -23,3 +25,34 @@ export interface GatewayRpcResponse<T = unknown> {
result?: T;
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;
}

View File

@@ -1,5 +1,6 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",