@ynhcj/xiaoyi-channel 0.0.27-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.
- package/dist/src/channel.js +2 -1
- package/dist/src/outbound.js +88 -76
- package/dist/src/tools/create-alarm-tool.js +74 -25
- package/dist/src/tools/modify-alarm-tool.js +60 -19
- package/dist/src/tools/search-file-tool.js +13 -1
- 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/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -21,6 +21,7 @@ import { searchAlarmTool } from "./tools/search-alarm-tool.js";
|
|
|
21
21
|
import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
|
|
22
22
|
import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
|
|
23
23
|
import { sendMessageTool } from "./tools/send-message-tool.js";
|
|
24
|
+
import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
|
|
24
25
|
/**
|
|
25
26
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
26
27
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -60,7 +61,7 @@ export const xyPlugin = {
|
|
|
60
61
|
},
|
|
61
62
|
outbound: xyOutbound,
|
|
62
63
|
onboarding: xyOnboardingAdapter,
|
|
63
|
-
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendMessageTool], // searchContactTool 已暂时禁用
|
|
64
|
+
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendMessageTool, sendFileToUserTool], // searchContactTool 已暂时禁用
|
|
64
65
|
messaging: {
|
|
65
66
|
normalizeTarget: (raw) => {
|
|
66
67
|
const trimmed = raw.trim();
|
package/dist/src/outbound.js
CHANGED
|
@@ -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 {
|
|
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
|
|
69
|
-
const sessionContext =
|
|
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}`);
|
|
@@ -130,81 +129,94 @@ export const xyOutbound = {
|
|
|
130
129
|
mediaUrl,
|
|
131
130
|
mediaLocalRoots,
|
|
132
131
|
});
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
const [sessionId, taskId] = parts;
|
|
139
|
-
// Resolve configuration
|
|
140
|
-
const config = resolveXYConfig(cfg);
|
|
141
|
-
// Create upload service
|
|
142
|
-
const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
|
|
143
|
-
// Validate mediaUrl
|
|
144
|
-
if (!mediaUrl) {
|
|
145
|
-
throw new Error("mediaUrl is required for sendMedia");
|
|
146
|
-
}
|
|
147
|
-
// Upload file
|
|
148
|
-
const fileId = await uploadService.uploadFile(mediaUrl);
|
|
149
|
-
// Check if fileId is empty
|
|
150
|
-
if (!fileId) {
|
|
151
|
-
console.log(`[xyOutbound.sendMedia] ⚠️ File upload failed: fileId is empty, aborting sendMedia`);
|
|
152
|
-
return {
|
|
153
|
-
channel: "xiaoyi-channel",
|
|
154
|
-
messageId: "",
|
|
155
|
-
chatId: to,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
console.log(`[xyOutbound.sendMedia] File uploaded:`, {
|
|
159
|
-
fileId,
|
|
160
|
-
sessionId,
|
|
161
|
-
taskId,
|
|
162
|
-
});
|
|
163
|
-
// Get filename and mime type from mediaUrl
|
|
164
|
-
// mediaUrl may be a local file path or URL
|
|
165
|
-
const fileName = mediaUrl.split("/").pop() || "unknown";
|
|
166
|
-
const mimeType = getMimeTypeFromFilename(fileName);
|
|
167
|
-
// Build agent_response message
|
|
168
|
-
const agentResponse = {
|
|
169
|
-
msgType: "agent_response",
|
|
170
|
-
agentId: config.agentId,
|
|
171
|
-
sessionId: sessionId,
|
|
172
|
-
taskId: taskId,
|
|
173
|
-
msgDetail: JSON.stringify({
|
|
174
|
-
jsonrpc: "2.0",
|
|
175
|
-
id: taskId,
|
|
176
|
-
result: {
|
|
177
|
-
kind: "artifact-update",
|
|
178
|
-
append: true,
|
|
179
|
-
lastChunk: false,
|
|
180
|
-
final: false,
|
|
181
|
-
artifact: {
|
|
182
|
-
artifactId: taskId,
|
|
183
|
-
parts: [
|
|
184
|
-
{
|
|
185
|
-
kind: "file",
|
|
186
|
-
file: {
|
|
187
|
-
name: fileName,
|
|
188
|
-
mimeType: mimeType,
|
|
189
|
-
fileId: fileId,
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
error: { code: 0 },
|
|
196
|
-
}),
|
|
197
|
-
};
|
|
198
|
-
// Get WebSocket manager and send message
|
|
199
|
-
const { getXYWebSocketManager } = await import("./client.js");
|
|
200
|
-
const wsManager = getXYWebSocketManager(config);
|
|
201
|
-
await wsManager.sendMessage(sessionId, agentResponse);
|
|
202
|
-
console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
|
|
203
|
-
// 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
|
|
204
136
|
return {
|
|
205
137
|
channel: "xiaoyi-channel",
|
|
206
|
-
messageId:
|
|
138
|
+
messageId: "",
|
|
207
139
|
chatId: to,
|
|
208
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
|
+
// };
|
|
209
221
|
},
|
|
210
222
|
};
|
|
@@ -28,7 +28,7 @@ export const createAlarmTool = {
|
|
|
28
28
|
- alarmSnoozeTotal: 再响次数,枚举值:0,1,3,5,10,默认0(表示不再响)
|
|
29
29
|
- alarmRingDuration: 响铃时长(分钟),枚举值:1,5,10,15,20,30,默认20
|
|
30
30
|
- daysOfWakeType: 闹钟响铃类型,枚举值:0=单次响铃,1=法定节假日,2=每天,3=自定义时间,4=法定工作日,默认0
|
|
31
|
-
- daysOfWeek:
|
|
31
|
+
- daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,Tue,Wed,Thu,Fri,Sat,Sun。注意:仅支持长度为1的数组,如果需要一周中不同的几天,需要多次调用此工具
|
|
32
32
|
|
|
33
33
|
注意事项:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。`,
|
|
34
34
|
parameters: {
|
|
@@ -52,18 +52,16 @@ export const createAlarmTool = {
|
|
|
52
52
|
},
|
|
53
53
|
alarmRingDuration: {
|
|
54
54
|
type: "number",
|
|
55
|
-
description: "响铃时长(分钟),枚举值:1,5,10,15,20,30,默认
|
|
55
|
+
description: "响铃时长(分钟),枚举值:1,5,10,15,20,30,默认5",
|
|
56
56
|
},
|
|
57
57
|
daysOfWakeType: {
|
|
58
58
|
type: "number",
|
|
59
59
|
description: "闹钟响铃类型:0=单次,1=法定节假日,2=每天,3=自定义,4=法定工作日,默认0",
|
|
60
60
|
},
|
|
61
61
|
daysOfWeek: {
|
|
62
|
-
type
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
},
|
|
66
|
-
description: "自定义响铃星期(当daysOfWakeType=3时需要),枚举值:Mon,Tue,Wed,Thu,Fri,Sat,Sun",
|
|
62
|
+
// 不指定 type,允许传入数组或 JSON 字符串
|
|
63
|
+
// 具体的类型验证和转换在 execute 函数内部进行
|
|
64
|
+
description: "自定义响铃星期(仅当daysOfWakeType=3时需要,其他情况不要传递),数组或JSON字符串,枚举值:Mon,Tue,Wed,Thu,Fri,Sat,Sun。注意:仅支持长度为1的数组,如果需要一周中不同的几天,需要多次调用此工具",
|
|
67
65
|
},
|
|
68
66
|
},
|
|
69
67
|
required: ["alarmTime"],
|
|
@@ -151,26 +149,68 @@ export const createAlarmTool = {
|
|
|
151
149
|
// daysOfWeek - only required when daysOfWakeType is 3
|
|
152
150
|
let daysOfWeek = [];
|
|
153
151
|
if (daysOfWakeType === 3) {
|
|
154
|
-
if (!params.daysOfWeek
|
|
152
|
+
if (!params.daysOfWeek) {
|
|
155
153
|
logger.error(`[CREATE_ALARM_TOOL] ❌ Missing daysOfWeek when daysOfWakeType=3`);
|
|
156
154
|
throw new Error("daysOfWeek is required when daysOfWakeType is 3 (custom)");
|
|
157
155
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
// ===== 参数规范化:兼容数组和 JSON 字符串 =====
|
|
157
|
+
let normalizedDaysOfWeek = null;
|
|
158
|
+
// 情况1: 已经是数组
|
|
159
|
+
if (Array.isArray(params.daysOfWeek)) {
|
|
160
|
+
logger.log(`[CREATE_ALARM_TOOL] ✅ daysOfWeek is already an array`);
|
|
161
|
+
normalizedDaysOfWeek = params.daysOfWeek;
|
|
162
|
+
}
|
|
163
|
+
// 情况2: 是字符串,尝试解析为 JSON 数组
|
|
164
|
+
else if (typeof params.daysOfWeek === 'string') {
|
|
165
|
+
logger.log(`[CREATE_ALARM_TOOL] 🔄 daysOfWeek is a string, attempting to parse as JSON...`);
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(params.daysOfWeek);
|
|
168
|
+
if (Array.isArray(parsed)) {
|
|
169
|
+
logger.log(`[CREATE_ALARM_TOOL] ✅ Successfully parsed JSON string to array`);
|
|
170
|
+
normalizedDaysOfWeek = parsed;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
logger.error(`[CREATE_ALARM_TOOL] ❌ Parsed JSON is not an array:`, typeof parsed);
|
|
174
|
+
throw new Error("daysOfWeek must be an array or a JSON string representing an array");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (parseError) {
|
|
178
|
+
logger.error(`[CREATE_ALARM_TOOL] ❌ Failed to parse daysOfWeek as JSON:`, parseError);
|
|
179
|
+
throw new Error(`daysOfWeek must be a valid JSON array string. Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// 情况3: 其他类型,报错
|
|
183
|
+
else {
|
|
184
|
+
logger.error(`[CREATE_ALARM_TOOL] ❌ Invalid daysOfWeek type:`, typeof params.daysOfWeek);
|
|
185
|
+
throw new Error(`daysOfWeek must be an array or a JSON string, got ${typeof params.daysOfWeek}`);
|
|
186
|
+
}
|
|
187
|
+
// 验证数组非空
|
|
188
|
+
if (!normalizedDaysOfWeek || normalizedDaysOfWeek.length === 0) {
|
|
189
|
+
logger.error(`[CREATE_ALARM_TOOL] ❌ daysOfWeek array is empty`);
|
|
190
|
+
throw new Error("daysOfWeek array cannot be empty");
|
|
191
|
+
}
|
|
192
|
+
// 验证数组长度必须为1
|
|
193
|
+
if (normalizedDaysOfWeek.length !== 1) {
|
|
194
|
+
logger.error(`[CREATE_ALARM_TOOL] ❌ daysOfWeek array length must be 1, got ${normalizedDaysOfWeek.length}`);
|
|
195
|
+
throw new Error("daysOfWeek 仅支持长度为1的数组。如果需要一周中不同的几天,需要多次调用此工具");
|
|
161
196
|
}
|
|
162
197
|
// Validate each day
|
|
163
|
-
for (const day of
|
|
198
|
+
for (const day of normalizedDaysOfWeek) {
|
|
164
199
|
if (typeof day !== "string" || !DAYS_OF_WEEK_VALUES.includes(day)) {
|
|
165
200
|
logger.error(`[CREATE_ALARM_TOOL] ❌ Invalid day value: ${day}`);
|
|
166
201
|
throw new Error(`daysOfWeek must contain only: ${DAYS_OF_WEEK_VALUES.join(", ")}`);
|
|
167
202
|
}
|
|
168
203
|
}
|
|
169
|
-
daysOfWeek =
|
|
204
|
+
daysOfWeek = normalizedDaysOfWeek;
|
|
170
205
|
logger.log(`[CREATE_ALARM_TOOL] - daysOfWeek: ${daysOfWeek.join(", ")}`);
|
|
171
206
|
}
|
|
172
|
-
else
|
|
173
|
-
|
|
207
|
+
else {
|
|
208
|
+
// daysOfWakeType is not 3, daysOfWeek should not be provided
|
|
209
|
+
if (params.daysOfWeek) {
|
|
210
|
+
logger.warn(`[CREATE_ALARM_TOOL] ⚠️ daysOfWeek parameter is ignored when daysOfWakeType is not 3 (current: ${daysOfWakeType}). Please remove daysOfWeek parameter.`);
|
|
211
|
+
}
|
|
212
|
+
// Explicitly set to empty array
|
|
213
|
+
daysOfWeek = [];
|
|
174
214
|
}
|
|
175
215
|
// Get session context
|
|
176
216
|
logger.log(`[CREATE_ALARM_TOOL] 🔍 Attempting to get session context...`);
|
|
@@ -191,6 +231,24 @@ export const createAlarmTool = {
|
|
|
191
231
|
logger.log(`[CREATE_ALARM_TOOL] ✅ WebSocket manager obtained`);
|
|
192
232
|
// Build CreateAlarm command
|
|
193
233
|
logger.log(`[CREATE_ALARM_TOOL] 📦 Building CreateAlarm command...`);
|
|
234
|
+
// Build intentParam - only include daysOfWeek when daysOfWakeType is 3
|
|
235
|
+
const intentParam = {
|
|
236
|
+
entityName: "Alarm",
|
|
237
|
+
alarmTime: alarmTimeMs,
|
|
238
|
+
alarmTitle: alarmTitle,
|
|
239
|
+
alarmSnoozeDuration: alarmSnoozeDuration,
|
|
240
|
+
alarmSnoozeTotal: alarmSnoozeTotal,
|
|
241
|
+
alarmRingDuration: alarmRingDuration,
|
|
242
|
+
daysOfWakeType: daysOfWakeType,
|
|
243
|
+
};
|
|
244
|
+
// Only include daysOfWeek when daysOfWakeType is 3
|
|
245
|
+
if (daysOfWakeType === 3 && daysOfWeek.length > 0) {
|
|
246
|
+
intentParam.daysOfWeek = daysOfWeek;
|
|
247
|
+
logger.log(`[CREATE_ALARM_TOOL] - Including daysOfWeek in intentParam: ${daysOfWeek.join(", ")}`);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
logger.log(`[CREATE_ALARM_TOOL] - Excluding daysOfWeek from intentParam (daysOfWakeType=${daysOfWakeType})`);
|
|
251
|
+
}
|
|
194
252
|
const command = {
|
|
195
253
|
header: {
|
|
196
254
|
namespace: "Common",
|
|
@@ -206,16 +264,7 @@ export const createAlarmTool = {
|
|
|
206
264
|
actionResponse: true,
|
|
207
265
|
appType: "OHOS_APP",
|
|
208
266
|
timeOut: 5,
|
|
209
|
-
intentParam:
|
|
210
|
-
entityName: "Alarm",
|
|
211
|
-
alarmTime: alarmTimeMs,
|
|
212
|
-
alarmTitle: alarmTitle,
|
|
213
|
-
alarmSnoozeDuration: alarmSnoozeDuration,
|
|
214
|
-
alarmSnoozeTotal: alarmSnoozeTotal,
|
|
215
|
-
alarmRingDuration: alarmRingDuration,
|
|
216
|
-
daysOfWakeType: daysOfWakeType,
|
|
217
|
-
daysOfWeek: daysOfWeek,
|
|
218
|
-
},
|
|
267
|
+
intentParam: intentParam,
|
|
219
268
|
permissionId: [],
|
|
220
269
|
achieveType: "INTENT",
|
|
221
270
|
},
|
|
@@ -32,7 +32,7 @@ export const modifyAlarmTool = {
|
|
|
32
32
|
- alarmSnoozeTotal: 再响次数,枚举值:0,1,3,5,10
|
|
33
33
|
- alarmRingDuration: 响铃时长(分钟),枚举值:1,5,10,15,20,30
|
|
34
34
|
- daysOfWakeType: 闹钟响铃类型,枚举值:0=单次,1=法定节假日,2=每天,3=自定义,4=法定工作日
|
|
35
|
-
- daysOfWeek:
|
|
35
|
+
- daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,Tue,Wed,Thu,Fri,Sat,Sun。注意:仅支持长度为1的数组,如果需要一周中不同的几天,需要多次调用此工具
|
|
36
36
|
|
|
37
37
|
使用流程:
|
|
38
38
|
1. 先调用 search_alarm 工具查询闹钟,获取 entityId
|
|
@@ -75,11 +75,9 @@ export const modifyAlarmTool = {
|
|
|
75
75
|
description: "闹钟响铃类型:0=单次,1=法定节假日,2=每天,3=自定义,4=法定工作日",
|
|
76
76
|
},
|
|
77
77
|
daysOfWeek: {
|
|
78
|
-
type
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
},
|
|
82
|
-
description: "自定义响铃星期(当daysOfWakeType=3时需要),枚举值:Mon,Tue,Wed,Thu,Fri,Sat,Sun",
|
|
78
|
+
// 不指定 type,允许传入数组或 JSON 字符串
|
|
79
|
+
// 具体的类型验证和转换在 execute 函数内部进行
|
|
80
|
+
description: "自定义响铃星期(仅当daysOfWakeType=3时需要,其他情况不要传递),数组或JSON字符串,枚举值:Mon,Tue,Wed,Thu,Fri,Sat,Sun。注意:仅支持长度为1的数组,如果需要一周中不同的几天,需要多次调用此工具",
|
|
83
81
|
},
|
|
84
82
|
},
|
|
85
83
|
required: ["entityId"],
|
|
@@ -177,7 +175,6 @@ export const modifyAlarmTool = {
|
|
|
177
175
|
logger.log(`[MODIFY_ALARM_TOOL] - alarmRingDuration: ${params.alarmRingDuration}`);
|
|
178
176
|
}
|
|
179
177
|
// Add daysOfWakeType if provided
|
|
180
|
-
let currentDaysOfWakeType = null;
|
|
181
178
|
if (params.daysOfWakeType !== undefined && params.daysOfWakeType !== null) {
|
|
182
179
|
if (typeof params.daysOfWakeType !== "number") {
|
|
183
180
|
logger.error(`[MODIFY_ALARM_TOOL] ❌ Invalid daysOfWakeType type`);
|
|
@@ -188,24 +185,68 @@ export const modifyAlarmTool = {
|
|
|
188
185
|
throw new Error(`daysOfWakeType must be one of: ${DAYS_OF_WAKE_TYPE_VALUES.join(", ")}`);
|
|
189
186
|
}
|
|
190
187
|
intentParam.daysOfWakeType = params.daysOfWakeType;
|
|
191
|
-
currentDaysOfWakeType = params.daysOfWakeType;
|
|
192
188
|
logger.log(`[MODIFY_ALARM_TOOL] - daysOfWakeType: ${params.daysOfWakeType}`);
|
|
193
189
|
}
|
|
194
|
-
// Add daysOfWeek if provided
|
|
190
|
+
// Add daysOfWeek if provided - only valid when daysOfWakeType is 3
|
|
195
191
|
if (params.daysOfWeek !== undefined && params.daysOfWeek !== null) {
|
|
196
|
-
if
|
|
197
|
-
|
|
198
|
-
|
|
192
|
+
// Check if daysOfWakeType is 3 or will be set to 3
|
|
193
|
+
const targetDaysOfWakeType = params.daysOfWakeType !== undefined ? params.daysOfWakeType : null;
|
|
194
|
+
if (targetDaysOfWakeType !== null && targetDaysOfWakeType !== 3) {
|
|
195
|
+
logger.warn(`[MODIFY_ALARM_TOOL] ⚠️ daysOfWeek parameter is ignored when daysOfWakeType is not 3 (current: ${targetDaysOfWakeType}). Please remove daysOfWeek parameter.`);
|
|
196
|
+
// Skip processing daysOfWeek when daysOfWakeType is not 3
|
|
199
197
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
else {
|
|
199
|
+
// ===== 参数规范化:兼容数组和 JSON 字符串 =====
|
|
200
|
+
let normalizedDaysOfWeek = null;
|
|
201
|
+
// 情况1: 已经是数组
|
|
202
|
+
if (Array.isArray(params.daysOfWeek)) {
|
|
203
|
+
logger.log(`[MODIFY_ALARM_TOOL] ✅ daysOfWeek is already an array`);
|
|
204
|
+
normalizedDaysOfWeek = params.daysOfWeek;
|
|
205
|
+
}
|
|
206
|
+
// 情况2: 是字符串,尝试解析为 JSON 数组
|
|
207
|
+
else if (typeof params.daysOfWeek === 'string') {
|
|
208
|
+
logger.log(`[MODIFY_ALARM_TOOL] 🔄 daysOfWeek is a string, attempting to parse as JSON...`);
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(params.daysOfWeek);
|
|
211
|
+
if (Array.isArray(parsed)) {
|
|
212
|
+
logger.log(`[MODIFY_ALARM_TOOL] ✅ Successfully parsed JSON string to array`);
|
|
213
|
+
normalizedDaysOfWeek = parsed;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
logger.error(`[MODIFY_ALARM_TOOL] ❌ Parsed JSON is not an array:`, typeof parsed);
|
|
217
|
+
throw new Error("daysOfWeek must be an array or a JSON string representing an array");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (parseError) {
|
|
221
|
+
logger.error(`[MODIFY_ALARM_TOOL] ❌ Failed to parse daysOfWeek as JSON:`, parseError);
|
|
222
|
+
throw new Error(`daysOfWeek must be a valid JSON array string. Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 情况3: 其他类型,报错
|
|
226
|
+
else {
|
|
227
|
+
logger.error(`[MODIFY_ALARM_TOOL] ❌ Invalid daysOfWeek type:`, typeof params.daysOfWeek);
|
|
228
|
+
throw new Error(`daysOfWeek must be an array or a JSON string, got ${typeof params.daysOfWeek}`);
|
|
229
|
+
}
|
|
230
|
+
// 验证数组非空
|
|
231
|
+
if (!normalizedDaysOfWeek || normalizedDaysOfWeek.length === 0) {
|
|
232
|
+
logger.error(`[MODIFY_ALARM_TOOL] ❌ daysOfWeek array is empty`);
|
|
233
|
+
throw new Error("daysOfWeek array cannot be empty");
|
|
234
|
+
}
|
|
235
|
+
// 验证数组长度必须为1
|
|
236
|
+
if (normalizedDaysOfWeek.length !== 1) {
|
|
237
|
+
logger.error(`[MODIFY_ALARM_TOOL] ❌ daysOfWeek array length must be 1, got ${normalizedDaysOfWeek.length}`);
|
|
238
|
+
throw new Error("daysOfWeek 仅支持长度为1的数组。如果需要一周中不同的几天,需要多次调用此工具");
|
|
239
|
+
}
|
|
240
|
+
// Validate each day
|
|
241
|
+
for (const day of normalizedDaysOfWeek) {
|
|
242
|
+
if (typeof day !== "string" || !DAYS_OF_WEEK_VALUES.includes(day)) {
|
|
243
|
+
logger.error(`[MODIFY_ALARM_TOOL] ❌ Invalid day value: ${day}`);
|
|
244
|
+
throw new Error(`daysOfWeek must contain only: ${DAYS_OF_WEEK_VALUES.join(", ")}`);
|
|
245
|
+
}
|
|
205
246
|
}
|
|
247
|
+
intentParam.daysOfWeek = normalizedDaysOfWeek;
|
|
248
|
+
logger.log(`[MODIFY_ALARM_TOOL] - daysOfWeek: ${normalizedDaysOfWeek.join(", ")}`);
|
|
206
249
|
}
|
|
207
|
-
intentParam.daysOfWeek = params.daysOfWeek;
|
|
208
|
-
logger.log(`[MODIFY_ALARM_TOOL] - daysOfWeek: ${params.daysOfWeek.join(", ")}`);
|
|
209
250
|
}
|
|
210
251
|
// Get session context
|
|
211
252
|
logger.log(`[MODIFY_ALARM_TOOL] 🔍 Attempting to get session context...`);
|
|
@@ -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: {
|
|
@@ -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
|
+
};
|