feat(core): initialize project skeleton with Electron + React + TypeScript
Set up the complete project foundation for ClawX, a graphical AI assistant: - Electron main process with IPC handlers, menu, tray, and gateway management - React renderer with routing, layout components, and page scaffolding - Zustand state management for gateway, settings, channels, skills, chat, and cron - shadcn/ui components with Tailwind CSS and CSS variable theming - Build tooling with Vite, electron-builder, and TypeScript configuration - Testing setup with Vitest and Playwright - Development configurations (ESLint, Prettier, gitignore, env example)
This commit is contained in:
186
electron/gateway/client.ts
Normal file
186
electron/gateway/client.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Gateway WebSocket Client
|
||||
* Provides a typed interface for Gateway RPC calls
|
||||
*/
|
||||
import { GatewayManager } from './manager';
|
||||
|
||||
/**
|
||||
* Channel types supported by OpenClaw
|
||||
*/
|
||||
export type ChannelType = 'whatsapp' | 'telegram' | 'discord' | 'slack' | 'wechat';
|
||||
|
||||
/**
|
||||
* Channel status
|
||||
*/
|
||||
export interface Channel {
|
||||
id: string;
|
||||
type: ChannelType;
|
||||
name: string;
|
||||
status: 'connected' | 'disconnected' | 'connecting' | 'error';
|
||||
lastActivity?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill definition
|
||||
*/
|
||||
export interface Skill {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
category?: string;
|
||||
icon?: string;
|
||||
configurable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat message
|
||||
*/
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: string;
|
||||
channel?: string;
|
||||
toolCalls?: ToolCall[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool call in a message
|
||||
*/
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
result?: unknown;
|
||||
status: 'pending' | 'running' | 'completed' | 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway Client
|
||||
* Typed wrapper around GatewayManager for making RPC calls
|
||||
*/
|
||||
export class GatewayClient {
|
||||
constructor(private manager: GatewayManager) {}
|
||||
|
||||
// ==================== Channel Methods ====================
|
||||
|
||||
/**
|
||||
* List all channels
|
||||
*/
|
||||
async listChannels(): Promise<Channel[]> {
|
||||
return this.manager.rpc<Channel[]>('channels.list');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channel by ID
|
||||
*/
|
||||
async getChannel(channelId: string): Promise<Channel> {
|
||||
return this.manager.rpc<Channel>('channels.get', { channelId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a channel
|
||||
*/
|
||||
async connectChannel(channelId: string): Promise<void> {
|
||||
return this.manager.rpc<void>('channels.connect', { channelId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect a channel
|
||||
*/
|
||||
async disconnectChannel(channelId: string): Promise<void> {
|
||||
return this.manager.rpc<void>('channels.disconnect', { channelId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get QR code for channel connection (e.g., WhatsApp)
|
||||
*/
|
||||
async getChannelQRCode(channelType: ChannelType): Promise<string> {
|
||||
return this.manager.rpc<string>('channels.getQRCode', { channelType });
|
||||
}
|
||||
|
||||
// ==================== Skill Methods ====================
|
||||
|
||||
/**
|
||||
* List all skills
|
||||
*/
|
||||
async listSkills(): Promise<Skill[]> {
|
||||
return this.manager.rpc<Skill[]>('skills.list');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a skill
|
||||
*/
|
||||
async enableSkill(skillId: string): Promise<void> {
|
||||
return this.manager.rpc<void>('skills.enable', { skillId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a skill
|
||||
*/
|
||||
async disableSkill(skillId: string): Promise<void> {
|
||||
return this.manager.rpc<void>('skills.disable', { skillId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get skill configuration
|
||||
*/
|
||||
async getSkillConfig(skillId: string): Promise<Record<string, unknown>> {
|
||||
return this.manager.rpc<Record<string, unknown>>('skills.getConfig', { skillId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update skill configuration
|
||||
*/
|
||||
async updateSkillConfig(skillId: string, config: Record<string, unknown>): Promise<void> {
|
||||
return this.manager.rpc<void>('skills.updateConfig', { skillId, config });
|
||||
}
|
||||
|
||||
// ==================== Chat Methods ====================
|
||||
|
||||
/**
|
||||
* Send a chat message
|
||||
*/
|
||||
async sendMessage(content: string, channelId?: string): Promise<ChatMessage> {
|
||||
return this.manager.rpc<ChatMessage>('chat.send', { content, channelId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chat history
|
||||
*/
|
||||
async getChatHistory(limit = 50, offset = 0): Promise<ChatMessage[]> {
|
||||
return this.manager.rpc<ChatMessage[]>('chat.history', { limit, offset });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear chat history
|
||||
*/
|
||||
async clearChatHistory(): Promise<void> {
|
||||
return this.manager.rpc<void>('chat.clear');
|
||||
}
|
||||
|
||||
// ==================== System Methods ====================
|
||||
|
||||
/**
|
||||
* Get Gateway health status
|
||||
*/
|
||||
async getHealth(): Promise<{ status: string; uptime: number }> {
|
||||
return this.manager.rpc<{ status: string; uptime: number }>('system.health');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Gateway configuration
|
||||
*/
|
||||
async getConfig(): Promise<Record<string, unknown>> {
|
||||
return this.manager.rpc<Record<string, unknown>>('system.config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Gateway configuration
|
||||
*/
|
||||
async updateConfig(config: Record<string, unknown>): Promise<void> {
|
||||
return this.manager.rpc<void>('system.updateConfig', config);
|
||||
}
|
||||
}
|
||||
370
electron/gateway/manager.ts
Normal file
370
electron/gateway/manager.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* Gateway Process Manager
|
||||
* Manages the OpenClaw Gateway process lifecycle
|
||||
*/
|
||||
import { spawn, ChildProcess, exec } from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import WebSocket from 'ws';
|
||||
import { promisify } from 'util';
|
||||
import { PORTS } from '../utils/config';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Gateway connection status
|
||||
*/
|
||||
export interface GatewayStatus {
|
||||
state: 'stopped' | 'starting' | 'running' | 'error';
|
||||
port: number;
|
||||
pid?: number;
|
||||
uptime?: number;
|
||||
error?: string;
|
||||
connectedAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway Manager Events
|
||||
*/
|
||||
export interface GatewayManagerEvents {
|
||||
status: (status: GatewayStatus) => void;
|
||||
message: (message: unknown) => void;
|
||||
exit: (code: number | null) => void;
|
||||
error: (error: Error) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway Manager
|
||||
* Handles starting, stopping, and communicating with the OpenClaw Gateway
|
||||
*/
|
||||
export class GatewayManager extends EventEmitter {
|
||||
private process: ChildProcess | null = null;
|
||||
private ws: WebSocket | null = null;
|
||||
private status: GatewayStatus = { state: 'stopped', port: PORTS.OPENCLAW_GATEWAY };
|
||||
private reconnectTimer: NodeJS.Timeout | null = null;
|
||||
private pingInterval: NodeJS.Timeout | null = null;
|
||||
private pendingRequests: Map<string, {
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeout: NodeJS.Timeout;
|
||||
}> = new Map();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current Gateway status
|
||||
*/
|
||||
getStatus(): GatewayStatus {
|
||||
return { ...this.status };
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Gateway process
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.status.state === 'running') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setStatus({ state: 'starting' });
|
||||
|
||||
try {
|
||||
// Check if Gateway is already running
|
||||
const existing = await this.findExistingGateway();
|
||||
if (existing) {
|
||||
console.log('Found existing Gateway on port', existing.port);
|
||||
await this.connect(existing.port);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start new Gateway process
|
||||
await this.startProcess();
|
||||
|
||||
// Wait for Gateway to be ready
|
||||
await this.waitForReady();
|
||||
|
||||
// Connect WebSocket
|
||||
await this.connect(this.status.port);
|
||||
|
||||
} catch (error) {
|
||||
this.setStatus({ state: 'error', error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Gateway process
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
// Clear timers
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
|
||||
// Close WebSocket
|
||||
if (this.ws) {
|
||||
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
|
||||
*/
|
||||
async rpc<T>(method: string, params?: unknown): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
reject(new Error('Gateway not connected'));
|
||||
return;
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
// Set timeout for request
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingRequests.delete(id);
|
||||
reject(new Error(`RPC timeout: ${method}`));
|
||||
}, 30000);
|
||||
|
||||
// Store pending request
|
||||
this.pendingRequests.set(id, {
|
||||
resolve: resolve as (value: unknown) => void,
|
||||
reject,
|
||||
timeout,
|
||||
});
|
||||
|
||||
// Send request
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
method,
|
||||
params,
|
||||
};
|
||||
|
||||
this.ws.send(JSON.stringify(request));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find existing Gateway process
|
||||
*/
|
||||
private async findExistingGateway(): Promise<{ port: number } | null> {
|
||||
try {
|
||||
// Try to connect to default port
|
||||
const port = PORTS.OPENCLAW_GATEWAY;
|
||||
const response = await fetch(`http://localhost:${port}/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return { port };
|
||||
}
|
||||
} catch {
|
||||
// Gateway not running
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Gateway process
|
||||
*/
|
||||
private async startProcess(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Find openclaw command
|
||||
const command = 'openclaw';
|
||||
const args = ['gateway', 'run', '--port', String(this.status.port)];
|
||||
|
||||
this.process = spawn(command, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: false,
|
||||
shell: true,
|
||||
});
|
||||
|
||||
this.process.on('error', (error) => {
|
||||
console.error('Gateway process error:', error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
this.process.on('exit', (code) => {
|
||||
console.log('Gateway process exited with code:', code);
|
||||
this.emit('exit', code);
|
||||
|
||||
if (this.status.state === 'running') {
|
||||
this.setStatus({ state: 'stopped' });
|
||||
// Attempt to reconnect
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// Log stdout
|
||||
this.process.stdout?.on('data', (data) => {
|
||||
console.log('Gateway:', data.toString());
|
||||
});
|
||||
|
||||
// Log stderr
|
||||
this.process.stderr?.on('data', (data) => {
|
||||
console.error('Gateway error:', data.toString());
|
||||
});
|
||||
|
||||
// Store PID
|
||||
if (this.process.pid) {
|
||||
this.setStatus({ pid: this.process.pid });
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Gateway to be ready
|
||||
*/
|
||||
private async waitForReady(retries = 30, interval = 1000): Promise<void> {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${this.status.port}/health`, {
|
||||
signal: AbortSignal.timeout(1000),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Gateway not ready yet
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||
}
|
||||
|
||||
throw new Error('Gateway failed to start');
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect WebSocket to Gateway
|
||||
*/
|
||||
private async connect(port: number): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const wsUrl = `ws://localhost:${port}/ws`;
|
||||
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.on('open', () => {
|
||||
console.log('WebSocket connected to Gateway');
|
||||
this.setStatus({
|
||||
state: 'running',
|
||||
port,
|
||||
connectedAt: Date.now(),
|
||||
});
|
||||
this.startPing();
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.ws.on('message', (data) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
this.handleMessage(message);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('close', () => {
|
||||
console.log('WebSocket disconnected');
|
||||
if (this.status.state === 'running') {
|
||||
this.setStatus({ state: 'stopped' });
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('error', (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)!;
|
||||
clearTimeout(request.timeout);
|
||||
this.pendingRequests.delete(message.id);
|
||||
|
||||
if (message.error) {
|
||||
request.reject(new Error(String(message.error)));
|
||||
} else {
|
||||
request.resolve(message.result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit message for other handlers
|
||||
this.emit('message', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start ping interval to keep connection alive
|
||||
*/
|
||||
private startPing(): void {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
}
|
||||
|
||||
this.pingInterval = setInterval(() => {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.ping();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule reconnection attempt
|
||||
*/
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectTimer = setTimeout(async () => {
|
||||
this.reconnectTimer = null;
|
||||
try {
|
||||
await this.start();
|
||||
} catch (error) {
|
||||
console.error('Reconnection failed:', error);
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status and emit event
|
||||
*/
|
||||
private setStatus(update: Partial<GatewayStatus>): void {
|
||||
this.status = { ...this.status, ...update };
|
||||
this.emit('status', this.status);
|
||||
}
|
||||
}
|
||||
200
electron/gateway/protocol.ts
Normal file
200
electron/gateway/protocol.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Gateway Protocol Definitions
|
||||
* JSON-RPC 2.0 protocol types for Gateway communication
|
||||
*/
|
||||
|
||||
/**
|
||||
* JSON-RPC 2.0 Request
|
||||
*/
|
||||
export interface JsonRpcRequest {
|
||||
jsonrpc: '2.0';
|
||||
id: string | number;
|
||||
method: string;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON-RPC 2.0 Response
|
||||
*/
|
||||
export interface JsonRpcResponse<T = unknown> {
|
||||
jsonrpc: '2.0';
|
||||
id: string | number;
|
||||
result?: T;
|
||||
error?: JsonRpcError;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON-RPC 2.0 Error
|
||||
*/
|
||||
export interface JsonRpcError {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON-RPC 2.0 Notification (no id, no response expected)
|
||||
*/
|
||||
export interface JsonRpcNotification {
|
||||
jsonrpc: '2.0';
|
||||
method: string;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard JSON-RPC 2.0 Error Codes
|
||||
*/
|
||||
export enum JsonRpcErrorCode {
|
||||
/** Invalid JSON was received */
|
||||
PARSE_ERROR = -32700,
|
||||
/** The JSON sent is not a valid Request object */
|
||||
INVALID_REQUEST = -32600,
|
||||
/** The method does not exist or is not available */
|
||||
METHOD_NOT_FOUND = -32601,
|
||||
/** Invalid method parameter(s) */
|
||||
INVALID_PARAMS = -32602,
|
||||
/** Internal JSON-RPC error */
|
||||
INTERNAL_ERROR = -32603,
|
||||
/** Server error range: -32000 to -32099 */
|
||||
SERVER_ERROR = -32000,
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway-specific error codes
|
||||
*/
|
||||
export enum GatewayErrorCode {
|
||||
/** Gateway not connected */
|
||||
NOT_CONNECTED = -32001,
|
||||
/** Authentication required */
|
||||
AUTH_REQUIRED = -32002,
|
||||
/** Permission denied */
|
||||
PERMISSION_DENIED = -32003,
|
||||
/** Resource not found */
|
||||
NOT_FOUND = -32004,
|
||||
/** Operation timeout */
|
||||
TIMEOUT = -32005,
|
||||
/** Rate limit exceeded */
|
||||
RATE_LIMITED = -32006,
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway event types
|
||||
*/
|
||||
export enum GatewayEventType {
|
||||
/** Gateway status changed */
|
||||
STATUS_CHANGED = 'gateway.status_changed',
|
||||
/** Channel status changed */
|
||||
CHANNEL_STATUS_CHANGED = 'channel.status_changed',
|
||||
/** New chat message received */
|
||||
MESSAGE_RECEIVED = 'chat.message_received',
|
||||
/** Message sent */
|
||||
MESSAGE_SENT = 'chat.message_sent',
|
||||
/** Tool call started */
|
||||
TOOL_CALL_STARTED = 'tool.call_started',
|
||||
/** Tool call completed */
|
||||
TOOL_CALL_COMPLETED = 'tool.call_completed',
|
||||
/** Error occurred */
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway event payload
|
||||
*/
|
||||
export interface GatewayEvent<T = unknown> {
|
||||
type: GatewayEventType;
|
||||
timestamp: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSON-RPC request
|
||||
*/
|
||||
export function createRequest(
|
||||
method: string,
|
||||
params?: unknown,
|
||||
id?: string | number
|
||||
): JsonRpcRequest {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id: id ?? crypto.randomUUID(),
|
||||
method,
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSON-RPC success response
|
||||
*/
|
||||
export function createSuccessResponse<T>(
|
||||
id: string | number,
|
||||
result: T
|
||||
): JsonRpcResponse<T> {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSON-RPC error response
|
||||
*/
|
||||
export function createErrorResponse(
|
||||
id: string | number,
|
||||
code: number,
|
||||
message: string,
|
||||
data?: unknown
|
||||
): JsonRpcResponse {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
data,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message is a JSON-RPC request
|
||||
*/
|
||||
export function isRequest(message: unknown): message is JsonRpcRequest {
|
||||
return (
|
||||
typeof message === 'object' &&
|
||||
message !== null &&
|
||||
'jsonrpc' in message &&
|
||||
message.jsonrpc === '2.0' &&
|
||||
'method' in message &&
|
||||
typeof message.method === 'string' &&
|
||||
'id' in message
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message is a JSON-RPC response
|
||||
*/
|
||||
export function isResponse(message: unknown): message is JsonRpcResponse {
|
||||
return (
|
||||
typeof message === 'object' &&
|
||||
message !== null &&
|
||||
'jsonrpc' in message &&
|
||||
message.jsonrpc === '2.0' &&
|
||||
'id' in message &&
|
||||
('result' in message || 'error' in message)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message is a JSON-RPC notification
|
||||
*/
|
||||
export function isNotification(message: unknown): message is JsonRpcNotification {
|
||||
return (
|
||||
typeof message === 'object' &&
|
||||
message !== null &&
|
||||
'jsonrpc' in message &&
|
||||
message.jsonrpc === '2.0' &&
|
||||
'method' in message &&
|
||||
!('id' in message)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user