@ynhcj/xiaoyi-channel 0.0.2 → 0.0.3-next

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.
Files changed (71) hide show
  1. package/dist/src/bot.js +115 -35
  2. package/dist/src/channel.js +22 -1
  3. package/dist/src/client.d.ts +15 -0
  4. package/dist/src/client.js +97 -2
  5. package/dist/src/file-download.js +10 -1
  6. package/dist/src/file-upload.js +1 -1
  7. package/dist/src/formatter.d.ts +17 -0
  8. package/dist/src/formatter.js +86 -6
  9. package/dist/src/heartbeat.d.ts +2 -1
  10. package/dist/src/heartbeat.js +9 -1
  11. package/dist/src/monitor.d.ts +5 -0
  12. package/dist/src/monitor.js +131 -25
  13. package/dist/src/onboarding.js +7 -7
  14. package/dist/src/outbound.js +150 -71
  15. package/dist/src/parser.d.ts +5 -0
  16. package/dist/src/parser.js +15 -0
  17. package/dist/src/push.d.ts +7 -1
  18. package/dist/src/push.js +110 -19
  19. package/dist/src/reply-dispatcher.d.ts +1 -0
  20. package/dist/src/reply-dispatcher.js +210 -57
  21. package/dist/src/task-manager.d.ts +55 -0
  22. package/dist/src/task-manager.js +136 -0
  23. package/dist/src/tools/calendar-tool.d.ts +6 -0
  24. package/dist/src/tools/calendar-tool.js +167 -0
  25. package/dist/src/tools/call-phone-tool.d.ts +5 -0
  26. package/dist/src/tools/call-phone-tool.js +183 -0
  27. package/dist/src/tools/create-alarm-tool.d.ts +7 -0
  28. package/dist/src/tools/create-alarm-tool.js +444 -0
  29. package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
  30. package/dist/src/tools/delete-alarm-tool.js +238 -0
  31. package/dist/src/tools/location-tool.js +48 -9
  32. package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
  33. package/dist/src/tools/modify-alarm-tool.js +474 -0
  34. package/dist/src/tools/modify-note-tool.d.ts +9 -0
  35. package/dist/src/tools/modify-note-tool.js +163 -0
  36. package/dist/src/tools/note-tool.d.ts +5 -0
  37. package/dist/src/tools/note-tool.js +146 -0
  38. package/dist/src/tools/search-alarm-tool.d.ts +8 -0
  39. package/dist/src/tools/search-alarm-tool.js +389 -0
  40. package/dist/src/tools/search-calendar-tool.d.ts +12 -0
  41. package/dist/src/tools/search-calendar-tool.js +259 -0
  42. package/dist/src/tools/search-contact-tool.d.ts +5 -0
  43. package/dist/src/tools/search-contact-tool.js +168 -0
  44. package/dist/src/tools/search-file-tool.d.ts +5 -0
  45. package/dist/src/tools/search-file-tool.js +185 -0
  46. package/dist/src/tools/search-message-tool.d.ts +5 -0
  47. package/dist/src/tools/search-message-tool.js +173 -0
  48. package/dist/src/tools/search-note-tool.d.ts +5 -0
  49. package/dist/src/tools/search-note-tool.js +130 -0
  50. package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
  51. package/dist/src/tools/search-photo-gallery-tool.js +184 -0
  52. package/dist/src/tools/search-photo-tool.d.ts +9 -0
  53. package/dist/src/tools/search-photo-tool.js +270 -0
  54. package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
  55. package/dist/src/tools/send-file-to-user-tool.js +318 -0
  56. package/dist/src/tools/send-message-tool.d.ts +5 -0
  57. package/dist/src/tools/send-message-tool.js +189 -0
  58. package/dist/src/tools/session-manager.d.ts +15 -0
  59. package/dist/src/tools/session-manager.js +126 -6
  60. package/dist/src/tools/upload-file-tool.d.ts +13 -0
  61. package/dist/src/tools/upload-file-tool.js +265 -0
  62. package/dist/src/tools/upload-photo-tool.d.ts +9 -0
  63. package/dist/src/tools/upload-photo-tool.js +223 -0
  64. package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
  65. package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
  66. package/dist/src/types.d.ts +5 -9
  67. package/dist/src/utils/config-manager.d.ts +26 -0
  68. package/dist/src/utils/config-manager.js +56 -0
  69. package/dist/src/websocket.d.ts +41 -0
  70. package/dist/src/websocket.js +192 -9
  71. package/package.json +1 -2
