@ynhcj/xiaoyi-channel 0.0.2 → 0.0.3-next

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/src/bot.js +115 -35
  2. package/dist/src/channel.js +22 -1
  3. package/dist/src/client.d.ts +15 -0
  4. package/dist/src/client.js +97 -2
  5. package/dist/src/file-download.js +10 -1
  6. package/dist/src/file-upload.js +1 -1
  7. package/dist/src/formatter.d.ts +17 -0
  8. package/dist/src/formatter.js +86 -6
  9. package/dist/src/heartbeat.d.ts +2 -1
  10. package/dist/src/heartbeat.js +9 -1
  11. package/dist/src/monitor.d.ts +5 -0
  12. package/dist/src/monitor.js +131 -25
  13. package/dist/src/onboarding.js +7 -7
  14. package/dist/src/outbound.js +150 -71
  15. package/dist/src/parser.d.ts +5 -0
  16. package/dist/src/parser.js +15 -0
  17. package/dist/src/push.d.ts +7 -1
  18. package/dist/src/push.js +110 -19
  19. package/dist/src/reply-dispatcher.d.ts +1 -0
  20. package/dist/src/reply-dispatcher.js +210 -57
  21. package/dist/src/task-manager.d.ts +55 -0
  22. package/dist/src/task-manager.js +136 -0
  23. package/dist/src/tools/calendar-tool.d.ts +6 -0
  24. package/dist/src/tools/calendar-tool.js +167 -0
  25. package/dist/src/tools/call-phone-tool.d.ts +5 -0
  26. package/dist/src/tools/call-phone-tool.js +183 -0
  27. package/dist/src/tools/create-alarm-tool.d.ts +7 -0
  28. package/dist/src/tools/create-alarm-tool.js +444 -0
  29. package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
  30. package/dist/src/tools/delete-alarm-tool.js +238 -0
  31. package/dist/src/tools/location-tool.js +48 -9
  32. package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
  33. package/dist/src/tools/modify-alarm-tool.js +474 -0
  34. package/dist/src/tools/modify-note-tool.d.ts +9 -0
  35. package/dist/src/tools/modify-note-tool.js +163 -0
  36. package/dist/src/tools/note-tool.d.ts +5 -0
  37. package/dist/src/tools/note-tool.js +146 -0
  38. package/dist/src/tools/search-alarm-tool.d.ts +8 -0
  39. package/dist/src/tools/search-alarm-tool.js +389 -0
  40. package/dist/src/tools/search-calendar-tool.d.ts +12 -0
  41. package/dist/src/tools/search-calendar-tool.js +259 -0
  42. package/dist/src/tools/search-contact-tool.d.ts +5 -0
  43. package/dist/src/tools/search-contact-tool.js +168 -0
  44. package/dist/src/tools/search-file-tool.d.ts +5 -0
  45. package/dist/src/tools/search-file-tool.js +185 -0
  46. package/dist/src/tools/search-message-tool.d.ts +5 -0
  47. package/dist/src/tools/search-message-tool.js +173 -0
  48. package/dist/src/tools/search-note-tool.d.ts +5 -0
  49. package/dist/src/tools/search-note-tool.js +130 -0
  50. package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
  51. package/dist/src/tools/search-photo-gallery-tool.js +184 -0
  52. package/dist/src/tools/search-photo-tool.d.ts +9 -0
  53. package/dist/src/tools/search-photo-tool.js +270 -0
  54. package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
  55. package/dist/src/tools/send-file-to-user-tool.js +318 -0
  56. package/dist/src/tools/send-message-tool.d.ts +5 -0
  57. package/dist/src/tools/send-message-tool.js +189 -0
  58. package/dist/src/tools/session-manager.d.ts +15 -0
  59. package/dist/src/tools/session-manager.js +126 -6
  60. package/dist/src/tools/upload-file-tool.d.ts +13 -0
  61. package/dist/src/tools/upload-file-tool.js +265 -0
  62. package/dist/src/tools/upload-photo-tool.d.ts +9 -0
  63. package/dist/src/tools/upload-photo-tool.js +223 -0
  64. package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
  65. package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
  66. package/dist/src/types.d.ts +5 -9
  67. package/dist/src/utils/config-manager.d.ts +26 -0
  68. package/dist/src/utils/config-manager.js +56 -0
  69. package/dist/src/websocket.d.ts +41 -0
  70. package/dist/src/websocket.js +192 -9
  71. package/package.json +1 -2
