@ynhcj/xiaoyi-channel 0.0.5 → 0.0.6-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 (75) hide show
  1. package/dist/src/bot.js +127 -48
  2. package/dist/src/channel.js +24 -4
  3. package/dist/src/client.d.ts +15 -0
  4. package/dist/src/client.js +84 -2
  5. package/dist/src/config.js +2 -2
  6. package/dist/src/file-download.js +10 -1
  7. package/dist/src/file-upload.js +1 -1
  8. package/dist/src/formatter.d.ts +17 -0
  9. package/dist/src/formatter.js +84 -4
  10. package/dist/src/heartbeat.d.ts +2 -1
  11. package/dist/src/heartbeat.js +9 -1
  12. package/dist/src/monitor.d.ts +5 -0
  13. package/dist/src/monitor.js +131 -25
  14. package/dist/src/onboarding.js +7 -7
  15. package/dist/src/outbound.js +75 -8
  16. package/dist/src/parser.d.ts +5 -0
  17. package/dist/src/parser.js +15 -0
  18. package/dist/src/push.d.ts +7 -1
  19. package/dist/src/push.js +110 -19
  20. package/dist/src/reply-dispatcher.d.ts +1 -0
  21. package/dist/src/reply-dispatcher.js +210 -57
  22. package/dist/src/task-manager.d.ts +55 -0
  23. package/dist/src/task-manager.js +136 -0
  24. package/dist/src/tools/calendar-tool.d.ts +6 -0
  25. package/dist/src/tools/calendar-tool.js +169 -0
  26. package/dist/src/tools/call-phone-tool.d.ts +5 -0
  27. package/dist/src/tools/call-phone-tool.js +183 -0
  28. package/dist/src/tools/create-alarm-tool.d.ts +7 -0
  29. package/dist/src/tools/create-alarm-tool.js +446 -0
  30. package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
  31. package/dist/src/tools/delete-alarm-tool.js +238 -0
  32. package/dist/src/tools/location-tool.js +48 -9
  33. package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
  34. package/dist/src/tools/modify-alarm-tool.js +467 -0
  35. package/dist/src/tools/modify-note-tool.d.ts +9 -0
  36. package/dist/src/tools/modify-note-tool.js +163 -0
  37. package/dist/src/tools/note-tool.d.ts +5 -0
  38. package/dist/src/tools/note-tool.js +151 -0
  39. package/dist/src/tools/search-alarm-tool.d.ts +8 -0
  40. package/dist/src/tools/search-alarm-tool.js +391 -0
  41. package/dist/src/tools/search-calendar-tool.d.ts +12 -0
  42. package/dist/src/tools/search-calendar-tool.js +262 -0
  43. package/dist/src/tools/search-contact-tool.d.ts +5 -0
  44. package/dist/src/tools/search-contact-tool.js +168 -0
  45. package/dist/src/tools/search-file-tool.d.ts +5 -0
  46. package/dist/src/tools/search-file-tool.js +185 -0
  47. package/dist/src/tools/search-message-tool.d.ts +5 -0
  48. package/dist/src/tools/search-message-tool.js +173 -0
  49. package/dist/src/tools/search-note-tool.d.ts +5 -0
  50. package/dist/src/tools/search-note-tool.js +130 -0
  51. package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
  52. package/dist/src/tools/search-photo-gallery-tool.js +212 -0
  53. package/dist/src/tools/search-photo-tool.d.ts +9 -0
  54. package/dist/src/tools/search-photo-tool.js +270 -0
  55. package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
  56. package/dist/src/tools/send-file-to-user-tool.js +318 -0
  57. package/dist/src/tools/send-message-tool.d.ts +5 -0
  58. package/dist/src/tools/send-message-tool.js +189 -0
  59. package/dist/src/tools/session-manager.d.ts +15 -0
  60. package/dist/src/tools/session-manager.js +126 -6
  61. package/dist/src/tools/upload-file-tool.d.ts +13 -0
  62. package/dist/src/tools/upload-file-tool.js +265 -0
  63. package/dist/src/tools/upload-photo-tool.d.ts +9 -0
  64. package/dist/src/tools/upload-photo-tool.js +223 -0
  65. package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
  66. package/dist/src/tools/xiaoyi-collection-tool.js +190 -0
  67. package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
  68. package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
  69. package/dist/src/types.d.ts +6 -17
  70. package/dist/src/types.js +4 -0
  71. package/dist/src/utils/config-manager.d.ts +26 -0
  72. package/dist/src/utils/config-manager.js +56 -0
  73. package/dist/src/websocket.d.ts +58 -25
  74. package/dist/src/websocket.js +298 -245
  75. package/package.json +1 -1
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";
4
- import { downloadFilesFromParts } from "./file-download.js";
3
+ import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId } from "./parser.js";
5
4
  import { resolveXYConfig } from "./config.js";
