@ynhcj/xiaoyi-channel 0.0.9 → 0.0.10-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 (79) hide show
  1. package/dist/src/bot.js +153 -48
  2. package/dist/src/channel.js +23 -4
  3. package/dist/src/client.d.ts +15 -0
  4. package/dist/src/client.js +81 -0
  5. package/dist/src/config.js +2 -2
  6. package/dist/src/file-download.js +10 -1
  7. package/dist/src/formatter.d.ts +31 -0
  8. package/dist/src/formatter.js +93 -1
  9. package/dist/src/heartbeat.d.ts +2 -1
  10. package/dist/src/heartbeat.js +6 -1
  11. package/dist/src/monitor.d.ts +5 -0
  12. package/dist/src/monitor.js +101 -9
  13. package/dist/src/outbound.js +97 -7
  14. package/dist/src/parser.d.ts +12 -0
  15. package/dist/src/parser.js +37 -0
  16. package/dist/src/push.d.ts +13 -1
  17. package/dist/src/push.js +125 -19
  18. package/dist/src/reply-dispatcher.d.ts +1 -0
  19. package/dist/src/reply-dispatcher.js +206 -51
  20. package/dist/src/task-manager.d.ts +55 -0
  21. package/dist/src/task-manager.js +136 -0
  22. package/dist/src/tools/calendar-tool.d.ts +6 -0
  23. package/dist/src/tools/calendar-tool.js +169 -0
  24. package/dist/src/tools/call-phone-tool.d.ts +5 -0
  25. package/dist/src/tools/call-phone-tool.js +183 -0
  26. package/dist/src/tools/create-alarm-tool.d.ts +7 -0
  27. package/dist/src/tools/create-alarm-tool.js +446 -0
  28. package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
  29. package/dist/src/tools/delete-alarm-tool.js +238 -0
  30. package/dist/src/tools/location-tool.js +18 -8
  31. package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
  32. package/dist/src/tools/modify-alarm-tool.js +467 -0
  33. package/dist/src/tools/modify-note-tool.d.ts +9 -0
  34. package/dist/src/tools/modify-note-tool.js +163 -0
  35. package/dist/src/tools/note-tool.js +32 -11
  36. package/dist/src/tools/search-alarm-tool.d.ts +8 -0
  37. package/dist/src/tools/search-alarm-tool.js +391 -0
  38. package/dist/src/tools/search-calendar-tool.d.ts +12 -0
  39. package/dist/src/tools/search-calendar-tool.js +262 -0
  40. package/dist/src/tools/search-contact-tool.d.ts +5 -0
  41. package/dist/src/tools/search-contact-tool.js +168 -0
  42. package/dist/src/tools/search-file-tool.d.ts +5 -0
  43. package/dist/src/tools/search-file-tool.js +185 -0
  44. package/dist/src/tools/search-message-tool.d.ts +5 -0
  45. package/dist/src/tools/search-message-tool.js +173 -0
  46. package/dist/src/tools/search-note-tool.js +6 -6
  47. package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
  48. package/dist/src/tools/search-photo-gallery-tool.js +212 -0
  49. package/dist/src/tools/search-photo-tool.d.ts +9 -0
  50. package/dist/src/tools/search-photo-tool.js +270 -0
  51. package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
  52. package/dist/src/tools/send-file-to-user-tool.js +318 -0
  53. package/dist/src/tools/send-message-tool.d.ts +5 -0
  54. package/dist/src/tools/send-message-tool.js +189 -0
  55. package/dist/src/tools/session-manager.d.ts +15 -0
  56. package/dist/src/tools/session-manager.js +101 -13
  57. package/dist/src/tools/upload-file-tool.d.ts +13 -0
  58. package/dist/src/tools/upload-file-tool.js +265 -0
  59. package/dist/src/tools/upload-photo-tool.d.ts +9 -0
  60. package/dist/src/tools/upload-photo-tool.js +223 -0
  61. package/dist/src/tools/view-push-result-tool.d.ts +5 -0
  62. package/dist/src/tools/view-push-result-tool.js +118 -0
  63. package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
  64. package/dist/src/tools/xiaoyi-collection-tool.js +190 -0
  65. package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
  66. package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
  67. package/dist/src/trigger-handler.d.ts +22 -0
  68. package/dist/src/trigger-handler.js +59 -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/utils/pushdata-manager.d.ts +28 -0
  74. package/dist/src/utils/pushdata-manager.js +171 -0
  75. package/dist/src/utils/pushid-manager.d.ts +12 -0
  76. package/dist/src/utils/pushid-manager.js +105 -0
  77. package/dist/src/websocket.d.ts +59 -25
  78. package/dist/src/websocket.js +315 -257
  79. package/package.json +1 -1
