Merge pull request #2 from multica-ai/ldnvnbl/gateway-docker
Add Docker support for Gateway
This commit is contained in:
commit
4b3592b5e4
8 changed files with 724 additions and 8 deletions
45
.dockerignore
Normal file
45
.dockerignore
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# Build output
|
||||
dist
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose*.yml
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
docs
|
||||
|
||||
# Tests
|
||||
*.test.ts
|
||||
*.spec.ts
|
||||
__tests__
|
||||
coverage
|
||||
|
||||
# Context directory
|
||||
.context
|
||||
|
|
@ -18,7 +18,6 @@
|
|||
"devDependencies": {
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
|
|
@ -35,6 +34,7 @@
|
|||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"uuid": "^13.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
|
|
@ -44,6 +44,9 @@ importers:
|
|||
socket.io:
|
||||
specifier: ^4.8.3
|
||||
version: 4.8.3
|
||||
socket.io-client:
|
||||
specifier: ^4.8.3
|
||||
version: 4.8.3
|
||||
uuid:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
|
@ -54,9 +57,6 @@ importers:
|
|||
'@types/uuid':
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
socket.io-client:
|
||||
specifier: ^4.8.3
|
||||
version: 4.8.3
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
|
|
|
|||
47
src/gateway/Dockerfile
Normal file
47
src/gateway/Dockerfile
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Build stage
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@10.16.1 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install all dependencies (including devDependencies for build)
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
|
||||
# Build TypeScript
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:22-alpine AS production
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@10.16.1 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install production dependencies only
|
||||
RUN pnpm install --frozen-lockfile --prod
|
||||
|
||||
# Copy built files from builder stage
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 3000
|
||||
|
||||
# Run the gateway
|
||||
CMD ["node", "dist/gateway/main.js"]
|
||||
33
src/gateway/scripts/build-and-push.sh
Executable file
33
src/gateway/scripts/build-and-push.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
GATEWAY_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
PROJECT_ROOT="$(cd "$GATEWAY_DIR/../.." && pwd)"
|
||||
|
||||
REPO="085931705009.dkr.ecr.us-west-2.amazonaws.com/super-multica/gateway"
|
||||
BRANCH="$(git symbolic-ref --short -q HEAD | tr '/' '-')"
|
||||
IMAGE_TAG="$(date +%F_%H-%M-%S)-${BRANCH}-$(git rev-parse --short HEAD)"
|
||||
IMAGE="$REPO:$IMAGE_TAG"
|
||||
|
||||
# Determine if sudo is needed for docker commands
|
||||
if [[ "$(uname -s)" == "Linux" ]]; then
|
||||
DOCKER_CMD="sudo docker"
|
||||
else
|
||||
DOCKER_CMD="docker"
|
||||
fi
|
||||
|
||||
echo "Building image: $IMAGE"
|
||||
echo "Using Dockerfile: $GATEWAY_DIR/Dockerfile"
|
||||
echo "Build context: $PROJECT_ROOT"
|
||||
|
||||
# Login to ECR
|
||||
aws ecr get-login-password --region us-west-2 | $DOCKER_CMD login --username AWS --password-stdin 085931705009.dkr.ecr.us-west-2.amazonaws.com
|
||||
|
||||
# Build from project root with gateway Dockerfile
|
||||
$DOCKER_CMD build -f "$GATEWAY_DIR/Dockerfile" -t "$IMAGE" "$PROJECT_ROOT"
|
||||
$DOCKER_CMD push "$IMAGE"
|
||||
|
||||
echo ""
|
||||
echo "Successfully pushed: $IMAGE"
|
||||
594
src/shared/gateway-sdk/USAGE.md
Normal file
594
src/shared/gateway-sdk/USAGE.md
Normal file
|
|
@ -0,0 +1,594 @@
|
|||
# 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));
|
||||
}
|
||||
```
|
||||
|
|
@ -121,7 +121,6 @@ export class GatewayClient {
|
|||
to,
|
||||
action,
|
||||
payload,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.socket.emit(GatewayEvents.SEND, message);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export interface RegisteredResponse {
|
|||
|
||||
/** 路由消息 */
|
||||
export interface RoutedMessage<T = unknown> {
|
||||
/** 消息唯一ID */
|
||||
/** 消息唯一ID (UUID v7,包含时间戳) */
|
||||
id: string;
|
||||
/** 用户ID(登录后填充) */
|
||||
uid: string | null;
|
||||
|
|
@ -54,8 +54,6 @@ export interface RoutedMessage<T = unknown> {
|
|||
action: string;
|
||||
/** 消息内容 */
|
||||
payload: T;
|
||||
/** 时间戳 */
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/** 发送失败响应 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue