@ynhcj/xiaoyi-channel 0.0.2-beta → 0.0.2-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 (56) hide show
  1. package/dist/src/bot.js +79 -31
  2. package/dist/src/channel.js +18 -2
  3. package/dist/src/file-download.js +10 -1
  4. package/dist/src/formatter.d.ts +17 -0
  5. package/dist/src/formatter.js +43 -0
  6. package/dist/src/monitor.js +35 -5
  7. package/dist/src/outbound.js +91 -77
  8. package/dist/src/push.js +2 -2
  9. package/dist/src/reply-dispatcher.d.ts +1 -0
  10. package/dist/src/reply-dispatcher.js +206 -51
  11. package/dist/src/task-manager.d.ts +55 -0
  12. package/dist/src/task-manager.js +136 -0
  13. package/dist/src/tools/calendar-tool.js +9 -9
  14. package/dist/src/tools/call-phone-tool.d.ts +5 -0
  15. package/dist/src/tools/call-phone-tool.js +183 -0
  16. package/dist/src/tools/create-alarm-tool.d.ts +7 -0
  17. package/dist/src/tools/create-alarm-tool.js +444 -0
  18. package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
  19. package/dist/src/tools/delete-alarm-tool.js +238 -0
  20. package/dist/src/tools/location-tool.js +8 -8
  21. package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
  22. package/dist/src/tools/modify-alarm-tool.js +474 -0
  23. package/dist/src/tools/modify-note-tool.d.ts +9 -0
  24. package/dist/src/tools/modify-note-tool.js +163 -0
  25. package/dist/src/tools/note-tool.js +25 -9
  26. package/dist/src/tools/search-alarm-tool.d.ts +8 -0
  27. package/dist/src/tools/search-alarm-tool.js +389 -0
  28. package/dist/src/tools/search-calendar-tool.d.ts +12 -0
  29. package/dist/src/tools/search-calendar-tool.js +259 -0
  30. package/dist/src/tools/search-contact-tool.d.ts +5 -0
  31. package/dist/src/tools/search-contact-tool.js +168 -0
  32. package/dist/src/tools/search-file-tool.d.ts +5 -0
  33. package/dist/src/tools/search-file-tool.js +185 -0
  34. package/dist/src/tools/search-message-tool.d.ts +5 -0
  35. package/dist/src/tools/search-message-tool.js +173 -0
  36. package/dist/src/tools/search-note-tool.js +6 -6
  37. package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
  38. package/dist/src/tools/search-photo-gallery-tool.js +184 -0
  39. package/dist/src/tools/search-photo-tool.d.ts +9 -0
  40. package/dist/src/tools/search-photo-tool.js +270 -0
  41. package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
  42. package/dist/src/tools/send-file-to-user-tool.js +318 -0
  43. package/dist/src/tools/send-message-tool.d.ts +5 -0
  44. package/dist/src/tools/send-message-tool.js +189 -0
  45. package/dist/src/tools/session-manager.d.ts +15 -0
  46. package/dist/src/tools/session-manager.js +99 -18
  47. package/dist/src/tools/upload-file-tool.d.ts +13 -0
  48. package/dist/src/tools/upload-file-tool.js +265 -0
  49. package/dist/src/tools/upload-photo-tool.d.ts +9 -0
  50. package/dist/src/tools/upload-photo-tool.js +223 -0
  51. package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
  52. package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
  53. package/dist/src/types.d.ts +5 -1
  54. package/dist/src/websocket.d.ts +1 -0
  55. package/dist/src/websocket.js +13 -1
  56. package/package.json +1 -1
package/dist/src/bot.js CHANGED
@@ -4,8 +4,9 @@ import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId
4
4
  import { downloadFilesFromParts } from "./file-download.js";
5
5
  import { resolveXYConfig } from "./config.js";
6
6
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
7
- import { registerSession, unregisterSession } from "./tools/session-manager.js";
7
+ import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
8
8
  import { configManager } from "./utils/config-manager.js";
