@ynhcj/xiaoyi-channel 0.0.4-next → 0.0.5-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 CHANGED
@@ -1,7 +1,6 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
2
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
3
3
  import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId } from "./parser.js";
4
- import { downloadFilesFromParts } from "./file-download.js";
5
4
  import { resolveXYConfig } from "./config.js";
6
5
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
7
6
  import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
@@ -129,10 +128,9 @@ export async function handleXYMessage(params) {
129
128
  // Extract text and files from parts
130
129
  const text = extractTextFromParts(parsed.parts);
131
130
  const fileParts = extractFileParts(parsed.parts);
132
- // Download files if present (using core's media download)
133
- const mediaList = await downloadFilesFromParts(fileParts);
134
- // Build media payload for inbound context (following feishu pattern)
135
- const mediaPayload = buildXYMediaPayload(mediaList);
131
+ // Build media payload directly from file URIs (openclaw can download them)
132
+ // No need to download files locally - pass URIs directly to openclaw
133
+ const mediaPayload = buildXYMediaPayload(fileParts);
136
134
  // Resolve envelope format options (following feishu pattern)
137
135
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
138
136
  // Build message body with speaker prefix (following feishu pattern)
@@ -276,17 +274,18 @@ export async function handleXYMessage(params) {
276
274
  /**
277
275
  * Build media payload for inbound context.
278
276
  * Following feishu pattern: buildFeishuMediaPayload().
277
+ * Uses remote URIs directly - openclaw will download them.
279
278
  */
280
- function buildXYMediaPayload(mediaList) {
281
- const first = mediaList[0];
282
- const mediaPaths = mediaList.map((media) => media.path);
283
- const mediaTypes = mediaList.map((media) => media.mimeType).filter(Boolean);
279
+ function buildXYMediaPayload(fileParts) {
280
+ const first = fileParts[0];
281
+ const uris = fileParts.map((file) => file.uri);
282
+ const mediaTypes = fileParts.map((file) => file.mimeType).filter(Boolean);
284
283
  return {
285
- MediaPath: first?.path,
284
+ MediaPath: first?.uri,
286
285
  MediaType: first?.mimeType,
287
- MediaUrl: first?.path,
288
- MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
289
- MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
286
+ MediaUrl: first?.uri,
287
+ MediaPaths: uris.length > 0 ? uris : undefined,
288
+ MediaUrls: uris.length > 0 ? uris : undefined,
290
289
  MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
291
290
  };
292
291
  }
@@ -8,7 +8,7 @@ import { searchNoteTool } from "./tools/search-note-tool.js";
8
8
  import { modifyNoteTool } from "./tools/modify-note-tool.js";
9
9
  import { calendarTool } from "./tools/calendar-tool.js";
10
10
  import { searchCalendarTool } from "./tools/search-calendar-tool.js";
11
- // import { searchContactTool } from "./tools/search-contact-tool.js"; // 暂时禁用
11
+ import { searchContactTool } from "./tools/search-contact-tool.js";
12
12
  import { searchPhotoGalleryTool } from "./tools/search-photo-gallery-tool.js";
13
13
  import { uploadPhotoTool } from "./tools/upload-photo-tool.js";
14
14
  import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
@@ -20,7 +20,6 @@ import { createAlarmTool } from "./tools/create-alarm-tool.js";
20
20
  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
- import { sendMessageTool } from "./tools/send-message-tool.js";
24
23
  import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
25
24
  /**
26
25
  * Xiaoyi Channel Plugin for OpenClaw.
@@ -61,7 +60,7 @@ export const xyPlugin = {
61
60
  },
62
61
  outbound: xyOutbound,
63
62
  onboarding: xyOnboardingAdapter,
64
- agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendMessageTool, sendFileToUserTool], // searchContactTool 已暂时禁用
63
+ agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool],
65
64
  messaging: {
66
65
  normalizeTarget: (raw) => {
67
66
  const trimmed = raw.trim();
@@ -1,4 +1,5 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
+ import { XYFileUploadService } from "./file-upload.js";
2
3
  import { XYPushService } from "./push.js";
3
4
  import { getCurrentSessionContext } from "./tools/session-manager.js";
4
5
  // Special marker for default push delivery when no target is specified
@@ -129,94 +130,81 @@ export const xyOutbound = {
129
130
  mediaUrl,
130
131
  mediaLocalRoots,
131
132
  });
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
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
136
204
  return {
137
205
  channel: "xiaoyi-channel",
138
- messageId: "",
206
+ messageId: fileId,
139
207
  chatId: to,
140
208
  };
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
- // };
221
209
  },
222
210
  };
@@ -10,7 +10,9 @@ import { logger } from "../utils/logger.js";
10
10
  export const calendarTool = {
11
11
  name: "create_calendar_event",
12
12
  label: "Create Calendar Event",
13
- description: "在用户设备上创建日程。需要提供日程标题、开始时间和结束时间。时间格式必须为:yyyy-mm-dd hh:mm:ss(例如:2024-01-15 14:30:00)。注意:该工具执行时间较长(最多60秒),请勿重复调用,超时或失败时最多重试一次。",
13
+ description: `在用户设备上创建日程。需要提供日程标题、开始时间和结束时间。时间格式必须为:yyyy-mm-dd hh:mm:ss(例如:2024-01-15 14:30:00)。注意:该工具执行时间较长(最多60秒),请勿重复调用,超时或失败时最多重试一次。
14
+ 注意事项:使用该工具之前需获取当前真实时间
15
+ `,
14
16
  parameters: {
15
17
  type: "object",
16
18
  properties: {
@@ -22,15 +22,17 @@ export const createAlarmTool = {
22
22
  必需参数:
23
23
  - alarmTime: 闹钟时间,格式必须为:YYYYMMDD hhmmss(例如:20240315 143000,表示2024年3月15日14:30:00)
24
24
 
25
- 可选参数:
25
+ 可选参数(针对用户没有提及的参数,如果有默认参数,则发送请求时使用默认参数):
26
26
  - alarmTitle: 闹钟名称/标题,默认为"闹钟"
27
27
  - alarmSnoozeDuration: 小睡间隔(分钟),枚举值:5,10,15,20,25,30,默认10
28
28
  - alarmSnoozeTotal: 再响次数,枚举值:0,1,3,5,10,默认0(表示不再响)
29
- - alarmRingDuration: 响铃时长(分钟),枚举值:1,5,10,15,20,30,默认20
29
+ - alarmRingDuration: 响铃时长(分钟),枚举值:1,5,10,15,20,30,默认5
30
30
  - daysOfWakeType: 闹钟响铃类型,枚举值:0=单次响铃,1=法定节假日,2=每天,3=自定义时间,4=法定工作日,默认0
31
31
  - daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,Tue,Wed,Thu,Fri,Sat,Sun。注意:仅支持长度为1的数组,如果需要一周中不同的几天,需要多次调用此工具
32
32
 
33
- 注意事项:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。`,
33
+ 注意事项:
34
+ a. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。
35
+ b. 使用该工具之前需获取当前真实时间`,
34
36
  parameters: {
35
37
  type: "object",
36
38
  properties: {
@@ -35,8 +35,9 @@ export const modifyAlarmTool = {
35
35
  - daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,Tue,Wed,Thu,Fri,Sat,Sun。注意:仅支持长度为1的数组,如果需要一周中不同的几天,需要多次调用此工具
36
36
 
37
37
  使用流程:
38
- 1. 先调用 search_alarm 工具查询闹钟,获取 entityId
38
+ 1. 先调用 search_alarm 工具查询闹钟,获取 entityId
39
39
  2. 调用此工具修改闹钟,传入 entityId 和需要修改的参数
40
+ 3. 其余不涉及需改的参数,如果search_alarm 或 create_alarm的结果中有相应的值,需要一并填上,需要与原有的保持一致,防止不填采用默认值
40
41
 
41
42
  注意事项:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。`,
42
43
  parameters: {
@@ -282,15 +283,7 @@ export const modifyAlarmTool = {
282
283
  actionResponse: true,
283
284
  appType: "OHOS_APP",
284
285
  timeOut: 5,
285
- intentParam: {
286
- bundleName: "com.huawei.hmos.clock",
287
- moduleName: "entry",
288
- abilityName: "com.huawei.hmos.clock.phone",
289
- intentName: "ModifyAlarm",
290
- executeMode: "background",
291
- intentRequestVersion: 100,
292
- intentParam: intentParam,
293
- },
286
+ intentParam: intentParam,
294
287
  permissionId: [],
295
288
  achieveType: "INTENT",
296
289
  },
@@ -31,7 +31,9 @@ export const searchAlarmTool = {
31
31
  - 查询每天响铃的闹钟:{"daysOfWakeType": 2}
32
32
  - 查询某个时间段的闹钟:{"startTime": "20240315 000000", "endTime": "20240315 235959"}
33
33
 
34
- 注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。`,
34
+ 注意:
35
+ a. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。
36
+ b. 使用该工具之前需获取当前真实时间`,
35
37
  parameters: {
36
38
  type: "object",
37
39
  properties: {
@@ -25,7 +25,10 @@ export const searchCalendarTool = {
25
25
  - 查询晚上的日程:使用 18:00:00 到 23:59:59
26
26
  - 查询某个时刻附近的日程:使用该时刻前后1小时的区间(例如:查询3点左右的日程,使用 14:00:00 到 16:00:00)
27
27
 
28
- 注意:该工具执行时间较长(最多60秒),请勿重复调用,超时或失败时最多重试一次。`,
28
+ 注意:
29
+ a. 该工具执行时间较长(最多60秒),请勿重复调用,超时或失败时最多重试一次。
30
+ b. 使用该工具之前需获取当前真实时间
31
+ `,
29
32
  parameters: {
30
33
  type: "object",
31
34
  properties: {
@@ -13,23 +13,51 @@ export const searchPhotoGalleryTool = {
13
13
  name: "search_photo_gallery",
14
14
  label: "Search Photo Gallery",
15
15
  description: `插件功能描述:搜索用户手机图库中的照片
16
+
16
17
  工具使用约束:如果用户说从手机图库中或者从相册中查询xx图片时调用此工具。
18
+
17
19
  工具输入输出简介:
18
20
  a. 根据图像描述语料检索匹配的照片,返回照片在手机本地的 mediaUri以及thumbnailUri。
19
21
  b. 返回的 mediaUri以及thumbnailUri 是本地路径,无法直接下载或访问。如果需要下载、查看、使用或展示照片,请使用 upload_photo 工具将 mediaUri或者thumbnailUri 转换为可访问的公网 URL。
20
22
  c. mediaUri代表手机相册中的图片原图路径,图片大小比较大,清晰度比较高
21
23
  d. thumbnailUri代表手机相册中的图片缩略图路径,图片大小比较小,清晰度适中,建议在upload_photo 工具的入参中优先使用此路径,不容易引起上传超时等问题
22
-
24
+
25
+ 搜索能力边界:
26
+ a. ✅ 支持口语化输入:改写模型会自动提取姓名、种类、地点等实体,可以使用自然语言描述(如"小狗的照片"、"南京拍的风景")
27
+ b. ✅ 支持相册搜索:可以在query中包含相册名称(如"西安之行相册的照片")
28
+ c. ✅ 支持人像搜索:前提是照片有人像tag,且需要口语化描述(如"张三的照片")
29
+ d. ❌ 不支持时间相对词:不支持"最新"、"最旧"、"最早"等表述,需要使用具体时间(如"2024年的照片"而非"去年的照片")
30
+ e. ❌ 不支持多实体查询:不支持"或"逻辑和时间范围(如"南京或上海的照片"、"近三年的照片"),需要拆分成多次独立查询
31
+ f. ❌ 不支持POI逆地理映射:照片的location是门牌号,用真实场地名称可能搜不到
32
+ g. ❌ 不支持收藏感知:无法感知照片是否被收藏
33
+ h. ❌ 不支持细粒度品种:对于动物、植物等的具体品种识别能力有限
34
+ i. ⚠️ POI提取可能不准确:地名可能作为语义搜索条件,可能导致"xx湖"搜到"yy江"或"zz湾"的照片
35
+
36
+ 查询优化建议:
37
+ a. 时间查询:将"最新"、"去年"、"近三年"等转换为具体年份(如"2024年"、"2023年到2025年"需拆分成"2023年"、"2024年"、"2025年"三次查询)
38
+ b. 多条件查询:将"或"逻辑拆分成多次查询(如"南京或上海的照片"→先查"南京的照片",再查"上海的照片")
39
+ c. 实体原子化:确保每个query只包含一个原子实体(地点、人名、物品等)
40
+ d. 相册名称:如果知道相册名,直接在query中包含相册名可以提高准确度
41
+
23
42
  注意事项:
24
43
  a. 只有当用户明确表达从手机相册搜索或者从图库搜索时才执行此工具,如果用户仅表达要搜索xxx图片,并没有说明搜索数据源,则不要贸然调用此插件,可以优先尝试websearch或者询问用户是否要从手机图库中搜索。
25
44
  b. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。
45
+ c. 如果用户请求包含多个实体或时间范围,需要主动拆分成多次查询并告知用户。
26
46
  `,
27
47
  parameters: {
28
48
  type: "object",
29
49
  properties: {
30
50
  query: {
31
51
  type: "string",
32
- description: "图像描述语料,用于检索匹配的照片(例如:'小狗的照片'、'带有键盘的图片'等)",
52
+ description: `图像描述语料,用于检索匹配的照片。支持口语化输入,会自动提取姓名、种类、地点等实体。
53
+
54
+ 使用示例:
55
+ - 正确:"小狗的照片"、"南京拍的风景"、"张三的照片"、"西安之行相册的照片"、"2024年的照片"
56
+ - 错误:"最新的照片"(应改为具体年份如"2024年的照片")
57
+ - 错误:"南京或上海的照片"(需拆分成两次查询:"南京的照片" 和 "上海的照片")
58
+ - 错误:"近三年的照片"(需拆分成"2023年的照片"、"2024年的照片"、"2025年的照片")
59
+
60
+ 重要:每次查询只能包含一个原子实体(单个地点、单个人名、单个年份等),不支持多实体或"或"逻辑。`,
33
61
  },
34
62
  },
35
63
  required: ["query"],
@@ -143,7 +143,7 @@ export class XYWebSocketManager extends EventEmitter {
143
143
  throw new Error(`WebSocket ${server} not ready`);
144
144
  }
145
145
  const messageStr = JSON.stringify(message);
146
- console.log(`[WS-${server}-SEND] Sending message frame:`, JSON.stringify(message, null, 2));
146
+ // console.log(`[WS-${server}-SEND] Sending message frame:`, JSON.stringify(message, null, 2));
147
147
  ws.send(messageStr);
148
148
  console.log(`[WS-${server}-SEND] Message sent successfully, size: ${messageStr.length} bytes`);
149
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.4-next",
3
+ "version": "0.0.5-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",