package/dist/src/bot.js CHANGED
@@ -1,10 +1,13 @@
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, extractTriggerData } from "./parser.js";
5
4
  import { resolveXYConfig } from "./config.js";
6
- import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
7
- import { registerSession, unregisterSession } from "./tools/session-manager.js";
5
+ import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
6
+ import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
7
+ import { configManager } from "./utils/config-manager.js";
8
+ import { addPushId } from "./utils/pushid-manager.js";
9
+ import { getPushDataById } from "./utils/pushdata-manager.js";
10
+ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
8
11
  /**
9
12
  * Handle an incoming A2A message.
10
13
  * This is the main entry point for message processing.
@@ -55,6 +58,76 @@ export async function handleXYMessage(params) {
55
58
  }
56
59
  // Parse the A2A message (for regular messages)
57
60
  const parsed = parseA2AMessage(message);
61
+ // ========== 检测 Trigger 消息 ==========
62
+ // 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
63
+ const triggerData = extractTriggerData(parsed.parts);
64
+ if (triggerData) {
65
+ log(`[BOT] 📌 Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
66
+ log(`[BOT] - Session ID: ${parsed.sessionId}`);
67
+ log(`[BOT] - Task ID: ${parsed.taskId}`);
68
+ try {
69
+ // 读取 pushData
70
+ const pushDataItem = await getPushDataById(triggerData.pushDataId);
71
+ if (!pushDataItem) {
72
+ error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
73
+ return;
74
+ }
75
+ log(`[BOT] ✅ Found pushData, sending direct response`);
76
+ log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
77
+ log(`[BOT] - time: ${pushDataItem.time}`);
78
+ log(`[BOT] - content length: ${pushDataItem.dataDetail.length} chars`);
79
+ const config = resolveXYConfig(cfg);
80
+ // 直接发送响应(final=true,不走 openclaw 流程)
81
+ await sendA2AResponse({
82
+ config,
83
+ sessionId: parsed.sessionId,
84
+ taskId: parsed.taskId,
85
+ messageId: parsed.messageId,
86
+ text: pushDataItem.dataDetail,
87
+ append: false,
88
+ final: true,
89
+ });
90
+ log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
91
+ return; // 提前返回,不继续处理
92
+ }
93
+ catch (err) {
94
+ error(`[BOT] ❌ Failed to handle Trigger message:`, err);
95
+ return;
96
+ }
97
+ }
98
+ // ========================================
99
+ // 🔑 检测steer模式和是否是第二条消息
100
+ const isSteerMode = cfg.messages?.queue?.mode === "steer";
101
+ const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
102
+ if (isSecondMessage) {
103
+ log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
104
+ log(`[BOT] - Session: ${parsed.sessionId}`);
105
+ log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
106
+ }
107
+ // 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
108
+ const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
109
+ );
110
+ // 🔑 如果是第一条消息,锁定taskId防止被过早清理
111
+ if (!isUpdate) {
112
+ lockTaskId(parsed.sessionId);
113
+ log(`[BOT] 🔒 Locked taskId for first message`);
114
+ }
115
+ // Extract and update push_id if present
116
+ const pushId = extractPushId(parsed.parts);
117
+ if (pushId) {
118
+ log(`[BOT] 📌 Extracted push_id from user message`);
119
+ log(`[BOT] - Session ID: ${parsed.sessionId}`);
120
+ log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
121
+ log(`[BOT] - Full push_id: ${pushId}`);
122
+ configManager.updatePushId(parsed.sessionId, pushId);
123
+ // 持久化 pushId 到本地文件(异步,不阻塞主流程)
124
+ addPushId(pushId).catch((err) => {
125
+ error(`[BOT] Failed to persist pushId:`, err);
126
+ });
127
+ }
128
+ else {
129
+ log(`[BOT] ℹ️ No push_id found in message, will use config default`);
130
+ }
58
131
  // Resolve configuration (needed for status updates)
59
132
  const config = resolveXYConfig(cfg);
60
133
  // ✅ Resolve agent route (following feishu pattern)
@@ -70,11 +143,12 @@ export async function handleXYMessage(params) {
70
143
  },
71
144
  });
72
145
  log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
73
- // Register session context for tools
146
+ // 🔑 注册session(带引用计数)
74
147
  log(`[BOT] 📝 About to register session for tools...`);
75
148
  log(`[BOT] - sessionKey: ${route.sessionKey}`);
76
149
  log(`[BOT] - sessionId: ${parsed.sessionId}`);
77
150
  log(`[BOT] - taskId: ${parsed.taskId}`);
151
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
78
152
  registerSession(route.sessionKey, {
79
153
  config,
80
154
  sessionId: parsed.sessionId,
@@ -83,13 +157,24 @@ export async function handleXYMessage(params) {
83
157
  agentId: route.accountId,
84
158
  });
85
159
  log(`[BOT] ✅ Session registered for tools`);
160
+ // 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
161
+ log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
162
+ void sendStatusUpdate({
163
+ config,
164
+ sessionId: parsed.sessionId,
165
+ taskId: parsed.taskId,
166
+ messageId: parsed.messageId,
167
+ text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍后~",
168
+ state: "working",
169
+ }).catch((err) => {
170
+ error(`Failed to send initial status update:`, err);
171
+ });
86
172
  // Extract text and files from parts
87
173
  const text = extractTextFromParts(parsed.parts);
88
174
  const fileParts = extractFileParts(parsed.parts);
89
- // Download files if present (using core's media download)
90
- const mediaList = await downloadFilesFromParts(fileParts);
91
- // Build media payload for inbound context (following feishu pattern)
92
- const mediaPayload = buildXYMediaPayload(mediaList);
175
+ // Build media payload directly from file URIs (openclaw can download them)
176
+ // No need to download files locally - pass URIs directly to openclaw
177
+ const mediaPayload = buildXYMediaPayload(fileParts);
93
178
  // Resolve envelope format options (following feishu pattern)
94
179
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
95
180
  // Build message body with speaker prefix (following feishu pattern)
@@ -99,7 +184,7 @@ export async function handleXYMessage(params) {
99
184
  messageBody = `${speaker}: ${messageBody}`;
100
185
  // Format agent envelope (following feishu pattern)
101
186
  const body = core.channel.reply.formatAgentEnvelope({
102
- channel: "XY",
187
+ channel: "xiaoyi-channel",
103
188
  from: speaker,
104
189
  timestamp: new Date(),
105
190
  envelope: envelopeOptions,
@@ -130,101 +215,121 @@ export async function handleXYMessage(params) {
130
215
  ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
131
216
  ...mediaPayload,
132
217
  });
133
- // Send initial status update immediately after parsing message
134
- log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
135
- void sendStatusUpdate({
136
- config,
137
- sessionId: parsed.sessionId,
138
- taskId: parsed.taskId,
139
- messageId: parsed.messageId,
140
- text: "任务正在处理中,请稍后~",
141
- state: "working",
142
- }).catch((err) => {
143
- error(`Failed to send initial status update:`, err);
144
- });
145
- // Create reply dispatcher (following feishu pattern)
146
- log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher for session=${parsed.sessionId}, taskId=${parsed.taskId}, messageId=${parsed.messageId}`);
218
+ // 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
219
+ log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
220
+ log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
221
+ log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
222
+ log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
147
223
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
148
224
  cfg,
149
225
  runtime,
150
226
  sessionId: parsed.sessionId,
151
227
  taskId: parsed.taskId,
152
228
  messageId: parsed.messageId,
153
- accountId: route.accountId, // ✅ Use route.accountId
229
+ accountId: route.accountId,
230
+ isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
154
231
  });
155
232
  log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
156
- // Start status update interval (will send updates every 60 seconds)
157
- // Interval will be automatically stopped when onIdle/onCleanup is triggered
158
- startStatusInterval();
233
+ // 🔑 只有第一条消息启动状态定时器
234
+ // 第二条消息会很快返回,不需要定时器
235
+ if (!isSecondMessage) {
236
+ startStatusInterval();
237
+ log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
238
+ }
239
+ else {
240
+ log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
241
+ }
159
242
  log(`xy: dispatching to agent (session=${parsed.sessionId})`);
160
243
  // Dispatch to OpenClaw core using correct API (following feishu pattern)
161
244
  log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
245
+ // Build session context for AsyncLocalStorage
246
+ const sessionContext = {
247
+ config,
248
+ sessionId: parsed.sessionId,
249
+ taskId: parsed.taskId,
250
+ messageId: parsed.messageId,
251
+ agentId: route.accountId,
252
+ };
162
253
  await core.channel.reply.withReplyDispatcher({
163
254
  dispatcher,
164
255
  onSettled: () => {
165
256
  log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
166
- log(`[BOT] - About to unregister session...`);
257
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
167
258
  markDispatchIdle();
168
- // Unregister session context when done
259
+ // 🔑 减少引用计数
260
+ decrementTaskIdRef(parsed.sessionId);
261
+ // 🔑 如果是第一条消息完成,解锁
262
+ if (!isSecondMessage) {
263
+ unlockTaskId(parsed.sessionId);
264
+ log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
265
+ }
266
+ // 减少session引用计数
169
267
  unregisterSession(route.sessionKey);
170
- log(`[BOT] ✅ Session unregistered in onSettled`);
268
+ log(`[BOT] ✅ Cleanup completed`);
171
269
  },
172
- run: () => core.channel.reply.dispatchReplyFromConfig({
270
+ run: () =>
271
+ // 🔐 Use AsyncLocalStorage to provide session context to tools
272
+ runWithSessionContext(sessionContext, () => core.channel.reply.dispatchReplyFromConfig({
173
273
  ctx: ctxPayload,
174
274
  cfg,
175
275
  dispatcher,
176
276
  replyOptions,
177
- }),
277
+ })),
178
278
  });
179
279
  log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
180
280
  log(`xy: dispatch complete (session=${parsed.sessionId})`);
181
281
  }
182
282
  catch (err) {
283
+ // ✅ Only log error, don't re-throw to prevent gateway restart
183
284
  error("Failed to handle XY message:", err);
184
285
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
185
286
  log(`[BOT] ❌ Error occurred, attempting cleanup...`);
186
- // Try to unregister session on error (if route was established)
287
+ // 🔑 错误时也要清理taskId和session
187
288
  try {
188
- const core = getXYRuntime();
189
289
  const params = message.params;
190
290
  const sessionId = params?.sessionId;
191
291
  if (sessionId) {
192
- log(`[BOT] 🧹 Cleaning up session after error: ${sessionId}`);
292
+ log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
293
+ // 清理 taskId
294
+ decrementTaskIdRef(sessionId);
295
+ unlockTaskId(sessionId);
296
+ // 清理 session
297
+ const core = getXYRuntime();
193
298
  const route = core.channel.routing.resolveAgentRoute({
194
299
  cfg,
195
300
  channel: "xiaoyi-channel",
196
301
  accountId,
197
302
  peer: {
198
303
  kind: "direct",
199
- id: sessionId, // ✅ Use sessionId for cleanup consistency
304
+ id: sessionId,
200
305
  },
201
306
  });
202
- log(`[BOT] - Unregistering session: ${route.sessionKey}`);
203
307
  unregisterSession(route.sessionKey);
204
- log(`[BOT] ✅ Session unregistered after error`);
308
+ log(`[BOT] ✅ Cleanup completed after error`);
205
309
  }
206
310
  }
207
311
  catch (cleanupErr) {
208
312
  log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
209
313
  // Ignore cleanup errors
210
314
  }
211
- throw err;
315
+ // ❌ Don't re-throw: message processing error should not affect gateway stability
212
316
  }
213
317
  }
214
318
  /**
215
319
  * Build media payload for inbound context.
216
320
  * Following feishu pattern: buildFeishuMediaPayload().
321
+ * Uses remote URIs directly - openclaw will download them.
217
322
  */
