@ynhcj/xiaoyi-channel 0.0.26-beta → 0.0.28-beta

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 (36) hide show
  1. package/dist/src/bot.js +13 -3
  2. package/dist/src/channel.js +11 -1
  3. package/dist/src/outbound.js +88 -76
  4. package/dist/src/tools/calendar-tool.js +2 -2
  5. package/dist/src/tools/call-phone-tool.d.ts +5 -0
  6. package/dist/src/tools/call-phone-tool.js +183 -0
  7. package/dist/src/tools/create-alarm-tool.d.ts +7 -0
  8. package/dist/src/tools/create-alarm-tool.js +444 -0
  9. package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
  10. package/dist/src/tools/delete-alarm-tool.js +238 -0
  11. package/dist/src/tools/location-tool.js +2 -2
  12. package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
  13. package/dist/src/tools/modify-alarm-tool.js +474 -0
  14. package/dist/src/tools/modify-note-tool.js +2 -2
  15. package/dist/src/tools/note-tool.js +2 -2
  16. package/dist/src/tools/search-alarm-tool.d.ts +8 -0
  17. package/dist/src/tools/search-alarm-tool.js +389 -0
  18. package/dist/src/tools/search-calendar-tool.js +2 -2
  19. package/dist/src/tools/search-contact-tool.js +2 -2
  20. package/dist/src/tools/search-file-tool.d.ts +5 -0
  21. package/dist/src/tools/search-file-tool.js +173 -0
  22. package/dist/src/tools/search-message-tool.d.ts +5 -0
  23. package/dist/src/tools/search-message-tool.js +173 -0
  24. package/dist/src/tools/search-note-tool.js +2 -2
  25. package/dist/src/tools/search-photo-gallery-tool.js +2 -2
  26. package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
  27. package/dist/src/tools/send-file-to-user-tool.js +290 -0
  28. package/dist/src/tools/send-message-tool.d.ts +5 -0
  29. package/dist/src/tools/send-message-tool.js +189 -0
  30. package/dist/src/tools/session-manager.d.ts +12 -0
  31. package/dist/src/tools/session-manager.js +33 -0
  32. package/dist/src/tools/upload-file-tool.d.ts +13 -0
  33. package/dist/src/tools/upload-file-tool.js +265 -0
  34. package/dist/src/tools/upload-photo-tool.js +2 -2
  35. package/dist/src/tools/xiaoyi-gui-tool.js +2 -2
  36. package/package.json +1 -1