@@ -10,6 +10,8 @@ import { sessionManager } from "./utils/session.js";
10
10
  *
11
11
  * Events:
12
12
  * - 'message': (message: A2AJsonRpcRequest, sessionId: string, serverId: ServerIdentifier) => void
13
+ * - 'data-event': (event: A2ADataEvent) => void
14
+ * - 'gui-agent-response': (event: any) => void
13
15
  * - 'connected': (serverId: ServerIdentifier) => void
14
16
  * - 'disconnected': (serverId: ServerIdentifier) => void
15
17
  * - 'error': (error: Error, serverId: ServerIdentifier) => void
@@ -40,6 +42,8 @@ export class XYWebSocketManager extends EventEmitter {
40
42
  // Logging functions following feishu pattern
41
43
  log;
42
44
  error;
45
+ // Health event callback
46
+ onHealthEvent;
43
47
  constructor(config, runtime) {
44
48
  super();
45
49
  this.config = config;
@@ -47,6 +51,12 @@ export class XYWebSocketManager extends EventEmitter {
47
51
  this.log = runtime?.log ?? console.log;
48
52
  this.error = runtime?.error ?? console.error;
49
53
  }
54
+ /**
55
+ * Set health event callback to report activity to OpenClaw framework.
56
+ */
57
+ setHealthEventCallback(callback) {
58
+ this.onHealthEvent = callback;
59
+ }
50
60
  /**
51
61
  * Check if config matches the current instance.
52
62
  */
@@ -107,6 +117,7 @@ export class XYWebSocketManager extends EventEmitter {
107
117
  * Send a message to the appropriate server based on session binding.
108
118
  */
109
119
  async sendMessage(sessionId, message) {
120
+ console.log(`[WEBSOCKET-SEND] <<<<<<< Preparing to send message for session: ${sessionId} <<<<<<<`);
110
121
  // Determine which server to use
111
122
  let server = sessionManager.getBinding(sessionId);
112
123
  // If no binding, choose the first ready server
@@ -120,6 +131,10 @@ export class XYWebSocketManager extends EventEmitter {
120
131
  else {
121
132
  throw new Error("No ready WebSocket servers available");
122
133
  }
134
+ console.log(`[WEBSOCKET-SEND] No binding found, selected: ${server}`);
135
+ }
136
+ else {
137
+ console.log(`[WEBSOCKET-SEND] Using bound server: ${server}`);
123
138
  }
124
139
  // Send to the selected server
125
140
  const ws = server === "server1" ? this.ws1 : this.ws2;
@@ -128,7 +143,9 @@ export class XYWebSocketManager extends EventEmitter {
128
143
  throw new Error(`WebSocket ${server} not ready`);
129
144
  }
130
145
  const messageStr = JSON.stringify(message);
146
+ console.log(`[WS-${server}-SEND] Sending message frame:`, JSON.stringify(message, null, 2));
131
147
  ws.send(messageStr);
148
+ console.log(`[WS-${server}-SEND] Message sent successfully, size: ${messageStr.length} bytes`);
132
149
  }
133
150
  /**
134
151
  * Check if at least one server is ready.
@@ -136,19 +153,98 @@ export class XYWebSocketManager extends EventEmitter {
136
153
  isReady() {
137
154
  return this.state1.ready || this.state2.ready;
138
155
  }
156
+ /**
157
+ * Get detailed connection diagnostics for monitoring and debugging.
158
+ * Helps identify orphan connections and connection leaks.
159
+ */
160
+ getConnectionDiagnostics() {
161
+ const cacheKey = `${this.config.apiKey}-${this.config.agentId}`;
162
+ const server1Diag = this.getServerDiagnostic("server1", this.ws1, this.state1, this.heartbeat1, this.reconnectTimer1);
163
+ const server2Diag = this.getServerDiagnostic("server2", this.ws2, this.state2, this.heartbeat2, this.reconnectTimer2);
164
+ // Count total event listeners on the manager
165
+ const totalEventListeners = this.listenerCount('message') +
166
+ this.listenerCount('connected') +
167
+ this.listenerCount('disconnected') +
168
+ this.listenerCount('error') +
169
+ this.listenerCount('ready') +
170
+ this.listenerCount('data-event') +
171
+ this.listenerCount('gui-agent-response');
172
+ return {
173
+ cacheKey,
174
+ server1: server1Diag,
175
+ server2: server2Diag,
176
+ isShuttingDown: this.isShuttingDown,
177
+ totalEventListeners,
178
+ };
179
+ }
180
+ /**
181
+ * Get diagnostic info for a single server connection.
182
+ */
183
+ getServerDiagnostic(serverId, ws, state, heartbeat, reconnectTimer) {
184
+ const exists = ws !== null;
185
+ let readyState = 'NULL';
186
+ let listenerCount = 0;
187
+ if (ws) {
188
+ switch (ws.readyState) {
189
+ case WebSocket.CONNECTING:
190
+ readyState = 'CONNECTING';
191
+ break;
192
+ case WebSocket.OPEN:
193
+ readyState = 'OPEN';
194
+ break;
195
+ case WebSocket.CLOSING:
196
+ readyState = 'CLOSING';
197
+ break;
198
+ case WebSocket.CLOSED:
199
+ readyState = 'CLOSED';
200
+ break;
201
+ }
202
+ // 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');
208
+ }
209
+ // Orphan detection: connection is OPEN but has no message listeners
210
+ const isOrphan = exists &&
211
+ ws.readyState === WebSocket.OPEN &&
212
+ ws.listenerCount('message') === 0;
213
+ return {
214
+ exists,
215
+ 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,
222
+ listenerCount,
223
+ isOrphan,
224
+ };
225
+ }
139
226
  /**
140
227
  * Connect to a specific server.
141
228
  */
142
229
  async connectServer(serverId, url) {
143
230
  return new Promise((resolve, reject) => {
144
- const ws = new WebSocket(url, {
231
+ // Check if URL is wss with IP address to bypass certificate validation
232
+ const urlObj = new URL(url);
233
+ const isWssWithIP = urlObj.protocol === 'wss:' && /^(\d{1,3}\.){3}\d{1,3}$/.test(urlObj.hostname);
234
+ const wsOptions = {
145
235
  headers: {
146
236
  "x-uid": this.config.uid,
147
237
  "x-api-key": this.config.apiKey,
148
238
  "x-agent-id": this.config.agentId,
149
239
  "x-request-from": "openclaw",
150
240
  },
151
- });
241
+ };
242
+ // Bypass certificate validation for wss with IP address
243
+ if (isWssWithIP) {
244
+ this.log(`${serverId}: Bypassing certificate validation for IP address: ${urlObj.hostname}`);
245
+ wsOptions.rejectUnauthorized = false;
246
+ }
247
+ const ws = new WebSocket(url, wsOptions);
152
248
  const state = serverId === "server1" ? this.state1 : this.state2;
153
249
  // Set the WebSocket instance
154
250
  if (serverId === "server1") {
@@ -234,7 +330,10 @@ export class XYWebSocketManager extends EventEmitter {
234
330
  agentId: this.config.agentId,
235
331
  msgDetail: JSON.stringify({ agentId: this.config.agentId }),
236
332
  };
237
- ws.send(JSON.stringify(initMessage));
333
+ const initMessageStr = JSON.stringify(initMessage);
334
+ console.log(`[WS-${serverId}-SEND] Sending init message frame:`, JSON.stringify(initMessage, null, 2));
335
+ ws.send(initMessageStr);
336
+ console.log(`[WS-${serverId}-SEND] Init message sent successfully, size: ${initMessageStr.length} bytes`);
238
337
  // Mark as ready after init
239
338
  const state = serverId === "server1" ? this.state1 : this.state2;
240
339
  state.ready = true;
@@ -260,7 +359,8 @@ export class XYWebSocketManager extends EventEmitter {
260
359
  }, () => {
261
360
  this.error(`Heartbeat timeout for ${serverId}, reconnecting...`);
262
361
  this.reconnectServer(serverId);
263
- }, serverId, this.log, this.error);
362
+ }, serverId, this.log, this.error, this.onHealthEvent // ✅ Pass health event callback
363
+ );
264
364
  heartbeat.start();
265
365
  if (serverId === "server1") {
266
366
  this.heartbeat1 = heartbeat;
@@ -273,11 +373,13 @@ export class XYWebSocketManager extends EventEmitter {
273
373
  * Handle incoming message from server.
274
374
  */
275
375
  handleMessage(serverId, data) {
376
+ console.log(`[WEBSOCKET-HANDLE] >>>>>>> serverId: ${serverId}, receiving message... <<<<<<<`);
276
377
  try {
277
378
  const messageStr = data.toString();
379
+ console.log(`[WS-${serverId}-RECV] Raw message frame, size: ${messageStr.length} bytes`);
278
380
  const parsed = JSON.parse(messageStr);
279
381
  // Log raw message
280
- console.log(`[XY-${serverId}] Received message:`, JSON.stringify(parsed, null, 2));
382
+ console.log(`[WS-${serverId}-RECV] Parsed message:`, JSON.stringify(parsed, null, 2));
281
383
  // Check if message is in direct A2A JSON-RPC format (server push)
282
384
  if (parsed.jsonrpc === "2.0") {
283
385
  // Direct A2A format
@@ -295,16 +397,96 @@ export class XYWebSocketManager extends EventEmitter {
295
397
  sessionManager.bind(sessionId, serverId);
296
398
  console.log(`[XY-${serverId}] Bound session ${sessionId} to ${serverId}`);
297
399
  }
298
- // Emit message event
400
+ // Check if message contains only data parts (tool results)
401
+ const dataParts = a2aRequest.params?.message?.parts?.filter((p) => p.kind === "data");
402
+ const hasOnlyDataParts = dataParts && dataParts.length > 0 &&
403
+ dataParts.length === a2aRequest.params?.message?.parts?.length;
404
+ if (hasOnlyDataParts) {
405
+ // This is a data-only message (e.g., intent execution result)
406
+ // Only emit data-event, don't send to openclaw
407
+ console.log(`[XY-${serverId}] Message contains only data parts, processing as tool result`);
408
+ for (const dataPart of dataParts) {
409
+ // Data format: {events: [{header, payload}, ...]}
410
+ const events = dataPart.data?.events;
411
+ if (!Array.isArray(events)) {
412
+ console.warn(`[XY-${serverId}] dataPart.data.events is not an array, skipping`);
413
+ continue;
414
+ }
415
+ console.log(`[XY-${serverId}] Processing ${events.length} events from data.events`);
416
+ for (const item of events) {
417
+ // Check if it's an UploadExeResult (intent execution result)
418
+ if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
419
+ const dataEvent = {
420
+ intentName: item.payload.intentName,
421
+ outputs: item.payload.outputs || {},
422
+ status: "success",
423
+ };
424
+ console.log(`[XY-${serverId}] Emitting data-event:`, dataEvent);
425
+ this.emit("data-event", dataEvent);
426
+ }
427
+ // Check if it's an InvokeJarvisGUIAgentResponse
428
+ else if (item.header?.namespace === "ClawAgent" && item.header?.name === "InvokeJarvisGUIAgentResponse") {
429
+ console.log(`[XY-${serverId}] Emitting gui-agent-response:`, item);
430
+ this.emit("gui-agent-response", item);
431
+ }
432
+ }
433
+ }
434
+ return; // Don't emit message event
435
+ }
436
+ // Emit message event for non-data-only messages
437
+ console.log(`[XY-${serverId}] *** EMITTING message event (Direct A2A path) ***`);
299
438
  this.emit("message", a2aRequest, sessionId, serverId);
300
439
  return;
301
440
  }
302
441
  // Wrapped format (InboundWebSocketMessage)
303
442
  const inboundMsg = parsed;
304
443
  console.log(`[XY-${serverId}] Message type: Wrapped, msgType: ${inboundMsg.msgType}`);
305
- // Skip heartbeat responses
306
- if (inboundMsg.msgType === "heartbeat" || inboundMsg.msgType === "data") {
307
- console.log(`[XY-${serverId}] Skipping ${inboundMsg.msgType} message`);
444
+ // Handle heartbeat responses
445
+ if (inboundMsg.msgType === "heartbeat") {
446
+ console.log(`[XY-${serverId}] Received heartbeat response`);
447
+ // ✅ Report health: application-level heartbeat received
448
+ // This prevents openclaw health-monitor from marking connection as stale
449
+ this.onHealthEvent?.();
450
+ return;
451
+ }
452
+ // Handle data messages (e.g., intent execution results)
453
+ if (inboundMsg.msgType === "data") {
454
+ console.log(`[XY-${serverId}] Processing data message`);
455
+ try {
456
+ const a2aRequest = JSON.parse(inboundMsg.msgDetail);
457
+ const dataParts = a2aRequest.params?.message?.parts?.filter((p) => p.kind === "data");
458
+ if (dataParts && dataParts.length > 0) {
459
+ for (const dataPart of dataParts) {
460
+ // Data format: {events: [{header, payload}, ...]}
461
+ const events = dataPart.data?.events;
462
+ if (!Array.isArray(events)) {
463
+ console.warn(`[XY-${serverId}] dataPart.data.events is not an array, skipping`);
464
+ continue;
465
+ }
466
+ console.log(`[XY-${serverId}] Processing ${events.length} events from data.events`);
467
+ for (const item of events) {
468
+ // Check if it's an UploadExeResult (intent execution result)
469
+ if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
470
+ const dataEvent = {
471
+ intentName: item.payload.intentName,
472
+ outputs: item.payload.outputs || {},
473
+ status: "success",
474
+ };
475
+ console.log(`[XY-${serverId}] Emitting data-event:`, dataEvent);
476
+ this.emit("data-event", dataEvent);
477
+ }
478
+ // Check if it's an InvokeJarvisGUIAgentResponse
479
+ else if (item.header?.namespace === "ClawAgent" && item.header?.name === "InvokeJarvisGUIAgentResponse") {
480
+ console.log(`[XY-${serverId}] Emitting gui-agent-response:`, item);
481
+ this.emit("gui-agent-response", item);
482
+ }
483
+ }
484
+ }
485
+ }
486
+ }
487
+ catch (error) {
488
+ console.error(`[XY-${serverId}] Failed to process data message:`, error);
489
+ }
308
490
  return;
309
491
  }
310
492
  // Parse msgDetail as A2AJsonRpcRequest
@@ -318,6 +500,7 @@ export class XYWebSocketManager extends EventEmitter {
318
500
  }
319
501
  console.log(`[XY-${serverId}] Session ID: ${sessionId}`);
320
502
  // Emit message event
503
+ console.log(`[XY-${serverId}] *** EMITTING message event (Wrapped path) ***`);
321
504
  this.emit("message", a2aRequest, sessionId, serverId);
322
505
  }
323
506
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.2",
3
+ "version": "0.0.3-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,7 +62,6 @@
62
62
  "node-fetch": "^2.7.0"
63
63
  },
64
64
  "devDependencies": {
65
- "openclaw": ">=2026.3.1",
66
65
  "@types/ws": "^8.5.8",
67
66
  "@types/uuid": "^9.0.5",
68
67
  "@types/node": "^20.8.0",