9
+ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
9
10
  /**
10
11
  * Handle an incoming A2A message.
11
12
  * This is the main entry point for message processing.
@@ -56,6 +57,22 @@ export async function handleXYMessage(params) {
56
57
  }
57
58
  // Parse the A2A message (for regular messages)
58
59
  const parsed = parseA2AMessage(message);
60
+ // 🔑 检测steer模式和是否是第二条消息
61
+ const isSteerMode = cfg.messages?.queue?.mode === "steer";
62
+ const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
63
+ if (isSecondMessage) {
64
+ log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
65
+ log(`[BOT] - Session: ${parsed.sessionId}`);
66
+ log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
67
+ }
68
+ // 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
69
+ const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
70
+ );
71
+ // 🔑 如果是第一条消息,锁定taskId防止被过早清理
72
+ if (!isUpdate) {
73
+ lockTaskId(parsed.sessionId);
74
+ log(`[BOT] 🔒 Locked taskId for first message`);
75
+ }
59
76
  // Extract and update push_id if present
60
77
  const pushId = extractPushId(parsed.parts);
61
78
  if (pushId) {
@@ -83,11 +100,12 @@ export async function handleXYMessage(params) {
83
100
  },
84
101
  });
85
102
  log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
86
- // Register session context for tools
103
+ // 🔑 注册session(带引用计数)
87
104
  log(`[BOT] 📝 About to register session for tools...`);
88
105
  log(`[BOT] - sessionKey: ${route.sessionKey}`);
89
106
  log(`[BOT] - sessionId: ${parsed.sessionId}`);
90
107
  log(`[BOT] - taskId: ${parsed.taskId}`);
108
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
91
109
  registerSession(route.sessionKey, {
92
110
  config,
93
111
  sessionId: parsed.sessionId,
@@ -96,6 +114,18 @@ export async function handleXYMessage(params) {
96
114
  agentId: route.accountId,
97
115
  });
98
116
  log(`[BOT] ✅ Session registered for tools`);
117
+ // 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
118
+ log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
119
+ void sendStatusUpdate({
120
+ config,
121
+ sessionId: parsed.sessionId,
122
+ taskId: parsed.taskId,
123
+ messageId: parsed.messageId,
124
+ text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍后~",
125
+ state: "working",
126
+ }).catch((err) => {
127
+ error(`Failed to send initial status update:`, err);
128
+ });
99
129
  // Extract text and files from parts
100
130
  const text = extractTextFromParts(parsed.parts);
101
131
  const fileParts = extractFileParts(parsed.parts);
@@ -143,51 +173,66 @@ export async function handleXYMessage(params) {
143
173
  ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
144
174
  ...mediaPayload,
145
175
  });
146
- // Send initial status update immediately after parsing message
147
- log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
148
- void sendStatusUpdate({
149
- config,
150
- sessionId: parsed.sessionId,
151
- taskId: parsed.taskId,
152
- messageId: parsed.messageId,
153
- text: "任务正在处理中,请稍后~",
154
- state: "working",
155
- }).catch((err) => {
156
- error(`Failed to send initial status update:`, err);
157
- });
158
- // Create reply dispatcher (following feishu pattern)
159
- log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher for session=${parsed.sessionId}, taskId=${parsed.taskId}, messageId=${parsed.messageId}`);
176
+ // 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
177
+ log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
178
+ log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
179
+ log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
180
+ log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
160
181
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
161
182
  cfg,
162
183
  runtime,
163
184
  sessionId: parsed.sessionId,
164
185
  taskId: parsed.taskId,
165
186
  messageId: parsed.messageId,
166
- accountId: route.accountId, // ✅ Use route.accountId
187
+ accountId: route.accountId,
188
+ isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
167
189
  });
168
190
  log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
169
- // Start status update interval (will send updates every 60 seconds)
170
- // Interval will be automatically stopped when onIdle/onCleanup is triggered
171
- startStatusInterval();
191
+ // 🔑 只有第一条消息启动状态定时器
192
+ // 第二条消息会很快返回,不需要定时器
193
+ if (!isSecondMessage) {
194
+ startStatusInterval();
195
+ log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
196
+ }
197
+ else {
198
+ log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
199
+ }
172
200
  log(`xy: dispatching to agent (session=${parsed.sessionId})`);
173
201
  // Dispatch to OpenClaw core using correct API (following feishu pattern)
174
202
  log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
203
+ // Build session context for AsyncLocalStorage
204
+ const sessionContext = {
205
+ config,
206
+ sessionId: parsed.sessionId,
207
+ taskId: parsed.taskId,
208
+ messageId: parsed.messageId,
209
+ agentId: route.accountId,
210
+ };
175
211
  await core.channel.reply.withReplyDispatcher({
176
212
  dispatcher,
177
213
  onSettled: () => {
178
214
  log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
179
- log(`[BOT] - About to unregister session...`);
215
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
180
216
  markDispatchIdle();
181
- // Unregister session context when done
217
+ // 🔑 减少引用计数
218
+ decrementTaskIdRef(parsed.sessionId);
219
+ // 🔑 如果是第一条消息完成,解锁
220
+ if (!isSecondMessage) {
221
+ unlockTaskId(parsed.sessionId);
222
+ log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
223
+ }
224
+ // 减少session引用计数
182
225
  unregisterSession(route.sessionKey);
183
- log(`[BOT] ✅ Session unregistered in onSettled`);
226
+ log(`[BOT] ✅ Cleanup completed`);
184
227
  },
185
- run: () => core.channel.reply.dispatchReplyFromConfig({
228
+ run: () =>
229
+ // 🔐 Use AsyncLocalStorage to provide session context to tools
230
+ runWithSessionContext(sessionContext, () => core.channel.reply.dispatchReplyFromConfig({
186
231
  ctx: ctxPayload,
187
232
  cfg,
188
233
  dispatcher,
189
234
  replyOptions,
190
- }),
235
+ })),
191
236
  });
192
237
  log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
193
238
  log(`xy: dispatch complete (session=${parsed.sessionId})`);
@@ -197,25 +242,28 @@ export async function handleXYMessage(params) {
197
242
  error("Failed to handle XY message:", err);
198
243
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
199
244
  log(`[BOT] ❌ Error occurred, attempting cleanup...`);
200
- // Try to unregister session on error (if route was established)
245
+ // 🔑 错误时也要清理taskId和session
201
246
  try {
202
- const core = getXYRuntime();
203
247
  const params = message.params;
204
248
  const sessionId = params?.sessionId;
205
249
  if (sessionId) {
206
- log(`[BOT] 🧹 Cleaning up session after error: ${sessionId}`);
250
+ log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
251
+ // 清理 taskId
252
+ decrementTaskIdRef(sessionId);
253
+ unlockTaskId(sessionId);
254
+ // 清理 session
255
+ const core = getXYRuntime();
207
256
  const route = core.channel.routing.resolveAgentRoute({
208
257
  cfg,
209
258
  channel: "xiaoyi-channel",
210
259
  accountId,
211
260
  peer: {
212
261
  kind: "direct",
213
- id: sessionId, // ✅ Use sessionId for cleanup consistency
262
+ id: sessionId,
214
263
  },
215
264
  });
216
- log(`[BOT] - Unregistering session: ${route.sessionKey}`);
217
265
  unregisterSession(route.sessionKey);
218
- log(`[BOT] ✅ Session unregistered after error`);
266
+ log(`[BOT] ✅ Cleanup completed after error`);
219
267
  }
220
268
  }
221
269
  catch (cleanupErr) {
@@ -5,7 +5,23 @@ 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";
8
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 { sendMessageTool } from "./tools/send-message-tool.js";
24
+ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
9
25
  /**
10
26
  * Xiaoyi Channel Plugin for OpenClaw.
11
27
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -23,7 +39,7 @@ export const xyPlugin = {
23
39
  agentPrompt: {
24
40
  messageToolHints: () => [
25
41
  "- xiaoyi targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `default`",
26
- "- sendMedia requires a text reply"
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."
27
43
  ],
28
44
  },
29
45
  capabilities: {
@@ -45,7 +61,7 @@ export const xyPlugin = {
45
61
  },
46
62
  outbound: xyOutbound,
47
63
  onboarding: xyOnboardingAdapter,
48
- agentTools: [locationTool, noteTool, searchNoteTool, calendarTool],
64
+ agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendMessageTool, sendFileToUserTool], // searchContactTool 已暂时禁用
49
65
  messaging: {
50
66
  normalizeTarget: (raw) => {
51
67
  const trimmed = raw.trim();
@@ -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
  */
