@ynhcj/xiaoyi-channel 0.0.46-beta → 0.0.48-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/bot.js CHANGED
@@ -329,9 +329,7 @@ function buildXYMediaPayload(mediaList) {
329
329
  return {
330
330
  MediaPath: first?.path,
331
331
  MediaType: first?.mimeType,
332
- MediaUrl: first?.path,
333
332
  MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
334
- MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
335
333
  MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
336
334
  };
337
335
  }
@@ -22,8 +22,9 @@ import { searchAlarmTool } from "./tools/search-alarm-tool.js";
22
22
  import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
23
23
  import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
24
24
  import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
25
- import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
25
+ // import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js"; // 暂时取消注册
26
26
  import { viewPushResultTool } from "./tools/view-push-result-tool.js";
27
+ import { imageReadingTool } from "./tools/image-reading-tool.js";
27
28
  /**
28
29
  * Xiaoyi Channel Plugin for OpenClaw.
29
30
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -63,7 +64,7 @@ export const xyPlugin = {
63
64
  },
64
65
  outbound: xyOutbound,
65
66
  onboarding: xyOnboardingAdapter,
66
- agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, xiaoyiCollectionTool, viewPushResultTool],
67
+ agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool],
67
68
  messaging: {
68
69
  normalizeTarget: (raw) => {
69
70
  const trimmed = raw.trim();
@@ -12,6 +12,11 @@ export declare class XYFileUploadService {
12
12
  * Returns the objectId (as fileId) for use in A2A messages.
13
13
  */
14
14
  uploadFile(filePath: string, objectType?: string): Promise<string>;
15
+ /**
16
+ * Upload a file and return its publicly accessible URL.
17
+ * Uses completeAndQuery endpoint to get the file URL directly.
18
+ */
19
+ uploadFileAndGetUrl(filePath: string, objectType?: string): Promise<string>;
15
20
  /**
16
21
  * Upload multiple files and return their file IDs.
17
22
  */
@@ -105,6 +105,98 @@ export class XYFileUploadService {
105
105
  return "";
106
106
  }
107
107
  }
