@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
@@ -67,6 +67,49 @@ export async function sendA2AResponse(params) {
67
67
  await wsManager.sendMessage(sessionId, outboundMessage);
68
68
  log(`[A2A_RESPONSE] ✅ Message sent successfully`);
69
69
  }
70
+ /**
71
+ * Send an A2A artifact-update with reasoningText part.
72
+ * Used for onToolStart, onToolResult, onReasoningStream, onReasoningEnd, onPartialReply.
73
+ * append=true, final=false, lastChunk=true, text is suffixed with newline for markdown rendering.
74
+ */
75
+ export async function sendReasoningTextUpdate(params) {
76
+ const { config, sessionId, taskId, messageId, text, append = true } = params;
77
+ const runtime = getXYRuntime();
78
+ const log = runtime?.log ?? console.log;
79
+ const error = runtime?.error ?? console.error;
80
+ const artifact = {
81
+ taskId,
82
+ kind: "artifact-update",
83
+ append,
84
+ lastChunk: true,
85
+ final: false,
86
+ artifact: {
87
+ artifactId: uuidv4(),
88
+ parts: [
89
+ {
90
+ kind: "reasoningText",
91
+ reasoningText: text,
92
+ },
93
+ ],
94
+ },
95
+ };
96
+ const jsonRpcResponse = {
97
+ jsonrpc: "2.0",
98
+ id: messageId,
99
+ result: artifact,
100
+ };
101
+ const wsManager = getXYWebSocketManager(config);
102
+ const outboundMessage = {
103
+ msgType: "agent_response",
104
+ agentId: config.agentId,
105
+ sessionId,
106
+ taskId,
107
+ msgDetail: JSON.stringify(jsonRpcResponse),
108
+ };
109
+ log(`[REASONING_TEXT] 📤 Sending reasoningText update: sessionId=${sessionId}, taskId=${taskId}, text.length=${text.length}`);
110
+ await wsManager.sendMessage(sessionId, outboundMessage);
111
+ log(`[REASONING_TEXT] ✅ Sent successfully`);
112
+ }
70
113
  /**
71
114
  * Send an A2A task status update.
72
115
  * Follows A2A protocol standard format with nested status object.
@@ -132,6 +175,7 @@ export async function sendCommand(params) {
132
175
  const log = runtime?.log ?? console.log;
133
176
  const error = runtime?.error ?? console.error;
134
177
  // Build artifact update with command as data
178
+ // Wrap command in commands array as per protocol requirement
135
179
  const artifact = {
136
180
  taskId,
137
181
  kind: "artifact-update",
@@ -143,7 +187,9 @@ export async function sendCommand(params) {
143
187
  parts: [
144
188
  {
145
189
  kind: "data",
146
- data: command,
190
+ data: {
191
+ commands: [command],
192
+ },
147
193
  },
148
194
  ],
149
195
  },
@@ -247,3 +293,49 @@ export async function sendTasksCancelResponse(params) {
247
293
  await wsManager.sendMessage(sessionId, outboundMessage);
248
294
  log(`Sent tasks/cancel response: sessionId=${sessionId}, taskId=${taskId}`);
249
295
  }
296
+ /**
297
+ * Send a Trigger response with pushData content.
298
+ */
299
+ export async function sendTriggerResponse(params) {
300
+ const { config, sessionId, taskId, messageId, content } = params;
301
+ const runtime = getXYRuntime();
302
+ const log = runtime?.log ?? console.log;
303
+ const error = runtime?.error ?? console.error;
304
+ // Build JSON-RPC response for Trigger
305
+ const jsonRpcResponse = {
306
+ jsonrpc: "2.0",
307
+ id: messageId,
308
+ result: {
309
+ taskId: taskId,
310
+ kind: "artifact-update",
311
+ append: false,
312
+ lastChunk: true,
313
+ final: true,
314
+ artifact: {
315
+ artifactId: uuidv4(),
316
+ parts: [
317
+ {
318
+ kind: "text",
319
+ text: content,
320
+ },
321
+ ],
322
+ },
323
+ },
324
+ error: {
325
+ code: 0,
326
+ message: "",
327
+ },
328
+ };
329
+ // Send via WebSocket
330
+ const wsManager = getXYWebSocketManager(config);
331
+ const outboundMessage = {
332
+ msgType: "agent_response",
333
+ agentId: config.agentId,
334
+ sessionId,
335
+ taskId,
336
+ msgDetail: JSON.stringify(jsonRpcResponse),
337
+ };
338
+ log(`[TRIGGER_RESPONSE] Sending Trigger response: sessionId=${sessionId}, taskId=${taskId}`);
339
+ await wsManager.sendMessage(sessionId, outboundMessage);
340
+ log(`[TRIGGER_RESPONSE] Trigger response sent successfully`);
341
+ }
@@ -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(() => {
@@ -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.
@@ -1,6 +1,9 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
- import { getXYWebSocketManager } from "./client.js";
2
+ import { getXYWebSocketManager, diagnoseAllManagers, cleanupOrphanConnections, removeXYWebSocketManager } from "./client.js";
3
3
  import { handleXYMessage } from "./bot.js";
4
+ import { parseA2AMessage } from "./parser.js";
5
+ import { hasActiveTask } from "./task-manager.js";
6
+ import { handleTriggerEvent } from "./trigger-handler.js";
4
7
  /**
5
8
  * Per-session serial queue that ensures messages from the same session are processed
6
9
  * in arrival order while allowing different sessions to run concurrently.
@@ -37,19 +40,36 @@ export async function monitorXYProvider(opts = {}) {
37
40
  throw new Error(`XY account is disabled`);
38
41
  }
39
42
  const accountId = opts.accountId ?? "default";
43
+ // Create trackEvent function to report health to OpenClaw framework
44
+ const trackEvent = opts.setStatus
45
+ ? () => {
46
+ opts.setStatus({ lastEventAt: Date.now(), lastInboundAt: Date.now() });
47
+ }
48
+ : undefined;
49
+ // 🔍 Diagnose WebSocket managers before gateway start
50
+ console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before gateway start...");
51
+ diagnoseAllManagers();
40
52
  // Get WebSocket manager (cached)
41
53
  const wsManager = getXYWebSocketManager(account);
54
+ // ✅ Set health event callback for heartbeat reporting
55
+ if (trackEvent) {
56
+ wsManager.setHealthEventCallback(trackEvent);
57
+ }
42
58
  // Track logged servers to avoid duplicate logs
43
59
  const loggedServers = new Set();
44
60
  // Track active message processing to detect duplicates
45
61
  const activeMessages = new Set();
46
62
  // Create session queue for ordered message processing
47
63
  const enqueue = createSessionQueue();
64
+ // Health check interval
65
+ let healthCheckInterval = null;
48
66
  return new Promise((resolve, reject) => {
49
67
  // Event handlers (defined early so they can be referenced in cleanup)
50
68
  const messageHandler = (message, sessionId, serverId) => {
51
69
  const messageKey = `${sessionId}::${message.id}`;
52
70
  log(`[MONITOR-HANDLER] ####### messageHandler triggered: serverId=${serverId}, sessionId=${sessionId}, messageId=${message.id} #######`);
71
+ // ✅ Report health: received a message
72
+ trackEvent?.();
53
73
  // Check for duplicate message handling
54
74
  if (activeMessages.has(messageKey)) {
55
75
  error(`[MONITOR-HANDLER] ⚠️ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
@@ -68,8 +88,8 @@ export async function monitorXYProvider(opts = {}) {
68
88
  log(`[MONITOR-HANDLER] ✅ Completed handleXYMessage for messageKey=${messageKey}`);
69
89
  }
70
90
  catch (err) {
91
+ // ✅ Only log error, don't re-throw to prevent gateway restart
71
92
  error(`XY gateway: error handling message from ${serverId}: ${String(err)}`);
72
- throw err;
73
93
  }
74
94
  finally {
75
95
  // Remove from active messages when done
@@ -77,37 +97,97 @@ export async function monitorXYProvider(opts = {}) {
77
97
  log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
78
98
  }
79
99
  };
80
- void enqueue(sessionId, task).catch((err) => {
81
- // Error already logged in task, this is for queue failures
82
- error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
83
- activeMessages.delete(messageKey);
84
- });
100
+ // 🔑 核心改造:检测steer模式
101
+ // 需要提前解析消息以获取sessionId
102
+ try {
103
+ const parsed = parseA2AMessage(message);
104
+ const steerMode = cfg.messages?.queue?.mode === "steer";
105
+ const hasActiveRun = hasActiveTask(parsed.sessionId);
106
+ if (steerMode && hasActiveRun) {
107
+ // Steer模式且有活跃任务:不入队列,直接并发执行
108
+ log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
109
+ log(`[MONITOR-HANDLER] - sessionId: ${parsed.sessionId}`);
110
+ log(`[MONITOR-HANDLER] - Bypassing queue to allow message insertion`);
111
+ void task().catch((err) => {
112
+ error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
113
+ activeMessages.delete(messageKey);
114
+ });
115
+ }
116
+ else {
117
+ // 正常模式:入队列串行执行
118
+ log(`[MONITOR-HANDLER] 📋 NORMAL MODE: Enqueuing for messageKey=${messageKey}`);
119
+ void enqueue(sessionId, task).catch((err) => {
120
+ error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
121
+ activeMessages.delete(messageKey);
122
+ });
123
+ }
124
+ }
125
+ catch (parseErr) {
126
+ // 解析失败,回退到正常队列模式
127
+ error(`[MONITOR-HANDLER] Failed to parse message for steer detection: ${String(parseErr)}`);
128
+ void enqueue(sessionId, task).catch((err) => {
129
+ error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
130
+ activeMessages.delete(messageKey);
131
+ });
132
+ }
85
133
  };
86
134
  const connectedHandler = (serverId) => {
87
135
  if (!loggedServers.has(serverId)) {
88
136
  log(`XY gateway: ${serverId} connected`);
89
137
  loggedServers.add(serverId);
90
138
  }
139
+ // ✅ Report health: connection established
140
+ trackEvent?.();
141
+ opts.setStatus?.({ connected: true });
91
142
  };
92
143
  const disconnectedHandler = (serverId) => {
93
144
  console.warn(`XY gateway: ${serverId} disconnected`);
94
145
  loggedServers.delete(serverId);
146
+ // ✅ Report disconnection status (only if all servers disconnected)
147
+ if (loggedServers.size === 0) {
148
+ opts.setStatus?.({ connected: false });
149
+ }
95
150
  };
96
151
  const errorHandler = (err, serverId) => {
97
152
  error(`XY gateway: ${serverId} error: ${String(err)}`);
98
153
  };
154
+ const triggerEventHandler = (context) => {
155
+ log(`[MONITOR] 📌 Received trigger-event, dispatching to handler...`);
156
+ log(`[MONITOR] - sessionId: ${context.sessionId}`);
157
+ log(`[MONITOR] - taskId: ${context.taskId}`);
158
+ // 异步处理 Trigger 事件,不阻塞主流程
159
+ handleTriggerEvent(context, cfg, runtime, accountId).catch((err) => {
160
+ error(`[MONITOR] Failed to handle trigger-event:`, err);
161
+ });
162
+ };
99
163
  const cleanup = () => {
100
164
  log("XY gateway: cleaning up...");
165
+ // 🔍 Diagnose before cleanup
166
+ console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
167
+ diagnoseAllManagers();
168
+ // Stop health check interval
169
+ if (healthCheckInterval) {
170
+ clearInterval(healthCheckInterval);
171
+ healthCheckInterval = null;
172
+ console.log("⏸️ Stopped periodic health check");
173
+ }
101
174
  // Remove event handlers to prevent duplicate calls on gateway restart
102
175
  wsManager.off("message", messageHandler);
103
176
  wsManager.off("connected", connectedHandler);
104
177
  wsManager.off("disconnected", disconnectedHandler);
105
178
  wsManager.off("error", errorHandler);
106
- // Don't disconnect the shared wsManager as it may be used elsewhere
107
- // wsManager.disconnect();
179
+ wsManager.off("trigger-event", triggerEventHandler);
180
+ // ✅ Disconnect the wsManager to prevent connection leaks
181
+ // This is safe because each gateway lifecycle should have clean connections
182
+ wsManager.disconnect();
183
+ // ✅ Remove manager from cache to prevent reusing dirty state
184
+ removeXYWebSocketManager(account);
108
185
  loggedServers.clear();
109
186
  activeMessages.clear();
110
187
  log(`[MONITOR-HANDLER] 🧹 Cleanup complete, cleared active messages`);
188
+ // 🔍 Diagnose after cleanup
189
+ console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
190
+ diagnoseAllManagers();
111
191
  };
112
192
  const handleAbort = () => {
113
193
  log("XY gateway: abort signal received, stopping");
@@ -126,6 +206,18 @@ export async function monitorXYProvider(opts = {}) {
126
206
  wsManager.on("connected", connectedHandler);
127
207
  wsManager.on("disconnected", disconnectedHandler);
128
208
  wsManager.on("error", errorHandler);
209
+ wsManager.on("trigger-event", triggerEventHandler);
210
+ // Start periodic health check (every 5 minutes)
211
+ console.log("🏥 Starting periodic health check (every 5 minutes)...");
212
+ healthCheckInterval = setInterval(() => {
213
+ console.log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
214
+ diagnoseAllManagers();
215
+ // Auto-cleanup orphan connections
216
+ const cleaned = cleanupOrphanConnections();
217
+ if (cleaned > 0) {
218
+ console.log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
219
+ }
220
+ }, 5 * 60 * 1000); // 5 minutes
129
221
  // Connect to WebSocket servers
130
222
  wsManager.connect()
131
223
  .then(() => {
@@ -1,9 +1,44 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
2
  import { XYFileUploadService } from "./file-upload.js";
3
3
  import { XYPushService } from "./push.js";
4
- import { getLatestSessionContext } from "./tools/session-manager.js";
4
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
5
+ import { savePushData } from "./utils/pushdata-manager.js";
6
+ import { getAllPushIds } from "./utils/pushid-manager.js";
5
7
  // Special marker for default push delivery when no target is specified
6
8
  const DEFAULT_PUSH_MARKER = "default";
9
+ // File extension to MIME type mapping
10
+ const FILE_TYPE_TO_MIME_TYPE = {
11
+ txt: "text/plain",
12
+ html: "text/html",
13
+ css: "text/css",
14
+ js: "application/javascript",
15
+ json: "application/json",
16
+ png: "image/png",
17
+ jpeg: "image/jpeg",
18
+ jpg: "image/jpeg",
19
+ gif: "image/gif",
20
+ svg: "image/svg+xml",
21
+ pdf: "application/pdf",
22
+ zip: "application/zip",
23
+ doc: "application/msword",
24
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
25
+ xls: "application/vnd.ms-excel",
26
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
27
+ ppt: "application/vnd.ms-powerpoint",
28
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
29
+ mp3: "audio/mpeg",
30
+ mp4: "video/mp4",
31
+ };
32
+ /**
33
+ * Get MIME type from file extension
34
+ */
35
+ function getMimeTypeFromFilename(filename) {
36
+ const extension = filename.split(".").pop()?.toLowerCase();
37
+ if (extension && FILE_TYPE_TO_MIME_TYPE[extension]) {
38
+ return FILE_TYPE_TO_MIME_TYPE[extension];
39
+ }
40
+ return "text/plain"; // Default fallback
41
+ }
7
42
  /**
8
43
  * Outbound adapter for sending messages from OpenClaw to XY.
9
44
  * Uses Push service for direct message delivery.
@@ -32,8 +67,8 @@ export const xyOutbound = {
32
67
  // If the target doesn't contain "::", try to enhance it with taskId from session context
33
68
  if (!trimmedTo.includes("::")) {
34
69
  console.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
35
- // Try to get the latest session context
36
- const sessionContext = getLatestSessionContext();
70
+ // Try to get the current session context
71
+ const sessionContext = getCurrentSessionContext();
37
72
  if (sessionContext && sessionContext.sessionId === trimmedTo) {
38
73
  const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
39
74
  console.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
@@ -72,17 +107,63 @@ export const xyOutbound = {
72
107
  // The push service will handle it based on config
73
108
  actualTo = config.defaultSessionId || "";
74
109
  }
110
+ // 1. 持久化推送消息内容,获取 pushDataId
111
+ console.log(`[xyOutbound.sendText] Saving push data to local storage...`);
112
+ let pushDataId;
113
+ try {
114
+ pushDataId = await savePushData(text);
115
+ console.log(`[xyOutbound.sendText] ✅ Push data saved with ID: ${pushDataId}`);
116
+ }
117
+ catch (error) {
118
+ console.error(`[xyOutbound.sendText] ❌ Failed to save push data:`, error);
119
+ // 如果持久化失败,仍然继续发送(不阻塞主流程)
120
+ pushDataId = "";
121
+ }
122
+ // 2. 读取所有 pushId
123
+ console.log(`[xyOutbound.sendText] Loading all pushIds...`);
124
+ let pushIdList = [];
125
+ try {
126
+ pushIdList = await getAllPushIds();
127
+ console.log(`[xyOutbound.sendText] ✅ Loaded ${pushIdList.length} pushIds`);
128
+ }
129
+ catch (error) {
130
+ console.error(`[xyOutbound.sendText] ❌ Failed to load pushIds:`, error);
131
+ }
132
+ // 3. 如果 pushIdList 为空,回退到原有逻辑(使用 config pushId)
133
+ if (pushIdList.length === 0) {
134
+ console.log(`[xyOutbound.sendText] ⚠️ No pushIds found, falling back to config pushId`);
135
+ pushIdList = [config.pushId];
136
+ }
75
137
  // Create push service
76
138
  const pushService = new XYPushService(config);
77
139
  // Extract title (first 57 chars or first line)
78
140
  const title = text.split("\n")[0].slice(0, 57);
79
- // Send push message
80
- await pushService.sendPush(text, title, actualTo);
141
+ // Truncate push content to max length 1000
142
+ const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
143
+ // 4. 遍历所有 pushId,依次发送推送通知
144
+ console.log(`[xyOutbound.sendText] 📤 Broadcasting to ${pushIdList.length} pushId(s)...`);
145
+ let successCount = 0;
146
+ let failureCount = 0;
147
+ for (const pushId of pushIdList) {
148
+ try {
149
+ console.log(`[xyOutbound.sendText] Sending to pushId: ${pushId.substring(0, 20)}...`);
150
+ // 传入 pushDataId,使用 kind="data" 格式
151
+ await pushService.sendPush(pushText, title, undefined, actualTo, pushDataId);
152
+ successCount++;
153
+ console.log(`[xyOutbound.sendText] ✅ Sent successfully to pushId: ${pushId.substring(0, 20)}...`);
154
+ }
155
+ catch (error) {
156
+ failureCount++;
157
+ console.error(`[xyOutbound.sendText] ❌ Failed to send to pushId: ${pushId.substring(0, 20)}...`, error);
158
+ // 单个 pushId 发送失败不影响其他,继续处理下一个
159
+ }
160
+ }
161
+ console.log(`[xyOutbound.sendText] 📊 Broadcast summary: ${successCount} success, ${failureCount} failures`);
81
162
  console.log(`[xyOutbound.sendText] Completed successfully`);
82
163
  // Return message info
83
164
  return {
84
165
  channel: "xiaoyi-channel",
85
- messageId: Date.now().toString(),
166
+ messageId: pushDataId || Date.now().toString(),
86
167
  chatId: actualTo,
87
168
  };
88
169
  },
@@ -111,6 +192,15 @@ export const xyOutbound = {
111
192
  }
112
193
  // Upload file
113
194
  const fileId = await uploadService.uploadFile(mediaUrl);
195
+ // Check if fileId is empty
196
+ if (!fileId) {
197
+ console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
198
+ return {
199
+ channel: "xiaoyi-channel",
200
+ messageId: "",
201
+ chatId: to,
202
+ };
203
+ }
114
204
  console.log(`[xyOutbound.sendMedia] File uploaded:`, {
115
205
  fileId,
116
206
  sessionId,
@@ -119,7 +209,7 @@ export const xyOutbound = {
119
209
  // Get filename and mime type from mediaUrl
120
210
  // mediaUrl may be a local file path or URL
121
211
  const fileName = mediaUrl.split("/").pop() || "unknown";
122
- const mimeType = text?.match(/\[ MediaType: ([^\]]+)\]/)?.[1] || "application/octet-stream";
212
+ const mimeType = getMimeTypeFromFilename(fileName);
123
213
  // Build agent_response message
124
214
  const agentResponse = {
125
215
  msgType: "agent_response",
@@ -38,6 +38,18 @@ export declare function isClearContextMessage(method: string): boolean;
38
38
  * Check if message is a tasks/cancel request.
39
39
  */
40
40
  export declare function isTasksCancelMessage(method: string): boolean;
41
+ /**
42
+ * Extract push_id from message parts.
43
+ * Looks for push_id in data parts under variables.systemVariables.push_id
44
+ */
45
+ export declare function extractPushId(parts: A2AMessagePart[]): string | null;
46
+ /**
47
+ * Extract Trigger event data from message parts.
48
+ * Looks for Trigger events with pushDataId in data parts.
49
+ */
50
+ export declare function extractTriggerData(parts: A2AMessagePart[]): {
51
+ pushDataId: string;
52
+ } | null;
41
53
  /**
42
54
  * Validate A2A request structure.
43
55
  */
@@ -57,6 +57,43 @@ export function isClearContextMessage(method) {
57
57
  export function isTasksCancelMessage(method) {
58
58
  return method === "tasks/cancel" || method === "tasks_cancel";
59
59
  }
60
+ /**
61
+ * Extract push_id from message parts.
62
+ * Looks for push_id in data parts under variables.systemVariables.push_id
63
+ */
64
+ export function extractPushId(parts) {
65
+ for (const part of parts) {
66
+ if (part.kind === "data" && part.data) {
67
+ const pushId = part.data.variables?.systemVariables?.push_id;
68
+ if (pushId && typeof pushId === "string") {
69
+ return pushId;
70
+ }
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ /**
76
+ * Extract Trigger event data from message parts.
77
+ * Looks for Trigger events with pushDataId in data parts.
78
+ */
79
+ export function extractTriggerData(parts) {
80
+ for (const part of parts) {
81
+ if (part.kind === "data" && part.data) {
82
+ const events = part.data.events;
83
+ if (Array.isArray(events)) {
84
+ for (const event of events) {
85
+ if (event.header?.namespace === "Common" && event.header?.name === "Trigger") {
86
+ const pushDataId = event.payload?.dataMap?.pushDataId;
87
+ if (pushDataId && typeof pushDataId === "string") {
88
+ return { pushDataId };
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ return null;
96
+ }
60
97
  /**
61
98
  * Validate A2A request structure.
62
99
  */
@@ -5,11 +5,23 @@ import type { XYChannelConfig } from "./types.js";
5
5
  */
6
6
  export declare class XYPushService {
7
7
  private config;
8
+ private readonly DEFAULT_PUSH_URL;
9
+ private readonly REQUEST_FROM;
8
10
  constructor(config: XYChannelConfig);
11
+ /**
12
+ * Generate a random trace ID for request tracking.
13
+ */
14
+ private generateTraceId;
9
15
  /**
10
16
  * Send a push message to a user session.
17
+ *
18
+ * @param content - Push message content
19
+ * @param title - Push message title
20
+ * @param data - Optional additional data
21
+ * @param sessionId - Optional session ID
22
+ * @param pushDataId - Optional pushDataId for kind="data" format
11
23
  */
12
- sendPush(content: string, title: string, sessionId?: string): Promise<void>;
24
+ sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string, pushDataId?: string): Promise<void>;
13
25
  /**
14
26
  * Send a push message with file attachments.
15
27
  */