@@ -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.
@@ -1,6 +1,8 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
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.
@@ -94,11 +96,39 @@ export async function monitorXYProvider(opts = {}) {
94
96
  log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
95
97
  }
96
98
  };
97
- void enqueue(sessionId, task).catch((err) => {
98
- // Error already logged in task, this is for queue failures
99
- error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
100
- activeMessages.delete(messageKey);
101
- });
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
+ }
102
132
  };
103
133
  const connectedHandler = (serverId) => {
104
134
  if (!loggedServers.has(serverId)) {
@@ -1,7 +1,6 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
- import { XYFileUploadService } from "./file-upload.js";
3
2
  import { XYPushService } from "./push.js";
4
- import { getLatestSessionContext } from "./tools/session-manager.js";
3
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
5
4
  // Special marker for default push delivery when no target is specified
6
5
  const DEFAULT_PUSH_MARKER = "default";
7
6
  // File extension to MIME type mapping
@@ -65,8 +64,8 @@ export const xyOutbound = {
65
64
  // If the target doesn't contain "::", try to enhance it with taskId from session context
66
65
  if (!trimmedTo.includes("::")) {
67
66
  console.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
68
- // Try to get the latest session context
69
- const sessionContext = getLatestSessionContext();
67
+ // Try to get the current session context
68
+ const sessionContext = getCurrentSessionContext();
70
69
  if (sessionContext && sessionContext.sessionId === trimmedTo) {
71
70
  const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
72
71
  console.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
@@ -109,8 +108,10 @@ export const xyOutbound = {
109
108
  const pushService = new XYPushService(config);
110
109
  // Extract title (first 57 chars or first line)
111
110
  const title = text.split("\n")[0].slice(0, 57);
111
+ // Truncate push content to max length 1000
112
+ const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
112
113
  // Send push message (content, title, data, sessionId)
113
- await pushService.sendPush(text, title, undefined, actualTo);
114
+ await pushService.sendPush(pushText, title, undefined, actualTo);
114
115
  console.log(`[xyOutbound.sendText] Completed successfully`);
115
116
  // Return message info
116
117
  return {
@@ -128,81 +129,94 @@ export const xyOutbound = {
128
129
  mediaUrl,
129
130
  mediaLocalRoots,
130
131
  });
131
- // Parse to: "sessionId::taskId"
132
- const parts = to.split("::");
133
- if (parts.length !== 2) {
134
- throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
135
- }
136
- const [sessionId, taskId] = parts;
137
- // Resolve configuration
138
- const config = resolveXYConfig(cfg);
139
- // Create upload service
140
- const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
141
- // Validate mediaUrl
142
- if (!mediaUrl) {
143
- throw new Error("mediaUrl is required for sendMedia");
144
- }
145
- // Upload file
146
- const fileId = await uploadService.uploadFile(mediaUrl);
147
- // Check if fileId is empty
148
- if (!fileId) {
149
- console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
150
- return {
151
- channel: "xiaoyi-channel",
152
- messageId: "",
153
- chatId: to,
154
- };
155
- }
156
- console.log(`[xyOutbound.sendMedia] File uploaded:`, {
157
- fileId,
158
- sessionId,
159
- taskId,
160
- });
161
- // Get filename and mime type from mediaUrl
162
- // mediaUrl may be a local file path or URL
163
- const fileName = mediaUrl.split("/").pop() || "unknown";
164
- const mimeType = getMimeTypeFromFilename(fileName);
165
- // Build agent_response message
166
- const agentResponse = {
167
- msgType: "agent_response",
168
- agentId: config.agentId,
169
- sessionId: sessionId,
170
- taskId: taskId,
171
- msgDetail: JSON.stringify({
172
- jsonrpc: "2.0",
173
- id: taskId,
174
- result: {
175
- kind: "artifact-update",
176
- append: true,
177
- lastChunk: false,
178
- final: false,
179
- artifact: {
180
- artifactId: taskId,
181
- parts: [
182
- {
183
- kind: "file",
184
- file: {
185
- name: fileName,
186
- mimeType: mimeType,
187
- fileId: fileId,
188
- },
189
- },
190
- ],
191
- },
192
- },
193
- error: { code: 0 },
194
- }),
195
- };
196
- // Get WebSocket manager and send message
197
- const { getXYWebSocketManager } = await import("./client.js");
198
- const wsManager = getXYWebSocketManager(config);
199
- await wsManager.sendMessage(sessionId, agentResponse);
200
- console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
201
- // 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
202
136
  return {
203
137
  channel: "xiaoyi-channel",
204
- messageId: fileId,
138
+ messageId: "",
205
139
  chatId: to,
206
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
+ // };
207
221
  },
208
222
  };
package/dist/src/push.js CHANGED
@@ -59,8 +59,8 @@ export class XYPushService {
59
59
  artifactId: randomUUID(),
60
60
  parts: [
61
61
  {
62
- kind: "data",
63
- data: data || { content },
62
+ kind: "text",
63
+ text: content,
64
64
  },
65
65
  ],
66
66
  },
@@ -6,6 +6,7 @@ export interface CreateXYReplyDispatcherParams {
6
6
  taskId: string;
7
7
  messageId: string;
8
8
  accountId: string;
9
+ isSteerFollower?: boolean;
9
10
  }
10
11
  /**
11
12
  * Create a reply dispatcher for XY channel messages.