@ynhcj/xiaoyi 2.5.5 → 2.5.7
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/auth.d.ts +1 -1
- package/dist/channel.d.ts +116 -14
- package/dist/channel.js +199 -665
- package/dist/config-schema.d.ts +8 -8
- package/dist/config-schema.js +5 -5
- package/dist/file-download.d.ts +17 -0
- package/dist/file-download.js +69 -0
- package/dist/heartbeat.d.ts +39 -0
- package/dist/heartbeat.js +102 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.js +7 -11
- package/dist/push.d.ts +28 -0
- package/dist/push.js +135 -0
- package/dist/runtime.d.ts +48 -2
- package/dist/runtime.js +117 -3
- package/dist/types.d.ts +95 -1
- package/dist/websocket.d.ts +49 -1
- package/dist/websocket.js +279 -20
- package/dist/xy-bot.d.ts +19 -0
- package/dist/xy-bot.js +277 -0
- package/dist/xy-client.d.ts +26 -0
- package/dist/xy-client.js +78 -0
- package/dist/xy-config.d.ts +18 -0
- package/dist/xy-config.js +37 -0
- package/dist/xy-formatter.d.ts +94 -0
- package/dist/xy-formatter.js +303 -0
- package/dist/xy-monitor.d.ts +17 -0
- package/dist/xy-monitor.js +187 -0
- package/dist/xy-parser.d.ts +49 -0
- package/dist/xy-parser.js +109 -0
- package/dist/xy-reply-dispatcher.d.ts +17 -0
- package/dist/xy-reply-dispatcher.js +308 -0
- package/dist/xy-tools/session-manager.d.ts +29 -0
- package/dist/xy-tools/session-manager.js +80 -0
- package/dist/xy-utils/config-manager.d.ts +26 -0
- package/dist/xy-utils/config-manager.js +61 -0
- package/dist/xy-utils/crypto.d.ts +8 -0
- package/dist/xy-utils/crypto.js +21 -0
- package/dist/xy-utils/logger.d.ts +6 -0
- package/dist/xy-utils/logger.js +37 -0
- package/dist/xy-utils/session.d.ts +34 -0
- package/dist/xy-utils/session.js +55 -0
- package/package.json +32 -16
package/dist/websocket.js
CHANGED
|
@@ -7,8 +7,9 @@ exports.XiaoYiWebSocketManager = void 0;
|
|
|
7
7
|
const ws_1 = __importDefault(require("ws"));
|
|
8
8
|
const events_1 = require("events");
|
|
9
9
|
const url_1 = require("url");
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const auth_js_1 = require("./auth.js");
|
|
11
|
+
const heartbeat_js_1 = require("./heartbeat.js");
|
|
12
|
+
const types_js_1 = require("./types.js");
|
|
12
13
|
class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
13
14
|
constructor(config) {
|
|
14
15
|
super();
|
|
@@ -30,15 +31,26 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
30
31
|
};
|
|
31
32
|
// ==================== Session → Server Mapping ====================
|
|
32
33
|
this.sessionServerMap = new Map();
|
|
34
|
+
// ==================== Session Cleanup State ====================
|
|
35
|
+
// Track sessions that are pending cleanup (user cleared context but task still running)
|
|
36
|
+
this.sessionCleanupStateMap = new Map();
|
|
33
37
|
// ==================== Active Tasks ====================
|
|
34
38
|
this.activeTasks = new Map();
|
|
35
39
|
// Resolve configuration with defaults and backward compatibility
|
|
36
40
|
this.config = this.resolveConfig(config);
|
|
37
|
-
this.auth = new
|
|
41
|
+
this.auth = new auth_js_1.XiaoYiAuth(this.config.ak, this.config.sk, this.config.agentId);
|
|
38
42
|
console.log(`[WS Manager] Initialized with dual server:`);
|
|
39
43
|
console.log(` Server 1: ${this.config.wsUrl1}`);
|
|
40
44
|
console.log(` Server 2: ${this.config.wsUrl2}`);
|
|
41
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Set health event callback to report activity to OpenClaw framework.
|
|
48
|
+
* This callback is invoked on heartbeat success to update lastEventAt.
|
|
49
|
+
*/
|
|
50
|
+
setHealthEventCallback(callback) {
|
|
51
|
+
this.onHealthEvent = callback;
|
|
52
|
+
console.log("[WS Manager] Health event callback registered");
|
|
53
|
+
}
|
|
42
54
|
/**
|
|
43
55
|
* Check if URL is wss + IP format (skip certificate verification)
|
|
44
56
|
*/
|
|
@@ -93,12 +105,12 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
93
105
|
}
|
|
94
106
|
// Apply defaults if not provided
|
|
95
107
|
if (!wsUrl1) {
|
|
96
|
-
console.warn(`[WS Manager] wsUrl1 not provided, using default: ${
|
|
97
|
-
wsUrl1 =
|
|
108
|
+
console.warn(`[WS Manager] wsUrl1 not provided, using default: ${types_js_1.DEFAULT_WS_URL_1}`);
|
|
109
|
+
wsUrl1 = types_js_1.DEFAULT_WS_URL_1;
|
|
98
110
|
}
|
|
99
111
|
if (!wsUrl2) {
|
|
100
|
-
console.warn(`[WS Manager] wsUrl2 not provided, using default: ${
|
|
101
|
-
wsUrl2 =
|
|
112
|
+
console.warn(`[WS Manager] wsUrl2 not provided, using default: ${types_js_1.DEFAULT_WS_URL_2}`);
|
|
113
|
+
wsUrl2 = types_js_1.DEFAULT_WS_URL_2;
|
|
102
114
|
}
|
|
103
115
|
return {
|
|
104
116
|
wsUrl1,
|
|
@@ -107,6 +119,7 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
107
119
|
ak: userConfig.ak,
|
|
108
120
|
sk: userConfig.sk,
|
|
109
121
|
enableStreaming: userConfig.enableStreaming ?? true,
|
|
122
|
+
sessionCleanupTimeoutMs: userConfig.sessionCleanupTimeoutMs ?? XiaoYiWebSocketManager.DEFAULT_CLEANUP_TIMEOUT_MS,
|
|
110
123
|
};
|
|
111
124
|
}
|
|
112
125
|
/**
|
|
@@ -137,6 +150,22 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
137
150
|
async connectToServer1() {
|
|
138
151
|
console.log(`[Server1] Connecting to ${this.config.wsUrl1}...`);
|
|
139
152
|
try {
|
|
153
|
+
// ✅ Close existing connection and heartbeat before creating new one
|
|
154
|
+
if (this.ws1) {
|
|
155
|
+
console.log(`[Server1] Closing existing connection before reconnect`);
|
|
156
|
+
if (this.heartbeat1) {
|
|
157
|
+
this.heartbeat1.stop();
|
|
158
|
+
this.heartbeat1 = undefined;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
this.ws1.removeAllListeners();
|
|
162
|
+
this.ws1.close();
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
console.warn(`[Server1] Error closing old connection:`, err);
|
|
166
|
+
}
|
|
167
|
+
this.ws1 = null;
|
|
168
|
+
}
|
|
140
169
|
const authHeaders = this.auth.generateAuthHeaders();
|
|
141
170
|
// Check if URL is wss + IP format, skip certificate verification
|
|
142
171
|
const skipCertVerify = this.isWssWithIp(this.config.wsUrl1);
|
|
@@ -147,6 +176,26 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
147
176
|
headers: authHeaders,
|
|
148
177
|
rejectUnauthorized: !skipCertVerify,
|
|
149
178
|
});
|
|
179
|
+
// ✅ Initialize HeartbeatManager for server1
|
|
180
|
+
this.heartbeat1 = new heartbeat_js_1.HeartbeatManager(this.ws1, {
|
|
181
|
+
interval: 30000, // 30 seconds
|
|
182
|
+
timeout: 10000, // 10 seconds timeout
|
|
183
|
+
message: JSON.stringify({
|
|
184
|
+
msgType: "heartbeat",
|
|
185
|
+
agentId: this.config.agentId,
|
|
186
|
+
timestamp: Date.now(),
|
|
187
|
+
}),
|
|
188
|
+
}, () => {
|
|
189
|
+
console.log(`[Server1] Heartbeat timeout, reconnecting...`);
|
|
190
|
+
if (this.ws1 && (this.ws1.readyState === ws_1.default.OPEN || this.ws1.readyState === ws_1.default.CONNECTING)) {
|
|
191
|
+
this.ws1.close();
|
|
192
|
+
}
|
|
193
|
+
}, "server1", console.log, console.error, () => {
|
|
194
|
+
// ✅ Heartbeat success callback - report health to OpenClaw
|
|
195
|
+
this.emit("heartbeat", "server1");
|
|
196
|
+
// ✅ Report liveness to OpenClaw framework to prevent stale-socket detection
|
|
197
|
+
this.onHealthEvent?.();
|
|
198
|
+
});
|
|
150
199
|
this.setupWebSocketHandlers(this.ws1, 'server1');
|
|
151
200
|
await new Promise((resolve, reject) => {
|
|
152
201
|
const timeout = setTimeout(() => reject(new Error("Connection timeout")), 30000);
|
|
@@ -167,8 +216,9 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
167
216
|
this.scheduleStableConnectionCheck('server1');
|
|
168
217
|
// Send init message
|
|
169
218
|
this.sendInitMessage(this.ws1, 'server1');
|
|
170
|
-
// Start
|
|
171
|
-
this.
|
|
219
|
+
// ✅ Start heartbeat (replaces old startProtocolHeartbeat)
|
|
220
|
+
this.heartbeat1.start();
|
|
221
|
+
console.log(`[Server1] Heartbeat started (30s interval, 10s timeout)`);
|
|
172
222
|
}
|
|
173
223
|
catch (error) {
|
|
174
224
|
console.error(`[Server1] Connection failed:`, error);
|
|
@@ -184,6 +234,22 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
184
234
|
async connectToServer2() {
|
|
185
235
|
console.log(`[Server2] Connecting to ${this.config.wsUrl2}...`);
|
|
186
236
|
try {
|
|
237
|
+
// ✅ Close existing connection and heartbeat before creating new one
|
|
238
|
+
if (this.ws2) {
|
|
239
|
+
console.log(`[Server2] Closing existing connection before reconnect`);
|
|
240
|
+
if (this.heartbeat2) {
|
|
241
|
+
this.heartbeat2.stop();
|
|
242
|
+
this.heartbeat2 = undefined;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
this.ws2.removeAllListeners();
|
|
246
|
+
this.ws2.close();
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
console.warn(`[Server2] Error closing old connection:`, err);
|
|
250
|
+
}
|
|
251
|
+
this.ws2 = null;
|
|
252
|
+
}
|
|
187
253
|
const authHeaders = this.auth.generateAuthHeaders();
|
|
188
254
|
// Check if URL is wss + IP format, skip certificate verification
|
|
189
255
|
const skipCertVerify = this.isWssWithIp(this.config.wsUrl2);
|
|
@@ -194,6 +260,26 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
194
260
|
headers: authHeaders,
|
|
195
261
|
rejectUnauthorized: !skipCertVerify,
|
|
196
262
|
});
|
|
263
|
+
// ✅ Initialize HeartbeatManager for server2
|
|
264
|
+
this.heartbeat2 = new heartbeat_js_1.HeartbeatManager(this.ws2, {
|
|
265
|
+
interval: 30000, // 30 seconds
|
|
266
|
+
timeout: 10000, // 10 seconds timeout
|
|
267
|
+
message: JSON.stringify({
|
|
268
|
+
msgType: "heartbeat",
|
|
269
|
+
agentId: this.config.agentId,
|
|
270
|
+
timestamp: Date.now(),
|
|
271
|
+
}),
|
|
272
|
+
}, () => {
|
|
273
|
+
console.log(`[Server2] Heartbeat timeout, reconnecting...`);
|
|
274
|
+
if (this.ws2 && (this.ws2.readyState === ws_1.default.OPEN || this.ws2.readyState === ws_1.default.CONNECTING)) {
|
|
275
|
+
this.ws2.close();
|
|
276
|
+
}
|
|
277
|
+
}, "server2", console.log, console.error, () => {
|
|
278
|
+
// ✅ Heartbeat success callback - report health to OpenClaw
|
|
279
|
+
this.emit("heartbeat", "server2");
|
|
280
|
+
// ✅ Report liveness to OpenClaw framework to prevent stale-socket detection
|
|
281
|
+
this.onHealthEvent?.();
|
|
282
|
+
});
|
|
197
283
|
this.setupWebSocketHandlers(this.ws2, 'server2');
|
|
198
284
|
await new Promise((resolve, reject) => {
|
|
199
285
|
const timeout = setTimeout(() => reject(new Error("Connection timeout")), 30000);
|
|
@@ -214,8 +300,9 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
214
300
|
this.scheduleStableConnectionCheck('server2');
|
|
215
301
|
// Send init message
|
|
216
302
|
this.sendInitMessage(this.ws2, 'server2');
|
|
217
|
-
// Start
|
|
218
|
-
this.
|
|
303
|
+
// ✅ Start heartbeat (replaces old startProtocolHeartbeat)
|
|
304
|
+
this.heartbeat2.start();
|
|
305
|
+
console.log(`[Server2] Heartbeat started (30s interval, 10s timeout)`);
|
|
219
306
|
}
|
|
220
307
|
catch (error) {
|
|
221
308
|
console.error(`[Server2] Connection failed:`, error);
|
|
@@ -231,12 +318,42 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
231
318
|
disconnect() {
|
|
232
319
|
console.log("[WS Manager] Disconnecting from all servers...");
|
|
233
320
|
this.clearTimers();
|
|
321
|
+
// ✅ Stop heartbeat managers
|
|
322
|
+
if (this.heartbeat1) {
|
|
323
|
+
console.log("[Server1] Stopping heartbeat manager");
|
|
324
|
+
this.heartbeat1.stop();
|
|
325
|
+
this.heartbeat1 = undefined;
|
|
326
|
+
}
|
|
327
|
+
if (this.heartbeat2) {
|
|
328
|
+
console.log("[Server2] Stopping heartbeat manager");
|
|
329
|
+
this.heartbeat2.stop();
|
|
330
|
+
this.heartbeat2 = undefined;
|
|
331
|
+
}
|
|
332
|
+
// ✅ Properly cleanup WebSocket connections to prevent ghost connections
|
|
234
333
|
if (this.ws1) {
|
|
235
|
-
|
|
334
|
+
try {
|
|
335
|
+
console.log("[Server1] Removing all listeners and closing connection");
|
|
336
|
+
this.ws1.removeAllListeners();
|
|
337
|
+
if (this.ws1.readyState === ws_1.default.OPEN || this.ws1.readyState === ws_1.default.CONNECTING) {
|
|
338
|
+
this.ws1.close();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
console.warn("[Server1] Error during disconnect:", err);
|
|
343
|
+
}
|
|
236
344
|
this.ws1 = null;
|
|
237
345
|
}
|
|
238
346
|
if (this.ws2) {
|
|
239
|
-
|
|
347
|
+
try {
|
|
348
|
+
console.log("[Server2] Removing all listeners and closing connection");
|
|
349
|
+
this.ws2.removeAllListeners();
|
|
350
|
+
if (this.ws2.readyState === ws_1.default.OPEN || this.ws2.readyState === ws_1.default.CONNECTING) {
|
|
351
|
+
this.ws2.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
console.warn("[Server2] Error during disconnect:", err);
|
|
356
|
+
}
|
|
240
357
|
this.ws2 = null;
|
|
241
358
|
}
|
|
242
359
|
this.state1.connected = false;
|
|
@@ -245,7 +362,15 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
245
362
|
this.state2.ready = false;
|
|
246
363
|
this.sessionServerMap.clear();
|
|
247
364
|
this.activeTasks.clear();
|
|
365
|
+
// Cleanup session cleanup state map
|
|
366
|
+
for (const [sessionId, state] of this.sessionCleanupStateMap.entries()) {
|
|
367
|
+
if (state.cleanupTimeoutId) {
|
|
368
|
+
clearTimeout(state.cleanupTimeoutId);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
this.sessionCleanupStateMap.clear();
|
|
248
372
|
this.emit("disconnected");
|
|
373
|
+
console.log("[WS Manager] Disconnect complete");
|
|
249
374
|
}
|
|
250
375
|
/**
|
|
251
376
|
* Send init message to specific server
|
|
@@ -382,7 +507,14 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
382
507
|
/**
|
|
383
508
|
* Send A2A response message with automatic routing
|
|
384
509
|
*/
|
|
385
|
-
async sendResponse(response, taskId, sessionId, isFinal = true, append =
|
|
510
|
+
async sendResponse(response, taskId, sessionId, isFinal = true, append = true) {
|
|
511
|
+
// Check if session is pending cleanup
|
|
512
|
+
const cleanupState = this.sessionCleanupStateMap.get(sessionId);
|
|
513
|
+
if (cleanupState) {
|
|
514
|
+
// Session is pending cleanup, silently discard response
|
|
515
|
+
console.log(`[RESPONSE] Discarding response for pending cleanup session ${sessionId}`);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
386
518
|
// Find which server this session belongs to
|
|
387
519
|
const targetServer = this.sessionServerMap.get(sessionId);
|
|
388
520
|
if (!targetServer) {
|
|
@@ -461,6 +593,13 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
461
593
|
* This uses "status-update" event type which keeps the conversation active
|
|
462
594
|
*/
|
|
463
595
|
async sendStatusUpdate(taskId, sessionId, message, targetServer) {
|
|
596
|
+
// Check if session is pending cleanup
|
|
597
|
+
const cleanupState = this.sessionCleanupStateMap.get(sessionId);
|
|
598
|
+
if (cleanupState) {
|
|
599
|
+
// Session is pending cleanup, silently discard status updates
|
|
600
|
+
console.log(`[STATUS] Discarding status update for pending cleanup session ${sessionId}`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
464
603
|
const serverId = targetServer || this.sessionServerMap.get(sessionId);
|
|
465
604
|
if (!serverId) {
|
|
466
605
|
console.error(`[STATUS] Unknown server for session ${sessionId}`);
|
|
@@ -535,6 +674,42 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
535
674
|
// TODO: Implement actual push message sending via HTTP API
|
|
536
675
|
// Need to confirm correct push message format with XiaoYi API documentation
|
|
537
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Send an outbound WebSocket message directly.
|
|
679
|
+
* This is a low-level method that sends a pre-formatted OutboundWebSocketMessage.
|
|
680
|
+
*
|
|
681
|
+
* @param sessionId - Session ID for routing
|
|
682
|
+
* @param message - Pre-formatted outbound message
|
|
683
|
+
*/
|
|
684
|
+
async sendMessage(sessionId, message) {
|
|
685
|
+
// Check if session is pending cleanup
|
|
686
|
+
const cleanupState = this.sessionCleanupStateMap.get(sessionId);
|
|
687
|
+
if (cleanupState) {
|
|
688
|
+
console.log(`[SEND_MESSAGE] Discarding message for pending cleanup session ${sessionId}`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// Find which server this session belongs to
|
|
692
|
+
const targetServer = this.sessionServerMap.get(sessionId);
|
|
693
|
+
if (!targetServer) {
|
|
694
|
+
console.error(`[SEND_MESSAGE] Unknown server for session ${sessionId}`);
|
|
695
|
+
throw new Error(`Cannot route message: unknown session ${sessionId}`);
|
|
696
|
+
}
|
|
697
|
+
// Get the corresponding WebSocket connection
|
|
698
|
+
const ws = targetServer === 'server1' ? this.ws1 : this.ws2;
|
|
699
|
+
const state = targetServer === 'server1' ? this.state1 : this.state2;
|
|
700
|
+
if (!ws || ws.readyState !== ws_1.default.OPEN) {
|
|
701
|
+
console.error(`[SEND_MESSAGE] ${targetServer} not connected for session ${sessionId}`);
|
|
702
|
+
throw new Error(`${targetServer} is not available`);
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
ws.send(JSON.stringify(message));
|
|
706
|
+
console.log(`[SEND_MESSAGE] Message sent to ${targetServer} for session ${sessionId}, msgType=${message.msgType}`);
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
console.error(`[SEND_MESSAGE] Failed to send to ${targetServer}:`, error);
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
538
713
|
/**
|
|
539
714
|
* Send tasks cancel response to specific server
|
|
540
715
|
*/
|
|
@@ -591,8 +766,8 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
591
766
|
id: message.id,
|
|
592
767
|
serverId: sourceServer,
|
|
593
768
|
});
|
|
594
|
-
//
|
|
595
|
-
this.
|
|
769
|
+
// Mark session for cleanup instead of immediate deletion
|
|
770
|
+
this.markSessionForCleanup(sessionId, sourceServer, this.config.sessionCleanupTimeoutMs ?? XiaoYiWebSocketManager.DEFAULT_CLEANUP_TIMEOUT_MS);
|
|
596
771
|
}
|
|
597
772
|
/**
|
|
598
773
|
* Handle clear message (legacy format)
|
|
@@ -606,7 +781,8 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
606
781
|
id: message.id,
|
|
607
782
|
serverId: sourceServer,
|
|
608
783
|
});
|
|
609
|
-
|
|
784
|
+
// Mark session for cleanup instead of immediate deletion
|
|
785
|
+
this.markSessionForCleanup(message.sessionId, sourceServer, this.config.sessionCleanupTimeoutMs ?? XiaoYiWebSocketManager.DEFAULT_CLEANUP_TIMEOUT_MS);
|
|
610
786
|
}
|
|
611
787
|
/**
|
|
612
788
|
* Handle tasks/cancel message
|
|
@@ -636,7 +812,7 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
636
812
|
/**
|
|
637
813
|
* Convert A2AResponseMessage to JSON-RPC 2.0 format
|
|
638
814
|
*/
|
|
639
|
-
convertToJsonRpcFormat(response, taskId, isFinal = true, append =
|
|
815
|
+
convertToJsonRpcFormat(response, taskId, isFinal = true, append = true) {
|
|
640
816
|
const artifactId = `artifact_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
641
817
|
if (response.status === "error" && response.error) {
|
|
642
818
|
return {
|
|
@@ -650,9 +826,11 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
650
826
|
}
|
|
651
827
|
const parts = [];
|
|
652
828
|
if (response.content.type === "text" && response.content.text) {
|
|
829
|
+
// When isFinal=true, use empty string for text (no content needed for final chunk)
|
|
830
|
+
const textContent = isFinal ? "" : response.content.text;
|
|
653
831
|
parts.push({
|
|
654
832
|
kind: "text",
|
|
655
|
-
text:
|
|
833
|
+
text: textContent,
|
|
656
834
|
});
|
|
657
835
|
}
|
|
658
836
|
else if (response.content.type === "file") {
|
|
@@ -665,10 +843,11 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
665
843
|
},
|
|
666
844
|
});
|
|
667
845
|
}
|
|
846
|
+
// When isFinal=true, append should be true and text should be empty
|
|
668
847
|
const artifactEvent = {
|
|
669
848
|
taskId: taskId,
|
|
670
849
|
kind: "artifact-update",
|
|
671
|
-
append: append,
|
|
850
|
+
append: isFinal ? true : append,
|
|
672
851
|
lastChunk: isFinal,
|
|
673
852
|
final: isFinal,
|
|
674
853
|
artifact: {
|
|
@@ -918,6 +1097,86 @@ class XiaoYiWebSocketManager extends events_1.EventEmitter {
|
|
|
918
1097
|
removeSession(sessionId) {
|
|
919
1098
|
this.sessionServerMap.delete(sessionId);
|
|
920
1099
|
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Mark a session for delayed cleanup
|
|
1102
|
+
* @param sessionId The session ID to mark for cleanup
|
|
1103
|
+
* @param serverId The server ID associated with this session
|
|
1104
|
+
* @param timeoutMs Timeout in milliseconds before forcing cleanup
|
|
1105
|
+
*/
|
|
1106
|
+
markSessionForCleanup(sessionId, serverId, timeoutMs) {
|
|
1107
|
+
// Check if already marked
|
|
1108
|
+
const existingState = this.sessionCleanupStateMap.get(sessionId);
|
|
1109
|
+
if (existingState) {
|
|
1110
|
+
// Already pending cleanup, reset timeout
|
|
1111
|
+
if (existingState.cleanupTimeoutId) {
|
|
1112
|
+
clearTimeout(existingState.cleanupTimeoutId);
|
|
1113
|
+
}
|
|
1114
|
+
console.log(`[CLEANUP] Session ${sessionId} already pending cleanup, resetting timeout`);
|
|
1115
|
+
}
|
|
1116
|
+
// Create new cleanup state
|
|
1117
|
+
const newState = {
|
|
1118
|
+
sessionId,
|
|
1119
|
+
serverId,
|
|
1120
|
+
markedForCleanupAt: Date.now(),
|
|
1121
|
+
reason: 'user_cleared',
|
|
1122
|
+
};
|
|
1123
|
+
// Start cleanup timeout
|
|
1124
|
+
const timeoutId = setTimeout(() => {
|
|
1125
|
+
console.log(`[CLEANUP] Timeout reached for session ${sessionId}, forcing cleanup`);
|
|
1126
|
+
this.forceCleanupSession(sessionId);
|
|
1127
|
+
}, timeoutMs);
|
|
1128
|
+
newState.cleanupTimeoutId = timeoutId;
|
|
1129
|
+
this.sessionCleanupStateMap.set(sessionId, newState);
|
|
1130
|
+
console.log(`[CLEANUP] Session ${sessionId} marked for cleanup (timeout: ${timeoutMs}ms)`);
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Force cleanup a session immediately
|
|
1134
|
+
* @param sessionId The session ID to cleanup
|
|
1135
|
+
*/
|
|
1136
|
+
forceCleanupSession(sessionId) {
|
|
1137
|
+
// Check if already cleaned
|
|
1138
|
+
const state = this.sessionCleanupStateMap.get(sessionId);
|
|
1139
|
+
if (!state) {
|
|
1140
|
+
console.log(`[CLEANUP] Session ${sessionId} already cleaned up, skipping`);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
// Clear timeout
|
|
1144
|
+
if (state.cleanupTimeoutId) {
|
|
1145
|
+
clearTimeout(state.cleanupTimeoutId);
|
|
1146
|
+
}
|
|
1147
|
+
// Remove from both maps
|
|
1148
|
+
this.sessionServerMap.delete(sessionId);
|
|
1149
|
+
this.sessionCleanupStateMap.delete(sessionId);
|
|
1150
|
+
console.log(`[CLEANUP] Session ${sessionId} cleanup completed`);
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Check if a session is pending cleanup
|
|
1154
|
+
* @param sessionId The session ID to check
|
|
1155
|
+
* @returns True if session is pending cleanup
|
|
1156
|
+
*/
|
|
1157
|
+
isSessionPendingCleanup(sessionId) {
|
|
1158
|
+
return this.sessionCleanupStateMap.has(sessionId);
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Get cleanup state for a session
|
|
1162
|
+
* @param sessionId The session ID to check
|
|
1163
|
+
* @returns Cleanup state if exists, undefined otherwise
|
|
1164
|
+
*/
|
|
1165
|
+
getSessionCleanupState(sessionId) {
|
|
1166
|
+
return this.sessionCleanupStateMap.get(sessionId);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Update accumulated text for a pending cleanup session
|
|
1170
|
+
* @param sessionId The session ID
|
|
1171
|
+
* @param text The accumulated text
|
|
1172
|
+
*/
|
|
1173
|
+
updateAccumulatedTextForCleanup(sessionId, text) {
|
|
1174
|
+
const state = this.sessionCleanupStateMap.get(sessionId);
|
|
1175
|
+
if (state) {
|
|
1176
|
+
state.accumulatedText = text;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
921
1179
|
}
|
|
922
1180
|
exports.XiaoYiWebSocketManager = XiaoYiWebSocketManager;
|
|
1181
|
+
XiaoYiWebSocketManager.DEFAULT_CLEANUP_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour
|
|
923
1182
|
XiaoYiWebSocketManager.STABLE_CONNECTION_THRESHOLD = 10000; // 10 seconds
|
package/dist/xy-bot.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { OpenClawConfig, RuntimeEnv } from "openclaw/dist/plugin-sdk/index.js";
|
|
2
|
+
type ClawdbotConfig = OpenClawConfig;
|
|
3
|
+
import type { A2AJsonRpcRequest } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Parameters for handling an XY message.
|
|
6
|
+
*/
|
|
7
|
+
export interface HandleXYMessageParams {
|
|
8
|
+
cfg: ClawdbotConfig;
|
|
9
|
+
runtime: RuntimeEnv;
|
|
10
|
+
message: A2AJsonRpcRequest;
|
|
11
|
+
accountId: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Handle an incoming A2A message.
|
|
15
|
+
* This is the main entry point for message processing.
|
|
16
|
+
* Runtime is expected to be validated before calling this function.
|
|
17
|
+
*/
|
|
18
|
+
export declare function handleXYMessage(params: HandleXYMessageParams): Promise<void>;
|
|
19
|
+
export {};
|