@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.
- package/dist/src/bot.js +115 -35
- package/dist/src/channel.js +22 -1
- package/dist/src/client.d.ts +15 -0
- package/dist/src/client.js +97 -2
- package/dist/src/file-download.js +10 -1
- package/dist/src/file-upload.js +1 -1
- package/dist/src/formatter.d.ts +17 -0
- package/dist/src/formatter.js +86 -6
- package/dist/src/heartbeat.d.ts +2 -1
- package/dist/src/heartbeat.js +9 -1
- package/dist/src/monitor.d.ts +5 -0
- package/dist/src/monitor.js +131 -25
- package/dist/src/onboarding.js +7 -7
- package/dist/src/outbound.js +150 -71
- package/dist/src/parser.d.ts +5 -0
- package/dist/src/parser.js +15 -0
- package/dist/src/push.d.ts +7 -1
- package/dist/src/push.js +110 -19
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +210 -57
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/calendar-tool.d.ts +6 -0
- package/dist/src/tools/calendar-tool.js +167 -0
- package/dist/src/tools/call-phone-tool.d.ts +5 -0
- package/dist/src/tools/call-phone-tool.js +183 -0
- package/dist/src/tools/create-alarm-tool.d.ts +7 -0
- package/dist/src/tools/create-alarm-tool.js +444 -0
- package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
- package/dist/src/tools/delete-alarm-tool.js +238 -0
- package/dist/src/tools/location-tool.js +48 -9
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +474 -0
- package/dist/src/tools/modify-note-tool.d.ts +9 -0
- package/dist/src/tools/modify-note-tool.js +163 -0
- package/dist/src/tools/note-tool.d.ts +5 -0
- package/dist/src/tools/note-tool.js +146 -0
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +389 -0
- package/dist/src/tools/search-calendar-tool.d.ts +12 -0
- package/dist/src/tools/search-calendar-tool.js +259 -0
- package/dist/src/tools/search-contact-tool.d.ts +5 -0
- package/dist/src/tools/search-contact-tool.js +168 -0
- package/dist/src/tools/search-file-tool.d.ts +5 -0
- package/dist/src/tools/search-file-tool.js +185 -0
- package/dist/src/tools/search-message-tool.d.ts +5 -0
- package/dist/src/tools/search-message-tool.js +173 -0
- package/dist/src/tools/search-note-tool.d.ts +5 -0
- package/dist/src/tools/search-note-tool.js +130 -0
- package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
- package/dist/src/tools/search-photo-gallery-tool.js +184 -0
- package/dist/src/tools/search-photo-tool.d.ts +9 -0
- package/dist/src/tools/search-photo-tool.js +270 -0
- package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
- package/dist/src/tools/send-file-to-user-tool.js +318 -0
- package/dist/src/tools/send-message-tool.d.ts +5 -0
- package/dist/src/tools/send-message-tool.js +189 -0
- package/dist/src/tools/session-manager.d.ts +15 -0
- package/dist/src/tools/session-manager.js +126 -6
- package/dist/src/tools/upload-file-tool.d.ts +13 -0
- package/dist/src/tools/upload-file-tool.js +265 -0
- package/dist/src/tools/upload-photo-tool.d.ts +9 -0
- package/dist/src/tools/upload-photo-tool.js +223 -0
- package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
- package/dist/src/types.d.ts +5 -9
- package/dist/src/utils/config-manager.d.ts +26 -0
- package/dist/src/utils/config-manager.js +56 -0
- package/dist/src/websocket.d.ts +41 -0
- package/dist/src/websocket.js +192 -9
- package/package.json +1 -2
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getLatestSessionContext } from "./session-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* XY search photo tool - searches photos in user's gallery.
|
|
7
|
+
* Returns publicly accessible URLs of matching photos based on query description.
|
|
8
|
+
*
|
|
9
|
+
* This tool performs a two-step operation:
|
|
10
|
+
* 1. Search for photos using query description
|
|
11
|
+
* 2. Upload found photos to get publicly accessible URLs
|
|
12
|
+
*/
|
|
13
|
+
export const searchPhotoTool = {
|
|
14
|
+
name: "search_photo",
|
|
15
|
+
label: "Search Photo",
|
|
16
|
+
description: "搜索用户手机图库中的照片。根据图像描述语料检索匹配的照片,并返回照片的可公网访问URL。注意:操作超时时间为120秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
|
|
17
|
+
parameters: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
query: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "图像描述语料,用于检索匹配的照片(例如:'小狗的照片'、'带有键盘的图片'等)",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["query"],
|
|
26
|
+
},
|
|
27
|
+
async execute(toolCallId, params) {
|
|
28
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 🚀 Starting execution`);
|
|
29
|
+
logger.log(`[SEARCH_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
|
|
30
|
+
logger.log(`[SEARCH_PHOTO_TOOL] - params:`, JSON.stringify(params));
|
|
31
|
+
logger.log(`[SEARCH_PHOTO_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
32
|
+
// Validate parameters
|
|
33
|
+
if (!params.query) {
|
|
34
|
+
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Missing required parameter: query`);
|
|
35
|
+
throw new Error("Missing required parameter: query is required");
|
|
36
|
+
}
|
|
37
|
+
// Get session context
|
|
38
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 🔍 Attempting to get session context...`);
|
|
39
|
+
const sessionContext = getLatestSessionContext();
|
|
40
|
+
if (!sessionContext) {
|
|
41
|
+
logger.error(`[SEARCH_PHOTO_TOOL] ❌ FAILED: No active session found!`);
|
|
42
|
+
logger.error(`[SEARCH_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
|
|
43
|
+
throw new Error("No active XY session found. Search photo tool can only be used during an active conversation.");
|
|
44
|
+
}
|
|
45
|
+
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Session context found`);
|
|
46
|
+
logger.log(`[SEARCH_PHOTO_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
47
|
+
logger.log(`[SEARCH_PHOTO_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
48
|
+
logger.log(`[SEARCH_PHOTO_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
49
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
50
|
+
// Get WebSocket manager
|
|
51
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 🔌 Getting WebSocket manager...`);
|
|
52
|
+
const wsManager = getXYWebSocketManager(config);
|
|
53
|
+
logger.log(`[SEARCH_PHOTO_TOOL] ✅ WebSocket manager obtained`);
|
|
54
|
+
// Step 1: Search for photos
|
|
55
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📸 STEP 1: Searching for photos...`);
|
|
56
|
+
const mediaUris = await searchPhotos(wsManager, config, sessionId, taskId, messageId, params.query);
|
|
57
|
+
if (!mediaUris || mediaUris.length === 0) {
|
|
58
|
+
logger.warn(`[SEARCH_PHOTO_TOOL] ⚠️ No photos found for query: ${params.query}`);
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: "text",
|
|
63
|
+
text: JSON.stringify({ imageUrls: [], message: "未找到匹配的照片" }),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Found ${mediaUris.length} photos`);
|
|
69
|
+
logger.log(`[SEARCH_PHOTO_TOOL] - mediaUris:`, JSON.stringify(mediaUris));
|
|
70
|
+
// Step 2: Get public URLs for the photos
|
|
71
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 🌐 STEP 2: Getting public URLs for photos...`);
|
|
72
|
+
const imageUrls = await getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris);
|
|
73
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 🎉 Successfully retrieved ${imageUrls.length} photo URLs`);
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: JSON.stringify({ imageUrls }),
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Step 1: Search for photos using query description
|
|
86
|
+
* Returns array of mediaUri strings
|
|
87
|
+
*/
|
|
88
|
+
async function searchPhotos(wsManager, config, sessionId, taskId, messageId, query) {
|
|
89
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📦 Building SearchPhotoVideo command...`);
|
|
90
|
+
const command = {
|
|
91
|
+
header: {
|
|
92
|
+
namespace: "Common",
|
|
93
|
+
name: "Action",
|
|
94
|
+
},
|
|
95
|
+
payload: {
|
|
96
|
+
cardParam: {},
|
|
97
|
+
executeParam: {
|
|
98
|
+
executeMode: "background",
|
|
99
|
+
intentName: "SearchPhotoVideo",
|
|
100
|
+
bundleName: "com.huawei.hmos.aidispatchservice",
|
|
101
|
+
needUnlock: true,
|
|
102
|
+
actionResponse: true,
|
|
103
|
+
appType: "OHOS_APP",
|
|
104
|
+
timeOut: 5,
|
|
105
|
+
intentParam: {
|
|
106
|
+
query: query,
|
|
107
|
+
},
|
|
108
|
+
permissionId: [],
|
|
109
|
+
achieveType: "INTENT",
|
|
110
|
+
},
|
|
111
|
+
responses: [
|
|
112
|
+
{
|
|
113
|
+
resultCode: "",
|
|
114
|
+
displayText: "",
|
|
115
|
+
ttsText: "",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
needUploadResult: true,
|
|
119
|
+
noHalfPage: false,
|
|
120
|
+
pageControlRelated: false,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const timeout = setTimeout(() => {
|
|
125
|
+
logger.error(`[SEARCH_PHOTO_TOOL] ⏰ Timeout: No response for SearchPhotoVideo within 60 seconds`);
|
|
126
|
+
wsManager.off("data-event", handler);
|
|
127
|
+
reject(new Error("搜索照片超时(60秒)"));
|
|
128
|
+
}, 60000);
|
|
129
|
+
const handler = (event) => {
|
|
130
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📨 Received data event (Step 1):`, JSON.stringify(event));
|
|
131
|
+
if (event.intentName === "SearchPhotoVideo") {
|
|
132
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 🎯 SearchPhotoVideo event received`);
|
|
133
|
+
logger.log(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
134
|
+
clearTimeout(timeout);
|
|
135
|
+
wsManager.off("data-event", handler);
|
|
136
|
+
if (event.status === "success" && event.outputs) {
|
|
137
|
+
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Photo search completed successfully`);
|
|
138
|
+
const result = event.outputs.result;
|
|
139
|
+
const items = result?.items || [];
|
|
140
|
+
// Extract mediaUri from each item
|
|
141
|
+
const mediaUris = items.map((item) => item.mediaUri).filter(Boolean);
|
|
142
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📊 Extracted ${mediaUris.length} mediaUris`);
|
|
143
|
+
resolve(mediaUris);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Photo search failed`);
|
|
147
|
+
logger.error(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
148
|
+
reject(new Error(`搜索照片失败: ${event.status}`));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📡 Registering data-event handler for SearchPhotoVideo`);
|
|
153
|
+
wsManager.on("data-event", handler);
|
|
154
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📤 Sending SearchPhotoVideo command...`);
|
|
155
|
+
sendCommand({
|
|
156
|
+
config,
|
|
157
|
+
sessionId,
|
|
158
|
+
taskId,
|
|
159
|
+
messageId,
|
|
160
|
+
command,
|
|
161
|
+
})
|
|
162
|
+
.then(() => {
|
|
163
|
+
logger.log(`[SEARCH_PHOTO_TOOL] ✅ SearchPhotoVideo command sent successfully`);
|
|
164
|
+
})
|
|
165
|
+
.catch((error) => {
|
|
166
|
+
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Failed to send SearchPhotoVideo command:`, error);
|
|
167
|
+
clearTimeout(timeout);
|
|
168
|
+
wsManager.off("data-event", handler);
|
|
169
|
+
reject(error);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Step 2: Get public URLs for photos using mediaUris
|
|
175
|
+
* Returns array of publicly accessible image URLs
|
|
176
|
+
*/
|
|
177
|
+
async function getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris) {
|
|
178
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📦 Building ImageUploadForClaw command...`);
|
|
179
|
+
// Build imageInfos array from mediaUris
|
|
180
|
+
const imageInfos = mediaUris.map(mediaUri => ({ mediaUri }));
|
|
181
|
+
const command = {
|
|
182
|
+
header: {
|
|
183
|
+
namespace: "Common",
|
|
184
|
+
name: "Action",
|
|
185
|
+
},
|
|
186
|
+
payload: {
|
|
187
|
+
cardParam: {},
|
|
188
|
+
executeParam: {
|
|
189
|
+
executeMode: "background",
|
|
190
|
+
intentName: "ImageUploadForClaw",
|
|
191
|
+
bundleName: "com.huawei.hmos.vassistant",
|
|
192
|
+
needUnlock: true,
|
|
193
|
+
actionResponse: true,
|
|
194
|
+
appType: "OHOS_APP",
|
|
195
|
+
timeOut: 5,
|
|
196
|
+
intentParam: {
|
|
197
|
+
imageInfos: imageInfos,
|
|
198
|
+
},
|
|
199
|
+
permissionId: [],
|
|
200
|
+
achieveType: "INTENT",
|
|
201
|
+
},
|
|
202
|
+
responses: [
|
|
203
|
+
{
|
|
204
|
+
resultCode: "",
|
|
205
|
+
displayText: "",
|
|
206
|
+
ttsText: "",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
needUploadResult: true,
|
|
210
|
+
noHalfPage: false,
|
|
211
|
+
pageControlRelated: false,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const timeout = setTimeout(() => {
|
|
216
|
+
logger.error(`[SEARCH_PHOTO_TOOL] ⏰ Timeout: No response for ImageUploadForClaw within 60 seconds`);
|
|
217
|
+
wsManager.off("data-event", handler);
|
|
218
|
+
reject(new Error("获取照片URL超时(60秒)"));
|
|
219
|
+
}, 60000);
|
|
220
|
+
const handler = (event) => {
|
|
221
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📨 Received data event (Step 2):`, JSON.stringify(event));
|
|
222
|
+
if (event.intentName === "ImageUploadForClaw") {
|
|
223
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 🎯 ImageUploadForClaw event received`);
|
|
224
|
+
logger.log(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
wsManager.off("data-event", handler);
|
|
227
|
+
if (event.status === "success" && event.outputs) {
|
|
228
|
+
logger.log(`[SEARCH_PHOTO_TOOL] ✅ Image URL retrieval completed successfully`);
|
|
229
|
+
const result = event.outputs.result;
|
|
230
|
+
let imageUrls = result?.imageUrls || [];
|
|
231
|
+
// Decode Unicode escape sequences in URLs
|
|
232
|
+
// Replace \u003d with = and \u0026 with &
|
|
233
|
+
imageUrls = imageUrls.map((url) => {
|
|
234
|
+
const decodedUrl = url
|
|
235
|
+
.replace(/\\u003d/g, '=')
|
|
236
|
+
.replace(/\\u0026/g, '&');
|
|
237
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 🔄 Decoded URL: ${url} -> ${decodedUrl}`);
|
|
238
|
+
return decodedUrl;
|
|
239
|
+
});
|
|
240
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📊 Retrieved and decoded ${imageUrls.length} image URLs`);
|
|
241
|
+
resolve(imageUrls);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Image URL retrieval failed`);
|
|
245
|
+
logger.error(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
|
|
246
|
+
reject(new Error(`获取照片URL失败: ${event.status}`));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📡 Registering data-event handler for ImageUploadForClaw`);
|
|
251
|
+
wsManager.on("data-event", handler);
|
|
252
|
+
logger.log(`[SEARCH_PHOTO_TOOL] 📤 Sending ImageUploadForClaw command...`);
|
|
253
|
+
sendCommand({
|
|
254
|
+
config,
|
|
255
|
+
sessionId,
|
|
256
|
+
taskId,
|
|
257
|
+
messageId,
|
|
258
|
+
command,
|
|
259
|
+
})
|
|
260
|
+
.then(() => {
|
|
261
|
+
logger.log(`[SEARCH_PHOTO_TOOL] ✅ ImageUploadForClaw command sent successfully`);
|
|
262
|
+
})
|
|
263
|
+
.catch((error) => {
|
|
264
|
+
logger.error(`[SEARCH_PHOTO_TOOL] ❌ Failed to send ImageUploadForClaw command:`, error);
|
|
265
|
+
clearTimeout(timeout);
|
|
266
|
+
wsManager.off("data-event", handler);
|
|
267
|
+
reject(error);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
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. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如果超时或失败,最多重试一次`,
|
|
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
|
+
// Set timeout for the entire operation (2 minutes)
|
|
129
|
+
const TOOL_TIMEOUT = 120000; // 2 minutes in milliseconds
|
|
130
|
+
let timeoutHandle = null;
|
|
131
|
+
// Create a timeout promise
|
|
132
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
133
|
+
timeoutHandle = setTimeout(() => {
|
|
134
|
+
reject(new Error("操作超时(2分钟)"));
|
|
135
|
+
}, TOOL_TIMEOUT);
|
|
136
|
+
});
|
|
137
|
+
// Create the main execution promise
|
|
138
|
+
const executionPromise = (async () => {
|
|
139
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 🚀 Starting execution`);
|
|
140
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] - toolCallId: ${toolCallId}`);
|
|
141
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] - params (raw):`, JSON.stringify(params));
|
|
142
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
143
|
+
// Validate that at least one parameter is provided
|
|
144
|
+
if (!params.fileLocalUrls && !params.fileRemoteUrls) {
|
|
145
|
+
logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ Missing both fileLocalUrls and fileRemoteUrls parameters`);
|
|
146
|
+
throw new Error("At least one of fileLocalUrls or fileRemoteUrls must be provided");
|
|
147
|
+
}
|
|
148
|
+
// Normalize fileLocalUrls parameter
|
|
149
|
+
let fileLocalUrls = [];
|
|
150
|
+
if (params.fileLocalUrls) {
|
|
151
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 🔄 Normalizing fileLocalUrls parameter...`);
|
|
152
|
+
fileLocalUrls = normalizeToArray(params.fileLocalUrls);
|
|
153
|
+
if (fileLocalUrls.length === 0) {
|
|
154
|
+
logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ fileLocalUrls array is empty`);
|
|
155
|
+
throw new Error("fileLocalUrls array cannot be empty");
|
|
156
|
+
}
|
|
157
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Normalized fileLocalUrls:`, JSON.stringify(fileLocalUrls));
|
|
158
|
+
}
|
|
159
|
+
// Normalize fileRemoteUrls parameter
|
|
160
|
+
let fileRemoteUrls = [];
|
|
161
|
+
if (params.fileRemoteUrls) {
|
|
162
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 🔄 Normalizing fileRemoteUrls parameter...`);
|
|
163
|
+
fileRemoteUrls = normalizeToArray(params.fileRemoteUrls);
|
|
164
|
+
if (fileRemoteUrls.length === 0) {
|
|
165
|
+
logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ fileRemoteUrls array is empty`);
|
|
166
|
+
throw new Error("fileRemoteUrls array cannot be empty");
|
|
167
|
+
}
|
|
168
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Normalized fileRemoteUrls:`, JSON.stringify(fileRemoteUrls));
|
|
169
|
+
}
|
|
170
|
+
// Get session context
|
|
171
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 🔍 Attempting to get session context...`);
|
|
172
|
+
const sessionContext = getCurrentSessionContext();
|
|
173
|
+
if (!sessionContext) {
|
|
174
|
+
logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ FAILED: No active session found!`);
|
|
175
|
+
logger.error(`[SEND_FILE_TO_USER_TOOL] - toolCallId: ${toolCallId}`);
|
|
176
|
+
throw new Error("No active XY session found. Send file to user tool can only be used during an active conversation.");
|
|
177
|
+
}
|
|
178
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Session context found`);
|
|
179
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
180
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
181
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
182
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
183
|
+
// Get WebSocket manager
|
|
184
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 🔌 Getting WebSocket manager...`);
|
|
185
|
+
const wsManager = getXYWebSocketManager(config);
|
|
186
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ WebSocket manager obtained`);
|
|
187
|
+
// Create upload service
|
|
188
|
+
const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
|
|
189
|
+
// Collect all local file paths to upload
|
|
190
|
+
const allLocalPaths = [...fileLocalUrls];
|
|
191
|
+
const downloadedFiles = [];
|
|
192
|
+
// Download remote files to local temp directory
|
|
193
|
+
if (fileRemoteUrls.length > 0) {
|
|
194
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 📥 Downloading ${fileRemoteUrls.length} remote files...`);
|
|
195
|
+
for (let i = 0; i < fileRemoteUrls.length; i++) {
|
|
196
|
+
const remoteUrl = fileRemoteUrls[i];
|
|
197
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 📥 Downloading remote file ${i + 1}/${fileRemoteUrls.length}: ${remoteUrl}`);
|
|
198
|
+
try {
|
|
199
|
+
const localPath = await downloadRemoteFile(remoteUrl);
|
|
200
|
+
allLocalPaths.push(localPath);
|
|
201
|
+
downloadedFiles.push(localPath);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ Failed to download file ${i + 1}:`, error);
|
|
205
|
+
throw new Error(`Failed to download remote file ${remoteUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Downloaded ${downloadedFiles.length} remote files`);
|
|
209
|
+
}
|
|
210
|
+
// Upload all local files and get fileIds
|
|
211
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 📤 Uploading ${allLocalPaths.length} files...`);
|
|
212
|
+
const uploadedFiles = [];
|
|
213
|
+
for (let i = 0; i < allLocalPaths.length; i++) {
|
|
214
|
+
const localPath = allLocalPaths[i];
|
|
215
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 📤 Uploading file ${i + 1}/${allLocalPaths.length}: ${localPath}`);
|
|
216
|
+
try {
|
|
217
|
+
// Upload file using three-phase upload
|
|
218
|
+
const fileId = await uploadService.uploadFile(localPath);
|
|
219
|
+
if (!fileId) {
|
|
220
|
+
logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ Failed to upload file: ${localPath} (fileId is empty)`);
|
|
221
|
+
throw new Error(`Failed to upload file: ${localPath}`);
|
|
222
|
+
}
|
|
223
|
+
// Get filename and mime type
|
|
224
|
+
const fileName = localPath.split("/").pop() || "unknown";
|
|
225
|
+
const mimeType = getMimeTypeFromFilename(fileName);
|
|
226
|
+
uploadedFiles.push({ fileName, fileId, mimeType });
|
|
227
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ File uploaded successfully: ${fileName} -> ${fileId}`);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
logger.error(`[SEND_FILE_TO_USER_TOOL] ❌ Failed to upload file ${i + 1}:`, error);
|
|
231
|
+
throw new Error(`Failed to upload file ${localPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Clean up downloaded files
|
|
235
|
+
if (downloadedFiles.length > 0) {
|
|
236
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 🧹 Cleaning up ${downloadedFiles.length} downloaded files...`);
|
|
237
|
+
for (const downloadedFile of downloadedFiles) {
|
|
238
|
+
try {
|
|
239
|
+
await fs.unlink(downloadedFile);
|
|
240
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Cleaned up: ${downloadedFile}`);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
logger.warn(`[SEND_FILE_TO_USER_TOOL] ⚠️ Failed to clean up file ${downloadedFile}:`, error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Build and send agent_response messages for each file
|
|
248
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 📦 Building and sending agent_response messages...`);
|
|
249
|
+
const sentFiles = [];
|
|
250
|
+
for (const uploadedFile of uploadedFiles) {
|
|
251
|
+
const { fileName, fileId, mimeType } = uploadedFile;
|
|
252
|
+
const agentResponse = {
|
|
253
|
+
msgType: "agent_response",
|
|
254
|
+
agentId: config.agentId,
|
|
255
|
+
sessionId: sessionId,
|
|
256
|
+
taskId: taskId,
|
|
257
|
+
msgDetail: JSON.stringify({
|
|
258
|
+
jsonrpc: "2.0",
|
|
259
|
+
id: taskId,
|
|
260
|
+
result: {
|
|
261
|
+
kind: "artifact-update",
|
|
262
|
+
append: true,
|
|
263
|
+
lastChunk: false,
|
|
264
|
+
final: false,
|
|
265
|
+
artifact: {
|
|
266
|
+
artifactId: taskId,
|
|
267
|
+
parts: [
|
|
268
|
+
{
|
|
269
|
+
kind: "file",
|
|
270
|
+
file: {
|
|
271
|
+
name: fileName,
|
|
272
|
+
mimeType: mimeType,
|
|
273
|
+
fileId: fileId,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
error: { code: 0 },
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
// Send WebSocket message
|
|
283
|
+
await wsManager.sendMessage(sessionId, agentResponse);
|
|
284
|
+
sentFiles.push({ fileName, fileId });
|
|
285
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Sent file to user: ${fileName} (fileId: ${fileId})`);
|
|
286
|
+
}
|
|
287
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] 🎉 Successfully sent ${sentFiles.length} files to user`);
|
|
288
|
+
return {
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: "text",
|
|
292
|
+
text: JSON.stringify({
|
|
293
|
+
sentFiles,
|
|
294
|
+
count: sentFiles.length,
|
|
295
|
+
message: `成功发送 ${sentFiles.length} 个文件到用户设备`
|
|
296
|
+
}),
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
})();
|
|
301
|
+
// Race between execution and timeout
|
|
302
|
+
try {
|
|
303
|
+
const result = await Promise.race([executionPromise, timeoutPromise]);
|
|
304
|
+
// Clear timeout if execution completed
|
|
305
|
+
if (timeoutHandle) {
|
|
306
|
+
clearTimeout(timeoutHandle);
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
// Clear timeout on error
|
|
312
|
+
if (timeoutHandle) {
|
|
313
|
+
clearTimeout(timeoutHandle);
|
|
314
|
+
}
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
};
|