6
5
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
7
- import { registerSession, unregisterSession } from "./tools/session-manager.js";
6
+ import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
7
+ import { configManager } from "./utils/config-manager.js";
8
+ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-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,34 @@ export async function handleXYMessage(params) {
54
56
  }
55
57
  // Parse the A2A message (for regular messages)
56
58
  const parsed = parseA2AMessage(message);
59
+ // 🔑 检测steer模式和是否是第二条消息
60
+ const isSteerMode = cfg.messages?.queue?.mode === "steer";
61
+ const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
62
+ if (isSecondMessage) {
63
+ log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
64
+ log(`[BOT] - Session: ${parsed.sessionId}`);
65
+ log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
66
+ }
67
+ // 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
68
+ const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
69
+ );
70
+ // 🔑 如果是第一条消息,锁定taskId防止被过早清理
71
+ if (!isUpdate) {
72
+ lockTaskId(parsed.sessionId);
73
+ log(`[BOT] 🔒 Locked taskId for first message`);
74
+ }
75
+ // Extract and update push_id if present
76
+ const pushId = extractPushId(parsed.parts);
77
+ if (pushId) {
78
+ log(`[BOT] 📌 Extracted push_id from user message`);
79
+ log(`[BOT] - Session ID: ${parsed.sessionId}`);
80
+ log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
81
+ log(`[BOT] - Full push_id: ${pushId}`);
82
+ configManager.updatePushId(parsed.sessionId, pushId);
83
+ }
84
+ else {
85
+ log(`[BOT] ℹ️ No push_id found in message, will use config default`);
86
+ }
57
87
  // Resolve configuration (needed for status updates)
58
88
  const config = resolveXYConfig(cfg);
59
89
  // ✅ Resolve agent route (following feishu pattern)
@@ -61,7 +91,7 @@ export async function handleXYMessage(params) {
61
91
  // Use sessionId as peer.id to ensure all messages in the same session share context
62
92
  let route = core.channel.routing.resolveAgentRoute({
63
93
  cfg,
64
- channel: "xy",
94
+ channel: "xiaoyi-channel",
65
95
  accountId, // "default"
66
96
  peer: {
67
97
  kind: "direct",
@@ -69,7 +99,12 @@ export async function handleXYMessage(params) {
69
99
  },
70
100
  });
71
101
  log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
72
- // Register session context for tools
102
+ // 🔑 注册session(带引用计数)
103
+ log(`[BOT] 📝 About to register session for tools...`);
104
+ log(`[BOT] - sessionKey: ${route.sessionKey}`);
105
+ log(`[BOT] - sessionId: ${parsed.sessionId}`);
106
+ log(`[BOT] - taskId: ${parsed.taskId}`);
107
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
73
108
  registerSession(route.sessionKey, {
74
109
  config,
75
110
  sessionId: parsed.sessionId,
@@ -77,13 +112,25 @@ export async function handleXYMessage(params) {
77
112
  messageId: parsed.messageId,
78
113
  agentId: route.accountId,
79
114
  });
115
+ log(`[BOT] ✅ Session registered for tools`);
116
+ // 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
117
+ log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
118
+ void sendStatusUpdate({
119
+ config,
120
+ sessionId: parsed.sessionId,
121
+ taskId: parsed.taskId,
122
+ messageId: parsed.messageId,
123
+ text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍后~",
124
+ state: "working",
125
+ }).catch((err) => {
126
+ error(`Failed to send initial status update:`, err);
127
+ });
80
128
  // Extract text and files from parts
81
129
  const text = extractTextFromParts(parsed.parts);
82
130
  const fileParts = extractFileParts(parsed.parts);
83
- // Download files if present (using core's media download)
84
- const mediaList = await downloadFilesFromParts(fileParts);
85
- // Build media payload for inbound context (following feishu pattern)
86
- const mediaPayload = buildXYMediaPayload(mediaList);
131
+ // Build media payload directly from file URIs (openclaw can download them)
132
+ // No need to download files locally - pass URIs directly to openclaw
133
+ const mediaPayload = buildXYMediaPayload(fileParts);
87
134
  // Resolve envelope format options (following feishu pattern)
88
135
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
89
136
  // Build message body with speaker prefix (following feishu pattern)
@@ -93,7 +140,7 @@ export async function handleXYMessage(params) {
93
140
  messageBody = `${speaker}: ${messageBody}`;
94
141
  // Format agent envelope (following feishu pattern)
95
142
  const body = core.channel.reply.formatAgentEnvelope({
96
- channel: "XY",
143
+ channel: "xiaoyi-channel",
97
144
  from: speaker,
98
145
  timestamp: new Date(),
99
146
  envelope: envelopeOptions,
@@ -113,100 +160,132 @@ export async function handleXYMessage(params) {
113
160
  GroupSubject: undefined,
114
161
  SenderName: parsed.sessionId,
115
162
  SenderId: parsed.sessionId,
116
- Provider: "xy",
117
- Surface: "xy",
163
+ Provider: "xiaoyi-channel",
164
+ Surface: "xiaoyi-channel",
118
165
  MessageSid: parsed.messageId,
119
166
  Timestamp: Date.now(),
120
167
  WasMentioned: false,
121
168
  CommandAuthorized: true,
122
- OriginatingChannel: "xy",
169
+ OriginatingChannel: "xiaoyi-channel",
123
170
  OriginatingTo: parsed.sessionId, // Original message target
124
171
  ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
125
172
  ...mediaPayload,
126
173
  });
127
- // Send initial status update immediately after parsing message
128
- log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
129
- void sendStatusUpdate({
130
- config,
131
- sessionId: parsed.sessionId,
132
- taskId: parsed.taskId,
133
- messageId: parsed.messageId,
134
- text: "任务正在处理中,请稍后~",
135
- state: "working",
136
- }).catch((err) => {
137
- error(`Failed to send initial status update:`, err);
138
- });
139
- // Create reply dispatcher (following feishu pattern)
174
+ // 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
175
+ log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
176
+ log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
177
+ log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
178
+ log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
140
179
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
141
180
  cfg,
142
181
  runtime,
143
182
  sessionId: parsed.sessionId,
144
183
  taskId: parsed.taskId,
145
184
  messageId: parsed.messageId,
146
- accountId: route.accountId, // ✅ Use route.accountId
185
+ accountId: route.accountId,
186
+ isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
147
187
  });
148
- // Start status update interval (will send updates every 60 seconds)
149
- // Interval will be automatically stopped when onIdle/onCleanup is triggered
150
- startStatusInterval();
188
+ log(`[BOT-DISPATCHER] Reply dispatcher created successfully`);
189
+ // 🔑 只有第一条消息启动状态定时器
190
+ // 第二条消息会很快返回,不需要定时器
191
+ if (!isSecondMessage) {
192
+ startStatusInterval();
193
+ log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
194
+ }
195
+ else {
196
+ log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
197
+ }
151
198
  log(`xy: dispatching to agent (session=${parsed.sessionId})`);
152
199
  // Dispatch to OpenClaw core using correct API (following feishu pattern)
200
+ log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
201
+ // Build session context for AsyncLocalStorage
202
+ const sessionContext = {
203
+ config,
204
+ sessionId: parsed.sessionId,
205
+ taskId: parsed.taskId,
206
+ messageId: parsed.messageId,
207
+ agentId: route.accountId,
208
+ };
153
209
  await core.channel.reply.withReplyDispatcher({
154
210
  dispatcher,
155
211
  onSettled: () => {
212
+ log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
213
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
156
214
  markDispatchIdle();
157
- // Unregister session context when done
215
+ // 🔑 减少引用计数
216
+ decrementTaskIdRef(parsed.sessionId);
217
+ // 🔑 如果是第一条消息完成,解锁
218
+ if (!isSecondMessage) {
219
+ unlockTaskId(parsed.sessionId);
220
+ log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
221
+ }
222
+ // 减少session引用计数
158
223
  unregisterSession(route.sessionKey);
224
+ log(`[BOT] ✅ Cleanup completed`);
159
225
  },
160
- run: () => core.channel.reply.dispatchReplyFromConfig({
226
+ run: () =>
227
+ // 🔐 Use AsyncLocalStorage to provide session context to tools
228
+ runWithSessionContext(sessionContext, () => core.channel.reply.dispatchReplyFromConfig({
161
229
  ctx: ctxPayload,
162
230
  cfg,
163
231
  dispatcher,
164
232
  replyOptions,
165
- }),
233
+ })),
166
234
  });
235
+ log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
167
236
  log(`xy: dispatch complete (session=${parsed.sessionId})`);
168
237
  }
169
238
  catch (err) {
239
+ // ✅ Only log error, don't re-throw to prevent gateway restart
170
240
  error("Failed to handle XY message:", err);
171
241
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
172
- // Try to unregister session on error (if route was established)
242
+ log(`[BOT] Error occurred, attempting cleanup...`);
243
+ // 🔑 错误时也要清理taskId和session
173
244
  try {
174
- const core = getXYRuntime();
175
245
  const params = message.params;
176
246
  const sessionId = params?.sessionId;
177
247
  if (sessionId) {
248
+ log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
249
+ // 清理 taskId
250
+ decrementTaskIdRef(sessionId);
251
+ unlockTaskId(sessionId);
252
+ // 清理 session
253
+ const core = getXYRuntime();
178
254
  const route = core.channel.routing.resolveAgentRoute({
179
255
  cfg,
180
- channel: "xy",
256
+ channel: "xiaoyi-channel",
181
257
  accountId,
182
258
  peer: {
183
259
  kind: "direct",
184
- id: sessionId, // ✅ Use sessionId for cleanup consistency
260
+ id: sessionId,
185
261
  },
186
262
  });
187
263
  unregisterSession(route.sessionKey);
264
+ log(`[BOT] ✅ Cleanup completed after error`);
188
265
  }
189
266
  }
190
- catch {
267
+ catch (cleanupErr) {
268
+ log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
191
269
  // Ignore cleanup errors
192
270
  }
193
- throw err;
271
+ // ❌ Don't re-throw: message processing error should not affect gateway stability
194
272
  }
195
273
  }
196
274
  /**
197
275
  * Build media payload for inbound context.
198
276
  * Following feishu pattern: buildFeishuMediaPayload().
277
+ * Uses remote URIs directly - openclaw will download them.
199
278
  */
200
- function buildXYMediaPayload(mediaList) {
201
- const first = mediaList[0];
202
- const mediaPaths = mediaList.map((media) => media.path);
203
- const mediaTypes = mediaList.map((media) => media.mimeType).filter(Boolean);
279
+ function buildXYMediaPayload(fileParts) {
280
+ const first = fileParts[0];
281
+ const uris = fileParts.map((file) => file.uri);
282
+ const mediaTypes = fileParts.map((file) => file.mimeType).filter(Boolean);
204
283
  return {
205
- MediaPath: first?.path,
284
+ MediaPath: first?.uri,
206
285
  MediaType: first?.mimeType,
207
- MediaUrl: first?.path,
208
- MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
209
- MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
286
+ MediaUrl: first?.uri,
287
+ MediaPaths: uris.length > 0 ? uris : undefined,
288
+ MediaUrls: uris.length > 0 ? uris : undefined,
210
289
  MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
211
290
  };
212
291
  }
@@ -3,6 +3,25 @@ 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 { modifyNoteTool } from "./tools/modify-note-tool.js";
9
+ import { calendarTool } from "./tools/calendar-tool.js";
10
+ import { searchCalendarTool } from "./tools/search-calendar-tool.js";
11
+ import { searchContactTool } from "./tools/search-contact-tool.js";
12
+ import { searchPhotoGalleryTool } from "./tools/search-photo-gallery-tool.js";
13
+ import { uploadPhotoTool } from "./tools/upload-photo-tool.js";
14
+ import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
15
+ import { callPhoneTool } from "./tools/call-phone-tool.js";
16
+ import { searchMessageTool } from "./tools/search-message-tool.js";
17
+ import { searchFileTool } from "./tools/search-file-tool.js";
18
+ import { uploadFileTool } from "./tools/upload-file-tool.js";
19
+ import { createAlarmTool } from "./tools/create-alarm-tool.js";
20
+ import { searchAlarmTool } from "./tools/search-alarm-tool.js";
21
+ import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
22
+ import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
23
+ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
24
+ import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
6
25
  /**
7
26
  * Xiaoyi Channel Plugin for OpenClaw.
8
27
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -20,6 +39,7 @@ export const xyPlugin = {
20
39
  agentPrompt: {
21
40
  messageToolHints: () => [
22
41
  "- xiaoyi targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `default`",
42
+ "- 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
43
  ],
24
44
  },
25
45
  capabilities: {
@@ -41,7 +61,7 @@ export const xyPlugin = {
41
61
  },
42
62
  outbound: xyOutbound,
43
63
  onboarding: xyOnboardingAdapter,
44
- agentTools: [locationTool],
64
+ agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, xiaoyiCollectionTool],
45
65
  messaging: {
46
66
  normalizeTarget: (raw) => {
47
67
  const trimmed = raw.trim();
@@ -68,15 +88,15 @@ export const xyPlugin = {
68
88
  const account = resolveXYConfig(context.cfg);
69
89
  context.setStatus?.({
70
90
  accountId: context.accountId,
71
- wsUrl1: account.wsUrl1,
72
- wsUrl2: account.wsUrl2,
91
+ wsUrl: account.wsUrl,
73
92
  });
74
- context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl1: ${account.wsUrl1}, wsUrl2: ${account.wsUrl2})`);
93
+ context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
75
94
  return monitorXYProvider({
76
95
  config: context.cfg,
77
96
  runtime: context.runtime,
78
97
  abortSignal: context.abortSignal,
79
98
  accountId: context.accountId,
99
+ setStatus: context.setStatus,
80
100
  });
81
101
  },
82
102
  },
@@ -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,67 @@ 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
+ // Connection
93
+ console.log(` 🔌 Connection:`);
94
+ console.log(` - Exists: ${diag.connection.exists}`);
95
+ console.log(` - ReadyState: ${diag.connection.readyState}`);
96
+ console.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
97
+ console.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
98
+ console.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
99
+ console.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
100
+ console.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
101
+ if (diag.connection.isOrphan) {
102
+ console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
103
+ orphanCount++;
104
+ }
105
+ console.log("");
106
+ });
107
+ if (orphanCount > 0) {
108
+ console.log(`⚠️ Total orphan connections found: ${orphanCount}`);
109
+ console.log(`💡 Suggestion: These connections should be cleaned up`);
110
+ }
111
+ else {
112
+ console.log(`✅ No orphan connections found`);
113
+ }
114
+ console.log("========================================");
115
+ }
116
+ /**
117
+ * Clean up orphan connections across all managers.
118
+ * Returns the number of managers that had orphan connections.
119
+ */
120
+ export function cleanupOrphanConnections() {
121
+ let cleanedCount = 0;
122
+ wsManagerCache.forEach((manager, key) => {
123
+ const diag = manager.getConnectionDiagnostics();
124
+ if (diag.connection.isOrphan) {
125
+ console.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
126
+ manager.disconnect();
127
+ cleanedCount++;
128
+ }
129
+ });
130
+ if (cleanedCount > 0) {
131
+ console.log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
132
+ }
133
+ return cleanedCount;
134
+ }
@@ -17,8 +17,8 @@ export function resolveXYConfig(cfg) {
17
17
  // Return configuration with defaults
18
18
  return {
19
19
  enabled: xyConfig.enabled ?? false,
20
- wsUrl1: xyConfig.wsUrl1 ?? "ws://localhost:8765/ws/link",
21
- wsUrl2: xyConfig.wsUrl2 ?? "ws://localhost:8768/ws/link",
20
+ // ✅ 兼容旧配置:优先使用 wsUrl,然后 wsUrl2(wsUrl1 被忽略)
21
+ wsUrl: xyConfig.wsUrl ?? xyConfig.wsUrl2 ?? "ws://localhost:8768/ws/link",
22
22
  apiKey: xyConfig.apiKey,
23
23
  uid: xyConfig.uid,
24
24
  agentId: xyConfig.agentId,
@@ -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) {
@@ -20,6 +20,23 @@ export interface SendA2AResponseParams {
20
20
  * Send an A2A artifact update response.
21
21
  */
22
22
  export declare function sendA2AResponse(params: SendA2AResponseParams): Promise<void>;
23
+ /**
24
+ * Parameters for sending a reasoning text update (intermediate, streamed).
25
+ */
26
+ export interface SendReasoningTextUpdateParams {
27
+ config: XYChannelConfig;
28
+ sessionId: string;
29
+ taskId: string;
30
+ messageId: string;
31
+ text: string;
32
+ append?: boolean;
33
+ }
34
+ /**
35
+ * Send an A2A artifact-update with reasoningText part.
36
+ * Used for onToolStart, onToolResult, onReasoningStream, onReasoningEnd, onPartialReply.
37
+ * append=true, final=false, lastChunk=true, text is suffixed with newline for markdown rendering.
38
+ */
39
+ export declare function sendReasoningTextUpdate(params: SendReasoningTextUpdateParams): Promise<void>;
23
40
  /**
24
41
  * Parameters for sending a status update.
25
42
  */