refactor(sdk): unify gateway-sdk into @multica/sdk package

Sync latest code from src/shared/gateway-sdk/ into packages/sdk/,
update all backend imports to use @multica/sdk, and remove the
duplicate src/shared/gateway-sdk/ directory.

- Translate Chinese comments to English in SDK source
- Fix package.json exports with default condition
- Add @multica/sdk as workspace dependency for backend
- Update imports in gateway, test-client, and hub

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-02-02 13:43:52 +08:00
parent f4500ad148
commit 3d7b13555f
17 changed files with 42 additions and 1108 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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;
}

View file

@ -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<T = unknown> {
/** 消息唯一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;
}

3
pnpm-lock.yaml generated
View file

@ -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)

View file

@ -21,7 +21,7 @@ import {
type PongResponse,
type DeviceInfo,
type DeviceType,
} from "../shared/gateway-sdk/index.js";
} from "@multica/sdk";
@Injectable()
@WebSocketGateway({

View file

@ -4,7 +4,7 @@ import {
HelloResponseAction,
type HelloPayload,
type HelloResponsePayload,
} from "../shared/gateway-sdk/index.js";
} from "@multica/sdk";
// 模拟一个 Client
const client = new GatewayClient({

View file

@ -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 {

View file

@ -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<T = unknown> {
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<HelloPayload>("agent-001", HelloAction, {
greeting: "Hello from client!",
});
// With custom message ID
const messageId = client.send<HelloPayload>(
"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<string, (response: ResponsePayload) => void>();
// Send RPC request
function callAgent<T>(agentId: string, method: string, params?: unknown): Promise<T> {
return new Promise((resolve, reject) => {
const messageId = client.send<RequestPayload>(agentId, RequestAction, {
method,
params,
});
pendingRequests.set(messageId, (response) => {
if (isResponseSuccess<T>(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<string, (data: unknown) => 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<HelloResponsePayload>(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<ResponseSuccessPayload>(message.from, ResponseAction, {
requestId: message.id,
ok: true,
payload: result,
});
} catch (error) {
agent.send<ResponseErrorPayload>(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<unknown> {
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<string>) {
const streamId = uuidv7();
for await (const chunk of generateChunks) {
agent.send<StreamPayload<string>>(clientId, StreamAction, {
streamId,
data: chunk,
});
}
// Send end-of-stream marker
agent.send<StreamPayload<null>>(clientId, StreamAction, {
streamId,
data: null,
});
}
// Usage with an async generator
async function* generateResponse(): AsyncIterable<string> {
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<HelloPayload>("agent-001", HelloAction, {
greeting: "Hello!",
});
// Agent responds
agent.send<HelloResponsePayload>(message.from, HelloResponseAction, {
reply: "Hi there!",
});
```
### Request/Response Action (RPC)
For request-response patterns.
```typescript
// Request
interface RequestPayload<T = unknown> {
method: string;
params?: T;
}
// Success Response
interface ResponseSuccessPayload<T = unknown> {
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<T = unknown> {
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<MyRequestPayload>("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<string, string[]>();
client
.onMessage((message) => {
if (message.action === StreamAction) {
const { streamId, data } = message.payload as StreamPayload<string | null>;
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<RequestPayload>("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<StreamPayload<string>>(message.from, StreamAction, {
streamId,
data: char,
});
await sleep(50); // Simulate delay
}
// End stream
agent.send<StreamPayload<null>>(message.from, StreamAction, {
streamId,
data: null,
});
}
}
})
.connect();
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
```

View file

@ -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;
}

View file

@ -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";

View file

@ -1,55 +0,0 @@
/** RPC Actions - 请求/响应模式 */
export const RequestAction = "request" as const;
export const ResponseAction = "response" as const;
/** 请求帧 payload */
export interface RequestPayload<T = unknown> {
/** 调用的方法名 */
method: string;
/** 方法参数 */
params?: T;
}
/** 响应帧 payload - 成功 */
export interface ResponseSuccessPayload<T = unknown> {
/** 与请求消息 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<T = unknown> =
| ResponseSuccessPayload<T>
| ResponseErrorPayload;
/** 类型守卫:判断响应是否成功 */
export function isResponseSuccess<T>(
response: ResponsePayload<T>
): response is ResponseSuccessPayload<T> {
return response.ok === true;
}
/** 类型守卫:判断响应是否失败 */
export function isResponseError(
response: ResponsePayload
): response is ResponseErrorPayload {
return response.ok === false;
}

View file

@ -1,11 +0,0 @@
/** Stream Action - 流式消息传输 */
export const StreamAction = "stream" as const;
/** 流消息 payload */
export interface StreamPayload<T = unknown> {
/** 流 ID用于关联同一个流的所有消息 */
streamId: string;
/** 数据 */
data: T;
}

View file

@ -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<string, string> = {
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<T = unknown>(
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<T> = {
id,
uid: null,
from: this.options.deviceId,
to,
action,
payload,
};
this.socket.emit(GatewayEvents.SEND, message);
return id;
}
/** 发送 ping */
ping(data: PingPayload = {}): Promise<string> {
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);
});
}
}

View file

@ -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";

View file

@ -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<T = unknown> {
/** 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;
}

View file

@ -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";