@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.
@@ -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();
@@ -1,7 +1,6 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
- import { XYFileUploadService } from "./file-upload.js";
3
2
  import { XYPushService } from "./push.js";
4
- import { getLatestSessionContext } from "./tools/session-manager.js";
3
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
5
4
  // Special marker for default push delivery when no target is specified
6
5
  const DEFAULT_PUSH_MARKER = "default";
7
6
  // File extension to MIME type mapping
@@ -65,8 +64,8 @@ export const xyOutbound = {
65
64
  // If the target doesn't contain "::", try to enhance it with taskId from session context
66
65
  if (!trimmedTo.includes("::")) {
67
66
  console.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
68
- // Try to get the latest session context
69
- const sessionContext = getLatestSessionContext();
67
+ // Try to get the current session context
68
+ const sessionContext = getCurrentSessionContext();
70
69
  if (sessionContext && sessionContext.sessionId === trimmedTo) {
71
70
  const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
72
71
  console.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
@@ -130,81 +129,94 @@ export const xyOutbound = {
130
129
  mediaUrl,
131
130
  mediaLocalRoots,
132
131
  });
133
- // Parse to: "sessionId::taskId"
134
- const parts = to.split("::");
135
- if (parts.length !== 2) {
136
- throw new Error(`Invalid to format: "${to}". Expected "sessionId::taskId"`);
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: fileId,
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: 自定义响铃星期,当daysOfWakeType=3时需要,数组,枚举值:Mon,Tue,Wed,Thu,Fri,Sat,Sun
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,默认20",
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: "array",
63
- items: {
64
- type: "string",
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 || !Array.isArray(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
- if (!Array.isArray(params.daysOfWeek)) {
159
- logger.error(`[CREATE_ALARM_TOOL] Invalid daysOfWeek type`);
160
- throw new Error("daysOfWeek must be an array");
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 params.daysOfWeek) {
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 = params.daysOfWeek;
204
+ daysOfWeek = normalizedDaysOfWeek;
170
205
  logger.log(`[CREATE_ALARM_TOOL] - daysOfWeek: ${daysOfWeek.join(", ")}`);
171
206
  }
172
- else if (params.daysOfWeek && params.daysOfWeek.length > 0) {
173
- logger.warn(`[CREATE_ALARM_TOOL] ⚠️ daysOfWeek is ignored when daysOfWakeType is not 3`);
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: 自定义响铃星期(当daysOfWakeType=3时需要),数组,枚举值:Mon,Tue,Wed,Thu,Fri,Sat,Sun
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: "array",
79
- items: {
80
- type: "string",
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 (!Array.isArray(params.daysOfWeek)) {
197
- logger.error(`[MODIFY_ALARM_TOOL] Invalid daysOfWeek type`);
198
- throw new Error("daysOfWeek must be an array");
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
- // Validate each day
201
- for (const day of params.daysOfWeek) {
202
- if (typeof day !== "string" || !DAYS_OF_WEEK_VALUES.includes(day)) {
203
- logger.error(`[MODIFY_ALARM_TOOL] ❌ Invalid day value: ${day}`);
204
- throw new Error(`daysOfWeek must contain only: ${DAYS_OF_WEEK_VALUES.join(", ")}`);
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: "搜索手机文件系统的文件。根据关键词搜索文件名称或内容,返回匹配的文件列表(包括文件名、路径、大小、修改时间等信息)。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
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,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,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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.27-beta",
3
+ "version": "0.0.29-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",