@@ -0,0 +1,173 @@
1
+ import { getXYWebSocketManager } from "../client.js";
2
+ import { sendCommand } from "../formatter.js";
3
+ import { getCurrentSessionContext } from "./session-manager.js";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * XY search message tool - searches SMS messages on user's device.
7
+ * Returns matching messages based on content keyword search.
8
+ */
9
+ export const searchMessageTool = {
10
+ name: "search_message",
11
+ label: "Search Message",
12
+ description: "搜索手机短信。根据关键词搜索短信内容。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ content: {
17
+ type: "string",
18
+ description: "搜索关键词,用于在短信内容中进行匹配",
19
+ },
20
+ },
21
+ required: ["content"],
22
+ },
23
+ async execute(toolCallId, params) {
24
+ logger.log(`[SEARCH_MESSAGE_TOOL] 🚀 Starting execution`);
25
+ logger.log(`[SEARCH_MESSAGE_TOOL] - toolCallId: ${toolCallId}`);
26
+ logger.log(`[SEARCH_MESSAGE_TOOL] - params:`, JSON.stringify(params));
27
+ logger.log(`[SEARCH_MESSAGE_TOOL] - timestamp: ${new Date().toISOString()}`);
28
+ // Validate content parameter
29
+ if (!params.content || typeof params.content !== "string" || params.content.trim() === "") {
30
+ logger.error(`[SEARCH_MESSAGE_TOOL] ❌ Missing or invalid content parameter`);
31
+ throw new Error("Missing required parameter: content must be a non-empty string");
32
+ }
33
+ logger.log(`[SEARCH_MESSAGE_TOOL] 🔍 Searching for messages with keyword: ${params.content}`);
34
+ // Get session context
35
+ logger.log(`[SEARCH_MESSAGE_TOOL] 🔍 Attempting to get session context...`);
36
+ const sessionContext = getCurrentSessionContext();
37
+ if (!sessionContext) {
38
+ logger.error(`[SEARCH_MESSAGE_TOOL] ❌ FAILED: No active session found!`);
39
+ logger.error(`[SEARCH_MESSAGE_TOOL] - toolCallId: ${toolCallId}`);
40
+ throw new Error("No active XY session found. Search message tool can only be used during an active conversation.");
41
+ }
42
+ logger.log(`[SEARCH_MESSAGE_TOOL] ✅ Session context found`);
43
+ logger.log(`[SEARCH_MESSAGE_TOOL] - sessionId: ${sessionContext.sessionId}`);
44
+ logger.log(`[SEARCH_MESSAGE_TOOL] - taskId: ${sessionContext.taskId}`);
45
+ logger.log(`[SEARCH_MESSAGE_TOOL] - messageId: ${sessionContext.messageId}`);
46
+ const { config, sessionId, taskId, messageId } = sessionContext;
47
+ // Get WebSocket manager
48
+ logger.log(`[SEARCH_MESSAGE_TOOL] 🔌 Getting WebSocket manager...`);
49
+ const wsManager = getXYWebSocketManager(config);
50
+ logger.log(`[SEARCH_MESSAGE_TOOL] ✅ WebSocket manager obtained`);
51
+ // Build SearchMessage command
52
+ logger.log(`[SEARCH_MESSAGE_TOOL] 📦 Building SearchMessage command...`);
53
+ const command = {
54
+ header: {
55
+ namespace: "Common",
56
+ name: "Action",
57
+ },
58
+ payload: {
59
+ cardParam: {},
60
+ executeParam: {
61
+ executeMode: "background",
62
+ intentName: "SearchMessage",
63
+ bundleName: "com.huawei.hmos.aidispatchservice",
64
+ needUnlock: true,
65
+ actionResponse: true,
66
+ appType: "OHOS_APP",
67
+ timeOut: 5,
68
+ intentParam: {
69
+ content: params.content.trim(),
70
+ },
71
+ permissionId: [],
72
+ achieveType: "INTENT",
73
+ },
74
+ responses: [
75
+ {
76
+ resultCode: "",
77
+ displayText: "",
78
+ ttsText: "",
79
+ },
80
+ ],
81
+ needUploadResult: true,
82
+ noHalfPage: false,
83
+ pageControlRelated: false,
84
+ },
85
+ };
86
+ logger.log(`[SEARCH_MESSAGE_TOOL] 📋 Command details:`, JSON.stringify(command, null, 2));
87
+ // Send command and wait for response (60 second timeout)
88
+ logger.log(`[SEARCH_MESSAGE_TOOL] ⏳ Setting up promise to wait for message search response...`);
89
+ logger.log(`[SEARCH_MESSAGE_TOOL] - Timeout: 60 seconds`);
90
+ return new Promise((resolve, reject) => {
91
+ const timeout = setTimeout(() => {
92
+ logger.error(`[SEARCH_MESSAGE_TOOL] ⏰ Timeout: No response received within 60 seconds`);
93
+ wsManager.off("data-event", handler);
94
+ reject(new Error("搜索短信超时(60秒)"));
95
+ }, 60000);
96
+ // Listen for data events from WebSocket
97
+ const handler = (event) => {
98
+ logger.log(`[SEARCH_MESSAGE_TOOL] 📨 Received data event:`, JSON.stringify(event));
99
+ if (event.intentName === "SearchMessage") {
100
+ logger.log(`[SEARCH_MESSAGE_TOOL] 🎯 SearchMessage event received`);
101
+ logger.log(`[SEARCH_MESSAGE_TOOL] - status: ${event.status}`);
102
+ clearTimeout(timeout);
103
+ wsManager.off("data-event", handler);
104
+ if (event.status === "success" && event.outputs) {
105
+ logger.log(`[SEARCH_MESSAGE_TOOL] ✅ Message search response received`);
106
+ logger.log(`[SEARCH_MESSAGE_TOOL] - outputs:`, JSON.stringify(event.outputs));
107
+ // Check for error code in outputs
108
+ const code = event.outputs.code !== undefined ? event.outputs.code : null;
109
+ if (code !== null && code !== 0) {
110
+ logger.error(`[SEARCH_MESSAGE_TOOL] ❌ Device returned error`);
111
+ logger.error(`[SEARCH_MESSAGE_TOOL] - code: ${code}`);
112
+ const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
113
+ logger.error(`[SEARCH_MESSAGE_TOOL] - errorMsg: ${errorMsg}`);
114
+ reject(new Error(`搜索短信失败: ${errorMsg} (错误代码: ${code})`));
115
+ return;
116
+ }
117
+ // Extract result.items with safe checks
118
+ const result = event.outputs.result;
119
+ let items = [];
120
+ if (result && typeof result === "object" && Array.isArray(result.items)) {
121
+ items = result.items;
122
+ logger.log(`[SEARCH_MESSAGE_TOOL] 📋 Found ${items.length} message(s)`);
123
+ }
124
+ else {
125
+ logger.warn(`[SEARCH_MESSAGE_TOOL] ⚠️ No items found in result or result is invalid`);
126
+ logger.warn(`[SEARCH_MESSAGE_TOOL] - result:`, JSON.stringify(result || {}));
127
+ }
128
+ // Return items array as JSON string
129
+ logger.log(`[SEARCH_MESSAGE_TOOL] 🎉 Message search completed successfully`);
130
+ logger.log(`[SEARCH_MESSAGE_TOOL] - keyword: ${params.content}`);
131
+ logger.log(`[SEARCH_MESSAGE_TOOL] - result count: ${items.length}`);
132
+ resolve({
133
+ content: [
134
+ {
135
+ type: "text",
136
+ text: JSON.stringify(items),
137
+ },
138
+ ],
139
+ });
140
+ }
141
+ else {
142
+ logger.error(`[SEARCH_MESSAGE_TOOL] ❌ Message search failed`);
143
+ logger.error(`[SEARCH_MESSAGE_TOOL] - status: ${event.status}`);
144
+ logger.error(`[SEARCH_MESSAGE_TOOL] - outputs:`, JSON.stringify(event.outputs || {}));
145
+ const errorDetail = event.outputs ? JSON.stringify(event.outputs) : event.status;
146
+ reject(new Error(`搜索短信失败: ${errorDetail}`));
147
+ }
148
+ }
149
+ };
150
+ // Register event handler
151
+ logger.log(`[SEARCH_MESSAGE_TOOL] 📡 Registering data-event handler on WebSocket manager`);
152
+ wsManager.on("data-event", handler);
153
+ // Send the command
154
+ logger.log(`[SEARCH_MESSAGE_TOOL] 📤 Sending SearchMessage command...`);
155
+ sendCommand({
156
+ config,
157
+ sessionId,
158
+ taskId,
159
+ messageId,
160
+ command,
161
+ })
162
+ .then(() => {
163
+ logger.log(`[SEARCH_MESSAGE_TOOL] ✅ Command sent successfully, waiting for response...`);
164
+ })
165
+ .catch((error) => {
166
+ logger.error(`[SEARCH_MESSAGE_TOOL] ❌ Failed to send command:`, error);
167
+ clearTimeout(timeout);
168
+ wsManager.off("data-event", handler);
169
+ reject(error);
170
+ });
171
+ });
172
+ },
173
+ };
@@ -1,6 +1,6 @@
1
1
  import { getXYWebSocketManager } from "../client.js";
2
2
  import { sendCommand } from "../formatter.js";
3
- import { getLatestSessionContext } from "./session-manager.js";
3
+ import { getCurrentSessionContext } from "./session-manager.js";
4
4
  import { logger } from "../utils/logger.js";
5
5
  /**
6
6
  * XY search note tool - searches notes on user's device.
@@ -27,7 +27,7 @@ export const searchNoteTool = {
27
27
  throw new Error("Missing required parameter: query is required");
28
28
  }
29
29
  // Get session context
30
- const sessionContext = getLatestSessionContext();
30
+ const sessionContext = getCurrentSessionContext();
31
31
  if (!sessionContext) {
32
32
  throw new Error("No active XY session found. Search note tool can only be used during an active conversation.");
33
33
  }
@@ -1,6 +1,6 @@
1
1
  import { getXYWebSocketManager } from "../client.js";
2
2
  import { sendCommand } from "../formatter.js";
3
- import { getLatestSessionContext } from "./session-manager.js";
3
+ import { getCurrentSessionContext } from "./session-manager.js";
4
4
  import { logger } from "../utils/logger.js";
5
5
  /**
6
6
  * XY search photo gallery tool - searches photos in user's gallery.
@@ -46,7 +46,7 @@ export const searchPhotoGalleryTool = {
46
46
  }
47
47
  // Get session context
48
48
  logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 🔍 Attempting to get session context...`);
49
- const sessionContext = getLatestSessionContext();
49
+ const sessionContext = getCurrentSessionContext();
50
50
  if (!sessionContext) {
51
51
  logger.error(`[SEARCH_PHOTO_GALLERY_TOOL] ❌ FAILED: No active session found!`);
52
52
  logger.error(`[SEARCH_PHOTO_GALLERY_TOOL] - toolCallId: ${toolCallId}`);
@@ -0,0 +1,5 @@
1
+ /**
2
+ * XY send file to user tool - sends local files or remote files to user's device.
3
+ * Supports both local file paths and remote URLs.
4
+ */
5
+ export declare const sendFileToUserTool: any;
@@ -0,0 +1,290 @@
1
+ import { getXYWebSocketManager } from "../client.js";
2
+ import { XYFileUploadService } from "../file-upload.js";
3
+ import { getCurrentSessionContext } from "./session-manager.js";
4
+ import { logger } from "../utils/logger.js";
5
+ import fetch from "node-fetch";
6
+ import fs from "fs/promises";
7
+ import path from "path";
8
+ /**
9
+ * File extension to MIME type mapping
10
+ */
11
+ const FILE_TYPE_TO_MIME_TYPE = {
12
+ txt: "text/plain",
13
+ html: "text/html",
14
+ css: "text/css",
15
+ js: "application/javascript",
16
+ json: "application/json",
17
+ png: "image/png",
18
+ jpeg: "image/jpeg",
19
+ jpg: "image/jpeg",
20
+ gif: "image/gif",
21
+ svg: "image/svg+xml",
22
+ pdf: "application/pdf",
23
+ zip: "application/zip",
24
+ doc: "application/msword",
25
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
26
+ xls: "application/vnd.ms-excel",
27
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
28
+ ppt: "application/vnd.ms-powerpoint",
29
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
30
+ mp3: "audio/mpeg",
31
+ mp4: "video/mp4",
32
+ };
33
+ /**
34
+ * Get MIME type from file extension
35
+ */
36
+ function getMimeTypeFromFilename(filename) {
37
+ const extension = filename.split(".").pop()?.toLowerCase();
38
+ if (extension && FILE_TYPE_TO_MIME_TYPE[extension]) {
39
+ return FILE_TYPE_TO_MIME_TYPE[extension];
40
+ }
41
+ return "text/plain";
42
+ }
43
+ /**
44
+ * Normalize parameter to array (supports both array and JSON string)
45
+ */
46
+ function normalizeToArray(param) {
47
+ if (Array.isArray(param)) {
48
+ return param;
49
+ }
50
+ if (typeof param === 'string') {
51
+ try {
52
+ const parsed = JSON.parse(param);
53
+ if (Array.isArray(parsed)) {
54
+ return parsed;
55
+ }
56
+ throw new Error("Parameter must be an array or a JSON string representing an array");
57
+ }
58
+ catch (parseError) {
59
+ throw new Error(`Parameter must be a valid JSON array string. Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
60
+ }
61
+ }
62
+ throw new Error(`Parameter must be an array or a JSON string, got ${typeof param}`);
63
+ }
64
+ /**
65
+ * Download remote file to local temp directory
66
+ */
67
+ async function downloadRemoteFile(url) {
68
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 📥 Downloading remote file: ${url}`);
69
+ try {
70
+ const response = await fetch(url);
71
+ if (!response.ok) {
72
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
73
+ }
74
+ // Get filename from URL or use default
75
+ let filename = url.split("/").pop() || "downloaded_file";
76
+ // Remove query parameters if present
77
+ filename = filename.split("?")[0];
78
+ // Ensure temp directory exists
79
+ const tempDir = "/tmp/xy_channel";
80
+ await fs.mkdir(tempDir, { recursive: true });
81
+ // Generate unique filename to avoid conflicts
82
+ const timestamp = Date.now();
83
+ const ext = path.extname(filename);
84
+ const baseName = path.basename(filename, ext);
85
+ const uniqueFilename = `${baseName}_${timestamp}${ext}`;
86
+ const localPath = path.join(tempDir, uniqueFilename);
87
+ // Save file to local temp directory
88
+ const arrayBuffer = await response.arrayBuffer();
89
+ const buffer = Buffer.from(arrayBuffer);
90
+ await fs.writeFile(localPath, buffer);
91
+ logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ File downloaded to: ${localPath}`);
92
+ return localPath;
93
+ }
94
+ catch (error) {
95
+ logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ Failed to download file from ${url}:`, error);
96
+ throw new Error(`Failed to download remote file: ${error instanceof Error ? error.message : String(error)}`);
97
+ }
98
+ }
99
+ /**
100
+ * XY send file to user tool - sends local files or remote files to user's device.
101
+ * Supports both local file paths and remote URLs.
102
+ */
103
+ export const sendFileToUserTool = {
104
+ name: "send_file_to_user",
105
+ label: "Send File to User",
106
+ description: `工具能力描述:帮助用户把本地的文件或者公网地址的文件传到手机。
107
+
108
+ 工具参数说明:
109
+ a. fileLocalUrls:本地文件路径数组,包含用户需要回传的文件在本地的地址
110
+ b. fileRemoteUrls:公网地址数组,包含用户需要回传的文件的公网地址(会先下载到本地再发送)
111
+ c. fileLocalUrls 与 fileRemoteUrls 任意一个不为空即可,两者都提供时都会处理
112
+
113
+ 注意事项:
114
+ a. 支持传入数组或 JSON 字符串格式
115
+ b. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次`,
116
+ parameters: {
117
+ type: "object",
118
+ properties: {
119
+ fileLocalUrls: {
120
+ description: "本地文件路径数组,包含用户需要回传的文件在本地的地址",
121
+ },
122
+ fileRemoteUrls: {
123
+ description: "公网地址数组,包含用户需要回传的文件的公网地址(会先下载到本地再发送)",
124
+ },
125
+ },
126
+ },
127
+ async execute(toolCallId, params) {
128
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 🚀 Starting execution`);
129
+ logger.log(`[SEND_FILE_TO_USER_TOOL] - toolCallId: ${toolCallId}`);
130
+ logger.log(`[SEND_FILE_TO_USER_TOOL] - params (raw):`, JSON.stringify(params));
131
+ logger.log(`[SEND_FILE_TO_USER_TOOL] - timestamp: ${new Date().toISOString()}`);
132
+ // Validate that at least one parameter is provided
133
+ if (!params.fileLocalUrls && !params.fileRemoteUrls) {
134
+ logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ Missing both fileLocalUrls and fileRemoteUrls parameters`);
135
+ throw new Error("At least one of fileLocalUrls or fileRemoteUrls must be provided");
136
+ }
137
+ // Normalize fileLocalUrls parameter
138
+ let fileLocalUrls = [];
139
+ if (params.fileLocalUrls) {
140
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 🔄 Normalizing fileLocalUrls parameter...`);
141
+ fileLocalUrls = normalizeToArray(params.fileLocalUrls);
142
+ if (fileLocalUrls.length === 0) {
143
+ logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ fileLocalUrls array is empty`);
144
+ throw new Error("fileLocalUrls array cannot be empty");
145
+ }
146
+ logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Normalized fileLocalUrls:`, JSON.stringify(fileLocalUrls));
147
+ }
148
+ // Normalize fileRemoteUrls parameter
149
+ let fileRemoteUrls = [];
150
+ if (params.fileRemoteUrls) {
151
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 🔄 Normalizing fileRemoteUrls parameter...`);
152
+ fileRemoteUrls = normalizeToArray(params.fileRemoteUrls);
153
+ if (fileRemoteUrls.length === 0) {
154
+ logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ fileRemoteUrls array is empty`);
155
+ throw new Error("fileRemoteUrls array cannot be empty");
156
+ }
157
+ logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Normalized fileRemoteUrls:`, JSON.stringify(fileRemoteUrls));
158
+ }
159
+ // Get session context
160
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 🔍 Attempting to get session context...`);
161
+ const sessionContext = getCurrentSessionContext();
162
+ if (!sessionContext) {
163
+ logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ FAILED: No active session found!`);
164
+ logger.error(`[SEND_FILE_TO_USER_TOOL] - toolCallId: ${toolCallId}`);
165
+ throw new Error("No active XY session found. Send file to user tool can only be used during an active conversation.");
166
+ }
167
+ logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Session context found`);
168
+ logger.log(`[SEND_FILE_TO_USER_TOOL] - sessionId: ${sessionContext.sessionId}`);
169
+ logger.log(`[SEND_FILE_TO_USER_TOOL] - taskId: ${sessionContext.taskId}`);
170
+ logger.log(`[SEND_FILE_TO_USER_TOOL] - messageId: ${sessionContext.messageId}`);
171
+ const { config, sessionId, taskId, messageId } = sessionContext;
172
+ // Get WebSocket manager
173
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 🔌 Getting WebSocket manager...`);
174
+ const wsManager = getXYWebSocketManager(config);
175
+ logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ WebSocket manager obtained`);
176
+ // Create upload service
177
+ const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
178
+ // Collect all local file paths to upload
179
+ const allLocalPaths = [...fileLocalUrls];
180
+ const downloadedFiles = [];
181
+ // Download remote files to local temp directory
182
+ if (fileRemoteUrls.length > 0) {
183
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 📥 Downloading ${fileRemoteUrls.length} remote files...`);
184
+ for (let i = 0; i < fileRemoteUrls.length; i++) {
185
+ const remoteUrl = fileRemoteUrls[i];
186
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 📥 Downloading remote file ${i + 1}/${fileRemoteUrls.length}: ${remoteUrl}`);
187
+ try {
188
+ const localPath = await downloadRemoteFile(remoteUrl);
189
+ allLocalPaths.push(localPath);
190
+ downloadedFiles.push(localPath);
191
+ }
192
+ catch (error) {
193
+ logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ Failed to download file ${i + 1}:`, error);
194
+ throw new Error(`Failed to download remote file ${remoteUrl}: ${error instanceof Error ? error.message : String(error)}`);
195
+ }
196
+ }
197
+ logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Downloaded ${downloadedFiles.length} remote files`);
198
+ }
199
+ // Upload all local files and get fileIds
200
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 📤 Uploading ${allLocalPaths.length} files...`);
201
+ const uploadedFiles = [];
202
+ for (let i = 0; i < allLocalPaths.length; i++) {
203
+ const localPath = allLocalPaths[i];
204
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 📤 Uploading file ${i + 1}/${allLocalPaths.length}: ${localPath}`);
205
+ try {
206
+ // Upload file using three-phase upload
207
+ const fileId = await uploadService.uploadFile(localPath);
208
+ if (!fileId) {
209
+ logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ Failed to upload file: ${localPath} (fileId is empty)`);
210
+ throw new Error(`Failed to upload file: ${localPath}`);
211
+ }
212
+ // Get filename and mime type
213
+ const fileName = localPath.split("/").pop() || "unknown";
214
+ const mimeType = getMimeTypeFromFilename(fileName);
215
+ uploadedFiles.push({ fileName, fileId, mimeType });
216
+ logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ File uploaded successfully: ${fileName} -> ${fileId}`);
217
+ }
218
+ catch (error) {
219
+ logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ Failed to upload file ${i + 1}:`, error);
220
+ throw new Error(`Failed to upload file ${localPath}: ${error instanceof Error ? error.message : String(error)}`);
221
+ }
222
+ }
223
+ // Clean up downloaded files
224
+ if (downloadedFiles.length > 0) {
225
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 🧹 Cleaning up ${downloadedFiles.length} downloaded files...`);
226
+ for (const downloadedFile of downloadedFiles) {
227
+ try {
228
+ await fs.unlink(downloadedFile);
229
+ logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Cleaned up: ${downloadedFile}`);
230
+ }
231
+ catch (error) {
232
+ logger.warn(`[SEND_FILE_TO_USER_TOOL] ⚠️ Failed to clean up file ${downloadedFile}:`, error);
233
+ }
234
+ }
235
+ }
236
+ // Build and send agent_response messages for each file
237
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 📦 Building and sending agent_response messages...`);
238
+ const sentFiles = [];
239
+ for (const uploadedFile of uploadedFiles) {
240
+ const { fileName, fileId, mimeType } = uploadedFile;
241
+ const agentResponse = {
242
+ msgType: "agent_response",
243
+ agentId: config.agentId,
244
+ sessionId: sessionId,
245
+ taskId: taskId,
246
+ msgDetail: JSON.stringify({
247
+ jsonrpc: "2.0",
248
+ id: taskId,
249
+ result: {
250
+ kind: "artifact-update",
251
+ append: true,
252
+ lastChunk: false,
253
+ final: false,
254
+ artifact: {
255
+ artifactId: taskId,
256
+ parts: [
257
+ {
258
+ kind: "file",
259
+ file: {
260
+ name: fileName,
261
+ mimeType: mimeType,
262
+ fileId: fileId,
263
+ },
264
+ },
265
+ ],
266
+ },
267
+ },
268
+ error: { code: 0 },
269
+ }),
270
+ };
271
+ // Send WebSocket message
272
+ await wsManager.sendMessage(sessionId, agentResponse);
273
+ sentFiles.push({ fileName, fileId });
274
+ logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Sent file to user: ${fileName} (fileId: ${fileId})`);
275
+ }
276
+ logger.log(`[SEND_FILE_TO_USER_TOOL] 🎉 Successfully sent ${sentFiles.length} files to user`);
277
+ return {
278
+ content: [
279
+ {
280
+ type: "text",
281
+ text: JSON.stringify({
282
+ sentFiles,
283
+ count: sentFiles.length,
284
+ message: `成功发送 ${sentFiles.length} 个文件到用户设备`
285
+ }),
286
+ },
287
+ ],
288
+ };
289
+ },
290
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * XY send message tool - sends SMS message on user's device.
3
+ * Requires phoneNumber (with +86 prefix) and content parameters.
4
+ */
5
+ export declare const sendMessageTool: any;