@@ -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.
@@ -1,6 +1,8 @@
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";
4
6
  /**
5
7
  * Per-session serial queue that ensures messages from the same session are processed
6
8
  * in arrival order while allowing different sessions to run concurrently.
@@ -37,69 +39,173 @@ export async function monitorXYProvider(opts = {}) {
37
39
  throw new Error(`XY account is disabled`);
38
40
  }
39
41
  const accountId = opts.accountId ?? "default";
42
+ // Create trackEvent function to report health to OpenClaw framework
43
+ const trackEvent = opts.setStatus
44
+ ? () => {
45
+ opts.setStatus({ lastEventAt: Date.now(), lastInboundAt: Date.now() });
46
+ }
47
+ : undefined;
48
+ // 🔍 Diagnose WebSocket managers before gateway start
49
+ console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before gateway start...");
50
+ diagnoseAllManagers();
40
51
  // Get WebSocket manager (cached)
41
52
  const wsManager = getXYWebSocketManager(account);
53
+ // ✅ Set health event callback for heartbeat reporting
54
+ if (trackEvent) {
55
+ wsManager.setHealthEventCallback(trackEvent);
56
+ }
42
57
  // Track logged servers to avoid duplicate logs
43
58
  const loggedServers = new Set();
59
+ // Track active message processing to detect duplicates
60
+ const activeMessages = new Set();
44
61
  // Create session queue for ordered message processing
45
62
  const enqueue = createSessionQueue();
63
+ // Health check interval
64
+ let healthCheckInterval = null;
46
65
  return new Promise((resolve, reject) => {
47
- const cleanup = () => {
48
- log("XY gateway: cleaning up...");
49
- wsManager.disconnect();
50
- loggedServers.clear();
51
- };
52
- const handleAbort = () => {
53
- log("XY gateway: abort signal received, stopping");
54
- cleanup();
55
- log("XY gateway stopped");
56
- resolve();
57
- };
58
- if (opts.abortSignal?.aborted) {
59
- cleanup();
60
- resolve();
61
- return;
62
- }
63
- opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
64
- // Setup event handlers
66
+ // Event handlers (defined early so they can be referenced in cleanup)
65
67
  const messageHandler = (message, sessionId, serverId) => {
68
+ const messageKey = `${sessionId}::${message.id}`;
69
+ log(`[MONITOR-HANDLER] ####### messageHandler triggered: serverId=${serverId}, sessionId=${sessionId}, messageId=${message.id} #######`);
70
+ // ✅ Report health: received a message
71
+ trackEvent?.();
72
+ // Check for duplicate message handling
73
+ if (activeMessages.has(messageKey)) {
74
+ error(`[MONITOR-HANDLER] ⚠️ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
75
+ }
76
+ activeMessages.add(messageKey);
77
+ log(`[MONITOR-HANDLER] 📝 Active messages count: ${activeMessages.size}, messageKey: ${messageKey}`);
66
78
  const task = async () => {
67
79
  try {
80
+ log(`[MONITOR-HANDLER] 🚀 Starting handleXYMessage for messageKey=${messageKey}`);
68
81
  await handleXYMessage({
69
82
  cfg,
70
83
  runtime,
71
84
  message,
72
85
  accountId, // ✅ Pass accountId ("default")
73
86
  });
87
+ log(`[MONITOR-HANDLER] ✅ Completed handleXYMessage for messageKey=${messageKey}`);
74
88
  }
75
89
  catch (err) {
90
+ // ✅ Only log error, don't re-throw to prevent gateway restart
76
91
  error(`XY gateway: error handling message from ${serverId}: ${String(err)}`);
77
- throw err;
92
+ }
93
+ finally {
94
+ // Remove from active messages when done
95
+ activeMessages.delete(messageKey);
96
+ log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
78
97
  }
79
98
  };
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
- });
99
+ // 🔑 核心改造:检测steer模式
100
+ // 需要提前解析消息以获取sessionId
101
+ try {
102
+ const parsed = parseA2AMessage(message);
103
+ const steerMode = cfg.messages?.queue?.mode === "steer";
104
+ const hasActiveRun = hasActiveTask(parsed.sessionId);
105
+ if (steerMode && hasActiveRun) {
106
+ // Steer模式且有活跃任务:不入队列,直接并发执行
107
+ log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
108
+ log(`[MONITOR-HANDLER] - sessionId: ${parsed.sessionId}`);
109
+ log(`[MONITOR-HANDLER] - Bypassing queue to allow message insertion`);
110
+ void task().catch((err) => {
111
+ error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
112
+ activeMessages.delete(messageKey);
113
+ });
114
+ }
115
+ else {
116
+ // 正常模式:入队列串行执行
117
+ log(`[MONITOR-HANDLER] 📋 NORMAL MODE: Enqueuing for messageKey=${messageKey}`);
118
+ void enqueue(sessionId, task).catch((err) => {
119
+ error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
120
+ activeMessages.delete(messageKey);
121
+ });
122
+ }
123
+ }
124
+ catch (parseErr) {
125
+ // 解析失败,回退到正常队列模式
126
+ error(`[MONITOR-HANDLER] Failed to parse message for steer detection: ${String(parseErr)}`);
127
+ void enqueue(sessionId, task).catch((err) => {
128
+ error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
129
+ activeMessages.delete(messageKey);
130
+ });
131
+ }
84
132
  };
85
133
  const connectedHandler = (serverId) => {
86
134
  if (!loggedServers.has(serverId)) {
87
135
  log(`XY gateway: ${serverId} connected`);
88
136
  loggedServers.add(serverId);
89
137
  }
138
+ // ✅ Report health: connection established
139
+ trackEvent?.();
140
+ opts.setStatus?.({ connected: true });
90
141
  };
91
142
  const disconnectedHandler = (serverId) => {
92
143
  console.warn(`XY gateway: ${serverId} disconnected`);
93
144
  loggedServers.delete(serverId);
145
+ // ✅ Report disconnection status (only if all servers disconnected)
146
+ if (loggedServers.size === 0) {
147
+ opts.setStatus?.({ connected: false });
148
+ }
94
149
  };
95
150
  const errorHandler = (err, serverId) => {
96
151
  error(`XY gateway: ${serverId} error: ${String(err)}`);
97
152
  };
98
- // Register event handlers
153
+ const cleanup = () => {
154
+ log("XY gateway: cleaning up...");
155
+ // 🔍 Diagnose before cleanup
156
+ console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
157
+ diagnoseAllManagers();
158
+ // Stop health check interval
159
+ if (healthCheckInterval) {
160
+ clearInterval(healthCheckInterval);
161
+ healthCheckInterval = null;
162
+ console.log("⏸️ Stopped periodic health check");
163
+ }
164
+ // Remove event handlers to prevent duplicate calls on gateway restart
165
+ wsManager.off("message", messageHandler);
166
+ wsManager.off("connected", connectedHandler);
167
+ wsManager.off("disconnected", disconnectedHandler);
168
+ wsManager.off("error", errorHandler);
169
+ // ✅ Disconnect the wsManager to prevent connection leaks
170
+ // This is safe because each gateway lifecycle should have clean connections
171
+ wsManager.disconnect();
172
+ // ✅ Remove manager from cache to prevent reusing dirty state
173
+ removeXYWebSocketManager(account);
174
+ loggedServers.clear();
175
+ activeMessages.clear();
176
+ log(`[MONITOR-HANDLER] 🧹 Cleanup complete, cleared active messages`);
177
+ // 🔍 Diagnose after cleanup
178
+ console.log("🔍 [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
179
+ diagnoseAllManagers();
180
+ };
181
+ const handleAbort = () => {
182
+ log("XY gateway: abort signal received, stopping");
183
+ cleanup();
184
+ log("XY gateway stopped");
185
+ resolve();
186
+ };
187
+ if (opts.abortSignal?.aborted) {
188
+ cleanup();
189
+ resolve();
190
+ return;
191
+ }
192
+ opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
193
+ // Register event handlers (handlers are defined above in cleanup scope)
99
194
  wsManager.on("message", messageHandler);
100
195
  wsManager.on("connected", connectedHandler);
101
196
  wsManager.on("disconnected", disconnectedHandler);
102
197
  wsManager.on("error", errorHandler);
198
+ // Start periodic health check (every 5 minutes)
199
+ console.log("🏥 Starting periodic health check (every 5 minutes)...");
200
+ healthCheckInterval = setInterval(() => {
201
+ console.log("🏥 [HEALTH CHECK] Periodic WebSocket diagnostics...");
202
+ diagnoseAllManagers();
203
+ // Auto-cleanup orphan connections
204
+ const cleaned = cleanupOrphanConnections();
205
+ if (cleaned > 0) {
206
+ console.log(`🧹 [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
207
+ }
208
+ }, 5 * 60 * 1000); // 5 minutes
103
209
  // Connect to WebSocket servers
104
210
  wsManager.connect()
105
211
  .then(() => {
@@ -1,10 +1,10 @@
1
- const channel = "xy";
1
+ const channel = "xiaoyi-channel";
2
2
  /**
3
3
  * Check if XY channel is properly configured with required fields
4
4
  */
