diff --git a/package.json b/package.json index 66d593d0..68fa350c 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "vitest": "^4.0.18" }, "dependencies": { + "@multica/sdk": "workspace:*", "@mariozechner/pi-agent-core": "^0.50.3", "@mariozechner/pi-ai": "^0.50.3", "@mariozechner/pi-coding-agent": "^0.50.3", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a0b2096e..d9720e3a 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -7,8 +7,10 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./*": "./src/*.ts" }, "scripts": { "build": "tsc", diff --git a/packages/sdk/src/actions/hello.ts b/packages/sdk/src/actions/hello.ts index 56b7950f..36e56ba8 100644 --- a/packages/sdk/src/actions/hello.ts +++ b/packages/sdk/src/actions/hello.ts @@ -1,14 +1,14 @@ -/** Hello Action - 测试用的问候消息 */ +/** Hello Action - test greeting message */ export const HelloAction = "hello" as const; export const HelloResponseAction = "hello_response" as const; -/** Hello 请求 payload */ +/** Hello request payload */ export interface HelloPayload { greeting: string; } -/** Hello 响应 payload */ +/** Hello response payload */ export interface HelloResponsePayload { reply: string; } diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 352e4060..eec02b8d 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -1,53 +1,53 @@ -/** WebSocket 事件名称 */ +/** WebSocket event names */ export const GatewayEvents = { - // 系统事件 + // System events PING: "ping", PONG: "pong", REGISTERED: "registered", - // 消息路由 + // Message routing SEND: "send", RECEIVE: "receive", SEND_ERROR: "send_error", } as const; -// ============ 设备相关 ============ +// ============ Device Related ============ -/** 设备类型 */ +/** Device type */ export type DeviceType = "client" | "agent"; -/** 设备信息 */ +/** Device information */ export interface DeviceInfo { deviceId: string; deviceType: DeviceType; } -/** 注册响应 */ +/** Registration response */ export interface RegisteredResponse { success: boolean; deviceId: string; error?: string; } -// ============ 消息路由 ============ +// ============ Message Routing ============ -/** 路由消息 */ +/** Routed message */ export interface RoutedMessage { - /** 消息唯一ID (UUID v7,包含时间戳) */ + /** Unique message ID (UUID v7, contains timestamp) */ id: string; - /** 用户ID(登录后填充) */ + /** User ID (populated after login) */ uid: string | null; - /** 发送者 deviceId */ + /** Sender deviceId */ from: string; - /** 接收者 deviceId */ + /** Recipient deviceId */ to: string; - /** 动作类型 */ + /** Action type */ action: string; - /** 消息内容 */ + /** Message payload */ payload: T; } -/** 发送失败响应 */ +/** Send failure response */ export interface SendErrorResponse { messageId: string; error: string; @@ -56,43 +56,43 @@ export interface SendErrorResponse { // ============ Ping/Pong ============ -/** Ping 请求 */ +/** Ping request */ export interface PingPayload { [key: string]: unknown; } -/** Ping 响应 */ +/** Ping response */ export interface PongResponse { event: string; data: string; } -// ============ 客户端配置 ============ +// ============ Client Configuration ============ -/** 连接配置 */ +/** Connection configuration */ export interface GatewayClientOptions { - /** 服务器地址,如 http://localhost:3000 */ + /** Server address, e.g. http://localhost:3000 */ url: string; - /** WebSocket 路径,默认 /ws */ + /** WebSocket path, defaults to /ws */ path?: string | undefined; - /** 设备ID */ + /** Device ID */ deviceId: string; - /** 设备类型 */ + /** Device type */ deviceType: DeviceType; - /** 自动重连,默认 true */ + /** Auto reconnect, defaults to true */ autoReconnect?: boolean | undefined; - /** 重连延迟(毫秒),默认 1000 */ + /** Reconnect delay (milliseconds), defaults to 1000 */ reconnectDelay?: number | undefined; } -/** 连接状态 */ +/** Connection state */ export type ConnectionState = | "disconnected" | "connecting" | "connected" | "registered"; -/** 事件回调类型 */ +/** Event callback types */ export interface GatewayClientCallbacks { onConnect?: (socketId: string) => void; onDisconnect?: (reason: string) => void; @@ -103,4 +103,3 @@ export interface GatewayClientCallbacks { onError?: (error: Error) => void; onStateChange?: (state: ConnectionState) => void; } - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08485893..6272ecc3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@mozilla/readability': specifier: ^0.6.0 version: 0.6.0 + '@multica/sdk': + specifier: workspace:* + version: link:packages/sdk '@nestjs/common': specifier: ^11.1.12 version: 11.1.12(reflect-metadata@0.2.2)(rxjs@7.8.2) diff --git a/src/gateway/events.gateway.ts b/src/gateway/events.gateway.ts index 917446e8..0197e0f4 100644 --- a/src/gateway/events.gateway.ts +++ b/src/gateway/events.gateway.ts @@ -21,7 +21,7 @@ import { type PongResponse, type DeviceInfo, type DeviceType, -} from "../shared/gateway-sdk/index.js"; +} from "@multica/sdk"; @Injectable() @WebSocketGateway({ diff --git a/src/gateway/test-client.ts b/src/gateway/test-client.ts index ac4ed253..5125541c 100644 --- a/src/gateway/test-client.ts +++ b/src/gateway/test-client.ts @@ -4,7 +4,7 @@ import { HelloResponseAction, type HelloPayload, type HelloResponsePayload, -} from "../shared/gateway-sdk/index.js"; +} from "@multica/sdk"; // 模拟一个 Client const client = new GatewayClient({ diff --git a/src/hub/hub.ts b/src/hub/hub.ts index 68a2d454..19a61b1b 100644 --- a/src/hub/hub.ts +++ b/src/hub/hub.ts @@ -1,8 +1,7 @@ import type { HubOptions } from "./types.js"; -import type { ConnectionState } from "../shared/gateway-sdk/types.js"; +import { GatewayClient, type ConnectionState } from "@multica/sdk"; import { AsyncAgent } from "../agent/async-agent.js"; import { getHubId } from "./hub-identity.js"; -import { GatewayClient } from "../shared/gateway-sdk/client.js"; import { loadAgentRecords, addAgentRecord, removeAgentRecord } from "./agent-store.js"; export class Hub { diff --git a/src/shared/gateway-sdk/USAGE.md b/src/shared/gateway-sdk/USAGE.md deleted file mode 100644 index 3ca4c807..00000000 --- a/src/shared/gateway-sdk/USAGE.md +++ /dev/null @@ -1,594 +0,0 @@ -# Gateway SDK Usage Guide - -This document describes how to use the Gateway SDK for both Client and Agent implementations. - -## Installation - -The SDK is located at `src/shared/gateway-sdk`. Import from the index file: - -```typescript -import { - GatewayClient, - type RoutedMessage, - type ConnectionState, - // Actions - HelloAction, - RequestAction, - ResponseAction, - StreamAction, - // Types - type HelloPayload, - type RequestPayload, - type ResponsePayload, - type StreamPayload, -} from "../shared/gateway-sdk/index.js"; -``` - -## Core Concepts - -### Device Types - -- `client`: End-user applications (web, mobile, desktop) -- `agent`: Backend processing units that handle requests from clients - -### Connection States - -```typescript -type ConnectionState = "disconnected" | "connecting" | "connected" | "registered"; -``` - -- `disconnected`: Not connected to gateway -- `connecting`: Connection in progress -- `connected`: WebSocket connected, not yet registered -- `registered`: Fully operational, can send/receive messages - -### Message Structure - -All messages follow the `RoutedMessage` interface: - -```typescript -interface RoutedMessage { - id: string; // Unique message ID (UUID v7, contains timestamp) - uid: string | null; // User ID (null if not authenticated) - from: string; // Sender's deviceId - to: string; // Recipient's deviceId - action: string; // Action type (e.g., "hello", "request", "stream") - payload: T; // Message payload -} -``` - -> Note: The `id` field uses UUID v7 which embeds a millisecond timestamp. To extract it: -> ```typescript -> function getTimestampFromId(id: string): Date { -> const hex = id.replace(/-/g, '').slice(0, 12); -> return new Date(parseInt(hex, 16)); -> } -> ``` - -## Client Implementation - -### Basic Setup - -```typescript -import { GatewayClient } from "../shared/gateway-sdk/index.js"; - -const client = new GatewayClient({ - url: "http://localhost:3000", // Gateway server URL - deviceId: "client-001", // Unique device identifier - deviceType: "client", // Device type - metadata: { name: "My App" }, // Optional metadata - autoReconnect: true, // Auto reconnect on disconnect (default: true) - reconnectDelay: 1000, // Reconnect delay in ms (default: 1000) -}); -``` - -### Connecting and Event Handling - -```typescript -client - .onStateChange((state) => { - console.log("Connection state:", state); - }) - .onConnect((socketId) => { - console.log("Connected with socket ID:", socketId); - }) - .onRegistered((deviceId) => { - console.log("Registered as:", deviceId); - // Now safe to send messages - }) - .onMessage((message) => { - console.log("Received:", message); - // Handle incoming messages from agents - }) - .onSendError((error) => { - console.error("Send failed:", error); - // error.code: "DEVICE_NOT_FOUND" | "NOT_REGISTERED" | "INVALID_MESSAGE" - }) - .onDisconnect((reason) => { - console.log("Disconnected:", reason); - }) - .onError((error) => { - console.error("Connection error:", error); - }) - .connect(); -``` - -### Sending Messages to an Agent - -```typescript -import { HelloAction, type HelloPayload } from "../shared/gateway-sdk/index.js"; - -// Send a hello message to agent-001 -client.send("agent-001", HelloAction, { - greeting: "Hello from client!", -}); - -// With custom message ID -const messageId = client.send( - "agent-001", - HelloAction, - { greeting: "Hello!" }, - "custom-message-id-123" -); -``` - -### RPC Pattern (Request/Response) - -```typescript -import { - RequestAction, - ResponseAction, - type RequestPayload, - type ResponsePayload, - isResponseSuccess, - isResponseError, -} from "../shared/gateway-sdk/index.js"; - -// Track pending requests -const pendingRequests = new Map void>(); - -// Send RPC request -function callAgent(agentId: string, method: string, params?: unknown): Promise { - return new Promise((resolve, reject) => { - const messageId = client.send(agentId, RequestAction, { - method, - params, - }); - - pendingRequests.set(messageId, (response) => { - if (isResponseSuccess(response)) { - resolve(response.payload); - } else { - reject(new Error(response.error.message)); - } - }); - - // Timeout after 30 seconds - setTimeout(() => { - if (pendingRequests.has(messageId)) { - pendingRequests.delete(messageId); - reject(new Error("Request timeout")); - } - }, 30000); - }); -} - -// Handle responses -client.onMessage((message) => { - if (message.action === ResponseAction) { - const payload = message.payload as ResponsePayload; - const callback = pendingRequests.get(payload.requestId); - if (callback) { - pendingRequests.delete(payload.requestId); - callback(payload); - } - } -}); - -// Usage -const result = await callAgent<{ data: string }>("agent-001", "getData", { id: 123 }); -``` - -### Receiving Streams - -```typescript -import { StreamAction, type StreamPayload } from "../shared/gateway-sdk/index.js"; - -// Track active streams -const activeStreams = new Map void>(); - -client.onMessage((message) => { - if (message.action === StreamAction) { - const payload = message.payload as StreamPayload; - const handler = activeStreams.get(payload.streamId); - if (handler) { - handler(payload.data); - } - } -}); - -// Subscribe to a stream -function subscribeToStream(streamId: string, onData: (data: unknown) => void) { - activeStreams.set(streamId, onData); - return () => activeStreams.delete(streamId); // Unsubscribe function -} -``` - -### Disconnecting - -```typescript -client.disconnect(); -``` - -### Checking Connection Status - -```typescript -client.isConnected; // true if connected or registered -client.isRegistered; // true if registered (can send messages) -client.state; // Current ConnectionState -client.deviceId; // Device ID -client.socketId; // Socket ID (available after connect) -``` - -## Agent Implementation - -### Basic Setup - -```typescript -import { GatewayClient } from "../shared/gateway-sdk/index.js"; - -const agent = new GatewayClient({ - url: "http://localhost:3000", - deviceId: "agent-001", - deviceType: "agent", - metadata: { - name: "Processing Agent", - capabilities: ["chat", "image-generation"], - }, -}); -``` - -### Handling Requests - -```typescript -import { - HelloAction, - HelloResponseAction, - RequestAction, - ResponseAction, - type HelloPayload, - type HelloResponsePayload, - type RequestPayload, - type ResponseSuccessPayload, - type ResponseErrorPayload, -} from "../shared/gateway-sdk/index.js"; - -agent - .onRegistered((deviceId) => { - console.log("Agent registered:", deviceId); - }) - .onMessage(async (message) => { - // Handle hello action - if (message.action === HelloAction) { - const payload = message.payload as HelloPayload; - agent.send(message.from, HelloResponseAction, { - reply: `Hello! You said: "${payload.greeting}"`, - }); - return; - } - - // Handle RPC requests - if (message.action === RequestAction) { - const request = message.payload as RequestPayload; - - try { - const result = await processRequest(request.method, request.params); - agent.send(message.from, ResponseAction, { - requestId: message.id, - ok: true, - payload: result, - }); - } catch (error) { - agent.send(message.from, ResponseAction, { - requestId: message.id, - ok: false, - error: { - code: "PROCESSING_ERROR", - message: error instanceof Error ? error.message : "Unknown error", - retryable: false, - }, - }); - } - } - }) - .connect(); - -async function processRequest(method: string, params: unknown): Promise { - switch (method) { - case "getData": - return { data: "some data" }; - case "processImage": - // Process image... - return { url: "https://..." }; - default: - throw new Error(`Unknown method: ${method}`); - } -} -``` - -### Sending Streams - -```typescript -import { StreamAction, type StreamPayload } from "../shared/gateway-sdk/index.js"; -import { v7 as uuidv7 } from "uuid"; - -async function sendStream(clientId: string, generateChunks: AsyncIterable) { - const streamId = uuidv7(); - - for await (const chunk of generateChunks) { - agent.send>(clientId, StreamAction, { - streamId, - data: chunk, - }); - } - - // Send end-of-stream marker - agent.send>(clientId, StreamAction, { - streamId, - data: null, - }); -} - -// Usage with an async generator -async function* generateResponse(): AsyncIterable { - yield "Hello"; - yield " "; - yield "World"; - yield "!"; -} - -sendStream("client-001", generateResponse()); -``` - -### Multiple Agent Instances - -For scaling, run multiple agent instances with unique IDs: - -```typescript -const agentId = `agent-${process.env.INSTANCE_ID || uuidv7()}`; - -const agent = new GatewayClient({ - url: process.env.GATEWAY_URL || "http://localhost:3000", - deviceId: agentId, - deviceType: "agent", - metadata: { - instanceId: process.env.INSTANCE_ID, - region: process.env.REGION, - }, -}); -``` - -## Predefined Actions - -### Hello Action - -Simple greeting for testing connectivity. - -```typescript -// Client sends -client.send("agent-001", HelloAction, { - greeting: "Hello!", -}); - -// Agent responds -agent.send(message.from, HelloResponseAction, { - reply: "Hi there!", -}); -``` - -### Request/Response Action (RPC) - -For request-response patterns. - -```typescript -// Request -interface RequestPayload { - method: string; - params?: T; -} - -// Success Response -interface ResponseSuccessPayload { - requestId: string; - ok: true; - payload: T; -} - -// Error Response -interface ResponseErrorPayload { - requestId: string; - ok: false; - error: { - code: string; - message: string; - retryable?: boolean; - }; -} -``` - -### Stream Action - -For streaming data (e.g., LLM token streaming). - -```typescript -interface StreamPayload { - streamId: string; // Correlates all messages in a stream - data: T; // Chunk data, null indicates end-of-stream -} -``` - -## Error Handling - -### Send Errors - -```typescript -client.onSendError((error) => { - switch (error.code) { - case "DEVICE_NOT_FOUND": - console.error(`Device ${error.messageId} not found`); - break; - case "NOT_REGISTERED": - console.error("You are not registered"); - break; - case "INVALID_MESSAGE": - console.error("Invalid message format"); - break; - } -}); -``` - -### Connection Errors - -```typescript -client.onError((error) => { - console.error("Connection error:", error.message); - // SDK will auto-reconnect if autoReconnect is true -}); -``` - -## Type Safety - -Use generics for type-safe payloads: - -```typescript -// Define your payload types -interface MyRequestPayload { - query: string; - limit: number; -} - -interface MyResponsePayload { - results: string[]; - total: number; -} - -// Send with type safety -client.send("agent-001", "search", { - query: "hello", - limit: 10, -}); - -// Receive with type assertion -agent.onMessage((message) => { - if (message.action === "search") { - const payload = message.payload as MyRequestPayload; - // payload.query and payload.limit are typed - } -}); -``` - -## Complete Example: Chat Application - -### Client Side - -```typescript -import { - GatewayClient, - RequestAction, - ResponseAction, - StreamAction, - type RequestPayload, - type ResponsePayload, - type StreamPayload, - isResponseSuccess, -} from "../shared/gateway-sdk/index.js"; - -const client = new GatewayClient({ - url: "http://localhost:3000", - deviceId: `user-${Date.now()}`, - deviceType: "client", -}); - -// Collect stream chunks -const streamBuffers = new Map(); - -client - .onMessage((message) => { - if (message.action === StreamAction) { - const { streamId, data } = message.payload as StreamPayload; - - if (data === null) { - // Stream ended - const chunks = streamBuffers.get(streamId) || []; - console.log("Complete response:", chunks.join("")); - streamBuffers.delete(streamId); - } else { - // Accumulate chunk - const chunks = streamBuffers.get(streamId) || []; - chunks.push(data); - streamBuffers.set(streamId, chunks); - process.stdout.write(data); // Print chunk immediately - } - } - }) - .onRegistered(() => { - // Send a chat message - client.send("chat-agent", RequestAction, { - method: "chat", - params: { message: "Tell me a joke" }, - }); - }) - .connect(); -``` - -### Agent Side - -```typescript -import { - GatewayClient, - RequestAction, - StreamAction, - type RequestPayload, - type StreamPayload, -} from "../shared/gateway-sdk/index.js"; -import { v7 as uuidv7 } from "uuid"; - -const agent = new GatewayClient({ - url: "http://localhost:3000", - deviceId: "chat-agent", - deviceType: "agent", -}); - -agent - .onMessage(async (message) => { - if (message.action === RequestAction) { - const { method, params } = message.payload as RequestPayload<{ message: string }>; - - if (method === "chat") { - const streamId = uuidv7(); - - // Simulate streaming response - const response = "Why did the programmer quit? Because he didn't get arrays!"; - - for (const char of response) { - agent.send>(message.from, StreamAction, { - streamId, - data: char, - }); - await sleep(50); // Simulate delay - } - - // End stream - agent.send>(message.from, StreamAction, { - streamId, - data: null, - }); - } - } - }) - .connect(); - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} -``` diff --git a/src/shared/gateway-sdk/actions/hello.ts b/src/shared/gateway-sdk/actions/hello.ts deleted file mode 100644 index 36e56ba8..00000000 --- a/src/shared/gateway-sdk/actions/hello.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** Hello Action - test greeting message */ - -export const HelloAction = "hello" as const; -export const HelloResponseAction = "hello_response" as const; - -/** Hello request payload */ -export interface HelloPayload { - greeting: string; -} - -/** Hello response payload */ -export interface HelloResponsePayload { - reply: string; -} diff --git a/src/shared/gateway-sdk/actions/index.ts b/src/shared/gateway-sdk/actions/index.ts deleted file mode 100644 index 8b49ef75..00000000 --- a/src/shared/gateway-sdk/actions/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { - HelloAction, - HelloResponseAction, - type HelloPayload, - type HelloResponsePayload, -} from "./hello.js"; - -export { - RequestAction, - ResponseAction, - type RequestPayload, - type ResponsePayload, - type ResponseSuccessPayload, - type ResponseErrorPayload, - isResponseSuccess, - isResponseError, -} from "./rpc.js"; - -export { StreamAction, type StreamPayload } from "./stream.js"; diff --git a/src/shared/gateway-sdk/actions/rpc.ts b/src/shared/gateway-sdk/actions/rpc.ts deleted file mode 100644 index 15344de2..00000000 --- a/src/shared/gateway-sdk/actions/rpc.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** RPC Actions - 请求/响应模式 */ - -export const RequestAction = "request" as const; -export const ResponseAction = "response" as const; - -/** 请求帧 payload */ -export interface RequestPayload { - /** 调用的方法名 */ - method: string; - /** 方法参数 */ - params?: T; -} - -/** 响应帧 payload - 成功 */ -export interface ResponseSuccessPayload { - /** 与请求消息 ID 匹配 */ - requestId: string; - /** 是否成功 */ - ok: true; - /** 返回数据 */ - payload: T; -} - -/** 响应帧 payload - 失败 */ -export interface ResponseErrorPayload { - /** 与请求消息 ID 匹配 */ - requestId: string; - /** 是否成功 */ - ok: false; - /** 错误信息 */ - error: { - code: string; - message: string; - retryable?: boolean; - }; -} - -/** 响应帧 payload(联合类型) */ -export type ResponsePayload = - | ResponseSuccessPayload - | ResponseErrorPayload; - -/** 类型守卫:判断响应是否成功 */ -export function isResponseSuccess( - response: ResponsePayload -): response is ResponseSuccessPayload { - return response.ok === true; -} - -/** 类型守卫:判断响应是否失败 */ -export function isResponseError( - response: ResponsePayload -): response is ResponseErrorPayload { - return response.ok === false; -} diff --git a/src/shared/gateway-sdk/actions/stream.ts b/src/shared/gateway-sdk/actions/stream.ts deleted file mode 100644 index 98f52423..00000000 --- a/src/shared/gateway-sdk/actions/stream.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** Stream Action - 流式消息传输 */ - -export const StreamAction = "stream" as const; - -/** 流消息 payload */ -export interface StreamPayload { - /** 流 ID,用于关联同一个流的所有消息 */ - streamId: string; - /** 数据 */ - data: T; -} diff --git a/src/shared/gateway-sdk/client.ts b/src/shared/gateway-sdk/client.ts deleted file mode 100644 index 4a126a2d..00000000 --- a/src/shared/gateway-sdk/client.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { io, Socket } from "socket.io-client"; -import { v7 as uuidv7 } from "uuid"; -import type { - GatewayClientOptions, - GatewayClientCallbacks, - ConnectionState, - RoutedMessage, - RegisteredResponse, - SendErrorResponse, - PingPayload, - DeviceType, -} from "./types.js"; -import { GatewayEvents } from "./types.js"; - -interface ResolvedOptions { - url: string; - path: string; - deviceId: string; - deviceType: DeviceType; - autoReconnect: boolean; - reconnectDelay: number; -} - -export class GatewayClient { - private socket: Socket | null = null; - private options: ResolvedOptions; - private callbacks: GatewayClientCallbacks = {}; - private _state: ConnectionState = "disconnected"; - - constructor(options: GatewayClientOptions) { - if (!options.deviceId) { - throw new Error("deviceId is required"); - } - - this.options = { - url: options.url, - path: options.path ?? "/ws", - deviceId: options.deviceId, - deviceType: options.deviceType, - autoReconnect: options.autoReconnect ?? true, - reconnectDelay: options.reconnectDelay ?? 1000, - }; - } - - /** 当前连接状态 */ - get state(): ConnectionState { - return this._state; - } - - /** 设备ID */ - get deviceId(): string { - return this.options.deviceId; - } - - /** 设备类型 */ - get deviceType(): DeviceType { - return this.options.deviceType; - } - - /** Socket ID(连接后可用) */ - get socketId(): string | undefined { - return this.socket?.id; - } - - /** 是否已连接 */ - get isConnected(): boolean { - return this._state === "connected" || this._state === "registered"; - } - - /** 是否已注册 */ - get isRegistered(): boolean { - return this._state === "registered"; - } - - /** 连接到服务器,deviceId 和 deviceType 通过 query 传递 */ - connect(): this { - if (this.socket) { - return this; - } - - this.setState("connecting"); - - const query: Record = { - deviceId: this.options.deviceId, - deviceType: this.options.deviceType, - }; - - this.socket = io(this.options.url, { - path: this.options.path, - query, - reconnection: this.options.autoReconnect, - reconnectionDelay: this.options.reconnectDelay, - }); - - this.setupListeners(); - return this; - } - - /** 断开连接 */ - disconnect(): this { - if (this.socket) { - this.socket.disconnect(); - this.socket = null; - } - this.setState("disconnected"); - return this; - } - - /** 发送消息给指定设备 */ - send( - to: string, - action: string, - payload: T, - messageId?: string - ): string { - if (!this.socket || !this.isRegistered) { - throw new Error("Not registered"); - } - - const id = messageId ?? this.generateMessageId(); - const message: RoutedMessage = { - id, - uid: null, - from: this.options.deviceId, - to, - action, - payload, - }; - - this.socket.emit(GatewayEvents.SEND, message); - return id; - } - - /** 发送 ping */ - ping(data: PingPayload = {}): Promise { - return new Promise((resolve, reject) => { - if (!this.socket || !this.isConnected) { - reject(new Error("Not connected")); - return; - } - - this.socket.emit( - GatewayEvents.PING, - data, - (response: { event: string; data: string }) => { - resolve(response.data); - } - ); - }); - } - - /** 注册连接回调 */ - onConnect(callback: (socketId: string) => void): this { - this.callbacks.onConnect = callback; - return this; - } - - /** 注册断开回调 */ - onDisconnect(callback: (reason: string) => void): this { - this.callbacks.onDisconnect = callback; - return this; - } - - /** 注册成功回调 */ - onRegistered(callback: (deviceId: string) => void): this { - this.callbacks.onRegistered = callback; - return this; - } - - /** 注册消息回调 */ - onMessage(callback: (message: RoutedMessage) => void): this { - this.callbacks.onMessage = callback; - return this; - } - - /** 注册发送失败回调 */ - onSendError(callback: (error: SendErrorResponse) => void): this { - this.callbacks.onSendError = callback; - return this; - } - - /** 注册 pong 回调 */ - onPong(callback: (data: string) => void): this { - this.callbacks.onPong = callback; - return this; - } - - /** 注册错误回调 */ - onError(callback: (error: Error) => void): this { - this.callbacks.onError = callback; - return this; - } - - /** 注册状态变化回调 */ - onStateChange(callback: (state: ConnectionState) => void): this { - this.callbacks.onStateChange = callback; - return this; - } - - private setState(state: ConnectionState): void { - if (this._state !== state) { - this._state = state; - this.callbacks.onStateChange?.(state); - } - } - - private generateMessageId(): string { - return uuidv7(); - } - - private setupListeners(): void { - if (!this.socket) return; - - this.socket.on("connect", () => { - this.setState("connected"); - this.callbacks.onConnect?.(this.socket!.id!); - // 服务端在连接时从 query 自动注册,等待 registered 事件即可 - }); - - this.socket.on("disconnect", (reason: string) => { - this.setState("disconnected"); - this.callbacks.onDisconnect?.(reason); - }); - - this.socket.on( - GatewayEvents.REGISTERED, - (response: RegisteredResponse) => { - if (response.success) { - this.setState("registered"); - this.callbacks.onRegistered?.(response.deviceId); - } else { - this.callbacks.onError?.(new Error(response.error ?? "Registration failed")); - } - } - ); - - this.socket.on(GatewayEvents.RECEIVE, (message: RoutedMessage) => { - this.callbacks.onMessage?.(message); - }); - - this.socket.on(GatewayEvents.SEND_ERROR, (error: SendErrorResponse) => { - this.callbacks.onSendError?.(error); - }); - - this.socket.on(GatewayEvents.PONG, (data: string) => { - this.callbacks.onPong?.(data); - }); - - this.socket.on("connect_error", (error: Error) => { - this.callbacks.onError?.(error); - }); - } -} diff --git a/src/shared/gateway-sdk/index.ts b/src/shared/gateway-sdk/index.ts deleted file mode 100644 index e1b696f2..00000000 --- a/src/shared/gateway-sdk/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { GatewayClient } from "./client.js"; -export { - GatewayEvents, - type DeviceType, - type DeviceInfo, - type RegisteredResponse, - type RoutedMessage, - type SendErrorResponse, - type GatewayClientOptions, - type GatewayClientCallbacks, - type ConnectionState, - type PingPayload, - type PongResponse, -} from "./types.js"; - -// Actions -export * from "./actions/index.js"; diff --git a/src/shared/gateway-sdk/types.ts b/src/shared/gateway-sdk/types.ts deleted file mode 100644 index 9911c64e..00000000 --- a/src/shared/gateway-sdk/types.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** WebSocket event names */ -export const GatewayEvents = { - // System events - PING: "ping", - PONG: "pong", - REGISTERED: "registered", - - // Message routing - SEND: "send", - RECEIVE: "receive", - SEND_ERROR: "send_error", -} as const; - -// ============ Device Related ============ - -/** Device type */ -export type DeviceType = "client" | "agent"; - -/** Device information */ -export interface DeviceInfo { - deviceId: string; - deviceType: DeviceType; -} - -/** Registration response */ -export interface RegisteredResponse { - success: boolean; - deviceId: string; - error?: string; -} - -// ============ Message Routing ============ - -/** Routed message */ -export interface RoutedMessage { - /** Unique message ID (UUID v7, contains timestamp) */ - id: string; - /** User ID (populated after login) */ - uid: string | null; - /** Sender deviceId */ - from: string; - /** Recipient deviceId */ - to: string; - /** Action type */ - action: string; - /** Message payload */ - payload: T; -} - -/** Send failure response */ -export interface SendErrorResponse { - messageId: string; - error: string; - code: "DEVICE_NOT_FOUND" | "NOT_REGISTERED" | "INVALID_MESSAGE"; -} - -// ============ Ping/Pong ============ - -/** Ping request */ -export interface PingPayload { - [key: string]: unknown; -} - -/** Ping response */ -export interface PongResponse { - event: string; - data: string; -} - -// ============ Client Configuration ============ - -/** Connection configuration */ -export interface GatewayClientOptions { - /** Server address, e.g. http://localhost:3000 */ - url: string; - /** WebSocket path, defaults to /ws */ - path?: string | undefined; - /** Device ID */ - deviceId: string; - /** Device type */ - deviceType: DeviceType; - /** Auto reconnect, defaults to true */ - autoReconnect?: boolean | undefined; - /** Reconnect delay (milliseconds), defaults to 1000 */ - reconnectDelay?: number | undefined; -} - -/** Connection state */ -export type ConnectionState = - | "disconnected" - | "connecting" - | "connected" - | "registered"; - -/** Event callback types */ -export interface GatewayClientCallbacks { - onConnect?: (socketId: string) => void; - onDisconnect?: (reason: string) => void; - onRegistered?: (deviceId: string) => void; - onMessage?: (message: RoutedMessage) => void; - onSendError?: (error: SendErrorResponse) => void; - onPong?: (data: string) => void; - onError?: (error: Error) => void; - onStateChange?: (state: ConnectionState) => void; -} - diff --git a/src/shared/index.ts b/src/shared/index.ts index f20f47fd..cd47d49f 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -3,4 +3,3 @@ export * from "./paths.js"; export * from "./errors.js"; export * from "./retry.js"; export * from "./cancellation.js"; -export * from "./gateway-sdk/index.js";