108
+ /**
109
+ * Upload a file and return its publicly accessible URL.
110
+ * Uses completeAndQuery endpoint to get the file URL directly.
111
+ */
112
+ async uploadFileAndGetUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
113
+ console.log(`[XY File Upload] Starting file upload with URL retrieval: ${filePath}`);
114
+ try {
115
+ // Read file
116
+ const fileBuffer = await fs.readFile(filePath);
117
+ const fileName = path.basename(filePath);
118
+ const fileSha256 = calculateSHA256(fileBuffer);
119
+ const fileSize = fileBuffer.length;
120
+ // Phase 1: Prepare
121
+ console.log(`[XY File Upload] Phase 1: Prepare upload for ${fileName}`);
122
+ const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
123
+ method: "POST",
124
+ headers: {
125
+ "Content-Type": "application/json",
126
+ "x-uid": this.uid,
127
+ "x-api-key": this.apiKey,
128
+ "x-request-from": "openclaw",
129
+ },
130
+ body: JSON.stringify({
131
+ objectType,
132
+ fileName,
133
+ fileSha256,
134
+ fileSize,
135
+ fileOwnerInfo: {
136
+ uid: this.uid,
137
+ teamId: this.uid,
138
+ },
139
+ useEdge: false,
140
+ }),
141
+ });
142
+ if (!prepareResp.ok) {
143
+ throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
144
+ }
145
+ const prepareData = await prepareResp.json();
146
+ console.log(`[XY File Upload] Prepare response:`, JSON.stringify(prepareData, null, 2));
147
+ if (prepareData.code !== "0") {
148
+ throw new Error(`Prepare failed: ${prepareData.desc}`);
149
+ }
150
+ const { objectId, draftId, uploadInfos } = prepareData;
151
+ console.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
152
+ // Phase 2: Upload
153
+ console.log(`[XY File Upload] Phase 2: Upload file data`);
154
+ const uploadInfo = uploadInfos[0]; // Single-part upload
155
+ const uploadResp = await fetch(uploadInfo.url, {
156
+ method: uploadInfo.method,
157
+ headers: uploadInfo.headers,
158
+ body: fileBuffer,
159
+ });
160
+ console.log(`[XY File Upload] Upload response status: ${uploadResp.status}`);
161
+ if (!uploadResp.ok) {
162
+ const uploadErrorText = await uploadResp.text();
163
+ console.log(`[XY File Upload] Upload error response:`, uploadErrorText);
164
+ throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
165
+ }
166
+ console.log(`[XY File Upload] Upload complete`);
167
+ // Phase 3: CompleteAndQuery - get file URL
168
+ console.log(`[XY File Upload] Phase 3: CompleteAndQuery to get file URL`);
169
+ const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/completeAndQuery`, {
170
+ method: "POST",
171
+ headers: {
172
+ "Content-Type": "application/json",
173
+ "x-uid": this.uid,
174
+ "x-api-key": this.apiKey,
175
+ "x-request-from": "openclaw",
176
+ },
177
+ body: JSON.stringify({
178
+ objectId,
179
+ draftId,
180
+ }),
181
+ });
182
+ if (!completeResp.ok) {
183
+ throw new Error(`CompleteAndQuery failed: HTTP ${completeResp.status}`);
184
+ }
185
+ const completeData = await completeResp.json();
186
+ console.log(`[XY File Upload] CompleteAndQuery response:`, JSON.stringify(completeData, null, 2));
187
+ // Extract file URL from response
188
+ const fileUrl = completeData?.fileDetailInfo?.url || "";
189
+ if (!fileUrl) {
190
+ throw new Error("No file URL returned from completeAndQuery");
191
+ }
192
+ console.log(`[XY File Upload] File upload successful: ${fileName} → URL=${fileUrl}`);
193
+ return fileUrl;
194
+ }
195
+ catch (error) {
196
+ console.error(`[XY File Upload] File upload with URL retrieval failed for ${filePath}:`, error);
197
+ throw error;
198
+ }
199
+ }
108
200
  /**
109
201
  * Upload multiple files and return their file IDs.
110
202
  */
@@ -302,16 +302,6 @@ b. 使用该工具之前需获取当前真实时间`,
302
302
  if (event.status === "success" && event.outputs) {
303
303
  logger.log(`[CREATE_ALARM_TOOL] ✅ Alarm creation completed successfully`);
304
304
  logger.log(`[CREATE_ALARM_TOOL] - outputs:`, JSON.stringify(event.outputs));
305
- // Check for error code in outputs
306
- const code = event.outputs.code !== undefined ? event.outputs.code : null;
307
- if (code !== null && code !== 0) {
308
- logger.error(`[CREATE_ALARM_TOOL] ❌ Device returned error`);
309
- logger.error(`[CREATE_ALARM_TOOL] - code: ${code}`);
310
- const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
311
- logger.error(`[CREATE_ALARM_TOOL] - errorMsg: ${errorMsg}`);
312
- reject(new Error(`创建闹钟失败: ${errorMsg} (错误代码: ${code})`));
313
- return;
314
- }
315
305
  // 成功,直接返回完整的 event.outputs JSON 字符串
316
306
  resolve({
317
307
  content: [
@@ -172,18 +172,7 @@ export const deleteAlarmTool = {
172
172
  if (event.status === "success" && event.outputs) {
173
173
  logger.log(`[DELETE_ALARM_TOOL] ✅ Alarm deletion completed successfully`);
174
174
  logger.log(`[DELETE_ALARM_TOOL] - outputs:`, JSON.stringify(event.outputs));
175
- // Check for error code in outputs
176
- const code = event.outputs.code !== undefined ? event.outputs.code : null;
177
- if (code !== null && code !== 0) {
178
- logger.error(`[DELETE_ALARM_TOOL] ❌ Device returned error`);
179
- logger.error(`[DELETE_ALARM_TOOL] - code: ${code}`);
180
- const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
181
- logger.error(`[DELETE_ALARM_TOOL] - errorMsg: ${errorMsg}`);
182
- reject(new Error(`删除闹钟失败: ${errorMsg} (错误代码: ${code})`));
183
- return;
184
- }
185
175
  // 成功,直接返回完整的 event.outputs JSON 字符串
186
- logger.log(`[DELETE_ALARM_TOOL] 🎉 Successfully deleted ${items.length} alarm(s)`);
187
176
  resolve({
188
177
  content: [
189
178
  {
@@ -0,0 +1,5 @@
1
+ /**
2
+ * XY Image Reading tool - performs image understanding using local or remote image URLs.
3
+ * Supports both local file paths and remote URLs.
4
+ */
5
+ export declare const imageReadingTool: any;
@@ -0,0 +1,353 @@
1
+ // Image Reading tool implementation
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
+ import { v4 as uuidv4 } from "uuid";
9
+ /**
10
+ * Check if value is a remote URL
11
+ */
12
+ function isRemoteUrl(value) {
13
+ try {
14
+ const url = new URL(value);
15
+ return url.protocol === "http:" || url.protocol === "https:";
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * Check if value is a local file path
23
+ */
24
+ async function isLocalFile(value) {
25
+ try {
26
+ const stats = await fs.stat(value);
27
+ return stats.isFile();
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ /**
34
+ * Download remote file to local temp directory
35
+ */
36
+ async function downloadRemoteFile(url) {
37
+ logger.log(`[IMAGE_READING_TOOL] 📥 Downloading remote file: ${url}`);
38
+ try {
39
+ const response = await fetch(url);
40
+ if (!response.ok) {
41
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
42
+ }
43
+ // Get filename from URL or use default
44
+ let filename = url.split("/").pop() || "downloaded_image";
45
+ filename = filename.split("?")[0];
46
+ // Ensure temp directory exists
47
+ const tempDir = "/tmp/xy_channel";
48
+ await fs.mkdir(tempDir, { recursive: true });
49
+ // Generate unique filename to avoid conflicts
50
+ const timestamp = Date.now();
51
+ const ext = path.extname(filename) || ".jpg";
52
+ const baseName = path.basename(filename, ext);
53
+ const uniqueFilename = `${baseName}_${timestamp}${ext}`;
54
+ const localPath = path.join(tempDir, uniqueFilename);
55
+ // Save file to local temp directory
56
+ const arrayBuffer = await response.arrayBuffer();
57
+ const buffer = Buffer.from(arrayBuffer);
58
+ await fs.writeFile(localPath, buffer);
59
+ logger.log(`[IMAGE_READING_TOOL] ✅ File downloaded to: ${localPath}`);
60
+ return localPath;
61
+ }
62
+ catch (error) {
63
+ logger.error(`[IMAGE_READING_TOOL] ❌ Failed to download file from ${url}:`, error);
64
+ throw new Error(`Failed to download remote file: ${error instanceof Error ? error.message : String(error)}`);
65
+ }
66
+ }
67
+ /**
68
+ * Process image input: validate and convert local file to OBS URL, keep remote URL unchanged
69
+ */
70
+ async function processImageInput(imageInput, uploadService) {
71
+ logger.log(`[IMAGE_READING_TOOL] 🔄 Processing image input: ${imageInput}`);
72
+ // Check if it's a remote URL
73
+ if (isRemoteUrl(imageInput)) {
74
+ logger.log(`[IMAGE_READING_TOOL] 🌐 Input is remote URL, downloading...`);
75
+ const localPath = await downloadRemoteFile(imageInput);
76
+ logger.log(`[IMAGE_READING_TOOL] 📤 Uploading downloaded file to OBS...`);
77
+ const imageUrl = await uploadService.uploadFileAndGetUrl(localPath, "TEMPORARY_MATERIAL_DOC");
78
+ logger.log(`[IMAGE_READING_TOOL] ✅ Uploaded to OBS: ${imageUrl}`);
79
+ return { imageUrl, localPath };
80
+ }
81
+ // Check if it's a local file
82
+ const isLocal = await isLocalFile(imageInput);
83
+ if (isLocal) {
84
+ logger.log(`[IMAGE_READING_TOOL] 📁 Input is local file, uploading...`);
85
+ const imageUrl = await uploadService.uploadFileAndGetUrl(imageInput, "TEMPORARY_MATERIAL_DOC");
86
+ logger.log(`[IMAGE_READING_TOOL] ✅ Uploaded to OBS: ${imageUrl}`);
87
+ return { imageUrl };
88
+ }
89
+ throw new Error(`Invalid image input: must be a remote URL or local file path, got: ${imageInput}`);
90
+ }
91
+ /**
92
+ * Call image understanding API with streaming response
93
+ */
94
+ async function callImageUnderstandingAPI(imageUrl, text, apiKey, uid) {
95
+ logger.log(`[IMAGE_READING_TOOL] 🧠 Calling image understanding API...`);
96
+ logger.log(`[IMAGE_READING_TOOL] - imageUrl: ${imageUrl}`);
97
+ logger.log(`[IMAGE_READING_TOOL] - prompt: ${text}`);
98
+ const apiUrl = "https://hag-drcn.op.dbankcloud.com/celia-claw/v1/sse-api/skill/execute";
99
+ const traceId = uuidv4();
100
+ const headers = {
101
+ "Content-Type": "application/json",
102
+ "Accept": "text/event-stream",
103
+ "x-hag-trace-id": traceId,
104
+ "x-api-key": apiKey,
105
+ "x-request-from": "openclaw",
106
+ "x-uid": uid,
107
+ "x-skill-id": "image_comprehension",
108
+ "x-prd-pkg-name": "com.huawei.hag",
109
+ };
110
+ const payload = {
111
+ version: "1.0",
112
+ session: {
113
+ isNew: false,
114
+ sessionId: "wangyu202410241921",
115
+ interactionId: 0,
116
+ },
117
+ endpoint: {
118
+ device: {
119
+ sid: "3df83a4a8124d7600f66206f96ea1e7e4e21c593adc4246bd20d450d8404cbf3",
120
+ deviceId: "3f35019f-ba4c-4ed5-80c0-6ddcef741200",
121
+ prdVer: "99.0.64.303",
122
+ phoneType: "WLZ-AL10",
123
+ sysVer: "HarmonyOS_2.0.0",
124
+ deviceType: 0,
125
+ timezone: "GMT+08:00",
126
+ },
127
+ locale: "zh-CN",
128
+ sysLocale: "zh",
129
+ countryCode: "CN",
130
+ },
131
+ utterance: { type: "text", original: text },
132
+ actions: [
133
+ {
134
+ actionSn: uuidv4(),
135
+ actionExecutorTask: {
136
+ pluginId: "aeac4e92c32949c1b7fc02de262615e6",
137
+ agentState: "OnShelf",
138
+ actionName: "imageUnderStandStream",
139
+ content: { imageUrl, text },
140
+ },
141
+ },
142
+ ],
143
+ };
144
+ logger.log(`[IMAGE_READING_TOOL] 📡 Sending request with trace ID: ${traceId}`);
145
+ try {
146
+ const response = await fetch(apiUrl, {
147
+ method: "POST",
148
+ headers,
149
+ body: JSON.stringify(payload),
150
+ // @ts-ignore - node-fetch supports this
151
+ timeout: 120000, // 2 minutes timeout
152
+ });
153
+ logger.log(`[IMAGE_READING_TOOL] 📨 Response status: ${response.status}`);
154
+ logger.log(`[IMAGE_READING_TOOL] 📨 Content-Type: ${response.headers.get("Content-Type")}`);
155
+ if (!response.ok) {
156
+ const errorText = await response.text();
157
+ logger.error(`[IMAGE_READING_TOOL] ❌ API request failed: ${response.status}`);
158
+ logger.error(`[IMAGE_READING_TOOL] ❌ Response: ${errorText}`);
159
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
160
+ }
161
+ // Process SSE stream
162
+ let lastCaption = "";
163
+ let lineCount = 0;
164
+ let buffer = "";
165
+ logger.log(`[IMAGE_READING_TOOL] 📖 Reading SSE stream...`);
166
+ // Read the response body as a stream
167
+ if (!response.body) {
168
+ throw new Error("Response body is null");
169
+ }
170
+ for await (const chunk of response.body) {
171
+ if (!chunk)
172
+ continue;
173
+ buffer += chunk.toString();
174
+ const lines = buffer.split("\n");
175
+ buffer = lines.pop() || "";
176
+ for (const line of lines) {
177
+ lineCount++;
178
+ const trimmedLine = line.replace(/\r$/, "");
179
+ if (!trimmedLine)
180
+ continue;
181
+ if (trimmedLine.startsWith("data:")) {
182
+ const dataContent = trimmedLine.substring(5).trim();
183
+ if (dataContent && dataContent !== "[DONE]") {
184
+ try {
185
+ const dataJson = JSON.parse(dataContent);
186
+ // Extract streamContent from abilityInfos
187
+ if (dataJson.abilityInfos && Array.isArray(dataJson.abilityInfos)) {
188
+ for (const info of dataJson.abilityInfos) {
189
+ if (info.actionExecutorResult?.reply?.streamInfo) {
190
+ const streamContent = info.actionExecutorResult.reply.streamInfo.streamContent;
191
+ if (streamContent) {
192
+ lastCaption = streamContent;
193
+ logger.log(`[IMAGE_READING_TOOL] 📝 Updated caption (length: ${streamContent.length})`);
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ catch (parseError) {
200
+ logger.warn(`[IMAGE_READING_TOOL] ⚠️ Failed to parse JSON data:`, parseError);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ logger.log(`[IMAGE_READING_TOOL] ✅ Stream processing complete`);
207
+ logger.log(`[IMAGE_READING_TOOL] - Total lines processed: ${lineCount}`);
208
+ logger.log(`[IMAGE_READING_TOOL] - Final caption length: ${lastCaption.length}`);
209
+ if (!lastCaption) {
210
+ throw new Error("No caption received from image understanding API");
211
+ }
212
+ return lastCaption;
213
+ }
214
+ catch (error) {
215
+ logger.error(`[IMAGE_READING_TOOL] ❌ API call failed:`, error);
216
+ throw error;
217
+ }
218
+ }
219
+ /**
220
+ * XY Image Reading tool - performs image understanding using local or remote image URLs.
221
+ * Supports both local file paths and remote URLs.
222
+ */
223
+ export const imageReadingTool = {
224
+ name: "image_reading",
225
+ label: "Image Reading",
226
+ description: `
227
+ 工具使用场景:
228
+ 【必须调用此工具的情况】
229
+ 1. 用户消息中包含 mediaPath 字段且不为空(表示用户发送了图片)
230
+ 2. 用户希望理解图片内容,询问图片是什么,例如:
231
+ - "这是什么?"
232
+ - "图片里有什么?"
233
+ - "帮我看看这张图"
234
+ - "描述一下这张图片"
235
+ - "分析一下这张照片"
236
+ - "这个图片是什么意思"
237
+ - "识别一下图片内容"
238
+ - 或任何关于图片内容的理解、识别、分析类询问
239
+
240
+ 当同时满足以上两个条件时,必须优先调用此工具进行图像理解。
241
+
242
+ 工具能力描述:对图片进行理解和分析,返回图片的描述内容。
243
+
244
+ 工具参数说明:
245
+ a. localUrl:本地图片文件路径(可选,通常从用户消息的 mediaPath 字段获取)
246
+ b. remoteUrl:公网图片地址(可选)
247
+ c. prompt:对图片的提示问题,默认为"描述这张图片内容",可根据用户的具体问题自定义
248
+ d. localUrl 与 remoteUrl 任意一个不为空即可,优先使用 localUrl
249
+
250
+ 注意事项:
251
+ a. 支持常见图片格式(jpg, png, gif等)
252
+ b. 远程图片会先下载到本地再处理
253
+ c. 操作超时时间为2分钟(120秒)
254
+ d. 返回图像理解的文本描述内容`,
255
+ parameters: {
256
+ type: "object",
257
+ properties: {
258
+ localUrl: {
259
+ type: "string",
260
+ description: "本地图片文件路径",
261
+ },
262
+ remoteUrl: {
263
+ type: "string",
264
+ description: "公网图片地址(HTTP/HTTPS URL)",
265
+ },
266
+ prompt: {
267
+ type: "string",
268
+ description: "对图片的提示问题,默认为'描述这张图片内容'",
269
+ },
270
+ },
271
+ },
272
+ async execute(toolCallId, params) {
273
+ logger.log(`[IMAGE_READING_TOOL] 🚀 Starting execution`);
274
+ logger.log(`[IMAGE_READING_TOOL] - toolCallId: ${toolCallId}`);
275
+ logger.log(`[IMAGE_READING_TOOL] - params:`, JSON.stringify(params));
276
+ logger.log(`[IMAGE_READING_TOOL] - timestamp: ${new Date().toISOString()}`);
277
+ // Validate that at least one parameter is provided
278
+ if (!params.localUrl && !params.remoteUrl) {
279
+ logger.error(`[IMAGE_READING_TOOL] ❌ Missing both localUrl and remoteUrl parameters`);
280
+ throw new Error("At least one of localUrl or remoteUrl must be provided");
281
+ }
282
+ // Get prompt (default to "描述这张图片内容")
283
+ const prompt = params.prompt || "描述这张图片内容";
284
+ logger.log(`[IMAGE_READING_TOOL] 📝 Using prompt: ${prompt}`);
285
+ // Get session context
286
+ logger.log(`[IMAGE_READING_TOOL] 🔍 Getting session context...`);
287
+ const sessionContext = getCurrentSessionContext();
288
+ if (!sessionContext) {
289
+ logger.error(`[IMAGE_READING_TOOL] ❌ No active session found!`);
290
+ throw new Error("No active XY session found. Image reading tool can only be used during an active conversation.");
291
+ }
292
+ logger.log(`[IMAGE_READING_TOOL] ✅ Session context found`);
293
+ const { config } = sessionContext;
294
+ // Create upload service
295
+ const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
296
+ let processedImage = null;
297
+ let downloadedFile = null;
298
+ try {
299
+ // Process image input (prefer localUrl over remoteUrl)
300
+ const imageInput = params.localUrl || params.remoteUrl;
301
+ logger.log(`[IMAGE_READING_TOOL] 🖼️ Processing image: ${imageInput}`);
302
+ processedImage = await processImageInput(imageInput, uploadService);
303
+ // Track downloaded file for cleanup
304
+ if (processedImage.localPath) {
305
+ downloadedFile = processedImage.localPath;
306
+ }
307
+ logger.log(`[IMAGE_READING_TOOL] ✅ Image processed successfully`);
308
+ logger.log(`[IMAGE_READING_TOOL] - OBS URL: ${processedImage.imageUrl}`);
309
+ // Call image understanding API
310
+ const caption = await callImageUnderstandingAPI(processedImage.imageUrl, prompt, config.apiKey, config.uid);
311
+ logger.log(`[IMAGE_READING_TOOL] 🎉 Image understanding completed successfully`);
312
+ logger.log(`[IMAGE_READING_TOOL] - Caption length: ${caption.length} characters`);
313
+ // Clean up downloaded file if any
314
+ if (downloadedFile) {
315
+ logger.log(`[IMAGE_READING_TOOL] 🧹 Cleaning up downloaded file...`);
316
+ try {
317
+ await fs.unlink(downloadedFile);
318
+ logger.log(`[IMAGE_READING_TOOL] ✅ Cleaned up: ${downloadedFile}`);
319
+ }
320
+ catch (error) {
321
+ logger.warn(`[IMAGE_READING_TOOL] ⚠️ Failed to clean up file:`, error);
322
+ }
323
+ }
324
+ return {
325
+ content: [
326
+ {
327
+ type: "text",
328
+ text: JSON.stringify({
329
+ caption,
330
+ prompt,
331
+ imageSource: params.localUrl ? "local" : "remote",
332
+ success: true,
333
+ }),
334
+ },
335
+ ],
336
+ };
337
+ }
338
+ catch (error) {
339
+ // Clean up downloaded file on error
340
+ if (downloadedFile) {
341
+ logger.log(`[IMAGE_READING_TOOL] 🧹 Cleaning up downloaded file after error...`);
342
+ try {
343
+ await fs.unlink(downloadedFile);
344
+ }
345
+ catch (cleanupError) {
346
+ logger.warn(`[IMAGE_READING_TOOL] ⚠️ Failed to clean up file:`, cleanupError);
347
+ }
348
+ }
349
+ logger.error(`[IMAGE_READING_TOOL] ❌ Execution failed:`, error);
350
+ throw error;
351
+ }
352
+ },
353
+ };
@@ -319,16 +319,6 @@ export const modifyAlarmTool = {
319
319
  if (event.status === "success" && event.outputs) {
320
320
  logger.log(`[MODIFY_ALARM_TOOL] ✅ Alarm modification completed successfully`);
321
321
  logger.log(`[MODIFY_ALARM_TOOL] - outputs:`, JSON.stringify(event.outputs));
322
- // Check for error code in outputs
323
- const code = event.outputs.code !== undefined ? event.outputs.code : null;
324
- if (code !== null && code !== 0) {
325
- logger.error(`[MODIFY_ALARM_TOOL] ❌ Device returned error`);
326
- logger.error(`[MODIFY_ALARM_TOOL] - code: ${code}`);
327
- const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
328
- logger.error(`[MODIFY_ALARM_TOOL] - errorMsg: ${errorMsg}`);
329
- reject(new Error(`修改闹钟失败: ${errorMsg} (错误代码: ${code})`));
330
- return;
331
- }
332
322
  // 成功,直接返回完整的 event.outputs JSON 字符串
333
323
  resolve({
334
324
  content: [
@@ -225,63 +225,15 @@ b. 使用该工具之前需获取当前真实时间`,
225
225
  if (event.status === "success" && event.outputs) {
226
226
  logger.log(`[SEARCH_ALARM_TOOL] ✅ Alarm search completed successfully`);
227
227
  logger.log(`[SEARCH_ALARM_TOOL] - outputs:`, JSON.stringify(event.outputs));
228
- // Check for error code in outputs
229
- const code = event.outputs.code !== undefined ? event.outputs.code : null;
230
- if (code !== null && code !== 0) {
231
- logger.error(`[SEARCH_ALARM_TOOL] ❌ Device returned error`);
232
- logger.error(`[SEARCH_ALARM_TOOL] - code: ${code}`);
233
- const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
234
- logger.error(`[SEARCH_ALARM_TOOL] - errorMsg: ${errorMsg}`);
235
- reject(new Error(`检索闹钟失败: ${errorMsg} (错误代码: ${code})`));
236
- return;
237
- }
238
- // Extract result.items with safe checks
239
- const result = event.outputs.result;
240
- let items = [];
241
- if (result && typeof result === "object" && Array.isArray(result.items)) {
242
- items = result.items;
243
- logger.log(`[SEARCH_ALARM_TOOL] 📋 Found ${items.length} alarm(s)`);
244
- // Parse JSON strings in items array
245
- // Items are returned as JSON strings that need to be parsed
246
- const parsedItems = items.map((itemStr, index) => {
247
- if (typeof itemStr !== "string") {
248
- logger.warn(`[SEARCH_ALARM_TOOL] ⚠️ Item at index ${index} is not a string:`, typeof itemStr);
249
- return null;
250
- }
251
- try {
252
- const parsed = JSON.parse(itemStr);
253
- logger.log(`[SEARCH_ALARM_TOOL] 📋 Parsed alarm [${index}]:`, JSON.stringify(parsed));
254
- return parsed;
255
- }
256
- catch (parseError) {
257
- logger.error(`[SEARCH_ALARM_TOOL] ❌ Failed to parse item at index ${index}:`, parseError);
258
- logger.error(`[SEARCH_ALARM_TOOL] - itemStr: ${itemStr}`);
259
- return null;
260
- }
261
- }).filter((item) => item !== null);
262
- logger.log(`[SEARCH_ALARM_TOOL] 🎉 Successfully parsed ${parsedItems.length} alarm(s)`);
263
- resolve({
264
- content: [
265
- {
266
- type: "text",
267
- text: JSON.stringify(parsedItems),
268
- },
269
- ],
270
- });
271
- }
272
- else {
273
- logger.warn(`[SEARCH_ALARM_TOOL] ⚠️ No items found in result or result is invalid`);
274
- logger.warn(`[SEARCH_ALARM_TOOL] - result:`, JSON.stringify(result || {}));
275
- // Return empty array
276
- resolve({
277
- content: [
278
- {
279
- type: "text",
280
- text: "[]",
281
- },
282
- ],
283
- });
284
- }
228
+ // 成功,直接返回完整的 event.outputs JSON 字符串
229
+ resolve({
230
+ content: [
231
+ {
232
+ type: "text",
233
+ text: JSON.stringify(event.outputs),
234
+ },
235
+ ],
236
+ });
285
237
  }
286
238
  else {
287
239
  logger.error(`[SEARCH_ALARM_TOOL] ❌ Alarm search failed`);
@@ -83,17 +83,6 @@ b. 使用该工具之前需获取当前真实时间
83
83
  const date = new Date(year, month, day, hours, minutes, seconds);
84
84
  return date.getTime();
85
85
  };
86
- // Helper function to convert timestamp to YYYYMMDD hhmmss format
87
- const formatTimestamp = (timestamp) => {
88
- const date = new Date(timestamp);
89
- const year = date.getFullYear();
90
- const month = String(date.getMonth() + 1).padStart(2, '0');
91
- const day = String(date.getDate()).padStart(2, '0');
92
- const hours = String(date.getHours()).padStart(2, '0');
93
- const minutes = String(date.getMinutes()).padStart(2, '0');
94
- const seconds = String(date.getSeconds()).padStart(2, '0');
95
- return `${year}${month}${day} ${hours}${minutes}${seconds}`;
96
- };
97
86
  let startTimeMs;
98
87
  let endTimeMs;
99
88
  try {
@@ -188,43 +177,14 @@ b. 使用该工具之前需获取当前真实时间
188
177
  clearTimeout(timeout);
189
178
  wsManager.off("data-event", handler);
190
179
  if (event.status === "success" && event.outputs) {
191
- logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Calendar events response received`);
180
+ logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Calendar events retrieved successfully`);
192
181
  logger.log(`[SEARCH_CALENDAR_TOOL] - outputs:`, JSON.stringify(event.outputs));
193
- // Check for error code in outputs
194
- if (event.outputs.retErrCode && event.outputs.retErrCode !== "0") {
195
- logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Device returned error`);
196
- logger.error(`[SEARCH_CALENDAR_TOOL] - retErrCode: ${event.outputs.retErrCode}`);
197
- logger.error(`[SEARCH_CALENDAR_TOOL] - errMsg: ${event.outputs.errMsg || "Unknown error"}`);
198
- reject(new Error(`检索日程失败: ${event.outputs.errMsg || "未知错误"} (错误代码: ${event.outputs.retErrCode})`));
199
- return;
200
- }
201
- // Return the result directly as requested
202
- const result = event.outputs.result;
203
- // Ensure result is not undefined
204
- if (result === undefined) {
205
- logger.warn(`[SEARCH_CALENDAR_TOOL] ⚠️ Result is undefined, returning empty result`);
206
- }
207
- // Convert dtStart and dtEnd from timestamps to YYYYMMDD hhmmss format
208
- if (result && result.items && Array.isArray(result.items)) {
209
- logger.log(`[SEARCH_CALENDAR_TOOL] 🔄 Converting timestamps to formatted dates...`);
210
- result.items = result.items.map((item) => {
211
- const formattedItem = { ...item };
212
- if (item.dtStart) {
213
- formattedItem.dtStart = formatTimestamp(item.dtStart);
214
- logger.log(`[SEARCH_CALENDAR_TOOL] - dtStart: ${item.dtStart} -> ${formattedItem.dtStart}`);
215
- }
216
- if (item.dtEnd) {
217
- formattedItem.dtEnd = formatTimestamp(item.dtEnd);
218
- logger.log(`[SEARCH_CALENDAR_TOOL] - dtEnd: ${item.dtEnd} -> ${formattedItem.dtEnd}`);
219
- }
220
- return formattedItem;
221
- });
222
- }
182
+ // 成功,直接返回完整的 event.outputs JSON 字符串
223
183
  resolve({
224
184
  content: [
225
185
  {
226
186
  type: "text",
227
- text: result !== undefined ? JSON.stringify(result) : "[]",
187
+ text: JSON.stringify(event.outputs),
228
188
  },
229
189
  ],
230
190
  });
@@ -102,35 +102,12 @@ export const searchContactTool = {
102
102
  if (event.status === "success" && event.outputs) {
103
103
  logger.log(`[SEARCH_CONTACT_TOOL] ✅ Contact search completed successfully`);
104
104
  logger.log(`[SEARCH_CONTACT_TOOL] - outputs:`, JSON.stringify(event.outputs));
105
- // Check for error code first
106
- if (event.outputs.retErrCode && event.outputs.retErrCode !== "0") {
107
- logger.error(`[SEARCH_CONTACT_TOOL] ❌ Search failed with error code: ${event.outputs.retErrCode}`);
108
- logger.error(`[SEARCH_CONTACT_TOOL] - errMsg: ${event.outputs.errMsg}`);
109
- reject(new Error(`搜索联系人失败: ${event.outputs.errMsg || '未知错误'} (错误码: ${event.outputs.retErrCode})`));
110
- return;
111
- }
112
- // Get the result
113
- const result = event.outputs.result;
114
- // Check if result exists
115
- if (!result) {
116
- logger.warn(`[SEARCH_CONTACT_TOOL] ⚠️ No result found for name "${params.name}"`);
117
- resolve({
118
- content: [
119
- {
120
- type: "text",
121
- text: JSON.stringify({ items: [], message: "未找到匹配的联系人" }),
122
- },
123
- ],
124
- });
125
- return;
126
- }
127
- logger.log(`[SEARCH_CONTACT_TOOL] 📊 Contacts found: ${result?.items?.length || 0} results for name "${params.name}"`);
128
- // Return the result with valid string content
105
+ // 成功,直接返回完整的 event.outputs JSON 字符串
129
106
  resolve({
130
107
  content: [
131
108
  {
132
109
  type: "text",
133
- text: JSON.stringify(result),
110
+ text: JSON.stringify(event.outputs),
134
111
  },
135
112
  ],
136
113
  });
@@ -114,40 +114,16 @@ export const searchFileTool = {
114
114
  clearTimeout(timeout);
115
115
  wsManager.off("data-event", handler);
116
116
  if (event.status === "success" && event.outputs) {
117
- logger.log(`[SEARCH_FILE_TOOL] ✅ File search response received`);
117
+ logger.log(`[SEARCH_FILE_TOOL] ✅ File search completed successfully`);
118
118
  logger.log(`[SEARCH_FILE_TOOL] - outputs:`, JSON.stringify(event.outputs));
119
- // Check for error code in outputs
120
- const code = event.outputs.code !== undefined ? event.outputs.code : null;
121
- if (code !== null && code !== 0) {
122
- logger.error(`[SEARCH_FILE_TOOL] ❌ Device returned error`);
123
- logger.error(`[SEARCH_FILE_TOOL] - code: ${code}`);
124
- const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
125
- logger.error(`[SEARCH_FILE_TOOL] - errorMsg: ${errorMsg}`);
126
- reject(new Error(`搜索文件失败: ${errorMsg} (错误代码: ${code})`));
127
- return;
128
- }
129
- // Extract result.items with safe checks
130
- const result = event.outputs.result;
131
- let items = [];
132
- if (result && typeof result === "object" && Array.isArray(result.items)) {
133
- items = result.items;
134
- logger.log(`[SEARCH_FILE_TOOL] 📋 Found ${items.length} file(s)`);
135
- }
136
- else {
137
- logger.warn(`[SEARCH_FILE_TOOL] ⚠️ No items found in result or result is invalid`);
138
- logger.warn(`[SEARCH_FILE_TOOL] - result:`, JSON.stringify(result || {}));
139
- }
140
- // Return items array as JSON string
141
- logger.log(`[SEARCH_FILE_TOOL] 🎉 File search completed successfully`);
142
- logger.log(`[SEARCH_FILE_TOOL] - keyword: ${params.query}`);
143
- logger.log(`[SEARCH_FILE_TOOL] - result count: ${items.length}`);
119
+ // 成功,直接返回完整的 event.outputs JSON 字符串
144
120
  resolve({
145
121
  content: [
146
122
  {
147
123
  type: "text",
148
- text: JSON.stringify(items),
149
- },
150
- ],
124
+ text: JSON.stringify(event.outputs),
125
+ }
126
+ ]
151
127
  });
152
128
  }
153
129
  else {
@@ -102,38 +102,14 @@ export const searchMessageTool = {
102
102
  clearTimeout(timeout);
103
103
  wsManager.off("data-event", handler);
104
104
  if (event.status === "success" && event.outputs) {
105
- logger.log(`[SEARCH_MESSAGE_TOOL] ✅ Message search response received`);
105
+ logger.log(`[SEARCH_MESSAGE_TOOL] ✅ Message search completed successfully`);
106
106
  logger.log(`[SEARCH_MESSAGE_TOOL] - outputs:`, JSON.stringify(event.outputs));
107
- // Check for error code in outputs
108
- const code = event.outputs.code !== undefined ? event.outputs.code : null;
109
- if (code !== null && code !== 0) {
110
- logger.error(`[SEARCH_MESSAGE_TOOL] ❌ Device returned error`);
111
- logger.error(`[SEARCH_MESSAGE_TOOL] - code: ${code}`);
112
- const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
113
- logger.error(`[SEARCH_MESSAGE_TOOL] - errorMsg: ${errorMsg}`);
114
- reject(new Error(`搜索短信失败: ${errorMsg} (错误代码: ${code})`));
115
- return;
116
- }
117
- // Extract result.items with safe checks
118
- const result = event.outputs.result;
119
- let items = [];
120
- if (result && typeof result === "object" && Array.isArray(result.items)) {
121
- items = result.items;
122
- logger.log(`[SEARCH_MESSAGE_TOOL] 📋 Found ${items.length} message(s)`);
123
- }
124
- else {
125
- logger.warn(`[SEARCH_MESSAGE_TOOL] ⚠️ No items found in result or result is invalid`);
126
- logger.warn(`[SEARCH_MESSAGE_TOOL] - result:`, JSON.stringify(result || {}));
127
- }
128
- // Return items array as JSON string
129
- logger.log(`[SEARCH_MESSAGE_TOOL] 🎉 Message search completed successfully`);
130
- logger.log(`[SEARCH_MESSAGE_TOOL] - keyword: ${params.content}`);
131
- logger.log(`[SEARCH_MESSAGE_TOOL] - result count: ${items.length}`);
107
+ // 成功,直接返回完整的 event.outputs JSON 字符串
132
108
  resolve({
133
109
  content: [
134
110
  {
135
111
  type: "text",
136
- text: JSON.stringify(items),
112
+ text: JSON.stringify(event.outputs),
137
113
  },
138
114
  ],
139
115
  });
@@ -21,19 +21,28 @@ export const searchNoteTool = {
21
21
  required: ["query"],
22
22
  },
23
23
  async execute(toolCallId, params) {
24
- logger.debug("Executing search note tool, toolCallId:", toolCallId);
24
+ logger.log(`[SEARCH_NOTE_TOOL] 🚀 Starting execution`);
25
+ logger.log(`[SEARCH_NOTE_TOOL] - toolCallId: ${toolCallId}`);
26
+ logger.log(`[SEARCH_NOTE_TOOL] - params:`, JSON.stringify(params));
27
+ logger.log(`[SEARCH_NOTE_TOOL] - timestamp: ${new Date().toISOString()}`);
25
28
  // Validate parameters
26
29
  if (!params.query) {
30
+ logger.error(`[SEARCH_NOTE_TOOL] ❌ Missing required parameter: query`);
27
31
  throw new Error("Missing required parameter: query is required");
28
32
  }
29
33
  // Get session context
34
+ logger.log(`[SEARCH_NOTE_TOOL] 🔍 Attempting to get session context...`);
30
35
  const sessionContext = getCurrentSessionContext();
31
36
  if (!sessionContext) {
37
+ logger.error(`[SEARCH_NOTE_TOOL] ❌ FAILED: No active session found!`);
32
38
  throw new Error("No active XY session found. Search note tool can only be used during an active conversation.");
33
39
  }
40
+ logger.log(`[SEARCH_NOTE_TOOL] ✅ Session context found`);
34
41
  const { config, sessionId, taskId, messageId } = sessionContext;
35
42
  // Get WebSocket manager
43
+ logger.log(`[SEARCH_NOTE_TOOL] 🔌 Getting WebSocket manager...`);
36
44
  const wsManager = getXYWebSocketManager(config);
45
+ logger.log(`[SEARCH_NOTE_TOOL] ✅ WebSocket manager obtained`);
37
46
  // Build SearchNote command
38
47
  const command = {
39
48
  header: {
@@ -68,59 +77,57 @@ export const searchNoteTool = {
68
77
  },
69
78
  };
70
79
  // Send command and wait for response (60 second timeout)
80
+ logger.log(`[SEARCH_NOTE_TOOL] ⏳ Setting up promise to wait for note search response...`);
81
+ logger.log(`[SEARCH_NOTE_TOOL] - Timeout: 60 seconds`);
71
82
  return new Promise((resolve, reject) => {
72
83
  const timeout = setTimeout(() => {
84
+ logger.error(`[SEARCH_NOTE_TOOL] ⏰ Timeout: No response received within 60 seconds`);
73
85
  wsManager.off("data-event", handler);
74
86
  reject(new Error("搜索备忘录超时(60秒)"));
75
87
  }, 60000);
76
88
  // Listen for data events from WebSocket
77
89
  const handler = (event) => {
78
- logger.debug("Received data event:", event);
90
+ logger.log(`[SEARCH_NOTE_TOOL] 📨 Received data event:`, JSON.stringify(event));
79
91
  if (event.intentName === "SearchNote") {
92
+ logger.log(`[SEARCH_NOTE_TOOL] 🎯 SearchNote event received`);
93
+ logger.log(`[SEARCH_NOTE_TOOL] - status: ${event.status}`);
80
94
  clearTimeout(timeout);
81
95
  wsManager.off("data-event", handler);
82
96
  if (event.status === "success" && event.outputs) {
83
- const { result, code } = event.outputs;
84
- const items = result?.items || [];
85
- logger.log(`Notes found: ${items.length} results for query "${params.query}"`);
97
+ logger.log(`[SEARCH_NOTE_TOOL] Note search completed successfully`);
98
+ logger.log(`[SEARCH_NOTE_TOOL] - outputs:`, JSON.stringify(event.outputs));
99
+ // 成功,直接返回完整的 event.outputs JSON 字符串
86
100
  resolve({
87
101
  content: [
88
102
  {
89
103
  type: "text",
90
- text: JSON.stringify({
91
- success: true,
92
- query: params.query,
93
- totalResults: items.length,
94
- notes: items.map((item) => ({
95
- entityId: item.entityId,
96
- entityName: item.entityName,
97
- title: item.title?.replace(/<\/?em>/g, ''), // Remove <em> tags
98
- content: item.content,
99
- createdDate: item.createdDate,
100
- modifiedDate: item.modifiedDate,
101
- })),
102
- indexName: result?.indexName,
103
- code,
104
- }),
104
+ text: JSON.stringify(event.outputs),
105
105
  },
106
106
  ],
107
107
  });
108
108
  }
109
109
  else {
110
+ logger.error(`[SEARCH_NOTE_TOOL] ❌ Note search failed`);
111
+ logger.error(`[SEARCH_NOTE_TOOL] - status: ${event.status}`);
110
112
  reject(new Error(`搜索备忘录失败: ${event.status}`));
111
113
  }
112
114
  }
113
115
  };
114
116
  // Register event handler
117
+ logger.log(`[SEARCH_NOTE_TOOL] 📡 Registering data-event handler on WebSocket manager`);
115
118
  wsManager.on("data-event", handler);
116
119
  // Send the command
120
+ logger.log(`[SEARCH_NOTE_TOOL] 📤 Sending SearchNote command...`);
117
121
  sendCommand({
118
122
  config,
119
123
  sessionId,
120
124
  taskId,
121
125
  messageId,
122
126
  command,
127
+ }).then(() => {
128
+ logger.log(`[SEARCH_NOTE_TOOL] ✅ Command sent successfully, waiting for response...`);
123
129
  }).catch((error) => {
130
+ logger.error(`[SEARCH_NOTE_TOOL] ❌ Failed to send command:`, error);
124
131
  clearTimeout(timeout);
125
132
  wsManager.off("data-event", handler);
126
133
  reject(error);
@@ -91,33 +91,14 @@ export const searchPhotoGalleryTool = {
91
91
  logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] ✅ WebSocket manager obtained`);
92
92
  // Search for photos
93
93
  logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 📸 Searching for photos...`);
94
- const items = await searchPhotos(wsManager, config, sessionId, taskId, messageId, params.query);
95
- if (!items || items.length === 0) {
96
- logger.warn(`[SEARCH_PHOTO_GALLERY_TOOL] ⚠️ No photos found for query: ${params.query}`);
97
- return {
98
- content: [
99
- {
100
- type: "text",
101
- text: JSON.stringify({
102
- items: [],
103
- count: 0,
104
- message: "未找到匹配的照片"
105
- }),
106
- },
107
- ],
108
- };
109
- }
110
- logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] ✅ Found ${items.length} photos`);
111
- logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - items:`, JSON.stringify(items));
94
+ const outputs = await searchPhotos(wsManager, config, sessionId, taskId, messageId, params.query);
95
+ logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] Photo search completed successfully`);
96
+ logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - outputs:`, JSON.stringify(outputs));
112
97
  return {
113
98
  content: [
114
99
  {
115
100
  type: "text",
116
- text: JSON.stringify({
117
- items,
118
- count: items.length,
119
- message: `找到 ${items.length} 张照片。注意:mediaUri 和 thumbnailUri 是本地路径,无法直接访问。如需下载或查看,请使用 upload_photo 工具。`
120
- }),
101
+ text: JSON.stringify(outputs),
121
102
  },
122
103
  ],
123
104
  };
@@ -125,7 +106,7 @@ export const searchPhotoGalleryTool = {
125
106
  };
126
107
  /**
127
108
  * Search for photos using query description
128
- * Returns array of photo items with complete information
109
+ * Returns complete event.outputs object
129
110
  */
130
111
  async function searchPhotos(wsManager, config, sessionId, taskId, messageId, query) {
131
112
  logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 📦 Building SearchPhotoVideo command...`);
@@ -177,10 +158,9 @@ async function searchPhotos(wsManager, config, sessionId, taskId, messageId, que
177
158
  wsManager.off("data-event", handler);
178
159
  if (event.status === "success" && event.outputs) {
179
160
  logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] ✅ Photo search completed successfully`);
180
- const result = event.outputs.result;
181
- const items = result?.items || [];
182
- logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 📊 Found ${items.length} photo items`);
183
- resolve(items);
161
+ logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - outputs:`, JSON.stringify(event.outputs));
162
+ // 成功,直接返回完整的 event.outputs
163
+ resolve(event.outputs);
184
164
  }
185
165
  else {
186
166
  logger.error(`[SEARCH_PHOTO_GALLERY_TOOL] ❌ Photo search failed`);
@@ -128,30 +128,17 @@ export const sendMessageTool = {
128
128
  clearTimeout(timeout);
129
129
  wsManager.off("data-event", handler);
130
130
  if (event.status === "success" && event.outputs) {
131
- logger.log(`[SEND_MESSAGE_TOOL] ✅ Send message response received`);
132
- logger.log(`[SEND_MESSAGE_TOOL] - outputs:`, JSON.stringify(event.outputs));
133
- // Check for error code in outputs
134
- const code = event.outputs.code !== undefined ? event.outputs.code : null;
135
- if (code !== null && code !== 0) {
136
- logger.error(`[SEND_MESSAGE_TOOL] ❌ Device returned error`);
137
- logger.error(`[SEND_MESSAGE_TOOL] - code: ${code}`);
138
- const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
139
- logger.error(`[SEND_MESSAGE_TOOL] - errorMsg: ${errorMsg}`);
140
- reject(new Error(`发送短信失败: ${errorMsg} (错误代码: ${code})`));
141
- return;
142
- }
143
- // Extract result with safe checks
144
- const result = event.outputs.result || {};
145
- logger.log(`[SEND_MESSAGE_TOOL] 🎉 Message sent successfully`);
131
+ logger.log(`[SEND_MESSAGE_TOOL] ✅ Message sent successfully`);
146
132
  logger.log(`[SEND_MESSAGE_TOOL] - phoneNumber: ${phoneNumber}`);
147
- logger.log(`[SEND_MESSAGE_TOOL] - result:`, JSON.stringify(result));
133
+ logger.log(`[SEND_MESSAGE_TOOL] - outputs:`, JSON.stringify(event.outputs));
134
+ // 成功,直接返回完整的 event.outputs JSON 字符串
148
135
  resolve({
149
136
  content: [
150
137
  {
151
138
  type: "text",
152
- text: JSON.stringify(result),
153
- },
154
- ],
139
+ text: JSON.stringify(event.outputs),
140
+ }
141
+ ]
155
142
  });
156
143
  }
157
144
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.46-beta",
3
+ "version": "0.0.48-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",