5
5
  function isXYConfigured(cfg) {
6
6
  try {
7
- const xyConfig = cfg.channels?.xy;
7
+ const xyConfig = cfg.channels?.["xiaoyi-channel"];
8
8
  if (!xyConfig) {
9
9
  return false;
10
10
  }
@@ -26,7 +26,7 @@ function isXYConfigured(cfg) {
26
26
  */
27
27
  async function getStatus({ cfg }) {
28
28
  const configured = isXYConfigured(cfg);
29
- const xyConfig = cfg.channels?.xy;
29
+ const xyConfig = cfg.channels?.["xiaoyi-channel"];
30
30
  const statusLines = [];
31
31
  if (configured) {
32
32
  const wsUrl1 = xyConfig?.wsUrl1 || "ws://localhost:8765/ws/link";
@@ -49,7 +49,7 @@ async function getStatus({ cfg }) {
49
49
  */
50
50
  async function configure({ cfg, prompter, }) {
51
51
  // Note current configuration status
52
- const currentConfig = cfg.channels?.xy;
52
+ const currentConfig = cfg.channels?.["xiaoyi-channel"];
53
53
  const isUpdate = Boolean(currentConfig);
54
54
  await prompter.note([
55
55
  "XY Channel - 小艺 A2A 协议配置",
@@ -117,7 +117,7 @@ async function configure({ cfg, prompter, }) {
117
117
  ...cfg,
118
118
  channels: {
119
119
  ...cfg.channels,
120
- xy: {
120
+ "xiaoyi-channel": {
121
121
  enabled: true,
122
122
  wsUrl1: wsUrl1.trim(),
123
123
  wsUrl2: wsUrl2.trim(),
@@ -164,8 +164,8 @@ export const xyOnboardingAdapter = {
164
164
  ...cfg,
165
165
  channels: {
166
166
  ...cfg.channels,
167
- xy: {
168
- ...(cfg.channels?.xy || {}),
167
+ "xiaoyi-channel": {
168
+ ...(cfg.channels?.["xiaoyi-channel"] || {}),
169
169
  enabled: false,
170
170
  },
171
171
  },
@@ -1,8 +1,41 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
- import { XYFileUploadService } from "./file-upload.js";
3
2
  import { XYPushService } from "./push.js";
3
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
4
4
  // Special marker for default push delivery when no target is specified
5
5
  const DEFAULT_PUSH_MARKER = "default";
6
+ // File extension to MIME type mapping
7
+ const FILE_TYPE_TO_MIME_TYPE = {
8
+ txt: "text/plain",
9
+ html: "text/html",
10
+ css: "text/css",
11
+ js: "application/javascript",
12
+ json: "application/json",
13
+ png: "image/png",
14
+ jpeg: "image/jpeg",
15
+ jpg: "image/jpeg",
16
+ gif: "image/gif",
17
+ svg: "image/svg+xml",
18
+ pdf: "application/pdf",
19
+ zip: "application/zip",
20
+ doc: "application/msword",
21
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
22
+ xls: "application/vnd.ms-excel",
23
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
24
+ ppt: "application/vnd.ms-powerpoint",
25
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
26
+ mp3: "audio/mpeg",
27
+ mp4: "video/mp4",
28
+ };
29
+ /**
30
+ * Get MIME type from file extension
31
+ */
32
+ function getMimeTypeFromFilename(filename) {
33
+ const extension = filename.split(".").pop()?.toLowerCase();
34
+ if (extension && FILE_TYPE_TO_MIME_TYPE[extension]) {
35
+ return FILE_TYPE_TO_MIME_TYPE[extension];
36
+ }
37
+ return "text/plain"; // Default fallback
38
+ }
6
39
  /**
7
40
  * Outbound adapter for sending messages from OpenClaw to XY.
8
41
  * Uses Push service for direct message delivery.
@@ -14,6 +47,9 @@ export const xyOutbound = {
14
47
  * Resolve delivery target for XY channel.
15
48
  * When no target is specified (e.g., in cron jobs with announce mode),
16
49
  * returns a default marker that will be handled by sendText.
50
+ *
51
+ * For message tool calls, if only sessionId is provided, it will look up
52
+ * the active session context to construct the full "sessionId::taskId" format.
17
53
  */
18
54
  resolveTarget: ({ cfg, to, accountId, mode }) => {
19
55
  // If no target provided, use default marker for push delivery
@@ -24,11 +60,30 @@ export const xyOutbound = {
24
60
  to: DEFAULT_PUSH_MARKER,
25
61
  };
26
62
  }
27
- // Otherwise, use the provided target
28
- console.log(`[xyOutbound.resolveTarget] Using provided target:`, to);
63
+ const trimmedTo = to.trim();
64
+ // If the target doesn't contain "::", try to enhance it with taskId from session context
65
+ if (!trimmedTo.includes("::")) {
66
+ console.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
67
+ // Try to get the current session context
68
+ const sessionContext = getCurrentSessionContext();
69
+ if (sessionContext && sessionContext.sessionId === trimmedTo) {
70
+ const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
71
+ console.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
72
+ return {
73
+ ok: true,
74
+ to: enhancedTarget,
75
+ };
76
+ }
77
+ else {
78
+ console.log(`[xyOutbound.resolveTarget] Could not find matching session context for "${trimmedTo}"`);
79
+ // Still return the original target, but it may fail in sendMedia
80
+ }
81
+ }
82
+ // Otherwise, use the provided target (either already in correct format or for sendText)
83
+ console.log(`[xyOutbound.resolveTarget] Using provided target:`, trimmedTo);
29
84
  return {
30
85
  ok: true,
31
- to: to.trim(),
86
+ to: trimmedTo,
32
87
  };
33
88
  },
34
89
  sendText: async ({ cfg, to, text, accountId }) => {
@@ -53,12 +108,14 @@ export const xyOutbound = {
53
108
  const pushService = new XYPushService(config);
54
109
  // Extract title (first 57 chars or first line)
55
110
  const title = text.split("\n")[0].slice(0, 57);
56
- // Send push message
57
- await pushService.sendPush(text, title, actualTo);
111
+ // Truncate push content to max length 1000
112
+ const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
113
+ // Send push message (content, title, data, sessionId)
114
+ await pushService.sendPush(pushText, title, undefined, actualTo);
58
115
  console.log(`[xyOutbound.sendText] Completed successfully`);
59
116
  // Return message info
60
117
  return {
61
- channel: "xy",
118
+ channel: "xiaoyi-channel",
62
119
  messageId: Date.now().toString(),
63
120
  chatId: actualTo,
64
121
  };
@@ -72,72 +129,94 @@ export const xyOutbound = {
72
129
  mediaUrl,
73
130
  mediaLocalRoots,
74
131
  });
75
- // Parse to: "sessionId::taskId"
76
- const parts = to.split("::");
77
- if (parts.length !== 2) {
78
- throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
79
- }
80
- const [sessionId, taskId] = parts;
81
- // Resolve configuration
82
- const config = resolveXYConfig(cfg);
83
- // Create upload service
84
- const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
85
- // Validate mediaUrl
86
- if (!mediaUrl) {
87
- throw new Error("mediaUrl is required for sendMedia");
88
- }
89
- // Upload file
90
- const fileId = await uploadService.uploadFile(mediaUrl);
91
- console.log(`[xyOutbound.sendMedia] File uploaded:`, {
92
- fileId,
93
- sessionId,
94
- taskId,
95
- });
96
- // Get filename and mime type from mediaUrl
97
- // mediaUrl may be a local file path or URL
98
- const fileName = mediaUrl.split("/").pop() || "unknown";
99
- const mimeType = text?.match(/\[ MediaType: ([^\]]+)\]/)?.[1] || "application/octet-stream";
100
- // Build agent_response message
101
- const agentResponse = {
102
- msgType: "agent_response",
103
- agentId: config.agentId,
104
- sessionId: sessionId,
105
- taskId: taskId,
106
- msgDetail: JSON.stringify({
107
- jsonrpc: "2.0",
108
- id: taskId,
109
- result: {
110
- kind: "artifact-update",
111
- append: true,
112
- lastChunk: false,
113
- final: false,
114
- artifact: {
115
- artifactId: taskId,
116
- parts: [
117
- {
118
- kind: "file",
119
- file: {
120
- name: fileName,
121
- mimeType: mimeType,
122
- fileId: fileId,
123
- },
124
- },
125
- ],
126
- },
127
- },
128
- error: { code: 0 },
129
- }),
130
- };
131
- // Get WebSocket manager and send message
132
- const { getXYWebSocketManager } = await import("./client.js");
133
- const wsManager = getXYWebSocketManager(config);
134
- await wsManager.sendMessage(sessionId, agentResponse);
135
- console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
136
- // Return message info
132
+ // All sendMedia processing logic has been disabled
133
+ // Use send_file_to_user tool instead for file transfers to user device
134
+ console.log(`[xyOutbound.sendMedia] Processing disabled, use send_file_to_user tool`);
135
+ // Return empty message info
137
136
  return {
138
- channel: "xy",
139
- messageId: fileId,
137
+ channel: "xiaoyi-channel",
138
+ messageId: "",
140
139
  chatId: to,
141
140
  };
141
+ // // Parse to: "sessionId::taskId"
142
+ // const parts = to.split("::");
143
+ // if (parts.length !== 2) {
144
+ // throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
145
+ // }
146
+ // const [sessionId, taskId] = parts;
147
+ // // Resolve configuration
148
+ // const config = resolveXYConfig(cfg);
149
+ // // Create upload service
150
+ // const uploadService = new XYFileUploadService(
151
+ // config.fileUploadUrl,
152
+ // config.apiKey,
153
+ // config.uid
154
+ // );
155
+ // // Validate mediaUrl
156
+ // if (!mediaUrl) {
157
+ // throw new Error("mediaUrl is required for sendMedia");
158
+ // }
159
+ // // Upload file
160
+ // const fileId = await uploadService.uploadFile(mediaUrl);
161
+ // // Check if fileId is empty
162
+ // if (!fileId) {
163
+ // console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
164
+ // return {
165
+ // channel: "xiaoyi-channel",
166
+ // messageId: "",
167
+ // chatId: to,
168
+ // };
169
+ // }
170
+ // console.log(`[xyOutbound.sendMedia] File uploaded:`, {
171
+ // fileId,
172
+ // sessionId,
173
+ // taskId,
174
+ // });
175
+ // // Get filename and mime type from mediaUrl
176
+ // // mediaUrl may be a local file path or URL
177
+ // const fileName = mediaUrl.split("/").pop() || "unknown";
178
+ // const mimeType = getMimeTypeFromFilename(fileName);
179
+ // // Build agent_response message
180
+ // const agentResponse: OutboundWebSocketMessage = {
181
+ // msgType: "agent_response",
182
+ // agentId: config.agentId,
183
+ // sessionId: sessionId,
184
+ // taskId: taskId,
185
+ // msgDetail: JSON.stringify({
186
+ // jsonrpc: "2.0",
187
+ // id: taskId,
188
+ // result: {
189
+ // kind: "artifact-update",
190
+ // append: true,
191
+ // lastChunk: false,
192
+ // final: false,
193
+ // artifact: {
194
+ // artifactId: taskId,
195
+ // parts: [
196
+ // {
197
+ // kind: "file",
198
+ // file: {
199
+ // name: fileName,
200
+ // mimeType: mimeType,
201
+ // fileId: fileId,
202
+ // },
203
+ // },
204
+ // ],
205
+ // },
206
+ // },
207
+ // error: { code: 0 },
208
+ // }),
209
+ // };
210
+ // // Get WebSocket manager and send message
211
+ // const { getXYWebSocketManager } = await import("./client.js");
212
+ // const wsManager = getXYWebSocketManager(config);
213
+ // await wsManager.sendMessage(sessionId, agentResponse);
214
+ // console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
215
+ // // Return message info
216
+ // return {
217
+ // channel: "xiaoyi-channel",
218
+ // messageId: fileId,
219
+ // chatId: to,
220
+ // };
142
221
  },
143
222
  };
@@ -38,6 +38,11 @@ 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;
41
46
  /**
42
47
  * Validate A2A request structure.
43
48
  */
@@ -57,6 +57,21 @@ 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
+ }
60
75
  /**
61
76
  * Validate A2A request structure.
62
77
  */
@@ -5,11 +5,17 @@ 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.
11
17
  */
12
- sendPush(content: string, title: string, sessionId?: string): Promise<void>;
18
+ sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string): Promise<void>;
13
19
  /**
14
20
  * Send a push message with file attachments.
15
21
  */