@ynhcj/xiaoyi-channel 0.0.39-beta → 0.0.41-beta
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/src/bot.js +17 -13
- package/dist/src/channel.js +5 -4
- package/dist/src/client.js +11 -24
- package/dist/src/config.js +2 -2
- package/dist/src/monitor.js +12 -0
- package/dist/src/outbound.js +122 -88
- package/dist/src/push.d.ts +7 -1
- package/dist/src/push.js +22 -7
- package/dist/src/tools/search-photo-gallery-tool.js +30 -2
- package/dist/src/tools/view-push-result-tool.d.ts +5 -0
- package/dist/src/tools/view-push-result-tool.js +118 -0
- package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +190 -0
- package/dist/src/trigger-handler.d.ts +19 -0
- package/dist/src/trigger-handler.js +91 -0
- package/dist/src/types.d.ts +1 -8
- package/dist/src/types.js +4 -0
- package/dist/src/utils/pushdata-manager.d.ts +28 -0
- package/dist/src/utils/pushdata-manager.js +171 -0
- package/dist/src/utils/pushid-manager.d.ts +12 -0
- package/dist/src/utils/pushid-manager.js +105 -0
- package/dist/src/websocket.d.ts +25 -31
- package/dist/src/websocket.js +218 -270
- package/package.json +1 -1
package/dist/src/websocket.js
CHANGED
|
@@ -1,45 +1,34 @@
|
|
|
1
|
-
//
|
|
2
|
-
// References xiaoyi_v2/websocket.ts for dual connection pattern
|
|
1
|
+
// WebSocket connection manager (Single connection)
|
|
3
2
|
import WebSocket from "ws";
|
|
4
3
|
import { EventEmitter } from "events";
|
|
5
4
|
import { HeartbeatManager } from "./heartbeat.js";
|
|
6
|
-
import { sessionManager } from "./utils/session.js";
|
|
7
5
|
/**
|
|
8
|
-
* Manages
|
|
9
|
-
* Implements session-to-server binding for message routing.
|
|
6
|
+
* Manages single WebSocket connection to XY server.
|
|
10
7
|
*
|
|
11
8
|
* Events:
|
|
12
|
-
* - 'message': (message: A2AJsonRpcRequest, sessionId: string
|
|
9
|
+
* - 'message': (message: A2AJsonRpcRequest, sessionId: string) => void
|
|
13
10
|
* - 'data-event': (event: A2ADataEvent) => void
|
|
14
11
|
* - 'gui-agent-response': (event: any) => void
|
|
15
|
-
* - '
|
|
16
|
-
* - '
|
|
17
|
-
* - '
|
|
18
|
-
* - '
|
|
12
|
+
* - 'trigger-event': (event: any) => void
|
|
13
|
+
* - 'connected': () => void
|
|
14
|
+
* - 'disconnected': () => void
|
|
15
|
+
* - 'error': (error: Error) => void
|
|
16
|
+
* - 'ready': () => void
|
|
19
17
|
*/
|
|
20
18
|
export class XYWebSocketManager extends EventEmitter {
|
|
21
19
|
config;
|
|
22
20
|
runtime;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
state1 = {
|
|
21
|
+
ws = null;
|
|
22
|
+
state = {
|
|
26
23
|
connected: false,
|
|
27
24
|
ready: false,
|
|
28
25
|
lastHeartbeat: 0,
|
|
29
26
|
reconnectAttempts: 0,
|
|
30
27
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
ready: false,
|
|
34
|
-
lastHeartbeat: 0,
|
|
35
|
-
reconnectAttempts: 0,
|
|
36
|
-
};
|
|
37
|
-
heartbeat1 = null;
|
|
38
|
-
heartbeat2 = null;
|
|
39
|
-
reconnectTimer1 = null;
|
|
40
|
-
reconnectTimer2 = null;
|
|
28
|
+
heartbeat = null;
|
|
29
|
+
reconnectTimer = null;
|
|
41
30
|
isShuttingDown = false;
|
|
42
|
-
// Logging functions
|
|
31
|
+
// Logging functions
|
|
43
32
|
log;
|
|
44
33
|
error;
|
|
45
34
|
// Health event callback
|
|
@@ -63,104 +52,66 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
63
52
|
isConfigMatch(config) {
|
|
64
53
|
return (this.config.apiKey === config.apiKey &&
|
|
65
54
|
this.config.agentId === config.agentId &&
|
|
66
|
-
this.config.
|
|
67
|
-
this.config.wsUrl2 === config.wsUrl2);
|
|
55
|
+
this.config.wsUrl === config.wsUrl);
|
|
68
56
|
}
|
|
69
57
|
/**
|
|
70
|
-
* Connect to
|
|
58
|
+
* Connect to WebSocket server.
|
|
71
59
|
* Does not throw error if connection fails - logs warning instead.
|
|
72
60
|
*/
|
|
73
61
|
async connect() {
|
|
74
|
-
this.log("Connecting to XY WebSocket
|
|
62
|
+
this.log("Connecting to XY WebSocket server...");
|
|
75
63
|
this.isShuttingDown = false;
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
this.
|
|
79
|
-
|
|
80
|
-
]);
|
|
81
|
-
const successCount = results.filter((r) => r.status === "fulfilled").length;
|
|
82
|
-
const failCount = results.filter((r) => r.status === "rejected").length;
|
|
83
|
-
if (successCount > 0) {
|
|
84
|
-
this.log(`Connected to ${successCount}/2 XY WebSocket servers`);
|
|
64
|
+
// ✅ Prevent re-entry: check if already connected or connecting
|
|
65
|
+
if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) {
|
|
66
|
+
this.log("Already connected or connecting, skipping duplicate connect()");
|
|
67
|
+
return;
|
|
85
68
|
}
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
});
|
|
69
|
+
try {
|
|
70
|
+
await this.connectServer(this.config.wsUrl);
|
|
71
|
+
this.log("Connected to XY WebSocket server");
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
this.error(`Failed to connect to WebSocket server: ${error.message}`);
|
|
75
|
+
this.error("Plugin will continue but cannot receive messages.");
|
|
94
76
|
}
|
|
95
77
|
}
|
|
96
78
|
/**
|
|
97
|
-
* Disconnect from
|
|
79
|
+
* Disconnect from WebSocket server.
|
|
98
80
|
*/
|
|
99
81
|
disconnect() {
|
|
100
|
-
this.log("Disconnecting from XY WebSocket
|
|
82
|
+
this.log("Disconnecting from XY WebSocket server...");
|
|
101
83
|
this.isShuttingDown = true;
|
|
102
|
-
if (this.
|
|
103
|
-
clearTimeout(this.
|
|
104
|
-
this.
|
|
105
|
-
}
|
|
106
|
-
if (this.reconnectTimer2) {
|
|
107
|
-
clearTimeout(this.reconnectTimer2);
|
|
108
|
-
this.reconnectTimer2 = null;
|
|
84
|
+
if (this.reconnectTimer) {
|
|
85
|
+
clearTimeout(this.reconnectTimer);
|
|
86
|
+
this.reconnectTimer = null;
|
|
109
87
|
}
|
|
110
|
-
this.
|
|
111
|
-
this.
|
|
112
|
-
// Clear session bindings
|
|
113
|
-
sessionManager.clear();
|
|
114
|
-
this.log("Disconnected from XY WebSocket servers");
|
|
88
|
+
this.cleanupConnection();
|
|
89
|
+
this.log("Disconnected from XY WebSocket server");
|
|
115
90
|
}
|
|
116
91
|
/**
|
|
117
|
-
* Send a message to the
|
|
92
|
+
* Send a message to the server.
|
|
118
93
|
*/
|
|
119
94
|
async sendMessage(sessionId, message) {
|
|
120
95
|
console.log(`[WEBSOCKET-SEND] <<<<<<< Preparing to send message for session: ${sessionId} <<<<<<<`);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// If no binding, choose the first ready server
|
|
124
|
-
if (!server) {
|
|
125
|
-
if (this.state1.ready) {
|
|
126
|
-
server = "server1";
|
|
127
|
-
}
|
|
128
|
-
else if (this.state2.ready) {
|
|
129
|
-
server = "server2";
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
throw new Error("No ready WebSocket servers available");
|
|
133
|
-
}
|
|
134
|
-
console.log(`[WEBSOCKET-SEND] No binding found, selected: ${server}`);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
console.log(`[WEBSOCKET-SEND] Using bound server: ${server}`);
|
|
138
|
-
}
|
|
139
|
-
// Send to the selected server
|
|
140
|
-
const ws = server === "server1" ? this.ws1 : this.ws2;
|
|
141
|
-
const state = server === "server1" ? this.state1 : this.state2;
|
|
142
|
-
if (!ws || !state.ready || ws.readyState !== WebSocket.OPEN) {
|
|
143
|
-
throw new Error(`WebSocket ${server} not ready`);
|
|
96
|
+
if (!this.ws || !this.state.ready || this.ws.readyState !== WebSocket.OPEN) {
|
|
97
|
+
throw new Error("WebSocket not ready");
|
|
144
98
|
}
|
|
145
99
|
const messageStr = JSON.stringify(message);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.log(`[WS-${server}-SEND] Message sent successfully, size: ${messageStr.length} bytes`);
|
|
100
|
+
this.ws.send(messageStr);
|
|
101
|
+
console.log(`[WS-SEND] Message sent successfully, size: ${messageStr.length} bytes`);
|
|
149
102
|
}
|
|
150
103
|
/**
|
|
151
|
-
* Check if
|
|
104
|
+
* Check if server is ready.
|
|
152
105
|
*/
|
|
153
106
|
isReady() {
|
|
154
|
-
return this.
|
|
107
|
+
return this.state.ready;
|
|
155
108
|
}
|
|
156
109
|
/**
|
|
157
110
|
* Get detailed connection diagnostics for monitoring and debugging.
|
|
158
|
-
* Helps identify orphan connections and connection leaks.
|
|
159
111
|
*/
|
|
160
112
|
getConnectionDiagnostics() {
|
|
161
113
|
const cacheKey = `${this.config.apiKey}-${this.config.agentId}`;
|
|
162
|
-
const
|
|
163
|
-
const server2Diag = this.getServerDiagnostic("server2", this.ws2, this.state2, this.heartbeat2, this.reconnectTimer2);
|
|
114
|
+
const connectionDiag = this.getConnectionDiagnostic();
|
|
164
115
|
// Count total event listeners on the manager
|
|
165
116
|
const totalEventListeners = this.listenerCount('message') +
|
|
166
117
|
this.listenerCount('connected') +
|
|
@@ -171,21 +122,20 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
171
122
|
this.listenerCount('gui-agent-response');
|
|
172
123
|
return {
|
|
173
124
|
cacheKey,
|
|
174
|
-
|
|
175
|
-
server2: server2Diag,
|
|
125
|
+
connection: connectionDiag,
|
|
176
126
|
isShuttingDown: this.isShuttingDown,
|
|
177
127
|
totalEventListeners,
|
|
178
128
|
};
|
|
179
129
|
}
|
|
180
130
|
/**
|
|
181
|
-
* Get diagnostic info for
|
|
131
|
+
* Get diagnostic info for the connection.
|
|
182
132
|
*/
|
|
183
|
-
|
|
184
|
-
const exists = ws !== null;
|
|
133
|
+
getConnectionDiagnostic() {
|
|
134
|
+
const exists = this.ws !== null;
|
|
185
135
|
let readyState = 'NULL';
|
|
186
136
|
let listenerCount = 0;
|
|
187
|
-
if (ws) {
|
|
188
|
-
switch (ws.readyState) {
|
|
137
|
+
if (this.ws) {
|
|
138
|
+
switch (this.ws.readyState) {
|
|
189
139
|
case WebSocket.CONNECTING:
|
|
190
140
|
readyState = 'CONNECTING';
|
|
191
141
|
break;
|
|
@@ -200,34 +150,70 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
200
150
|
break;
|
|
201
151
|
}
|
|
202
152
|
// Count event listeners on the WebSocket
|
|
203
|
-
listenerCount = ws.listenerCount('message') +
|
|
204
|
-
ws.listenerCount('close') +
|
|
205
|
-
ws.listenerCount('error') +
|
|
206
|
-
ws.listenerCount('open') +
|
|
207
|
-
ws.listenerCount('pong');
|
|
153
|
+
listenerCount = this.ws.listenerCount('message') +
|
|
154
|
+
this.ws.listenerCount('close') +
|
|
155
|
+
this.ws.listenerCount('error') +
|
|
156
|
+
this.ws.listenerCount('open') +
|
|
157
|
+
this.ws.listenerCount('pong');
|
|
208
158
|
}
|
|
209
159
|
// Orphan detection: connection is OPEN but has no message listeners
|
|
210
160
|
const isOrphan = exists &&
|
|
211
|
-
ws.readyState === WebSocket.OPEN &&
|
|
212
|
-
ws.listenerCount('message') === 0;
|
|
161
|
+
this.ws.readyState === WebSocket.OPEN &&
|
|
162
|
+
this.ws.listenerCount('message') === 0;
|
|
213
163
|
return {
|
|
214
164
|
exists,
|
|
215
165
|
readyState,
|
|
216
|
-
stateConnected: state.connected,
|
|
217
|
-
stateReady: state.ready,
|
|
218
|
-
reconnectAttempts: state.reconnectAttempts,
|
|
219
|
-
lastHeartbeat: state.lastHeartbeat,
|
|
220
|
-
heartbeatActive: heartbeat !== null,
|
|
221
|
-
hasReconnectTimer: reconnectTimer !== null,
|
|
166
|
+
stateConnected: this.state.connected,
|
|
167
|
+
stateReady: this.state.ready,
|
|
168
|
+
reconnectAttempts: this.state.reconnectAttempts,
|
|
169
|
+
lastHeartbeat: this.state.lastHeartbeat,
|
|
170
|
+
heartbeatActive: this.heartbeat !== null,
|
|
171
|
+
hasReconnectTimer: this.reconnectTimer !== null,
|
|
222
172
|
listenerCount,
|
|
223
173
|
isOrphan,
|
|
224
174
|
};
|
|
225
175
|
}
|
|
226
176
|
/**
|
|
227
|
-
*
|
|
177
|
+
* Clean up connection without triggering reconnection.
|
|
228
178
|
*/
|
|
229
|
-
|
|
179
|
+
cleanupConnection() {
|
|
180
|
+
// Stop heartbeat
|
|
181
|
+
if (this.heartbeat) {
|
|
182
|
+
this.heartbeat.stop();
|
|
183
|
+
this.heartbeat = null;
|
|
184
|
+
}
|
|
185
|
+
// Clear reconnect timer
|
|
186
|
+
if (this.reconnectTimer) {
|
|
187
|
+
clearTimeout(this.reconnectTimer);
|
|
188
|
+
this.reconnectTimer = null;
|
|
189
|
+
}
|
|
190
|
+
// Clean up WebSocket
|
|
191
|
+
if (this.ws) {
|
|
192
|
+
// Remove all event listeners
|
|
193
|
+
this.ws.removeAllListeners();
|
|
194
|
+
// Close the connection if still open
|
|
195
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
196
|
+
try {
|
|
197
|
+
this.ws.close();
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
this.error("Error closing WebSocket:", err);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Clear reference
|
|
204
|
+
this.ws = null;
|
|
205
|
+
}
|
|
206
|
+
// Reset state
|
|
207
|
+
this.state.connected = false;
|
|
208
|
+
this.state.ready = false;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Connect to server.
|
|
212
|
+
*/
|
|
213
|
+
async connectServer(url) {
|
|
230
214
|
return new Promise((resolve, reject) => {
|
|
215
|
+
// ✅ Clean up old connection first
|
|
216
|
+
this.cleanupConnection();
|
|
231
217
|
// Check if URL is wss with IP address to bypass certificate validation
|
|
232
218
|
const urlObj = new URL(url);
|
|
233
219
|
const isWssWithIP = urlObj.protocol === 'wss:' && /^(\d{1,3}\.){3}\d{1,3}$/.test(urlObj.hostname);
|
|
@@ -241,88 +227,49 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
241
227
|
};
|
|
242
228
|
// Bypass certificate validation for wss with IP address
|
|
243
229
|
if (isWssWithIP) {
|
|
244
|
-
this.log(
|
|
230
|
+
this.log(`Bypassing certificate validation for IP address: ${urlObj.hostname}`);
|
|
245
231
|
wsOptions.rejectUnauthorized = false;
|
|
246
232
|
}
|
|
247
233
|
const ws = new WebSocket(url, wsOptions);
|
|
248
|
-
|
|
249
|
-
// Set the WebSocket instance
|
|
250
|
-
if (serverId === "server1") {
|
|
251
|
-
this.ws1 = ws;
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
this.ws2 = ws;
|
|
255
|
-
}
|
|
234
|
+
this.ws = ws;
|
|
256
235
|
// Connection timeout
|
|
257
236
|
const connectTimeout = setTimeout(() => {
|
|
258
|
-
if (!state.connected) {
|
|
259
|
-
reject(new Error(
|
|
237
|
+
if (!this.state.connected) {
|
|
238
|
+
reject(new Error("Connection timeout"));
|
|
260
239
|
ws.close();
|
|
261
240
|
}
|
|
262
241
|
}, 30000); // 30 seconds
|
|
263
242
|
ws.on("open", () => {
|
|
264
243
|
clearTimeout(connectTimeout);
|
|
265
|
-
state.connected = true;
|
|
266
|
-
state.reconnectAttempts = 0;
|
|
267
|
-
this.log(
|
|
268
|
-
this.emit("connected"
|
|
244
|
+
this.state.connected = true;
|
|
245
|
+
this.state.reconnectAttempts = 0;
|
|
246
|
+
this.log("WebSocket connected");
|
|
247
|
+
this.emit("connected");
|
|
269
248
|
// Send init message
|
|
270
|
-
this.sendInitMessage(
|
|
249
|
+
this.sendInitMessage();
|
|
271
250
|
resolve();
|
|
272
251
|
});
|
|
273
252
|
ws.on("message", (data) => {
|
|
274
|
-
this.handleMessage(
|
|
253
|
+
this.handleMessage(data);
|
|
275
254
|
});
|
|
276
255
|
ws.on("close", (code, reason) => {
|
|
277
|
-
this.handleClose(
|
|
256
|
+
this.handleClose(code, reason.toString());
|
|
278
257
|
});
|
|
279
258
|
ws.on("error", (error) => {
|
|
280
|
-
this.handleError(
|
|
281
|
-
if (!state.connected) {
|
|
259
|
+
this.handleError(error);
|
|
260
|
+
if (!this.state.connected) {
|
|
282
261
|
clearTimeout(connectTimeout);
|
|
283
262
|
reject(error);
|
|
284
263
|
}
|
|
285
264
|
});
|
|
286
265
|
});
|
|
287
266
|
}
|
|
288
|
-
/**
|
|
289
|
-
* Disconnect from a specific server.
|
|
290
|
-
*/
|
|
291
|
-
disconnectServer(serverId) {
|
|
292
|
-
const ws = serverId === "server1" ? this.ws1 : this.ws2;
|
|
293
|
-
const heartbeat = serverId === "server1" ? this.heartbeat1 : this.heartbeat2;
|
|
294
|
-
const state = serverId === "server1" ? this.state1 : this.state2;
|
|
295
|
-
if (heartbeat) {
|
|
296
|
-
heartbeat.stop();
|
|
297
|
-
if (serverId === "server1") {
|
|
298
|
-
this.heartbeat1 = null;
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
this.heartbeat2 = null;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
if (ws) {
|
|
305
|
-
ws.removeAllListeners();
|
|
306
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
307
|
-
ws.close();
|
|
308
|
-
}
|
|
309
|
-
if (serverId === "server1") {
|
|
310
|
-
this.ws1 = null;
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
this.ws2 = null;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
state.connected = false;
|
|
317
|
-
state.ready = false;
|
|
318
|
-
}
|
|
319
267
|
/**
|
|
320
268
|
* Send init message to server.
|
|
321
269
|
*/
|
|
322
|
-
sendInitMessage(
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
this.error(`Cannot send init message: ${serverId} not open`);
|
|
270
|
+
sendInitMessage() {
|
|
271
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
272
|
+
this.error("Cannot send init message: WebSocket not open");
|
|
326
273
|
return;
|
|
327
274
|
}
|
|
328
275
|
const initMessage = {
|
|
@@ -331,24 +278,22 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
331
278
|
msgDetail: JSON.stringify({ agentId: this.config.agentId }),
|
|
332
279
|
};
|
|
333
280
|
const initMessageStr = JSON.stringify(initMessage);
|
|
334
|
-
console.log(
|
|
335
|
-
ws.send(initMessageStr);
|
|
336
|
-
console.log(`[WS
|
|
281
|
+
console.log("[WS-SEND] Sending init message frame:", JSON.stringify(initMessage, null, 2));
|
|
282
|
+
this.ws.send(initMessageStr);
|
|
283
|
+
console.log(`[WS-SEND] Init message sent successfully, size: ${initMessageStr.length} bytes`);
|
|
337
284
|
// Mark as ready after init
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
this.emit("ready", serverId);
|
|
285
|
+
this.state.ready = true;
|
|
286
|
+
this.emit("ready");
|
|
341
287
|
// Start heartbeat
|
|
342
|
-
this.startHeartbeat(
|
|
288
|
+
this.startHeartbeat();
|
|
343
289
|
}
|
|
344
290
|
/**
|
|
345
|
-
* Start heartbeat
|
|
291
|
+
* Start heartbeat.
|
|
346
292
|
*/
|
|
347
|
-
startHeartbeat(
|
|
348
|
-
|
|
349
|
-
if (!ws)
|
|
293
|
+
startHeartbeat() {
|
|
294
|
+
if (!this.ws)
|
|
350
295
|
return;
|
|
351
|
-
const heartbeat = new HeartbeatManager(ws, {
|
|
296
|
+
const heartbeat = new HeartbeatManager(this.ws, {
|
|
352
297
|
interval: 30000, // 30 seconds
|
|
353
298
|
timeout: 10000, // 10 seconds
|
|
354
299
|
message: JSON.stringify({
|
|
@@ -357,203 +302,206 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
357
302
|
msgDetail: JSON.stringify({ timestamp: Date.now() }),
|
|
358
303
|
}),
|
|
359
304
|
}, () => {
|
|
360
|
-
this.error(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
305
|
+
this.error("Heartbeat timeout, reconnecting...");
|
|
306
|
+
// ✅ Close connection first before reconnecting
|
|
307
|
+
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
|
308
|
+
this.log("Closing connection due to heartbeat timeout");
|
|
309
|
+
this.ws.close(); // This will trigger handleClose which will call reconnectServer
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// Connection already closed, just reconnect
|
|
313
|
+
this.reconnectServer();
|
|
314
|
+
}
|
|
315
|
+
}, "websocket", this.log, this.error, this.onHealthEvent);
|
|
364
316
|
heartbeat.start();
|
|
365
|
-
|
|
366
|
-
this.heartbeat1 = heartbeat;
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
this.heartbeat2 = heartbeat;
|
|
370
|
-
}
|
|
317
|
+
this.heartbeat = heartbeat;
|
|
371
318
|
}
|
|
372
319
|
/**
|
|
373
320
|
* Handle incoming message from server.
|
|
374
321
|
*/
|
|
375
|
-
handleMessage(
|
|
376
|
-
console.log(
|
|
322
|
+
handleMessage(data) {
|
|
323
|
+
console.log("[WEBSOCKET-HANDLE] >>>>>>> Receiving message... <<<<<<<");
|
|
377
324
|
try {
|
|
378
325
|
const messageStr = data.toString();
|
|
379
|
-
console.log(`[WS
|
|
326
|
+
console.log(`[WS-RECV] Raw message frame, size: ${messageStr.length} bytes`);
|
|
380
327
|
const parsed = JSON.parse(messageStr);
|
|
381
|
-
|
|
382
|
-
console.log(`[WS-${serverId}-RECV] Parsed message:`, JSON.stringify(parsed, null, 2));
|
|
328
|
+
console.log("[WS-RECV] Parsed message:", JSON.stringify(parsed, null, 2));
|
|
383
329
|
// Check if message is in direct A2A JSON-RPC format (server push)
|
|
384
330
|
if (parsed.jsonrpc === "2.0") {
|
|
385
|
-
// Direct A2A format
|
|
386
331
|
const a2aRequest = parsed;
|
|
387
|
-
console.log(`[XY
|
|
332
|
+
console.log(`[XY] Message type: Direct A2A JSON-RPC, method: ${a2aRequest.method}`);
|
|
388
333
|
// Extract sessionId from params
|
|
389
334
|
const sessionId = a2aRequest.params?.sessionId;
|
|
390
335
|
if (!sessionId) {
|
|
391
|
-
console.error(
|
|
336
|
+
console.error("[XY] Message missing sessionId");
|
|
392
337
|
return;
|
|
393
338
|
}
|
|
394
|
-
console.log(`[XY
|
|
395
|
-
// Bind session to this server if not already bound
|
|
396
|
-
if (!sessionManager.isBound(sessionId)) {
|
|
397
|
-
sessionManager.bind(sessionId, serverId);
|
|
398
|
-
console.log(`[XY-${serverId}] Bound session ${sessionId} to ${serverId}`);
|
|
399
|
-
}
|
|
339
|
+
console.log(`[XY] Session ID: ${sessionId}`);
|
|
400
340
|
// Check if message contains only data parts (tool results)
|
|
401
341
|
const dataParts = a2aRequest.params?.message?.parts?.filter((p) => p.kind === "data");
|
|
402
342
|
const hasOnlyDataParts = dataParts && dataParts.length > 0 &&
|
|
403
343
|
dataParts.length === a2aRequest.params?.message?.parts?.length;
|
|
404
344
|
if (hasOnlyDataParts) {
|
|
405
|
-
|
|
406
|
-
// Only emit data-event, don't send to openclaw
|
|
407
|
-
console.log(`[XY-${serverId}] Message contains only data parts, processing as tool result`);
|
|
345
|
+
console.log("[XY] Message contains only data parts, processing as tool result");
|
|
408
346
|
for (const dataPart of dataParts) {
|
|
409
|
-
// Data format: {events: [{header, payload}, ...]}
|
|
410
347
|
const events = dataPart.data?.events;
|
|
411
348
|
if (!Array.isArray(events)) {
|
|
412
|
-
console.warn(
|
|
349
|
+
console.warn("[XY] dataPart.data.events is not an array, skipping");
|
|
413
350
|
continue;
|
|
414
351
|
}
|
|
415
|
-
console.log(`[XY
|
|
352
|
+
console.log(`[XY] Processing ${events.length} events from data.events`);
|
|
416
353
|
for (const item of events) {
|
|
417
|
-
// Check if it's an UploadExeResult (intent execution result)
|
|
418
354
|
if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
|
|
419
355
|
const dataEvent = {
|
|
420
356
|
intentName: item.payload.intentName,
|
|
421
357
|
outputs: item.payload.outputs || {},
|
|
422
358
|
status: "success",
|
|
423
359
|
};
|
|
424
|
-
console.log(
|
|
360
|
+
console.log("[XY] Emitting data-event:", dataEvent);
|
|
425
361
|
this.emit("data-event", dataEvent);
|
|
426
362
|
}
|
|
427
|
-
// Check if it's an InvokeJarvisGUIAgentResponse
|
|
428
363
|
else if (item.header?.namespace === "ClawAgent" && item.header?.name === "InvokeJarvisGUIAgentResponse") {
|
|
429
|
-
console.log(
|
|
364
|
+
console.log("[XY] Emitting gui-agent-response:", item);
|
|
430
365
|
this.emit("gui-agent-response", item);
|
|
431
366
|
}
|
|
367
|
+
else if (item.header?.namespace === "Common" && item.header?.name === "Trigger") {
|
|
368
|
+
console.log("[XY] Trigger event detected, emitting trigger-event with context");
|
|
369
|
+
// 传递完整上下文:event、sessionId、taskId
|
|
370
|
+
this.emit("trigger-event", {
|
|
371
|
+
event: item,
|
|
372
|
+
sessionId: sessionId,
|
|
373
|
+
taskId: a2aRequest.params?.id, // 新的 taskId(点击推送时生成)
|
|
374
|
+
});
|
|
375
|
+
}
|
|
432
376
|
}
|
|
433
377
|
}
|
|
434
|
-
return;
|
|
378
|
+
return;
|
|
435
379
|
}
|
|
436
380
|
// Emit message event for non-data-only messages
|
|
437
|
-
console.log(
|
|
438
|
-
this.emit("message", a2aRequest, sessionId
|
|
381
|
+
console.log("[XY] *** EMITTING message event (Direct A2A path) ***");
|
|
382
|
+
this.emit("message", a2aRequest, sessionId);
|
|
439
383
|
return;
|
|
440
384
|
}
|
|
441
385
|
// Wrapped format (InboundWebSocketMessage)
|
|
442
386
|
const inboundMsg = parsed;
|
|
443
|
-
console.log(`[XY
|
|
387
|
+
console.log(`[XY] Message type: Wrapped, msgType: ${inboundMsg.msgType}`);
|
|
444
388
|
// Handle heartbeat responses
|
|
445
389
|
if (inboundMsg.msgType === "heartbeat") {
|
|
446
|
-
console.log(
|
|
447
|
-
// ✅ Report health: application-level heartbeat received
|
|
448
|
-
// This prevents openclaw health-monitor from marking connection as stale
|
|
390
|
+
console.log("[XY] Received heartbeat response");
|
|
449
391
|
this.onHealthEvent?.();
|
|
450
392
|
return;
|
|
451
393
|
}
|
|
452
|
-
// Handle data messages
|
|
394
|
+
// Handle data messages
|
|
453
395
|
if (inboundMsg.msgType === "data") {
|
|
454
|
-
console.log(
|
|
396
|
+
console.log("[XY] Processing data message");
|
|
455
397
|
try {
|
|
456
398
|
const a2aRequest = JSON.parse(inboundMsg.msgDetail);
|
|
457
399
|
const dataParts = a2aRequest.params?.message?.parts?.filter((p) => p.kind === "data");
|
|
458
400
|
if (dataParts && dataParts.length > 0) {
|
|
459
401
|
for (const dataPart of dataParts) {
|
|
460
|
-
// Data format: {events: [{header, payload}, ...]}
|
|
461
402
|
const events = dataPart.data?.events;
|
|
462
403
|
if (!Array.isArray(events)) {
|
|
463
|
-
console.warn(
|
|
404
|
+
console.warn("[XY] dataPart.data.events is not an array, skipping");
|
|
464
405
|
continue;
|
|
465
406
|
}
|
|
466
|
-
console.log(`[XY
|
|
407
|
+
console.log(`[XY] Processing ${events.length} events from data.events`);
|
|
467
408
|
for (const item of events) {
|
|
468
|
-
// Check if it's an UploadExeResult (intent execution result)
|
|
469
409
|
if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
|
|
470
410
|
const dataEvent = {
|
|
471
411
|
intentName: item.payload.intentName,
|
|
472
412
|
outputs: item.payload.outputs || {},
|
|
473
413
|
status: "success",
|
|
474
414
|
};
|
|
475
|
-
console.log(
|
|
415
|
+
console.log("[XY] Emitting data-event:", dataEvent);
|
|
476
416
|
this.emit("data-event", dataEvent);
|
|
477
417
|
}
|
|
478
|
-
// Check if it's an InvokeJarvisGUIAgentResponse
|
|
479
418
|
else if (item.header?.namespace === "ClawAgent" && item.header?.name === "InvokeJarvisGUIAgentResponse") {
|
|
480
|
-
console.log(
|
|
419
|
+
console.log("[XY] Emitting gui-agent-response:", item);
|
|
481
420
|
this.emit("gui-agent-response", item);
|
|
482
421
|
}
|
|
422
|
+
else if (item.header?.namespace === "Common" && item.header?.name === "Trigger") {
|
|
423
|
+
console.log("[XY] Trigger event detected (wrapped format), emitting trigger-event with context");
|
|
424
|
+
// 传递完整上下文:event、sessionId、taskId
|
|
425
|
+
this.emit("trigger-event", {
|
|
426
|
+
event: item,
|
|
427
|
+
sessionId: inboundMsg.sessionId || a2aRequest.params?.sessionId,
|
|
428
|
+
taskId: inboundMsg.taskId || a2aRequest.params?.id,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
483
431
|
}
|
|
484
432
|
}
|
|
485
433
|
}
|
|
486
434
|
}
|
|
487
435
|
catch (error) {
|
|
488
|
-
console.error(
|
|
436
|
+
console.error("[XY] Failed to process data message:", error);
|
|
489
437
|
}
|
|
490
438
|
return;
|
|
491
439
|
}
|
|
492
440
|
// Parse msgDetail as A2AJsonRpcRequest
|
|
493
441
|
const a2aRequest = JSON.parse(inboundMsg.msgDetail);
|
|
494
|
-
console.log(`[XY
|
|
495
|
-
// Bind session to this server if not already bound
|
|
442
|
+
console.log(`[XY] Parsed A2A request, method: ${a2aRequest.method}`);
|
|
496
443
|
const sessionId = inboundMsg.sessionId;
|
|
497
|
-
|
|
498
|
-
sessionManager.bind(sessionId, serverId);
|
|
499
|
-
console.log(`[XY-${serverId}] Bound session ${sessionId} to ${serverId}`);
|
|
500
|
-
}
|
|
501
|
-
console.log(`[XY-${serverId}] Session ID: ${sessionId}`);
|
|
444
|
+
console.log(`[XY] Session ID: ${sessionId}`);
|
|
502
445
|
// Emit message event
|
|
503
|
-
console.log(
|
|
504
|
-
this.emit("message", a2aRequest, sessionId
|
|
446
|
+
console.log("[XY] *** EMITTING message event (Wrapped path) ***");
|
|
447
|
+
this.emit("message", a2aRequest, sessionId);
|
|
505
448
|
}
|
|
506
449
|
catch (error) {
|
|
507
|
-
console.error(
|
|
450
|
+
console.error("[XY] Failed to parse message:", error);
|
|
508
451
|
}
|
|
509
452
|
}
|
|
510
453
|
/**
|
|
511
454
|
* Handle connection close.
|
|
512
455
|
*/
|
|
513
|
-
handleClose(
|
|
514
|
-
console.warn(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
456
|
+
handleClose(code, reason) {
|
|
457
|
+
console.warn(`WebSocket disconnected: code=${code}, reason=${reason}`);
|
|
458
|
+
// Only process if this is the current connection
|
|
459
|
+
if (!this.ws) {
|
|
460
|
+
this.log("Ignoring close event for already cleaned connection");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
this.state.connected = false;
|
|
464
|
+
this.state.ready = false;
|
|
465
|
+
this.emit("disconnected");
|
|
466
|
+
// Clean up
|
|
467
|
+
if (this.heartbeat) {
|
|
468
|
+
this.heartbeat.stop();
|
|
469
|
+
this.heartbeat = null;
|
|
523
470
|
}
|
|
471
|
+
this.ws.removeAllListeners();
|
|
472
|
+
this.ws = null;
|
|
524
473
|
// Attempt reconnection if not shutting down
|
|
525
474
|
if (!this.isShuttingDown) {
|
|
526
|
-
this.reconnectServer(
|
|
475
|
+
this.reconnectServer();
|
|
527
476
|
}
|
|
528
477
|
}
|
|
529
478
|
/**
|
|
530
479
|
* Handle connection error.
|
|
531
480
|
*/
|
|
532
|
-
handleError(
|
|
533
|
-
this.error(
|
|
534
|
-
this.emit("error", error
|
|
481
|
+
handleError(error) {
|
|
482
|
+
this.error("WebSocket error:", error);
|
|
483
|
+
this.emit("error", error);
|
|
535
484
|
}
|
|
536
485
|
/**
|
|
537
|
-
* Reconnect
|
|
486
|
+
* Reconnect with exponential backoff.
|
|
538
487
|
*/
|
|
539
|
-
reconnectServer(
|
|
488
|
+
reconnectServer() {
|
|
540
489
|
if (this.isShuttingDown)
|
|
541
490
|
return;
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
491
|
+
// Clear existing reconnect timer
|
|
492
|
+
if (this.reconnectTimer) {
|
|
493
|
+
clearTimeout(this.reconnectTimer);
|
|
494
|
+
this.log("Cleared existing reconnect timer to prevent concurrent reconnection");
|
|
495
|
+
}
|
|
496
|
+
this.state.reconnectAttempts++;
|
|
497
|
+
const delay = Math.min(1000 * Math.pow(2, this.state.reconnectAttempts - 1), 30000);
|
|
498
|
+
this.log(`Reconnecting in ${delay}ms (attempt ${this.state.reconnectAttempts})...`);
|
|
546
499
|
const timer = setTimeout(() => {
|
|
547
|
-
|
|
548
|
-
this.connectServer(
|
|
549
|
-
this.error(
|
|
500
|
+
this.reconnectTimer = null;
|
|
501
|
+
this.connectServer(this.config.wsUrl).catch((error) => {
|
|
502
|
+
this.error("Reconnection failed:", error);
|
|
550
503
|
});
|
|
551
504
|
}, delay);
|
|
552
|
-
|
|
553
|
-
this.reconnectTimer1 = timer;
|
|
554
|
-
}
|
|
555
|
-
else {
|
|
556
|
-
this.reconnectTimer2 = timer;
|
|
557
|
-
}
|
|
505
|
+
this.reconnectTimer = timer;
|
|
558
506
|
}
|
|
559
507
|
}
|