openclaw-liveavatar 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +42 -0
- package/.next/app-path-routes-manifest.json +6 -0
- package/.next/build-manifest.json +33 -0
- package/.next/cache/.previewinfo +1 -0
- package/.next/cache/.rscinfo +1 -0
- package/.next/cache/.tsbuildinfo +1 -0
- package/.next/cache/chrome-devtools-workspace-uuid +1 -0
- package/.next/cache/next-devtools-config.json +1 -0
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/3.pack +0 -0
- package/.next/cache/webpack/client-production/4.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +58 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +61 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +320 -0
- package/.next/routes-manifest.json +53 -0
- package/.next/server/app/_not-found/page.js +5 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +4 -0
- package/.next/server/app/_not-found.meta +8 -0
- package/.next/server/app/_not-found.rsc +15 -0
- package/.next/server/app/api/get-avatars/route.js +1 -0
- package/.next/server/app/api/get-avatars/route.js.nft.json +1 -0
- package/.next/server/app/api/get-avatars/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/start-session/route.js +1 -0
- package/.next/server/app/api/start-session/route.js.nft.json +1 -0
- package/.next/server/app/api/start-session/route_client-reference-manifest.js +1 -0
- package/.next/server/app/index.html +4 -0
- package/.next/server/app/index.meta +7 -0
- package/.next/server/app/index.rsc +16 -0
- package/.next/server/app/page.js +9 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +6 -0
- package/.next/server/chunks/361.js +9 -0
- package/.next/server/chunks/611.js +6 -0
- package/.next/server/chunks/873.js +22 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +4 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +19 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +6 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/144d3bae-37bcc55d23f188ee.js +1 -0
- package/.next/static/chunks/255-35bf8c00c5dde345.js +1 -0
- package/.next/static/chunks/336-a66237a0a1db954a.js +1 -0
- package/.next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
- package/.next/static/chunks/app/_not-found/page-dfc6e5d8e6c6203c.js +1 -0
- package/.next/static/chunks/app/api/get-avatars/route-8017e1cff542d5d0.js +1 -0
- package/.next/static/chunks/app/api/start-session/route-8017e1cff542d5d0.js +1 -0
- package/.next/static/chunks/app/layout-ff675313cc8f8fcf.js +1 -0
- package/.next/static/chunks/app/page-9e4b703722bef650.js +1 -0
- package/.next/static/chunks/framework-de98b93a850cfc71.js +1 -0
- package/.next/static/chunks/main-1a0dcce460eb61ce.js +1 -0
- package/.next/static/chunks/main-app-e7f1007edc7ad7e1.js +1 -0
- package/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
- package/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-4a462cecab786e93.js +1 -0
- package/.next/static/css/bfd73afa11897439.css +3 -0
- package/.next/static/v_GdCj8lVweDVhmIhhEcM/_buildManifest.js +1 -0
- package/.next/static/v_GdCj8lVweDVhmIhhEcM/_ssgManifest.js +1 -0
- package/.next/trace +2 -0
- package/.next/types/app/api/get-avatars/route.ts +347 -0
- package/.next/types/app/api/start-session/route.ts +347 -0
- package/.next/types/app/layout.ts +84 -0
- package/.next/types/app/page.ts +84 -0
- package/.next/types/cache-life.d.ts +141 -0
- package/.next/types/package.json +1 -0
- package/.next/types/routes.d.ts +74 -0
- package/.next/types/validator.ts +88 -0
- package/README.md +241 -0
- package/app/api/config.ts +18 -0
- package/app/api/get-avatars/route.ts +117 -0
- package/app/api/start-session/route.ts +95 -0
- package/app/globals.css +3 -0
- package/app/layout.tsx +37 -0
- package/app/page.tsx +9 -0
- package/bin/cli.js +100 -0
- package/package.json +66 -0
- package/src/components/LiveAvatarSession.tsx +825 -0
- package/src/components/OpenClawDemo.tsx +399 -0
- package/src/gateway/client.ts +522 -0
- package/src/gateway/types.ts +83 -0
- package/src/liveavatar/context.tsx +750 -0
- package/src/liveavatar/index.ts +6 -0
- package/src/liveavatar/types.ts +10 -0
- package/src/liveavatar/useAvatarActions.ts +41 -0
- package/src/liveavatar/useChatHistory.ts +7 -0
- package/src/liveavatar/useSession.ts +37 -0
- package/src/liveavatar/useTextChat.ts +32 -0
- package/src/liveavatar/useVoiceChat.ts +70 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
// OpenClaw Gateway WebSocket Client
|
|
2
|
+
// Connects to the local OpenClaw Gateway to send messages to the agent
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
GatewayMessage,
|
|
6
|
+
GatewayResponse,
|
|
7
|
+
GatewayEvent,
|
|
8
|
+
GatewayConnectionState,
|
|
9
|
+
AgentResponse,
|
|
10
|
+
AgentEvent,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
type MessageHandler = (message: GatewayMessage) => void;
|
|
14
|
+
type ConnectionStateHandler = (state: GatewayConnectionState) => void;
|
|
15
|
+
|
|
16
|
+
export class OpenClawGatewayClient {
|
|
17
|
+
private ws: WebSocket | null = null;
|
|
18
|
+
private url: string;
|
|
19
|
+
private token: string;
|
|
20
|
+
private reconnectAttempts = 0;
|
|
21
|
+
private maxReconnectAttempts = 5;
|
|
22
|
+
private reconnectDelay = 1000;
|
|
23
|
+
private messageId = 0;
|
|
24
|
+
private pendingRequests = new Map<
|
|
25
|
+
string,
|
|
26
|
+
{ resolve: (value: unknown) => void; reject: (error: Error) => void }
|
|
27
|
+
>();
|
|
28
|
+
private connectionState: GatewayConnectionState = "disconnected";
|
|
29
|
+
private conversationId: string | null = null;
|
|
30
|
+
private sessionKey: string | null = null;
|
|
31
|
+
|
|
32
|
+
// Event handlers
|
|
33
|
+
private onMessageHandlers: MessageHandler[] = [];
|
|
34
|
+
private onConnectionStateHandlers: ConnectionStateHandler[] = [];
|
|
35
|
+
|
|
36
|
+
constructor(url: string, token?: string) {
|
|
37
|
+
this.url = url;
|
|
38
|
+
this.token = token || "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get state(): GatewayConnectionState {
|
|
42
|
+
return this.connectionState;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private setConnectionState(state: GatewayConnectionState) {
|
|
46
|
+
this.connectionState = state;
|
|
47
|
+
this.onConnectionStateHandlers.forEach((handler) => handler(state));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async connect(): Promise<void> {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
try {
|
|
53
|
+
this.setConnectionState("connecting");
|
|
54
|
+
|
|
55
|
+
// Build URL with token if provided
|
|
56
|
+
let wsUrl = this.url;
|
|
57
|
+
if (this.token) {
|
|
58
|
+
const separator = wsUrl.includes("?") ? "&" : "?";
|
|
59
|
+
wsUrl = `${wsUrl}${separator}token=${encodeURIComponent(this.token)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.ws = new WebSocket(wsUrl);
|
|
63
|
+
|
|
64
|
+
this.ws.onopen = () => {
|
|
65
|
+
console.log("[Gateway] WebSocket connected");
|
|
66
|
+
this.reconnectAttempts = 0;
|
|
67
|
+
this.performHandshake()
|
|
68
|
+
.then(() => {
|
|
69
|
+
this.setConnectionState("connected");
|
|
70
|
+
resolve();
|
|
71
|
+
})
|
|
72
|
+
.catch((err) => {
|
|
73
|
+
this.setConnectionState("error");
|
|
74
|
+
reject(err);
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
this.ws.onclose = (event) => {
|
|
79
|
+
console.log("[Gateway] WebSocket closed:", event.code, event.reason);
|
|
80
|
+
this.setConnectionState("disconnected");
|
|
81
|
+
this.handleReconnect();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
this.ws.onerror = (error) => {
|
|
85
|
+
console.error("[Gateway] WebSocket error:", error);
|
|
86
|
+
this.setConnectionState("error");
|
|
87
|
+
reject(new Error("WebSocket connection failed"));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
this.ws.onmessage = (event) => {
|
|
91
|
+
this.handleMessage(event.data);
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
this.setConnectionState("error");
|
|
95
|
+
reject(error);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async performHandshake(): Promise<void> {
|
|
101
|
+
// Send connect request matching OpenClaw protocol schema
|
|
102
|
+
// Protocol version 3 is required by OpenClaw Gateway 2026.1.30
|
|
103
|
+
const connectRequest: Record<string, unknown> = {
|
|
104
|
+
minProtocol: 3,
|
|
105
|
+
maxProtocol: 3,
|
|
106
|
+
client: {
|
|
107
|
+
id: "webchat" as const,
|
|
108
|
+
version: "0.1.0",
|
|
109
|
+
platform: "web",
|
|
110
|
+
mode: "webchat" as const,
|
|
111
|
+
displayName: "OpenClaw LiveAvatar",
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Add token auth if provided - this allows skipping device identity
|
|
116
|
+
if (this.token) {
|
|
117
|
+
connectRequest.auth = { token: this.token };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const response = (await this.sendRequest("connect", connectRequest)) as {
|
|
121
|
+
type?: string;
|
|
122
|
+
snapshot?: {
|
|
123
|
+
sessionDefaults?: {
|
|
124
|
+
mainSessionKey?: string;
|
|
125
|
+
defaultAgentId?: string;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Capture session key for agent event routing
|
|
131
|
+
if (response.snapshot?.sessionDefaults?.mainSessionKey) {
|
|
132
|
+
this.sessionKey = response.snapshot.sessionDefaults.mainSessionKey;
|
|
133
|
+
console.log("[Gateway] Session key captured:", this.sessionKey);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log("[Gateway] Handshake complete:", response);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private handleMessage(data: string) {
|
|
140
|
+
try {
|
|
141
|
+
const message = JSON.parse(data) as GatewayMessage;
|
|
142
|
+
|
|
143
|
+
// Notify all message handlers
|
|
144
|
+
this.onMessageHandlers.forEach((handler) => handler(message));
|
|
145
|
+
|
|
146
|
+
if (message.type === "res") {
|
|
147
|
+
// Handle response to a pending request
|
|
148
|
+
const pending = this.pendingRequests.get(message.id);
|
|
149
|
+
if (pending) {
|
|
150
|
+
this.pendingRequests.delete(message.id);
|
|
151
|
+
if (message.ok) {
|
|
152
|
+
pending.resolve(message.payload);
|
|
153
|
+
} else {
|
|
154
|
+
pending.reject(
|
|
155
|
+
new Error(message.error?.message || "Request failed")
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else if (message.type === "event") {
|
|
160
|
+
// Handle events
|
|
161
|
+
this.handleEvent(message);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error("[Gateway] Failed to parse message:", error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private handleEvent(event: GatewayEvent) {
|
|
169
|
+
// Log all events with full payload for debugging
|
|
170
|
+
console.log("[Gateway] Event:", event.event, JSON.stringify(event.payload));
|
|
171
|
+
|
|
172
|
+
// Handle agent events - these are streamed events with runId, stream, data
|
|
173
|
+
if (event.event === "agent") {
|
|
174
|
+
const payload = event.payload as AgentEvent;
|
|
175
|
+
console.log("[Gateway] Agent event received:", payload.runId, payload.stream, payload.data);
|
|
176
|
+
this.onAgentEventHandlers.forEach((handler) => handler(payload));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Agent event handlers (for streaming events)
|
|
181
|
+
private onAgentEventHandlers: ((event: AgentEvent) => void)[] = [];
|
|
182
|
+
|
|
183
|
+
onAgentEvent(handler: (event: AgentEvent) => void) {
|
|
184
|
+
this.onAgentEventHandlers.push(handler);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
offAgentEvent(handler: (event: AgentEvent) => void) {
|
|
188
|
+
const index = this.onAgentEventHandlers.indexOf(handler);
|
|
189
|
+
if (index > -1) {
|
|
190
|
+
this.onAgentEventHandlers.splice(index, 1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private handleReconnect() {
|
|
195
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
196
|
+
console.log("[Gateway] Max reconnect attempts reached");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.reconnectAttempts++;
|
|
201
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
202
|
+
console.log(
|
|
203
|
+
`[Gateway] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
this.connect().catch((err) => {
|
|
208
|
+
console.error("[Gateway] Reconnect failed:", err);
|
|
209
|
+
});
|
|
210
|
+
}, delay);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async sendRequest(
|
|
214
|
+
method: string,
|
|
215
|
+
params?: Record<string, unknown>
|
|
216
|
+
): Promise<unknown> {
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
219
|
+
reject(new Error("WebSocket not connected"));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const id = `${++this.messageId}`;
|
|
224
|
+
const request = {
|
|
225
|
+
type: "req" as const,
|
|
226
|
+
id,
|
|
227
|
+
method,
|
|
228
|
+
params,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
232
|
+
|
|
233
|
+
// Set timeout for request
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
if (this.pendingRequests.has(id)) {
|
|
236
|
+
this.pendingRequests.delete(id);
|
|
237
|
+
reject(new Error("Request timeout"));
|
|
238
|
+
}
|
|
239
|
+
}, 30000);
|
|
240
|
+
|
|
241
|
+
this.ws.send(JSON.stringify(request));
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Send a message to the OpenClaw agent and get a response
|
|
247
|
+
* Wraps user message with instructions to return structured response with TTS summary
|
|
248
|
+
*/
|
|
249
|
+
async sendToAgent(text: string): Promise<AgentResponse> {
|
|
250
|
+
// Generate a unique idempotency key for this request
|
|
251
|
+
const idempotencyKey = `liveavatar-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
252
|
+
|
|
253
|
+
// Wrap user message with instructions for structured response
|
|
254
|
+
const wrappedMessage = `${text}
|
|
255
|
+
|
|
256
|
+
[RESPONSE FORMAT: Start with a 3-5 sentence spoken summary wrapped in [TTS]...[/TTS] tags that captures the key points of your response, then provide your full detailed response. The TTS summary should be informative and conversational, giving the user the gist while they read the full text. Example:
|
|
257
|
+
[TTS]Here's what I found. The main issue is X, which can be solved by Y. I'd recommend starting with Z approach because it's the most straightforward.[/TTS]
|
|
258
|
+
Full detailed response here with all the specifics...]`;
|
|
259
|
+
|
|
260
|
+
const params: Record<string, unknown> = {
|
|
261
|
+
message: wrappedMessage,
|
|
262
|
+
idempotencyKey,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Include session key for agent event routing
|
|
266
|
+
if (this.sessionKey) {
|
|
267
|
+
params.sessionKey = this.sessionKey;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Include conversation ID for context continuity
|
|
271
|
+
if (this.conversationId) {
|
|
272
|
+
params.conversationId = this.conversationId;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Create promise to collect streaming events BEFORE sending request
|
|
276
|
+
// This prevents race condition where events arrive before handler is registered
|
|
277
|
+
let runId: string | null = null;
|
|
278
|
+
let collectedText = "";
|
|
279
|
+
let completed = false;
|
|
280
|
+
let resolveResponse: (value: AgentResponse) => void;
|
|
281
|
+
let rejectResponse: (error: Error) => void;
|
|
282
|
+
|
|
283
|
+
// Buffer events that arrive before we have runId
|
|
284
|
+
const bufferedEvents: AgentEvent[] = [];
|
|
285
|
+
|
|
286
|
+
const responsePromise = new Promise<AgentResponse>((resolve, reject) => {
|
|
287
|
+
resolveResponse = resolve;
|
|
288
|
+
rejectResponse = reject;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const timeout = setTimeout(() => {
|
|
292
|
+
this.offAgentEvent(handler);
|
|
293
|
+
rejectResponse(new Error("Agent response timeout"));
|
|
294
|
+
}, 60000);
|
|
295
|
+
|
|
296
|
+
// Track processed event sequences to avoid duplicates
|
|
297
|
+
const processedSeqs = new Set<number>();
|
|
298
|
+
|
|
299
|
+
const processEvent = (event: AgentEvent) => {
|
|
300
|
+
if (completed) return;
|
|
301
|
+
|
|
302
|
+
// Deduplicate events by sequence number
|
|
303
|
+
if (event.seq !== undefined && processedSeqs.has(event.seq)) {
|
|
304
|
+
return; // Skip duplicate event
|
|
305
|
+
}
|
|
306
|
+
if (event.seq !== undefined) {
|
|
307
|
+
processedSeqs.add(event.seq);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log("[Gateway] Agent event:", event.stream, event.seq, event.data?.delta?.substring(0, 20));
|
|
311
|
+
|
|
312
|
+
// Collect text from assistant stream - use delta (incremental) not text (cumulative)
|
|
313
|
+
if (event.stream === "assistant") {
|
|
314
|
+
// Only use delta for incremental text
|
|
315
|
+
if (event.data?.delta) {
|
|
316
|
+
collectedText += event.data.delta;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check for lifecycle end
|
|
321
|
+
if (event.stream === "lifecycle" && event.data?.phase === "end") {
|
|
322
|
+
completed = true;
|
|
323
|
+
clearTimeout(timeout);
|
|
324
|
+
this.offAgentEvent(handler);
|
|
325
|
+
resolveResponse({
|
|
326
|
+
runId: runId!,
|
|
327
|
+
status: "completed",
|
|
328
|
+
text: collectedText || undefined,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check for lifecycle error
|
|
333
|
+
if (event.stream === "lifecycle" && event.data?.phase === "error") {
|
|
334
|
+
completed = true;
|
|
335
|
+
clearTimeout(timeout);
|
|
336
|
+
this.offAgentEvent(handler);
|
|
337
|
+
resolveResponse({
|
|
338
|
+
runId: runId!,
|
|
339
|
+
status: "failed",
|
|
340
|
+
text: event.data?.error || "Agent encountered an error",
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const handler = (event: AgentEvent) => {
|
|
346
|
+
// If we don't have runId yet, buffer all events
|
|
347
|
+
if (!runId) {
|
|
348
|
+
bufferedEvents.push(event);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Only handle events for this run
|
|
353
|
+
if (event.runId !== runId) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
processEvent(event);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Register handler BEFORE sending request
|
|
361
|
+
this.onAgentEvent(handler);
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const response = (await this.sendRequest("agent", params)) as {
|
|
365
|
+
runId: string;
|
|
366
|
+
status: string;
|
|
367
|
+
conversationId?: string;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
console.log("[Gateway] Agent request accepted:", response);
|
|
371
|
+
|
|
372
|
+
// Now set runId
|
|
373
|
+
runId = response.runId;
|
|
374
|
+
|
|
375
|
+
// Store conversation ID for future messages
|
|
376
|
+
if (response.conversationId) {
|
|
377
|
+
this.conversationId = response.conversationId;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Process any buffered events that match this runId
|
|
381
|
+
for (const event of bufferedEvents) {
|
|
382
|
+
if (event.runId === runId) {
|
|
383
|
+
processEvent(event);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} catch (error) {
|
|
387
|
+
clearTimeout(timeout);
|
|
388
|
+
this.offAgentEvent(handler);
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return responsePromise;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get current connection status
|
|
397
|
+
*/
|
|
398
|
+
async getStatus(): Promise<unknown> {
|
|
399
|
+
return this.sendRequest("status");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Parse structured response to extract TTS summary and full message
|
|
404
|
+
* Expected format:
|
|
405
|
+
* [TTS]Short summary for speech[/TTS]
|
|
406
|
+
* Full detailed response...
|
|
407
|
+
*/
|
|
408
|
+
parseResponse(text: string): { tts: string; full: string } {
|
|
409
|
+
// Look for [TTS]...[/TTS] block
|
|
410
|
+
const ttsMatch = text.match(/\[TTS\]([\s\S]*?)\[\/TTS\]/i);
|
|
411
|
+
|
|
412
|
+
if (ttsMatch) {
|
|
413
|
+
const ttsSummary = ttsMatch[1].trim();
|
|
414
|
+
// Remove the entire TTS block from display message
|
|
415
|
+
const fullMessage = text.replace(/\[TTS\][\s\S]*?\[\/TTS\]\n?/i, "").trim();
|
|
416
|
+
console.log("[Gateway] Parsed TTS summary:", ttsSummary.length, "chars");
|
|
417
|
+
return { tts: ttsSummary, full: fullMessage || text };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Fallback: extract first 3-5 sentences as TTS
|
|
421
|
+
console.log("[Gateway] No [TTS] block found, extracting first sentences");
|
|
422
|
+
const sentences = text.match(/[^.!?]+[.!?]+/g) || [];
|
|
423
|
+
|
|
424
|
+
let tts = "";
|
|
425
|
+
const maxSentences = 5;
|
|
426
|
+
const maxChars = 400; // Allow longer TTS for more informative summary
|
|
427
|
+
|
|
428
|
+
for (let i = 0; i < Math.min(maxSentences, sentences.length); i++) {
|
|
429
|
+
const sentence = sentences[i].trim();
|
|
430
|
+
if (tts.length + sentence.length > maxChars && tts.length > 0) break;
|
|
431
|
+
tts += (tts ? " " : "") + sentence;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
tts: tts || this.truncateToSentences(text, 300),
|
|
436
|
+
full: text
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Truncate text to complete sentences within a max length
|
|
442
|
+
*/
|
|
443
|
+
private truncateToSentences(text: string, maxLength: number): string {
|
|
444
|
+
if (text.length <= maxLength) return text;
|
|
445
|
+
|
|
446
|
+
const truncated = text.substring(0, maxLength);
|
|
447
|
+
const lastSentence = Math.max(
|
|
448
|
+
truncated.lastIndexOf(". "),
|
|
449
|
+
truncated.lastIndexOf("! "),
|
|
450
|
+
truncated.lastIndexOf("? "),
|
|
451
|
+
truncated.lastIndexOf(".\n"),
|
|
452
|
+
truncated.lastIndexOf("!\n"),
|
|
453
|
+
truncated.lastIndexOf("?\n")
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (lastSentence > maxLength * 0.5) {
|
|
457
|
+
return truncated.substring(0, lastSentence + 1).trim();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Fall back to cutting at last space
|
|
461
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
462
|
+
if (lastSpace > maxLength * 0.7) {
|
|
463
|
+
return truncated.substring(0, lastSpace).trim() + "...";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return truncated.trim() + "...";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
disconnect() {
|
|
470
|
+
if (this.ws) {
|
|
471
|
+
this.ws.close();
|
|
472
|
+
this.ws = null;
|
|
473
|
+
}
|
|
474
|
+
this.setConnectionState("disconnected");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Event subscription methods
|
|
478
|
+
onMessage(handler: MessageHandler) {
|
|
479
|
+
this.onMessageHandlers.push(handler);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
offMessage(handler: MessageHandler) {
|
|
483
|
+
const index = this.onMessageHandlers.indexOf(handler);
|
|
484
|
+
if (index > -1) {
|
|
485
|
+
this.onMessageHandlers.splice(index, 1);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
onConnectionState(handler: ConnectionStateHandler) {
|
|
490
|
+
this.onConnectionStateHandlers.push(handler);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
offConnectionState(handler: ConnectionStateHandler) {
|
|
494
|
+
const index = this.onConnectionStateHandlers.indexOf(handler);
|
|
495
|
+
if (index > -1) {
|
|
496
|
+
this.onConnectionStateHandlers.splice(index, 1);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Singleton instance for the app
|
|
503
|
+
let gatewayClient: OpenClawGatewayClient | null = null;
|
|
504
|
+
|
|
505
|
+
export function getGatewayClient(): OpenClawGatewayClient {
|
|
506
|
+
if (!gatewayClient) {
|
|
507
|
+
// Get config from environment or use defaults
|
|
508
|
+
const url =
|
|
509
|
+
typeof window !== "undefined"
|
|
510
|
+
? (window as unknown as { __OPENCLAW_GATEWAY_URL?: string })
|
|
511
|
+
.__OPENCLAW_GATEWAY_URL || "ws://127.0.0.1:18789"
|
|
512
|
+
: "ws://127.0.0.1:18789";
|
|
513
|
+
const token =
|
|
514
|
+
typeof window !== "undefined"
|
|
515
|
+
? (window as unknown as { __OPENCLAW_GATEWAY_TOKEN?: string })
|
|
516
|
+
.__OPENCLAW_GATEWAY_TOKEN || ""
|
|
517
|
+
: "";
|
|
518
|
+
|
|
519
|
+
gatewayClient = new OpenClawGatewayClient(url, token);
|
|
520
|
+
}
|
|
521
|
+
return gatewayClient;
|
|
522
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// OpenClaw Gateway WebSocket Protocol Types
|
|
2
|
+
|
|
3
|
+
export interface GatewayRequest {
|
|
4
|
+
type: "req";
|
|
5
|
+
id: string;
|
|
6
|
+
method: string;
|
|
7
|
+
params?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GatewayResponse {
|
|
11
|
+
type: "res";
|
|
12
|
+
id: string;
|
|
13
|
+
ok: boolean;
|
|
14
|
+
payload?: unknown;
|
|
15
|
+
error?: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GatewayEvent {
|
|
22
|
+
type: "event";
|
|
23
|
+
event: string;
|
|
24
|
+
payload?: unknown;
|
|
25
|
+
seq?: number;
|
|
26
|
+
stateVersion?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type GatewayMessage = GatewayRequest | GatewayResponse | GatewayEvent;
|
|
30
|
+
|
|
31
|
+
// Connection handshake
|
|
32
|
+
export interface ConnectRequest {
|
|
33
|
+
minProtocol: number;
|
|
34
|
+
maxProtocol: number;
|
|
35
|
+
client: {
|
|
36
|
+
name: string;
|
|
37
|
+
version: string;
|
|
38
|
+
};
|
|
39
|
+
role: "operator" | "node";
|
|
40
|
+
scopes?: string[];
|
|
41
|
+
caps?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ConnectResponse {
|
|
45
|
+
protocol: number;
|
|
46
|
+
policy?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Agent request/response
|
|
50
|
+
export interface AgentRequest {
|
|
51
|
+
message: string;
|
|
52
|
+
idempotencyKey: string;
|
|
53
|
+
conversationId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Agent event from gateway (matches OpenClaw protocol)
|
|
57
|
+
export interface AgentEvent {
|
|
58
|
+
runId: string;
|
|
59
|
+
seq: number;
|
|
60
|
+
stream: string;
|
|
61
|
+
ts: number;
|
|
62
|
+
sessionKey?: string;
|
|
63
|
+
data?: {
|
|
64
|
+
phase?: string;
|
|
65
|
+
text?: string;
|
|
66
|
+
delta?: string;
|
|
67
|
+
error?: string;
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Simplified response returned by sendToAgent
|
|
73
|
+
export interface AgentResponse {
|
|
74
|
+
runId: string;
|
|
75
|
+
status: "completed" | "failed" | "cancelled";
|
|
76
|
+
text?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type GatewayConnectionState =
|
|
80
|
+
| "disconnected"
|
|
81
|
+
| "connecting"
|
|
82
|
+
| "connected"
|
|
83
|
+
| "error";
|