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:
parent
f4500ad148
commit
3d7b13555f
17 changed files with 42 additions and 1108 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
3
pnpm-lock.yaml
generated
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
type PongResponse,
|
||||
type DeviceInfo,
|
||||
type DeviceType,
|
||||
} from "../shared/gateway-sdk/index.js";
|
||||
} from "@multica/sdk";
|
||||
|
||||
@Injectable()
|
||||
@WebSocketGateway({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
HelloResponseAction,
|
||||
type HelloPayload,
|
||||
type HelloResponsePayload,
|
||||
} from "../shared/gateway-sdk/index.js";
|
||||
} from "@multica/sdk";
|
||||
|
||||
// 模拟一个 Client
|
||||
const client = new GatewayClient({
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
```
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
/** Stream Action - 流式消息传输 */
|
||||
|
||||
export const StreamAction = "stream" as const;
|
||||
|
||||
/** 流消息 payload */
|
||||
export interface StreamPayload<T = unknown> {
|
||||
/** 流 ID,用于关联同一个流的所有消息 */
|
||||
streamId: string;
|
||||
/** 数据 */
|
||||
data: T;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue