@ynhcj/xiaoyi-channel 0.0.2 β†’ 0.0.3-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 CHANGED
@@ -1,10 +1,11 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
2
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
3
- import { parseA2AMessage, extractTextFromParts, extractFileParts } from "./parser.js";
3
+ import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId } from "./parser.js";
4
4
  import { downloadFilesFromParts } from "./file-download.js";
5
5
  import { resolveXYConfig } from "./config.js";
6
6
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
7
7
  import { registerSession, unregisterSession } from "./tools/session-manager.js";
8
+ import { configManager } from "./utils/config-manager.js";
8
9
  /**
9
10
  * Handle an incoming A2A message.
10
11
  * This is the main entry point for message processing.
@@ -19,7 +20,8 @@ export async function handleXYMessage(params) {
19
20
  try {
20
21
  // Check for special messages BEFORE parsing (these have different param structures)
21
22
  const messageMethod = message.method;
22
- log(`[DEBUG] Received message with method: ${messageMethod}, id: ${message.id}`);
23
+ log(`[BOT-ENTRY] <<<<<<< Received message with method: ${messageMethod}, id: ${message.id} >>>>>>>`);
24
+ log(`[BOT-ENTRY] Stack trace for debugging:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
23
25
  // Handle clearContext messages (params only has sessionId)
24
26
  if (messageMethod === "clearContext" || messageMethod === "clear_context") {
25
27
  const sessionId = message.params?.sessionId;
@@ -54,6 +56,18 @@ export async function handleXYMessage(params) {
54
56
  }
55
57
  // Parse the A2A message (for regular messages)
56
58
  const parsed = parseA2AMessage(message);
59
+ // Extract and update push_id if present
60
+ const pushId = extractPushId(parsed.parts);
61
+ if (pushId) {
62
+ log(`[BOT] πŸ“Œ Extracted push_id from user message`);
63
+ log(`[BOT] - Session ID: ${parsed.sessionId}`);
64
+ log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
65
+ log(`[BOT] - Full push_id: ${pushId}`);
66
+ configManager.updatePushId(parsed.sessionId, pushId);
67
+ }
68
+ else {
69
+ log(`[BOT] ℹ️ No push_id found in message, will use config default`);
70
+ }
57
71
  // Resolve configuration (needed for status updates)
58
72
  const config = resolveXYConfig(cfg);
59
73
  // βœ… Resolve agent route (following feishu pattern)
@@ -61,7 +75,7 @@ export async function handleXYMessage(params) {
61
75
  // Use sessionId as peer.id to ensure all messages in the same session share context
62
76
  let route = core.channel.routing.resolveAgentRoute({
63
77
  cfg,
64
- channel: "xy",
78
+ channel: "xiaoyi-channel",
65
79
  accountId, // "default"
66
80
  peer: {
67
81
  kind: "direct",
@@ -70,6 +84,10 @@ export async function handleXYMessage(params) {
70
84
  });
71
85
  log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
72
86
  // Register session context for tools
87
+ log(`[BOT] πŸ“ About to register session for tools...`);
88
+ log(`[BOT] - sessionKey: ${route.sessionKey}`);
89
+ log(`[BOT] - sessionId: ${parsed.sessionId}`);
90
+ log(`[BOT] - taskId: ${parsed.taskId}`);
73
91
  registerSession(route.sessionKey, {
74
92
  config,
75
93
  sessionId: parsed.sessionId,
@@ -77,6 +95,19 @@ export async function handleXYMessage(params) {
77
95
  messageId: parsed.messageId,
78
96
  agentId: route.accountId,
79
97
  });
98
+ log(`[BOT] βœ… Session registered for tools`);
99
+ // Send initial status update immediately after parsing message
100
+ log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
101
+ void sendStatusUpdate({
102
+ config,
103
+ sessionId: parsed.sessionId,
104
+ taskId: parsed.taskId,
105
+ messageId: parsed.messageId,
106
+ text: "δ»»εŠ‘ζ­£εœ¨ε€„η†δΈ­οΌŒθ―·η¨εŽ~",
107
+ state: "working",
108
+ }).catch((err) => {
109
+ error(`Failed to send initial status update:`, err);
110
+ });
80
111
  // Extract text and files from parts
81
112
  const text = extractTextFromParts(parsed.parts);
82
113
  const fileParts = extractFileParts(parsed.parts);
@@ -93,7 +124,7 @@ export async function handleXYMessage(params) {
93
124
  messageBody = `${speaker}: ${messageBody}`;
94
125
  // Format agent envelope (following feishu pattern)
95
126
  const body = core.channel.reply.formatAgentEnvelope({
96
- channel: "XY",
127
+ channel: "xiaoyi-channel",
97
128
  from: speaker,
98
129
  timestamp: new Date(),
99
130
  envelope: envelopeOptions,
@@ -113,13 +144,13 @@ export async function handleXYMessage(params) {
113
144
  GroupSubject: undefined,
114
145
  SenderName: parsed.sessionId,
115
146
  SenderId: parsed.sessionId,
116
- Provider: "xy",
117
- Surface: "xy",
147
+ Provider: "xiaoyi-channel",
148
+ Surface: "xiaoyi-channel",
118
149
  MessageSid: parsed.messageId,
119
150
  Timestamp: Date.now(),
120
151
  WasMentioned: false,
121
152
  CommandAuthorized: true,
122
- OriginatingChannel: "xy",
153
+ OriginatingChannel: "xiaoyi-channel",
123
154
  OriginatingTo: parsed.sessionId, // Original message target
124
155
  ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
125
156
  ...mediaPayload,
@@ -137,6 +168,7 @@ export async function handleXYMessage(params) {
137
168
  error(`Failed to send initial status update:`, err);
138
169
  });
139
170
  // Create reply dispatcher (following feishu pattern)
171
+ log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher for session=${parsed.sessionId}, taskId=${parsed.taskId}, messageId=${parsed.messageId}`);
140
172
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
141
173
  cfg,
142
174
  runtime,
@@ -145,17 +177,22 @@ export async function handleXYMessage(params) {
145
177
  messageId: parsed.messageId,
146
178
  accountId: route.accountId, // βœ… Use route.accountId
147
179
  });
180
+ log(`[BOT-DISPATCHER] βœ… Reply dispatcher created successfully`);
148
181
  // Start status update interval (will send updates every 60 seconds)
149
182
  // Interval will be automatically stopped when onIdle/onCleanup is triggered
150
183
  startStatusInterval();
151
184
  log(`xy: dispatching to agent (session=${parsed.sessionId})`);
152
185
  // Dispatch to OpenClaw core using correct API (following feishu pattern)
186
+ log(`[BOT] πŸš€ Starting dispatcher with session: ${route.sessionKey}`);
153
187
  await core.channel.reply.withReplyDispatcher({
154
188
  dispatcher,
155
189
  onSettled: () => {
190
+ log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
191
+ log(`[BOT] - About to unregister session...`);
156
192
  markDispatchIdle();
157
193
  // Unregister session context when done
158
194
  unregisterSession(route.sessionKey);
195
+ log(`[BOT] βœ… Session unregistered in onSettled`);
159
196
  },
160
197
  run: () => core.channel.reply.dispatchReplyFromConfig({
161
198
  ctx: ctxPayload,
@@ -164,33 +201,40 @@ export async function handleXYMessage(params) {
164
201
  replyOptions,
165
202
  }),
166
203
  });
204
+ log(`[BOT] βœ… Dispatcher completed for session: ${parsed.sessionId}`);
167
205
  log(`xy: dispatch complete (session=${parsed.sessionId})`);
168
206
  }
169
207
  catch (err) {
208
+ // βœ… Only log error, don't re-throw to prevent gateway restart
170
209
  error("Failed to handle XY message:", err);
171
210
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
211
+ log(`[BOT] ❌ Error occurred, attempting cleanup...`);
172
212
  // Try to unregister session on error (if route was established)
173
213
  try {
174
214
  const core = getXYRuntime();
175
215
  const params = message.params;
176
216
  const sessionId = params?.sessionId;
177
217
  if (sessionId) {
218
+ log(`[BOT] 🧹 Cleaning up session after error: ${sessionId}`);
178
219
  const route = core.channel.routing.resolveAgentRoute({
179
220
  cfg,
180
- channel: "xy",
221
+ channel: "xiaoyi-channel",
181
222
  accountId,
182
223
  peer: {
183
224
  kind: "direct",
184
225
  id: sessionId, // βœ… Use sessionId for cleanup consistency
185
226
  },
186
227
  });
228
+ log(`[BOT] - Unregistering session: ${route.sessionKey}`);
187
229
  unregisterSession(route.sessionKey);
230
+ log(`[BOT] βœ… Session unregistered after error`);
188
231
  }
189
232
  }
190
- catch {
233
+ catch (cleanupErr) {
234
+ log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
191
235
  // Ignore cleanup errors
192
236
  }
193
- throw err;
237
+ // ❌ Don't re-throw: message processing error should not affect gateway stability
194
238
  }
195
239
  }
196
240
  /**
@@ -3,6 +3,9 @@ import { xyConfigSchema } from "./config-schema.js";
3
3
  import { xyOutbound } from "./outbound.js";
4
4
  import { xyOnboardingAdapter } from "./onboarding.js";
5
5
  import { locationTool } from "./tools/location-tool.js";
6
+ import { noteTool } from "./tools/note-tool.js";
7
+ import { searchNoteTool } from "./tools/search-note-tool.js";
8
+ import { calendarTool } from "./tools/calendar-tool.js";
6
9
  /**
7
10
  * Xiaoyi Channel Plugin for OpenClaw.
8
11
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -20,6 +23,7 @@ export const xyPlugin = {
20
23
  agentPrompt: {
21
24
  messageToolHints: () => [
22
25
  "- xiaoyi targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `default`",
26
+ "- If the user requests a file, you can call the message tool with the xiaoyi-channel channel to return it. Note: sendMedia requires a text reply."
23
27
  ],
24
28
  },
25
29
  capabilities: {
@@ -41,7 +45,7 @@ export const xyPlugin = {
41
45
  },
42
46
  outbound: xyOutbound,
43
47
  onboarding: xyOnboardingAdapter,
44
- agentTools: [locationTool],
48
+ agentTools: [locationTool, noteTool, searchNoteTool, calendarTool],
45
49
  messaging: {
46
50
  normalizeTarget: (raw) => {
47
51
  const trimmed = raw.trim();
@@ -77,6 +81,7 @@ export const xyPlugin = {
77
81
  runtime: context.runtime,
78
82
  abortSignal: context.abortSignal,
79
83
  accountId: context.accountId,
84
+ setStatus: context.setStatus,
80
85
  });
81
86
  },
82
87
  },
@@ -10,6 +10,11 @@ export declare function setClientRuntime(rt: RuntimeEnv | undefined): void;
10
10
  * Reuses existing managers if config matches.
11
11
  */
