@ynhcj/xiaoyi-channel 0.0.28-beta → 0.0.29-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.
|
@@ -9,7 +9,19 @@ import { logger } from "../utils/logger.js";
|
|
|
9
9
|
export const searchFileTool = {
|
|
10
10
|
name: "search_file",
|
|
11
11
|
label: "Search File",
|
|
12
|
-
description:
|
|
12
|
+
description: `搜索手机文件系统的文件。
|
|
13
|
+
|
|
14
|
+
【重要】使用约束:此工具仅在用户显著说明要从手机搜索时才执行,例如:
|
|
15
|
+
- "从我手机里面搜索xxxx"
|
|
16
|
+
- "从手机文件系统找一下xxxx"
|
|
17
|
+
- "在手机上查找文件xxxx"
|
|
18
|
+
- "搜索手机里的文件"
|
|
19
|
+
|
|
20
|
+
如果用户没有明确说明从手机搜索(如仅说"搜索文件"、"找一下xxxx"),应默认从 openclaw 本地的文件系统查询,不要调用此工具。
|
|
21
|
+
|
|
22
|
+
功能说明:根据关键词搜索文件名称或内容,返回匹配的文件列表(包括文件名、路径、大小、修改时间等信息)。
|
|
23
|
+
|
|
24
|
+
注意事项:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。`,
|
|
13
25
|
parameters: {
|
|
14
26
|
type: "object",
|
|
15
27
|
properties: {
|
|
@@ -112,7 +112,7 @@ c. fileLocalUrls 与 fileRemoteUrls 任意一个不为空即可,两者都提
|
|
|
112
112
|
|
|
113
113
|
注意事项:
|
|
114
114
|
a. 支持传入数组或 JSON 字符串格式
|
|
115
|
-
b. 操作超时时间为
|
|
115
|
+
b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如果超时或失败,最多重试一次`,
|
|
116
116
|
parameters: {
|
|
117
117
|
type: "object",
|
|
118
118
|
properties: {
|
|
@@ -125,166 +125,194 @@ b. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失
|
|
|
125
125
|
},
|
|
126
126
|
},
|
|
127
127
|
async execute(toolCallId, params) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
logger.log(`[SEND_FILE_TO_USER_TOOL]
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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");
|
|
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");
|
|
156
147
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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)}`);
|
|
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");
|
|
195
156
|
}
|
|
157
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Normalized fileLocalUrls:`, JSON.stringify(fileLocalUrls));
|
|
196
158
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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}`);
|
|
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");
|
|
211
167
|
}
|
|
212
|
-
|
|
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}`);
|
|
168
|
+
logger.log(`[SEND_FILE_TO_USER_TOOL] ✅ Normalized fileRemoteUrls:`, JSON.stringify(fileRemoteUrls));
|
|
217
169
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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.");
|
|
221
177
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
logger.log(`[SEND_FILE_TO_USER_TOOL]
|
|
226
|
-
|
|
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}`);
|
|
227
216
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
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}`);
|
|
230
228
|
}
|
|
231
229
|
catch (error) {
|
|
232
|
-
logger.
|
|
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)}`);
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
},
|
|
263
275
|
},
|
|
264
|
-
|
|
265
|
-
|
|
276
|
+
],
|
|
277
|
+
},
|
|
266
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
|
+
}),
|
|
267
297
|
},
|
|
268
|
-
|
|
269
|
-
}),
|
|
298
|
+
],
|
|
270
299
|
};
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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;
|
|
275
316
|
}
|
|
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
317
|
},
|
|
290
318
|
};
|