218
- function buildXYMediaPayload(mediaList) {
219
- const first = mediaList[0];
220
- const mediaPaths = mediaList.map((media) => media.path);
221
- const mediaTypes = mediaList.map((media) => media.mimeType).filter(Boolean);
323
+ function buildXYMediaPayload(fileParts) {
324
+ const first = fileParts[0];
325
+ const uris = fileParts.map((file) => file.uri);
326
+ const mediaTypes = fileParts.map((file) => file.mimeType).filter(Boolean);
222
327
  return {
223
- MediaPath: first?.path,
328
+ MediaPath: first?.uri,
224
329
  MediaType: first?.mimeType,
225
- MediaUrl: first?.path,
226
- MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
227
- MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
330
+ MediaUrl: first?.uri,
331
+ MediaPaths: uris.length > 0 ? uris : undefined,
332
+ MediaUrls: uris.length > 0 ? uris : undefined,
228
333
  MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
229
334
  };
230
335
  }
@@ -5,6 +5,24 @@ import { xyOnboardingAdapter } from "./onboarding.js";
5
5
  import { locationTool } from "./tools/location-tool.js";
6
6
  import { noteTool } from "./tools/note-tool.js";
7
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";
25
+ import { viewPushResultTool } from "./tools/view-push-result-tool.js";
8
26
  /**
9
27
  * Xiaoyi Channel Plugin for OpenClaw.
10
28
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -22,6 +40,7 @@ export const xyPlugin = {
22
40
  agentPrompt: {
23
41
  messageToolHints: () => [
24
42
  "- xiaoyi targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `default`",
43
+ "- 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."
25
44
  ],
26
45
  },
27
46
  capabilities: {
@@ -43,7 +62,7 @@ export const xyPlugin = {
43
62
  },
44
63
  outbound: xyOutbound,
45
64
  onboarding: xyOnboardingAdapter,
46
- agentTools: [locationTool, noteTool, searchNoteTool],
65
+ agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, xiaoyiCollectionTool, viewPushResultTool],
47
66
  messaging: {
48
67
  normalizeTarget: (raw) => {
49
68
  const trimmed = raw.trim();
@@ -70,15 +89,15 @@ export const xyPlugin = {
70
89
  const account = resolveXYConfig(context.cfg);
71
90
  context.setStatus?.({
72
91
  accountId: context.accountId,
73
- wsUrl1: account.wsUrl1,
74
- wsUrl2: account.wsUrl2,
92
+ wsUrl: account.wsUrl,
75
93
  });
76
- context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl1: ${account.wsUrl1}, wsUrl2: ${account.wsUrl2})`);
94
+ context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
77
95
  return monitorXYProvider({
78
96
  config: context.cfg,
79
97
  runtime: context.runtime,
80
98
  abortSignal: context.abortSignal,
81
99
  accountId: context.accountId,
100
+ setStatus: context.setStatus,
82
101
  });
83
102
  },
84
103
  },
@@ -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;
@@ -34,6 +34,23 @@ export function getXYWebSocketManager(config) {
34
34
  log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
35
35
  return cached;
36
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
+ }
37
54
  /**
38
55
  * Clear all cached WebSocket managers.
39
56
  */
@@ -51,3 +68,67 @@ export function clearXYWebSocketManagers() {
51
68
  export function getCachedManagerCount() {
52
69
  return wsManagerCache.size;
53
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.
@@ -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
  */
@@ -75,3 +92,17 @@ export interface SendTasksCancelResponseParams {
75
92
  * Send a tasks/cancel response.
76
93
  */
77
94
  export declare function sendTasksCancelResponse(params: SendTasksCancelResponseParams): Promise<void>;
95
+ /**
96
+ * Parameters for sending a Trigger response.
97
+ */
98
+ export interface SendTriggerResponseParams {
99
+ config: XYChannelConfig;
100
+ sessionId: string;
101
+ taskId: string;
102
+ messageId: string;
103
+ content: string;
104
+ }
105
+ /**
106
+ * Send a Trigger response with pushData content.
107
+ */
108
+ export declare function sendTriggerResponse(params: SendTriggerResponseParams): Promise<void>;