@ynhcj/xiaoyi 2.2.0 → 2.2.2

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/dist/websocket.js CHANGED
@@ -7,102 +7,315 @@ exports.XiaoYiWebSocketManager = void 0;
7
7
  const ws_1 = __importDefault(require("ws"));
8
8
  const events_1 = require("events");
9
9
  const auth_1 = require("./auth");
10
+ const types_1 = require("./types");
10
11
  class XiaoYiWebSocketManager extends events_1.EventEmitter {
11
12
  constructor(config) {
12
13
  super();
13
- this.ws = null;
14
- this.protocolHeartbeatInterval = null;
15
- this.appHeartbeatInterval = null;
16
- this.reconnectTimeout = null;
17
- this.activeTasks = new Map(); // Track active tasks for cancellation
18
- this.config = config;
19
- this.auth = new auth_1.XiaoYiAuth(config.ak, config.sk, config.agentId);
20
- this.state = {
14
+ // ==================== Dual WebSocket Connections ====================
15
+ this.ws1 = null;
16
+ this.ws2 = null;
17
+ // ==================== Dual Server States ====================
18
+ this.state1 = {
21
19
  connected: false,
22
- authenticated: false,
20
+ ready: false,
23
21
  lastHeartbeat: 0,
24
- lastAppHeartbeat: 0,
25
- reconnectAttempts: 0,
26
- maxReconnectAttempts: 50, // Increased from 10 to 50
22
+ reconnectAttempts: 0
23
+ };
24
+ this.state2 = {
25
+ connected: false,
26
+ ready: false,
27
+ lastHeartbeat: 0,
28
+ reconnectAttempts: 0
27
29
  };
30
+ // ==================== Session → Server Mapping ====================
31
+ this.sessionServerMap = new Map();
32
+ // ==================== Active Tasks ====================
33
+ this.activeTasks = new Map();
34
+ // Resolve configuration with defaults and backward compatibility
35
+ this.config = this.resolveConfig(config);
36
+ this.auth = new auth_1.XiaoYiAuth(this.config.ak, this.config.sk, this.config.agentId);
37
+ console.log(`[WS Manager] Initialized with dual server:`);
38
+ console.log(` Server 1: ${this.config.wsUrl1}`);
39
+ console.log(` Server 2: ${this.config.wsUrl2}`);
28
40
  }
29
41
  /**
30
- * Connect to XiaoYi WebSocket server with header authentication
42
+ * Resolve configuration with defaults and backward compatibility
43
+ */
44
+ resolveConfig(userConfig) {
45
+ // Backward compatibility: if wsUrl is provided but wsUrl1/wsUrl2 are not,
46
+ // use wsUrl for server1 and default for server2
47
+ let wsUrl1 = userConfig.wsUrl1;
48
+ let wsUrl2 = userConfig.wsUrl2;
49
+ if (!wsUrl1 && userConfig.wsUrl) {
50
+ wsUrl1 = userConfig.wsUrl;
51
+ }
52
+ // Apply defaults if not provided
53
+ if (!wsUrl1) {
54
+ console.warn(`[WS Manager] wsUrl1 not provided, using default: ${types_1.DEFAULT_WS_URL_1}`);
55
+ wsUrl1 = types_1.DEFAULT_WS_URL_1;
56
+ }
57
+ if (!wsUrl2) {
58
+ console.warn(`[WS Manager] wsUrl2 not provided, using default: ${types_1.DEFAULT_WS_URL_2}`);
59
+ wsUrl2 = types_1.DEFAULT_WS_URL_2;
60
+ }
61
+ return {
62
+ wsUrl1,
63
+ wsUrl2,
64
+ agentId: userConfig.agentId,
65
+ ak: userConfig.ak,
66
+ sk: userConfig.sk,
67
+ enableStreaming: userConfig.enableStreaming,
68
+ };
69
+ }
70
+ /**
71
+ * Connect to both WebSocket servers
31
72
  */
32
73
  async connect() {
33
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
34
- return;
74
+ console.log("[WS Manager] Connecting to both servers...");
75
+ const results = await Promise.allSettled([
76
+ this.connectToServer1(),
77
+ this.connectToServer2(),
78
+ ]);
79
+ // Check if at least one connection succeeded
80
+ const server1Success = results[0].status === 'fulfilled';
81
+ const server2Success = results[1].status === 'fulfilled';
82
+ if (!server1Success && !server2Success) {
83
+ console.error("[WS Manager] Failed to connect to both servers");
84
+ throw new Error("Failed to connect to both servers");
85
+ }
86
+ console.log(`[WS Manager] Connection results: Server1=${server1Success}, Server2=${server2Success}`);
87
+ // Start application-level heartbeat (only if at least one connection is ready)
88
+ if (this.state1.connected || this.state2.connected) {
89
+ this.startAppHeartbeat();
90
+ }
91
+ }
92
+ /**
93
+ * Connect to server 1
94
+ */
95
+ async connectToServer1() {
96
+ console.log(`[Server1] Connecting to ${this.config.wsUrl1}...`);
97
+ try {
98
+ const authHeaders = this.auth.generateAuthHeaders();
99
+ this.ws1 = new ws_1.default(this.config.wsUrl1, {
100
+ headers: authHeaders,
101
+ });
102
+ this.setupWebSocketHandlers(this.ws1, 'server1');
103
+ await new Promise((resolve, reject) => {
104
+ const timeout = setTimeout(() => reject(new Error("Connection timeout")), 30000);
105
+ this.ws1.once("open", () => {
106
+ clearTimeout(timeout);
107
+ resolve();
108
+ });
109
+ this.ws1.once("error", (error) => {
110
+ clearTimeout(timeout);
111
+ reject(error);
112
+ });
113
+ });
114
+ this.state1.connected = true;
115
+ this.state1.ready = true;
116
+ this.state1.reconnectAttempts = 0;
117
+ console.log(`[Server1] Connected successfully`);
118
+ this.emit("connected", "server1");
119
+ // Send init message
120
+ this.sendInitMessage(this.ws1, 'server1');
121
+ // Start protocol heartbeat
122
+ this.startProtocolHeartbeat('server1');
123
+ }
124
+ catch (error) {
125
+ console.error(`[Server1] Connection failed:`, error);
126
+ this.state1.connected = false;
127
+ this.state1.ready = false;
128
+ this.emit("error", { serverId: 'server1', error });
129
+ throw error;
35
130
  }
131
+ }
132
+ /**
133
+ * Connect to server 2
134
+ */
135
+ async connectToServer2() {
136
+ console.log(`[Server2] Connecting to ${this.config.wsUrl2}...`);
36
137
  try {
37
- // Generate authentication headers
38
138
  const authHeaders = this.auth.generateAuthHeaders();
39
- // Create WebSocket connection with headers
40
- this.ws = new ws_1.default(this.config.wsUrl, {
139
+ this.ws2 = new ws_1.default(this.config.wsUrl2, {
41
140
  headers: authHeaders,
42
141
  });
43
- this.setupWebSocketHandlers();
44
- return new Promise((resolve, reject) => {
45
- this.ws.once("open", () => {
46
- this.state.connected = true;
47
- this.state.authenticated = true; // Authenticated via headers
48
- this.state.reconnectAttempts = 0;
49
- this.emit("connected");
50
- // Send clawd_bot_init message
51
- this.sendInitMessage();
52
- // Start heartbeats
53
- this.startProtocolHeartbeat();
54
- this.startAppHeartbeat();
142
+ this.setupWebSocketHandlers(this.ws2, 'server2');
143
+ await new Promise((resolve, reject) => {
144
+ const timeout = setTimeout(() => reject(new Error("Connection timeout")), 30000);
145
+ this.ws2.once("open", () => {
146
+ clearTimeout(timeout);
55
147
  resolve();
56
148
  });
57
- this.ws.once("error", (error) => {
149
+ this.ws2.once("error", (error) => {
150
+ clearTimeout(timeout);
58
151
  reject(error);
59
152
  });
60
153
  });
154
+ this.state2.connected = true;
155
+ this.state2.ready = true;
156
+ this.state2.reconnectAttempts = 0;
157
+ console.log(`[Server2] Connected successfully`);
158
+ this.emit("connected", "server2");
159
+ // Send init message
160
+ this.sendInitMessage(this.ws2, 'server2');
161
+ // Start protocol heartbeat
162
+ this.startProtocolHeartbeat('server2');
61
163
  }
62
164
  catch (error) {
63
- this.emit("error", error);
165
+ console.error(`[Server2] Connection failed:`, error);
166
+ this.state2.connected = false;
167
+ this.state2.ready = false;
168
+ this.emit("error", { serverId: 'server2', error });
64
169
  throw error;
65
170
  }
66
171
  }
67
172
  /**
68
- * Disconnect from WebSocket server
173
+ * Disconnect from all servers
69
174
  */
70
175
  disconnect() {
176
+ console.log("[WS Manager] Disconnecting from all servers...");
71
177
  this.clearTimers();
72
- if (this.ws) {
73
- this.ws.close();
74
- this.ws = null;
178
+ if (this.ws1) {
179
+ this.ws1.close();
180
+ this.ws1 = null;
75
181
  }
76
- this.state.connected = false;
77
- this.state.authenticated = false;
182
+ if (this.ws2) {
183
+ this.ws2.close();
184
+ this.ws2 = null;
185
+ }
186
+ this.state1.connected = false;
187
+ this.state1.ready = false;
188
+ this.state2.connected = false;
189
+ this.state2.ready = false;
190
+ this.sessionServerMap.clear();
78
191
  this.activeTasks.clear();
79
192
  this.emit("disconnected");
80
193
  }
81
194
  /**
82
- * Send clawd_bot_init message on connection/reconnection
195
+ * Send init message to specific server
83
196
  */
84
- sendInitMessage() {
197
+ sendInitMessage(ws, serverId) {
85
198
  const initMessage = {
86
199
  msgType: "clawd_bot_init",
87
200
  agentId: this.config.agentId,
88
201
  };
89
- this.sendMessage(initMessage);
90
- console.log("Sent clawd_bot_init message");
202
+ try {
203
+ ws.send(JSON.stringify(initMessage));
204
+ console.log(`[${serverId}] Sent clawd_bot_init message`);
205
+ }
206
+ catch (error) {
207
+ console.error(`[${serverId}] Failed to send init message:`, error);
208
+ }
91
209
  }
92
210
  /**
93
- * Send A2A response message (converts to JSON-RPC 2.0 format)
94
- * This method is for regular agent responses only
95
- * @param response - The response message
96
- * @param taskId - The task ID
97
- * @param sessionId - The session ID
98
- * @param isFinal - Whether this is the final frame (default: true)
99
- * @param append - Whether to append to previous content (default: false for complete content)
211
+ * Setup WebSocket event handlers for specific server
212
+ */
213
+ setupWebSocketHandlers(ws, serverId) {
214
+ ws.on("open", () => {
215
+ console.log(`[${serverId}] WebSocket opened`);
216
+ });
217
+ ws.on("message", (data) => {
218
+ this.handleIncomingMessage(data, serverId);
219
+ });
220
+ ws.on("close", (code, reason) => {
221
+ console.log(`[${serverId}] WebSocket closed: ${code} ${reason.toString()}`);
222
+ if (serverId === 'server1') {
223
+ this.state1.connected = false;
224
+ this.state1.ready = false;
225
+ this.clearProtocolHeartbeat('server1');
226
+ }
227
+ else {
228
+ this.state2.connected = false;
229
+ this.state2.ready = false;
230
+ this.clearProtocolHeartbeat('server2');
231
+ }
232
+ this.emit("disconnected", serverId);
233
+ this.scheduleReconnect(serverId);
234
+ });
235
+ ws.on("error", (error) => {
236
+ console.error(`[${serverId}] WebSocket error:`, error);
237
+ this.emit("error", { serverId, error });
238
+ });
239
+ ws.on("pong", () => {
240
+ if (serverId === 'server1') {
241
+ this.state1.lastHeartbeat = Date.now();
242
+ }
243
+ else {
244
+ this.state2.lastHeartbeat = Date.now();
245
+ }
246
+ });
247
+ }
248
+ /**
249
+ * Handle incoming message from specific server
250
+ */
251
+ handleIncomingMessage(data, sourceServer) {
252
+ try {
253
+ const message = JSON.parse(data.toString());
254
+ // Log received message
255
+ console.log("\n" + "=".repeat(80));
256
+ console.log(`[${sourceServer}] Received message:`);
257
+ console.log(JSON.stringify(message, null, 2));
258
+ console.log("=".repeat(80) + "\n");
259
+ // Validate agentId
260
+ if (message.agentId && message.agentId !== this.config.agentId) {
261
+ console.warn(`[${sourceServer}] Mismatched agentId: ${message.agentId}, expected: ${this.config.agentId}. Discarding.`);
262
+ return;
263
+ }
264
+ // Record session → server mapping
265
+ if (message.sessionId) {
266
+ this.sessionServerMap.set(message.sessionId, sourceServer);
267
+ console.log(`[MAP] Session ${message.sessionId} -> ${sourceServer}`);
268
+ }
269
+ // Handle special messages (clearContext, tasks/cancel)
270
+ if (message.method === "clearContext") {
271
+ this.handleClearContext(message, sourceServer);
272
+ return;
273
+ }
274
+ if (message.action === "clear") {
275
+ this.handleClearMessage(message, sourceServer);
276
+ return;
277
+ }
278
+ if (message.method === "tasks/cancel" || message.action === "tasks/cancel") {
279
+ this.handleTasksCancelMessage(message, sourceServer);
280
+ return;
281
+ }
282
+ // Handle regular A2A request
283
+ if (this.isA2ARequestMessage(message)) {
284
+ // Store task for potential cancellation
285
+ this.activeTasks.set(message.id, {
286
+ sessionId: message.sessionId,
287
+ timestamp: Date.now(),
288
+ });
289
+ // Emit with server info
290
+ this.emit("message", message);
291
+ }
292
+ else {
293
+ console.warn(`[${sourceServer}] Unknown message format`);
294
+ }
295
+ }
296
+ catch (error) {
297
+ console.error(`[${sourceServer}] Failed to parse message:`, error);
298
+ this.emit("error", { serverId: sourceServer, error });
299
+ }
300
+ }
301
+ /**
302
+ * Send A2A response message with automatic routing
100
303
  */
101
304
  async sendResponse(response, taskId, sessionId, isFinal = true, append = false) {
102
- if (!this.isReady()) {
103
- throw new Error("WebSocket not ready");
305
+ // Find which server this session belongs to
306
+ const targetServer = this.sessionServerMap.get(sessionId);
307
+ if (!targetServer) {
308
+ console.error(`[ROUTE] Unknown server for session ${sessionId}`);
309
+ throw new Error(`Cannot route response: unknown session ${sessionId}`);
310
+ }
311
+ // Get the corresponding WebSocket connection
312
+ const ws = targetServer === 'server1' ? this.ws1 : this.ws2;
313
+ const state = targetServer === 'server1' ? this.state1 : this.state2;
314
+ if (!ws || ws.readyState !== ws_1.default.OPEN) {
315
+ console.error(`[ROUTE] ${targetServer} not connected for session ${sessionId}`);
316
+ throw new Error(`${targetServer} is not available`);
104
317
  }
105
- // Convert A2AResponseMessage to A2A JSON-RPC 2.0 format
318
+ // Convert to JSON-RPC format
106
319
  const jsonRpcResponse = this.convertToJsonRpcFormat(response, taskId, isFinal, append);
107
320
  const message = {
108
321
  msgType: "agent_response",
@@ -111,15 +324,28 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
111
324
  taskId: taskId,
112
325
  msgDetail: JSON.stringify(jsonRpcResponse),
113
326
  };
114
- this.sendMessage(message);
327
+ try {
328
+ ws.send(JSON.stringify(message));
329
+ console.log(`[ROUTE] Response sent to ${targetServer} for session ${sessionId} (isFinal=${isFinal}, append=${append})`);
330
+ }
331
+ catch (error) {
332
+ console.error(`[ROUTE] Failed to send to ${targetServer}:`, error);
333
+ throw error;
334
+ }
115
335
  }
116
336
  /**
117
- * Send A2A clear context response (uses specific clear context format)
118
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
337
+ * Send clear context response to specific server
119
338
  */
120
- async sendClearContextResponse(requestId, sessionId, success = true) {
121
- if (!this.isReady()) {
122
- throw new Error("WebSocket not ready");
339
+ async sendClearContextResponse(requestId, sessionId, success = true, targetServer) {
340
+ const serverId = targetServer || this.sessionServerMap.get(sessionId);
341
+ if (!serverId) {
342
+ console.error(`[CLEAR] Unknown server for session ${sessionId}`);
343
+ throw new Error(`Cannot send clear response: unknown session ${sessionId}`);
344
+ }
345
+ const ws = serverId === 'server1' ? this.ws1 : this.ws2;
346
+ if (!ws || ws.readyState !== ws_1.default.OPEN) {
347
+ console.error(`[CLEAR] ${serverId} not connected`);
348
+ throw new Error(`${serverId} is not available`);
123
349
  }
124
350
  const jsonRpcResponse = {
125
351
  jsonrpc: "2.0",
@@ -129,13 +355,6 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
129
355
  state: success ? "cleared" : "failed"
130
356
  }
131
357
  },
132
- error: success ? {
133
- code: 0,
134
- message: ""
135
- } : {
136
- code: -1,
137
- message: "Failed to clear context"
138
- },
139
358
  };
140
359
  const message = {
141
360
  msgType: "agent_response",
@@ -144,39 +363,41 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
144
363
  taskId: requestId,
145
364
  msgDetail: JSON.stringify(jsonRpcResponse),
146
365
  };
147
- console.log("\n[CLEAR] Sending clearContext response:");
148
- console.log(` msgType: ${message.msgType}`);
149
- console.log(` agentId: ${message.agentId}`);
150
- console.log(` sessionId: ${message.sessionId}`);
151
- console.log(` taskId: ${message.taskId}`);
152
- console.log(` msgDetail: ${message.msgDetail}`);
153
- console.log("");
154
- this.sendMessage(message);
366
+ console.log(`\n[CLEAR] Sending clearContext response to ${serverId}:`);
367
+ console.log(` sessionId: ${sessionId}`);
368
+ console.log(` requestId: ${requestId}`);
369
+ console.log(` success: ${success}\n`);
370
+ try {
371
+ ws.send(JSON.stringify(message));
372
+ }
373
+ catch (error) {
374
+ console.error(`[CLEAR] Failed to send to ${serverId}:`, error);
375
+ throw error;
376
+ }
155
377
  }
156
378
  /**
157
- * Send A2A tasks cancel response (uses specific cancel format)
158
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/tasks-cancel-0000002537561193
379
+ * Send tasks cancel response to specific server
159
380
  */
160
- async sendTasksCancelResponse(requestId, sessionId, success = true) {
161
- if (!this.isReady()) {
162
- throw new Error("WebSocket not ready");
381
+ async sendTasksCancelResponse(requestId, sessionId, success = true, targetServer) {
382
+ const serverId = targetServer || this.sessionServerMap.get(sessionId);
383
+ if (!serverId) {
384
+ console.error(`[CANCEL] Unknown server for session ${sessionId}`);
385
+ throw new Error(`Cannot send cancel response: unknown session ${sessionId}`);
386
+ }
387
+ const ws = serverId === 'server1' ? this.ws1 : this.ws2;
388
+ if (!ws || ws.readyState !== ws_1.default.OPEN) {
389
+ console.error(`[CANCEL] ${serverId} not connected`);
390
+ throw new Error(`${serverId} is not available`);
163
391
  }
164
392
  const jsonRpcResponse = {
165
393
  jsonrpc: "2.0",
166
394
  id: requestId,
167
395
  result: {
168
- id: requestId, // 使用请求中的该字段返回
396
+ id: requestId,
169
397
  status: {
170
398
  state: success ? "canceled" : "failed"
171
399
  }
172
400
  },
173
- error: success ? {
174
- code: 0,
175
- message: ""
176
- } : {
177
- code: -1,
178
- message: "Failed to cancel task"
179
- },
180
401
  };
181
402
  const message = {
182
403
  msgType: "agent_response",
@@ -185,19 +406,68 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
185
406
  taskId: requestId,
186
407
  msgDetail: JSON.stringify(jsonRpcResponse),
187
408
  };
188
- this.sendMessage(message);
409
+ try {
410
+ ws.send(JSON.stringify(message));
411
+ }
412
+ catch (error) {
413
+ console.error(`[CANCEL] Failed to send to ${serverId}:`, error);
414
+ throw error;
415
+ }
189
416
  }
190
417
  /**
191
- * Convert A2AResponseMessage to A2A JSON-RPC 2.0 format
192
- * @param response - The response message
193
- * @param taskId - The task ID
194
- * @param isFinal - Whether this is the final frame (default: true)
195
- * @param append - Whether to append to previous content (default: false)
418
+ * Handle clearContext method
419
+ */
420
+ handleClearContext(message, sourceServer) {
421
+ console.log(`[${sourceServer}] Received clearContext for session: ${message.sessionId}`);
422
+ this.sendClearContextResponse(message.id, message.sessionId, true, sourceServer)
423
+ .catch(error => console.error(`[${sourceServer}] Failed to send clearContext response:`, error));
424
+ this.emit("clear", {
425
+ sessionId: message.sessionId,
426
+ id: message.id,
427
+ serverId: sourceServer,
428
+ });
429
+ // Remove session mapping
430
+ this.sessionServerMap.delete(message.sessionId);
431
+ }
432
+ /**
433
+ * Handle clear message (legacy format)
434
+ */
435
+ handleClearMessage(message, sourceServer) {
436
+ console.log(`[${sourceServer}] Received clear message for session: ${message.sessionId}`);
437
+ this.sendClearContextResponse(message.id, message.sessionId, true, sourceServer)
438
+ .catch(error => console.error(`[${sourceServer}] Failed to send clear response:`, error));
439
+ this.emit("clear", {
440
+ sessionId: message.sessionId,
441
+ id: message.id,
442
+ serverId: sourceServer,
443
+ });
444
+ this.sessionServerMap.delete(message.sessionId);
445
+ }
446
+ /**
447
+ * Handle tasks/cancel message
448
+ */
449
+ handleTasksCancelMessage(message, sourceServer) {
450
+ const effectiveTaskId = message.taskId || message.id;
451
+ console.log("\n" + "=".repeat(60));
452
+ console.log(`[${sourceServer}] Received cancel request`);
453
+ console.log(` Session: ${message.sessionId}`);
454
+ console.log(` Task ID: ${effectiveTaskId}`);
455
+ console.log("=".repeat(60) + "\n");
456
+ this.sendTasksCancelResponse(message.id, message.sessionId, true, sourceServer)
457
+ .catch(error => console.error(`[${sourceServer}] Failed to send cancel response:`, error));
458
+ this.emit("cancel", {
459
+ sessionId: message.sessionId,
460
+ taskId: effectiveTaskId,
461
+ id: message.id,
462
+ serverId: sourceServer,
463
+ });
464
+ this.activeTasks.delete(effectiveTaskId);
465
+ }
466
+ /**
467
+ * Convert A2AResponseMessage to JSON-RPC 2.0 format
196
468
  */
197
469
  convertToJsonRpcFormat(response, taskId, isFinal = true, append = false) {
198
- // Generate artifact ID
199
470
  const artifactId = `artifact_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
200
- // Check if there's an error
201
471
  if (response.status === "error" && response.error) {
202
472
  return {
203
473
  jsonrpc: "2.0",
@@ -208,7 +478,6 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
208
478
  },
209
479
  };
210
480
  }
211
- // Convert content to artifact parts
212
481
  const parts = [];
213
482
  if (response.content.type === "text" && response.content.text) {
214
483
  parts.push({
@@ -226,13 +495,12 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
226
495
  },
227
496
  });
228
497
  }
229
- // Create TaskArtifactUpdateEvent with configurable flags
230
498
  const artifactEvent = {
231
499
  taskId: taskId,
232
500
  kind: "artifact-update",
233
- append: append, // Controls whether to append or replace content
234
- lastChunk: isFinal, // Set based on isFinal parameter
235
- final: isFinal, // Set based on isFinal parameter
501
+ append: append,
502
+ lastChunk: isFinal,
503
+ final: isFinal,
236
504
  artifact: {
237
505
  artifactId: artifactId,
238
506
  parts: parts,
@@ -245,266 +513,179 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
245
513
  };
246
514
  }
247
515
  /**
248
- * Send generic outbound message
249
- */
250
- sendMessage(message) {
251
- if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
252
- console.error("Cannot send message: WebSocket not open");
253
- return;
254
- }
255
- try {
256
- this.ws.send(JSON.stringify(message));
257
- }
258
- catch (error) {
259
- console.error("Failed to send message:", error);
260
- this.emit("error", error);
261
- }
262
- }
263
- /**
264
- * Check if connection is ready for sending messages
516
+ * Check if at least one server is ready
265
517
  */
266
518
  isReady() {
267
- return !!(this.state.connected && this.state.authenticated &&
268
- this.ws && this.ws.readyState === ws_1.default.OPEN);
519
+ return (this.state1.ready && this.ws1?.readyState === ws_1.default.OPEN) ||
520
+ (this.state2.ready && this.ws2?.readyState === ws_1.default.OPEN);
269
521
  }
270
522
  /**
271
- * Get current connection state
523
+ * Get combined connection state
272
524
  */
273
525
  getState() {
274
- return { ...this.state };
526
+ const connected = this.state1.connected || this.state2.connected;
527
+ const authenticated = connected; // Auth via headers
528
+ return {
529
+ connected,
530
+ authenticated,
531
+ lastHeartbeat: Math.max(this.state1.lastHeartbeat, this.state2.lastHeartbeat),
532
+ lastAppHeartbeat: 0,
533
+ reconnectAttempts: Math.max(this.state1.reconnectAttempts, this.state2.reconnectAttempts),
534
+ maxReconnectAttempts: 50,
535
+ };
275
536
  }
276
537
  /**
277
- * Setup WebSocket event handlers
538
+ * Get individual server states
278
539
  */
279
- setupWebSocketHandlers() {
280
- if (!this.ws)
281
- return;
282
- this.ws.on("open", () => {
283
- console.log("XiaoYi WebSocket connected");
284
- });
285
- this.ws.on("message", (data) => {
286
- try {
287
- const message = JSON.parse(data.toString());
288
- this.handleMessage(message);
289
- }
290
- catch (error) {
291
- console.error("Failed to parse WebSocket message:", error);
292
- this.emit("error", error);
293
- }
294
- });
295
- this.ws.on("close", (code, reason) => {
296
- console.log(`XiaoYi WebSocket closed: ${code} ${reason.toString()}`);
297
- this.state.connected = false;
298
- this.state.authenticated = false;
299
- this.clearTimers();
300
- this.emit("disconnected");
301
- this.scheduleReconnect();
302
- });
303
- this.ws.on("error", (error) => {
304
- console.error("XiaoYi WebSocket error:", error);
305
- this.emit("error", error);
306
- });
307
- this.ws.on("pong", () => {
308
- this.state.lastHeartbeat = Date.now();
309
- });
540
+ getServerStates() {
541
+ return {
542
+ server1: { ...this.state1 },
543
+ server2: { ...this.state2 },
544
+ };
310
545
  }
311
546
  /**
312
- * Handle incoming WebSocket messages
547
+ * Start protocol-level heartbeat for specific server
313
548
  */
314
- handleMessage(message) {
315
- // 打印完整的接收消息
316
- console.log("\n" + "=".repeat(80));
317
- console.log(`[XiaoYi WS] Received message from server:`);
318
- console.log(JSON.stringify(message, null, 2));
319
- console.log("=".repeat(80) + "\n");
320
- // Validate agentId
321
- if (message.agentId && message.agentId !== this.config.agentId) {
322
- console.warn(`Received message with mismatched agentId: ${message.agentId}, expected: ${this.config.agentId}. Discarding.`);
323
- return;
324
- }
325
- // Handle JSON-RPC 2.0 clearContext method (直接响应,不走 OpenClaw)
326
- // Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
327
- if (message.method === "clearContext") {
328
- console.log(`[CLEAR] Received clearContext for session: ${message.sessionId}`);
329
- // 直接返回成功响应
330
- this.sendClearContextResponse(message.id, message.sessionId, true).catch(error => {
331
- console.error("Failed to send clearContext response:", error);
332
- });
333
- // 可选:通知应用清除会话上下文
334
- this.emit("clear", {
335
- sessionId: message.sessionId,
336
- id: message.id,
337
- });
338
- return;
339
- }
340
- // Check if it's a clear message (兼容旧格式)
341
- if (message.action === "clear") {
342
- this.handleClearMessage(message);
343
- return;
344
- }
345
- // Check if it's a tasks/cancel message (支持 method 和 action 两种格式)
346
- if (message.method === "tasks/cancel" || message.action === "tasks/cancel") {
347
- this.handleTasksCancelMessage(message);
348
- return;
349
- }
350
- // Handle regular A2A request message
351
- if (this.isA2ARequestMessage(message)) {
352
- // Store task for potential cancellation
353
- this.activeTasks.set(message.id, {
354
- sessionId: message.sessionId,
355
- timestamp: Date.now(),
356
- });
357
- this.emit("message", message);
549
+ startProtocolHeartbeat(serverId) {
550
+ const interval = setInterval(() => {
551
+ const ws = serverId === 'server1' ? this.ws1 : this.ws2;
552
+ const state = serverId === 'server1' ? this.state1 : this.state2;
553
+ if (ws && ws.readyState === ws_1.default.OPEN) {
554
+ ws.ping();
555
+ const now = Date.now();
556
+ if (state.lastHeartbeat > 0 && now - state.lastHeartbeat > 90000) {
557
+ console.warn(`[${serverId}] Heartbeat timeout, reconnecting...`);
558
+ ws.close();
559
+ }
560
+ }
561
+ }, 30000);
562
+ if (serverId === 'server1') {
563
+ this.heartbeatTimeout1 = interval;
358
564
  }
359
565
  else {
360
- console.warn("Received unknown message format:", message);
566
+ this.heartbeatTimeout2 = interval;
361
567
  }
362
568
  }
363
569
  /**
364
- * Handle A2A clear message
365
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/clear-context-0000002537681163
366
- */
367
- handleClearMessage(message) {
368
- console.log(`Received clear message for session: ${message.sessionId}`);
369
- // Send success response according to A2A spec using the correct format
370
- this.sendClearContextResponse(message.id, message.sessionId, true).catch(error => {
371
- console.error("Failed to send clear response:", error);
372
- });
373
- // Emit clear event for application to handle
374
- this.emit("clear", {
375
- sessionId: message.sessionId,
376
- id: message.id,
377
- });
378
- }
379
- /**
380
- * Handle A2A tasks/cancel message
381
- * Reference: https://developer.huawei.com/consumer/cn/doc/service/tasks-cancel-0000002537561193
382
- *
383
- * Simplified implementation similar to clearContext:
384
- * 1. Send success response immediately
385
- * 2. Emit cancel event for application to handle
386
- */
387
- handleTasksCancelMessage(message) {
388
- // Use taskId if available, otherwise use id as the task identifier
389
- const effectiveTaskId = message.taskId || message.id;
390
- console.log(`\n============================================================`);
391
- console.log(`XiaoYi: [CANCEL] Received cancel request`);
392
- console.log(` Session: ${message.sessionId}`);
393
- console.log(` Task ID: ${effectiveTaskId}`);
394
- console.log(` Message ID: ${message.id}`);
395
- console.log(`===========================================================\n`);
396
- // Send success response immediately (similar to clearContext)
397
- this.sendTasksCancelResponse(message.id, message.sessionId, true).catch(error => {
398
- console.error("Failed to send tasks/cancel response:", error);
399
- });
400
- // Emit cancel event for application to handle
401
- // The application can decide how to handle the cancellation
402
- this.emit("cancel", {
403
- sessionId: message.sessionId,
404
- taskId: effectiveTaskId,
405
- id: message.id,
406
- });
407
- // Remove from active tasks
408
- this.activeTasks.delete(effectiveTaskId);
409
- }
410
- /**
411
- * Send tasks/cancel success response
412
- */
413
- async sendCancelSuccessResponse(sessionId, taskId, requestId) {
414
- // Use the dedicated tasks cancel response method with correct format
415
- await this.sendTasksCancelResponse(requestId, sessionId, true);
416
- // Remove from active tasks
417
- this.activeTasks.delete(taskId);
418
- }
419
- /**
420
- * Type guard for A2A request messages (JSON-RPC 2.0 format)
570
+ * Clear protocol heartbeat for specific server
421
571
  */
422
- isA2ARequestMessage(data) {
423
- return data &&
424
- typeof data.agentId === "string" &&
425
- typeof data.sessionId === "string" &&
426
- data.jsonrpc === "2.0" &&
427
- typeof data.id === "string" &&
428
- data.method === "message/stream" &&
429
- data.params &&
430
- typeof data.params.id === "string" &&
431
- typeof data.params.sessionId === "string" &&
432
- data.params.message &&
433
- typeof data.params.message.role === "string" &&
434
- Array.isArray(data.params.message.parts);
435
- }
436
- /**
437
- * Start protocol-level heartbeat (ping/pong)
438
- */
439
- startProtocolHeartbeat() {
440
- this.protocolHeartbeatInterval = setInterval(() => {
441
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
442
- this.ws.ping();
443
- // Check if we haven't received a pong in too long
444
- const now = Date.now();
445
- if (this.state.lastHeartbeat > 0 && now - this.state.lastHeartbeat > 90000) {
446
- console.warn("Protocol heartbeat timeout, reconnecting...");
447
- this.disconnect();
448
- this.scheduleReconnect();
449
- }
572
+ clearProtocolHeartbeat(serverId) {
573
+ const interval = serverId === 'server1' ? this.heartbeatTimeout1 : this.heartbeatTimeout2;
574
+ if (interval) {
575
+ clearInterval(interval);
576
+ if (serverId === 'server1') {
577
+ this.heartbeatTimeout1 = undefined;
578
+ }
579
+ else {
580
+ this.heartbeatTimeout2 = undefined;
450
581
  }
451
- }, 30000); // Send ping every 30 seconds
582
+ }
452
583
  }
453
584
  /**
454
- * Start application-level heartbeat
585
+ * Start application-level heartbeat (shared across both servers)
455
586
  */
456
587
  startAppHeartbeat() {
457
588
  this.appHeartbeatInterval = setInterval(() => {
458
- if (this.isReady()) {
459
- const heartbeatMessage = {
460
- msgType: "heartbeat",
461
- agentId: this.config.agentId,
462
- };
463
- this.sendMessage(heartbeatMessage);
464
- this.state.lastAppHeartbeat = Date.now();
589
+ const heartbeatMessage = {
590
+ msgType: "heartbeat",
591
+ agentId: this.config.agentId,
592
+ };
593
+ // Send to all connected servers
594
+ if (this.ws1?.readyState === ws_1.default.OPEN) {
595
+ try {
596
+ this.ws1.send(JSON.stringify(heartbeatMessage));
597
+ }
598
+ catch (error) {
599
+ console.error('[Server1] Failed to send app heartbeat:', error);
600
+ }
465
601
  }
466
- }, 20000); // Send application heartbeat every 20 seconds
602
+ if (this.ws2?.readyState === ws_1.default.OPEN) {
603
+ try {
604
+ this.ws2.send(JSON.stringify(heartbeatMessage));
605
+ }
606
+ catch (error) {
607
+ console.error('[Server2] Failed to send app heartbeat:', error);
608
+ }
609
+ }
610
+ }, 20000);
467
611
  }
468
612
  /**
469
- * Schedule reconnection attempt with exponential backoff
613
+ * Schedule reconnection for specific server
470
614
  */
471
- scheduleReconnect() {
472
- if (this.state.reconnectAttempts >= this.state.maxReconnectAttempts) {
473
- console.error("Max reconnection attempts reached");
474
- this.emit("maxReconnectAttemptsReached");
615
+ scheduleReconnect(serverId) {
616
+ const state = serverId === 'server1' ? this.state1 : this.state2;
617
+ if (state.reconnectAttempts >= 50) {
618
+ console.error(`[${serverId}] Max reconnection attempts reached`);
619
+ this.emit("maxReconnectAttemptsReached", serverId);
475
620
  return;
476
621
  }
477
- // Exponential backoff with longer intervals: 2s, 4s, 8s, 16s, 32s, 60s (max)
478
- const delay = Math.min(2000 * Math.pow(2, this.state.reconnectAttempts), 60000);
479
- this.state.reconnectAttempts++;
480
- console.log(`Scheduling reconnect attempt ${this.state.reconnectAttempts}/${this.state.maxReconnectAttempts} in ${delay}ms`);
481
- this.reconnectTimeout = setTimeout(async () => {
622
+ const delay = Math.min(2000 * Math.pow(2, state.reconnectAttempts), 60000);
623
+ state.reconnectAttempts++;
624
+ console.log(`[${serverId}] Scheduling reconnect attempt ${state.reconnectAttempts}/50 in ${delay}ms`);
625
+ const timeout = setTimeout(async () => {
482
626
  try {
483
- await this.connect();
627
+ if (serverId === 'server1') {
628
+ await this.connectToServer1();
629
+ }
630
+ else {
631
+ await this.connectToServer2();
632
+ }
633
+ console.log(`[${serverId}] Reconnected successfully`);
484
634
  }
485
635
  catch (error) {
486
- console.error("Reconnection failed:", error);
487
- this.scheduleReconnect();
636
+ console.error(`[${serverId}] Reconnection failed:`, error);
637
+ this.scheduleReconnect(serverId);
488
638
  }
489
639
  }, delay);
640
+ if (serverId === 'server1') {
641
+ this.reconnectTimeout1 = timeout;
642
+ }
643
+ else {
644
+ this.reconnectTimeout2 = timeout;
645
+ }
490
646
  }
491
647
  /**
492
648
  * Clear all timers
493
649
  */
494
650
  clearTimers() {
495
- if (this.protocolHeartbeatInterval) {
496
- clearInterval(this.protocolHeartbeatInterval);
497
- this.protocolHeartbeatInterval = null;
651
+ if (this.heartbeatTimeout1) {
652
+ clearInterval(this.heartbeatTimeout1);
653
+ this.heartbeatTimeout1 = undefined;
654
+ }
655
+ if (this.heartbeatTimeout2) {
656
+ clearInterval(this.heartbeatTimeout2);
657
+ this.heartbeatTimeout2 = undefined;
498
658
  }
499
659
  if (this.appHeartbeatInterval) {
500
660
  clearInterval(this.appHeartbeatInterval);
501
- this.appHeartbeatInterval = null;
661
+ this.appHeartbeatInterval = undefined;
662
+ }
663
+ if (this.reconnectTimeout1) {
664
+ clearTimeout(this.reconnectTimeout1);
665
+ this.reconnectTimeout1 = undefined;
502
666
  }
503
- if (this.reconnectTimeout) {
504
- clearTimeout(this.reconnectTimeout);
505
- this.reconnectTimeout = null;
667
+ if (this.reconnectTimeout2) {
668
+ clearTimeout(this.reconnectTimeout2);
669
+ this.reconnectTimeout2 = undefined;
506
670
  }
507
671
  }
672
+ /**
673
+ * Type guard for A2A request messages
674
+ */
675
+ isA2ARequestMessage(data) {
676
+ return data &&
677
+ typeof data.agentId === "string" &&
678
+ typeof data.sessionId === "string" &&
679
+ data.jsonrpc === "2.0" &&
680
+ typeof data.id === "string" &&
681
+ data.method === "message/stream" &&
682
+ data.params &&
683
+ typeof data.params.id === "string" &&
684
+ typeof data.params.sessionId === "string" &&
685
+ data.params.message &&
686
+ typeof data.params.message.role === "string" &&
687
+ Array.isArray(data.params.message.parts);
688
+ }
508
689
  /**
509
690
  * Get active tasks
510
691
  */
@@ -517,5 +698,17 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
517
698
  removeActiveTask(taskId) {
518
699
  this.activeTasks.delete(taskId);
519
700
  }
701
+ /**
702
+ * Get server for a specific session
703
+ */
704
+ getServerForSession(sessionId) {
705
+ return this.sessionServerMap.get(sessionId);
706
+ }
707
+ /**
708
+ * Remove session mapping
709
+ */
710
+ removeSession(sessionId) {
711
+ this.sessionServerMap.delete(sessionId);
712
+ }
520
713
  }
521
714
  exports.XiaoYiWebSocketManager = XiaoYiWebSocketManager;