12
12
  export declare function getXYWebSocketManager(config: XYChannelConfig): XYWebSocketManager;
13
+ /**
14
+ * Remove a specific WebSocket manager from cache.
15
+ * Disconnects the manager and removes it from the cache.
16
+ */
17
+ export declare function removeXYWebSocketManager(config: XYChannelConfig): void;
13
18
  /**
14
19
  * Clear all cached WebSocket managers.
15
20
  */
@@ -18,3 +23,13 @@ export declare function clearXYWebSocketManagers(): void;
18
23
  * Get the number of cached managers.
19
24
  */
20
25
  export declare function getCachedManagerCount(): number;
26
+ /**
27
+ * Diagnose all cached WebSocket managers.
28
+ * Helps identify connection issues and orphan connections.
29
+ */
30
+ export declare function diagnoseAllManagers(): void;
31
+ /**
32
+ * Clean up orphan connections across all managers.
33
+ * Returns the number of managers that had orphan connections.
34
+ */
35
+ export declare function cleanupOrphanConnections(): number;
@@ -23,16 +23,34 @@ export function getXYWebSocketManager(config) {
23
23
  let cached = wsManagerCache.get(cacheKey);
24
24
  if (cached && cached.isConfigMatch(config)) {
25
25
  const log = runtime?.log ?? console.log;
26
- log(`[DEBUG] Reusing cached WebSocket manager: ${cacheKey}`);
26
+ log(`[WS-MANAGER-CACHE] βœ… Reusing cached WebSocket manager: ${cacheKey}, total managers: ${wsManagerCache.size}`);
27
27
  return cached;
28
28
  }
29
29
  // Create new manager
30
30
  const log = runtime?.log ?? console.log;
31
- log(`Creating new WebSocket manager: ${cacheKey}`);
31
+ log(`[WS-MANAGER-CACHE] πŸ†• Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
32
32
  cached = new XYWebSocketManager(config, runtime);
33
33
  wsManagerCache.set(cacheKey, cached);
34
+ log(`[WS-MANAGER-CACHE] πŸ“Š Total managers after creation: ${wsManagerCache.size}`);
34
35
  return cached;
35
36
  }
37
+ /**
38
+ * Remove a specific WebSocket manager from cache.
39
+ * Disconnects the manager and removes it from the cache.
40
+ */
41
+ export function removeXYWebSocketManager(config) {
42
+ const cacheKey = `${config.apiKey}-${config.agentId}`;
43
+ const manager = wsManagerCache.get(cacheKey);
44
+ if (manager) {
45
+ console.log(`πŸ—‘οΈ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
46
+ manager.disconnect();
47
+ wsManagerCache.delete(cacheKey);
48
+ console.log(`πŸ—‘οΈ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
49
+ }
50
+ else {
51
+ console.log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
52
+ }
53
+ }
36
54
  /**
37
55
  * Clear all cached WebSocket managers.
38
56
  */
@@ -50,3 +68,80 @@ export function clearXYWebSocketManagers() {
50
68
  export function getCachedManagerCount() {
51
69
  return wsManagerCache.size;
52
70
  }
71
+ /**
72
+ * Diagnose all cached WebSocket managers.
73
+ * Helps identify connection issues and orphan connections.
74
+ */
75
+ export function diagnoseAllManagers() {
76
+ console.log("========================================");
77
+ console.log("πŸ“Š WebSocket Manager Global Diagnostics");
78
+ console.log("========================================");
79
+ console.log(`Total cached managers: ${wsManagerCache.size}`);
80
+ console.log("");
81
+ if (wsManagerCache.size === 0) {
82
+ console.log("ℹ️ No managers in cache");
83
+ console.log("========================================");
84
+ return;
85
+ }
86
+ let orphanCount = 0;
87
+ wsManagerCache.forEach((manager, key) => {
88
+ const diag = manager.getConnectionDiagnostics();
89
+ console.log(`πŸ“Œ Manager: ${key}`);
90
+ console.log(` Shutting down: ${diag.isShuttingDown}`);
91
+ console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
92
+ // Server 1
93
+ console.log(` πŸ”Œ Server1:`);
94
+ console.log(` - Exists: ${diag.server1.exists}`);
95
+ console.log(` - ReadyState: ${diag.server1.readyState}`);
96
+ console.log(` - State connected/ready: ${diag.server1.stateConnected}/${diag.server1.stateReady}`);
97
+ console.log(` - Reconnect attempts: ${diag.server1.reconnectAttempts}`);
98
+ console.log(` - Listeners on WebSocket: ${diag.server1.listenerCount}`);
99
+ console.log(` - Heartbeat active: ${diag.server1.heartbeatActive}`);
100
+ console.log(` - Has reconnect timer: ${diag.server1.hasReconnectTimer}`);
101
+ if (diag.server1.isOrphan) {
102
+ console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
103
+ orphanCount++;
104
+ }
105
+ // Server 2
106
+ console.log(` πŸ”Œ Server2:`);
107
+ console.log(` - Exists: ${diag.server2.exists}`);
108
+ console.log(` - ReadyState: ${diag.server2.readyState}`);
109
+ console.log(` - State connected/ready: ${diag.server2.stateConnected}/${diag.server2.stateReady}`);
110
+ console.log(` - Reconnect attempts: ${diag.server2.reconnectAttempts}`);
111
+ console.log(` - Listeners on WebSocket: ${diag.server2.listenerCount}`);
112
+ console.log(` - Heartbeat active: ${diag.server2.heartbeatActive}`);
113
+ console.log(` - Has reconnect timer: ${diag.server2.hasReconnectTimer}`);
114
+ if (diag.server2.isOrphan) {
115
+ console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
116
+ orphanCount++;
117
+ }
118
+ console.log("");
119
+ });
120
+ if (orphanCount > 0) {
121
+ console.log(`⚠️ Total orphan connections found: ${orphanCount}`);
122
+ console.log(`πŸ’‘ Suggestion: These connections should be cleaned up`);
123
+ }
124
+ else {
125
+ console.log(`βœ… No orphan connections found`);
126
+ }
127
+ console.log("========================================");
128
+ }
129
+ /**
130
+ * Clean up orphan connections across all managers.
131
+ * Returns the number of managers that had orphan connections.
132
+ */
133
+ export function cleanupOrphanConnections() {
134
+ let cleanedCount = 0;
135
+ wsManagerCache.forEach((manager, key) => {
136
+ const diag = manager.getConnectionDiagnostics();
137
+ if (diag.server1.isOrphan || diag.server2.isOrphan) {
138
+ console.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
139
+ manager.disconnect();
140
+ cleanedCount++;
141
+ }
142
+ });
143
+ if (cleanedCount > 0) {
144
+ console.log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
145
+ }
146
+ return cleanedCount;
147
+ }
@@ -8,8 +8,10 @@ import { logger } from "./utils/logger.js";
8
8
  */
9
9
  export async function downloadFile(url, destPath) {
10
10
  logger.debug(`Downloading file from ${url} to ${destPath}`);
11
+ const controller = new AbortController();
12
+ const timeout = setTimeout(() => controller.abort(), 30000); // 30 seconds timeout
11
13
  try {
12
- const response = await fetch(url);
14
+ const response = await fetch(url, { signal: controller.signal });
13
15
  if (!response.ok) {
14
16
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
15
17
  }
@@ -19,9 +21,16 @@ export async function downloadFile(url, destPath) {
19
21
  logger.debug(`File downloaded successfully: ${destPath}`);
20
22
  }
21
23
  catch (error) {
24
+ if (error.name === 'AbortError') {
25
+ logger.error(`Download timeout (30s) for ${url}`);
26
+ throw new Error(`Download timeout after 30 seconds`);
27
+ }
22
28
  logger.error(`Failed to download file from ${url}:`, error);
23
29
  throw error;
24
30
  }
31
+ finally {
32
+ clearTimeout(timeout);
33
+ }
25
34
  }
26
35
  /**
27
36
  * Download files from A2A file parts.
@@ -48,7 +48,7 @@ export class XYFileUploadService {
48
48
  uid: this.uid,
49
49
  teamId: this.uid,
50
50
  },
51
- useEdge: true,
51
+ useEdge: false,
52
52
  }),
53
53
  });
54
54
  if (!prepareResp.ok) {
@@ -51,8 +51,21 @@ export async function sendA2AResponse(params) {
51
51
  taskId,
52
52
  msgDetail: JSON.stringify(jsonRpcResponse),
53
53
  };
54
+ // πŸ“‹ Log complete response body
55
+ log(`[A2A_RESPONSE] πŸ“€ Sending A2A artifact-update response:`);
56
+ log(`[A2A_RESPONSE] - sessionId: ${sessionId}`);
57
+ log(`[A2A_RESPONSE] - taskId: ${taskId}`);
58
+ log(`[A2A_RESPONSE] - messageId: ${messageId}`);
59
+ log(`[A2A_RESPONSE] - append: ${append}`);
60
+ log(`[A2A_RESPONSE] - final: ${final}`);
61
+ log(`[A2A_RESPONSE] - text length: ${text?.length ?? 0}`);
62
+ log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
63
+ log(`[A2A_RESPONSE] πŸ“¦ Complete outbound message:`);
64
+ log(JSON.stringify(outboundMessage, null, 2));
65
+ log(`[A2A_RESPONSE] πŸ“¦ JSON-RPC response body:`);
66
+ log(JSON.stringify(jsonRpcResponse, null, 2));
54
67
  await wsManager.sendMessage(sessionId, outboundMessage);
55
- log(`Sent A2A response: sessionId=${sessionId}, taskId=${taskId}, final=${final}`);
68
+ log(`[A2A_RESPONSE] βœ… Message sent successfully`);
56
69
  }
57
70
  /**
58
71
  * Send an A2A task status update.
@@ -96,8 +109,19 @@ export async function sendStatusUpdate(params) {
96
109
  taskId,
97
110
  msgDetail: JSON.stringify(jsonRpcResponse),
98
111
  };
112
+ // πŸ“‹ Log complete response body
113
+ log(`[A2A_STATUS] πŸ“€ Sending A2A status-update:`);
114
+ log(`[A2A_STATUS] - sessionId: ${sessionId}`);
115
+ log(`[A2A_STATUS] - taskId: ${taskId}`);
116
+ log(`[A2A_STATUS] - messageId: ${messageId}`);
117
+ log(`[A2A_STATUS] - state: ${state}`);
118
+ log(`[A2A_STATUS] - text: "${text}"`);
119
+ log(`[A2A_STATUS] πŸ“¦ Complete outbound message:`);
120
+ log(JSON.stringify(outboundMessage, null, 2));
121
+ log(`[A2A_STATUS] πŸ“¦ JSON-RPC response body:`);
122
+ log(JSON.stringify(jsonRpcResponse, null, 2));
99
123
  await wsManager.sendMessage(sessionId, outboundMessage);
100
- log(`Sent status update: sessionId=${sessionId}, state=${state}, text="${text}"`);
124
+ log(`[A2A_STATUS] βœ… Status update sent successfully`);
101
125
  }
102
126
  /**
103
127
  * Send a command as an artifact update (final=false).
@@ -107,7 +131,8 @@ export async function sendCommand(params) {
107
131
  const runtime = getXYRuntime();
108
132
  const log = runtime?.log ?? console.log;
109
133
  const error = runtime?.error ?? console.error;
110
- // Build artifact update with command
134
+ // Build artifact update with command as data
135
+ // Wrap command in commands array as per protocol requirement
111
136
  const artifact = {
112
137
  taskId,
113
138
  kind: "artifact-update",
@@ -118,8 +143,10 @@ export async function sendCommand(params) {
118
143
  artifactId: uuidv4(),
119
144
  parts: [
120
145
  {
121
- kind: "command",
122
- command,
146
+ kind: "data",
147
+ data: {
148
+ commands: [command],
149
+ },
123
150
  },
124
151
  ],
125
152
  },
@@ -139,8 +166,18 @@ export async function sendCommand(params) {
139
166
  taskId,
140
167
  msgDetail: JSON.stringify(jsonRpcResponse),
141
168
  };
169
+ // πŸ“‹ Log complete response body
170
+ log(`[A2A_COMMAND] πŸ“€ Sending A2A command:`);
171
+ log(`[A2A_COMMAND] - sessionId: ${sessionId}`);
172
+ log(`[A2A_COMMAND] - taskId: ${taskId}`);
173
+ log(`[A2A_COMMAND] - messageId: ${messageId}`);
174
+ log(`[A2A_COMMAND] - command: ${command.header.namespace}::${command.header.name}`);
175
+ log(`[A2A_COMMAND] πŸ“¦ Complete outbound message:`);
176
+ log(JSON.stringify(outboundMessage, null, 2));
177
+ log(`[A2A_COMMAND] πŸ“¦ JSON-RPC response body:`);
178
+ log(JSON.stringify(jsonRpcResponse, null, 2));
142
179
  await wsManager.sendMessage(sessionId, outboundMessage);
143
- log(`Sent command: sessionId=${sessionId}, command=${command.header.name}`);
180
+ log(`[A2A_COMMAND] βœ… Command sent successfully`);
144
181
  }
145
182
  /**
146
183
  * Send a clearContext response.
@@ -13,12 +13,13 @@ export declare class HeartbeatManager {
13
13
  private config;
14
14
  private onTimeout;
15
15
  private serverName;
16
+ private onHeartbeatSuccess?;
16
17
  private intervalTimer;
17
18
  private timeoutTimer;
18
19
  private lastPongTime;
19
20
  private log;
20
21
  private error;
21
- constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void);
22
+ constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void, onHeartbeatSuccess?: () => void);
22
23
  /**
23
24
  * Start heartbeat monitoring.
24
25
  */
@@ -9,17 +9,20 @@ export class HeartbeatManager {
9
9
  config;
10
10
  onTimeout;
11
11
  serverName;
12
+ onHeartbeatSuccess;
12
13
  intervalTimer = null;
13
14
  timeoutTimer = null;
14
15
  lastPongTime = 0;
15
16
  // Logging functions following feishu pattern
16
17
  log;
17
18
  error;
18
- constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn) {
19
+ constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn, onHeartbeatSuccess // βœ… ζ–°ε’žοΌšεΏƒθ·³ζˆεŠŸε›žθ°ƒ
20
+ ) {
19
21
  this.ws = ws;
20
22
  this.config = config;
21
23
  this.onTimeout = onTimeout;
22
24
  this.serverName = serverName;
25
+ this.onHeartbeatSuccess = onHeartbeatSuccess;
23
26
  this.log = logFn ?? console.log;
24
27
  this.error = errorFn ?? console.error;
25
28
  }
@@ -36,6 +39,8 @@ export class HeartbeatManager {
36
39
  clearTimeout(this.timeoutTimer);
37
40
  this.timeoutTimer = null;
38
41
  }
42
+ // βœ… Report health: heartbeat successful
43
+ this.onHeartbeatSuccess?.();
39
44
  });
40
45
  // Start interval timer
41
46
  this.intervalTimer = setInterval(() => {
@@ -67,9 +72,12 @@ export class HeartbeatManager {
67
72
  }
68
73
  try {
69
74
  // Send application-level heartbeat message
75
+ console.log(`[WS-${this.serverName}-SEND] Sending heartbeat frame:`, this.config.message);
70
76
  this.ws.send(this.config.message);
77
+ console.log(`[WS-${this.serverName}-SEND] Heartbeat message sent, size: ${this.config.message.length} bytes`);
71
78
  // Send protocol-level ping
72
79
  this.ws.ping();
80
+ console.log(`[WS-${this.serverName}-SEND] Protocol-level ping sent`);
73
81
  // Setup timeout timer
74
82
  this.timeoutTimer = setTimeout(() => {
75
83
  this.error(`Heartbeat timeout for ${this.serverName}`);
@@ -4,6 +4,11 @@ export type MonitorXYOpts = {
4
4
  runtime?: RuntimeEnv;
5
5
  abortSignal?: AbortSignal;
6
6
  accountId?: string;
7
+ setStatus?: (status: {
8
+ lastEventAt?: number;
9
+ lastInboundAt?: number;
10
+ connected?: boolean;
11
+ }) => void;
7
12
  };
8
13
  /**
9
14
  * Monitor XY channel WebSocket connections.