@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.
- package/dist/src/bot.js +115 -35
- package/dist/src/channel.js +22 -1
- package/dist/src/client.d.ts +15 -0
- package/dist/src/client.js +97 -2
- package/dist/src/file-download.js +10 -1
- package/dist/src/file-upload.js +1 -1
- package/dist/src/formatter.d.ts +17 -0
- package/dist/src/formatter.js +86 -6
- package/dist/src/heartbeat.d.ts +2 -1
- package/dist/src/heartbeat.js +9 -1
- package/dist/src/monitor.d.ts +5 -0
- package/dist/src/monitor.js +131 -25
- package/dist/src/onboarding.js +7 -7
- package/dist/src/outbound.js +150 -71
- package/dist/src/parser.d.ts +5 -0
- package/dist/src/parser.js +15 -0
- package/dist/src/push.d.ts +7 -1
- package/dist/src/push.js +110 -19
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +210 -57
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/calendar-tool.d.ts +6 -0
- package/dist/src/tools/calendar-tool.js +167 -0
- package/dist/src/tools/call-phone-tool.d.ts +5 -0
- package/dist/src/tools/call-phone-tool.js +183 -0
- package/dist/src/tools/create-alarm-tool.d.ts +7 -0
- package/dist/src/tools/create-alarm-tool.js +444 -0
- package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
- package/dist/src/tools/delete-alarm-tool.js +238 -0
- package/dist/src/tools/location-tool.js +48 -9
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +474 -0
- package/dist/src/tools/modify-note-tool.d.ts +9 -0
- package/dist/src/tools/modify-note-tool.js +163 -0
- package/dist/src/tools/note-tool.d.ts +5 -0
- package/dist/src/tools/note-tool.js +146 -0
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +389 -0
- package/dist/src/tools/search-calendar-tool.d.ts +12 -0
- package/dist/src/tools/search-calendar-tool.js +259 -0
- package/dist/src/tools/search-contact-tool.d.ts +5 -0
- package/dist/src/tools/search-contact-tool.js +168 -0
- package/dist/src/tools/search-file-tool.d.ts +5 -0
- package/dist/src/tools/search-file-tool.js +185 -0
- package/dist/src/tools/search-message-tool.d.ts +5 -0
- package/dist/src/tools/search-message-tool.js +173 -0
- package/dist/src/tools/search-note-tool.d.ts +5 -0
- package/dist/src/tools/search-note-tool.js +130 -0
- package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
- package/dist/src/tools/search-photo-gallery-tool.js +184 -0
- package/dist/src/tools/search-photo-tool.d.ts +9 -0
- package/dist/src/tools/search-photo-tool.js +270 -0
- package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
- package/dist/src/tools/send-file-to-user-tool.js +318 -0
- package/dist/src/tools/send-message-tool.d.ts +5 -0
- package/dist/src/tools/send-message-tool.js +189 -0
- package/dist/src/tools/session-manager.d.ts +15 -0
- package/dist/src/tools/session-manager.js +126 -6
- package/dist/src/tools/upload-file-tool.d.ts +13 -0
- package/dist/src/tools/upload-file-tool.js +265 -0
- package/dist/src/tools/upload-photo-tool.d.ts +9 -0
- package/dist/src/tools/upload-photo-tool.js +223 -0
- package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
- package/dist/src/types.d.ts +5 -9
- package/dist/src/utils/config-manager.d.ts +26 -0
- package/dist/src/utils/config-manager.js +56 -0
- package/dist/src/websocket.d.ts +41 -0
- package/dist/src/websocket.js +192 -9
- package/package.json +1 -2
package/dist/src/websocket.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(`[
|
|
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
|
-
//
|
|
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
|
-
//
|
|
306
|
-
if (inboundMsg.msgType === "heartbeat"
|
|
307
|
-
console.log(`[XY-${serverId}]
|
|
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.
|
|
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",
|