@ynhcj/xiaoyi-channel 0.0.11-beta → 0.0.12-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ 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
11
  import { searchContactTool } from "./tools/search-contact-tool.js";
12
+ import { searchPhotoTool } from "./tools/search-photo-tool.js";
12
13
  /**
13
14
  * Xiaoyi Channel Plugin for OpenClaw.
14
15
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -48,7 +49,7 @@ export const xyPlugin = {
48
49
  },
49
50
  outbound: xyOutbound,
50
51
  onboarding: xyOnboardingAdapter,
51
- agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool],
52
+ agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoTool],
52
53
  messaging: {
53
54
  normalizeTarget: (raw) => {
54
55
  const trimmed = raw.trim();
@@ -174,15 +174,27 @@ export const searchCalendarTool = {
174
174
  clearTimeout(timeout);
175
175
  wsManager.off("data-event", handler);
176
176
  if (event.status === "success" && event.outputs) {
177
- logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Calendar events retrieved successfully`);
177
+ logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Calendar events response received`);
178
178
  logger.log(`[SEARCH_CALENDAR_TOOL] - outputs:`, JSON.stringify(event.outputs));
179
+ // Check for error code in outputs
180
+ if (event.outputs.retErrCode && event.outputs.retErrCode !== "0") {
181
+ logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Device returned error`);
182
+ logger.error(`[SEARCH_CALENDAR_TOOL] - retErrCode: ${event.outputs.retErrCode}`);
183
+ logger.error(`[SEARCH_CALENDAR_TOOL] - errMsg: ${event.outputs.errMsg || "Unknown error"}`);
184
+ reject(new Error(`检索日程失败: ${event.outputs.errMsg || "未知错误"} (错误代码: ${event.outputs.retErrCode})`));
185
+ return;
186
+ }
179
187
  // Return the result directly as requested
180
188
  const result = event.outputs.result;
189
+ // Ensure result is not undefined
190
+ if (result === undefined) {
191
+ logger.warn(`[SEARCH_CALENDAR_TOOL] ⚠️ Result is undefined, returning empty result`);
192
+ }
181
193
  resolve({
182
194
  content: [
183
195
  {
184
196
  type: "text",
185
- text: JSON.stringify(result),
197
+ text: result !== undefined ? JSON.stringify(result) : "[]",
186
198
  },
187
199
  ],
188
200
  });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * XY search photo tool - searches photos in user's gallery.
3
+ * Returns publicly accessible URLs of matching photos based on query description.
4
+ *
5
+ * This tool performs a two-step operation:
6
+ * 1. Search for photos using query description
7
+ * 2. Upload found photos to get publicly accessible URLs
8
+ */
9
+ export declare const searchPhotoTool: any;
@@ -0,0 +1,270 @@
1
+ import { getXYWebSocketManager } from "../client.js";
2
+ import { sendCommand } from "../formatter.js";
3
+ import { getLatestSessionContext } from "./session-manager.js";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * XY search photo tool - searches photos in user's gallery.
7
+ * Returns publicly accessible URLs of matching photos based on query description.
8
+ *
9
+ * This tool performs a two-step operation:
10
+ * 1. Search for photos using query description
11
+ * 2. Upload found photos to get publicly accessible URLs
12
+ */
13
+ export const searchPhotoTool = {
14
+ name: "search_photo",
15
+ label: "Search Photo",
16
+ description: "搜索用户手机图库中的照片。根据图像描述语料检索匹配的照片,并返回照片的可公网访问URL。注意:操作超时时间为120秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
17
+ parameters: {
18
+ type: "object",
19
+ properties: {
20
+ query: {
21
+ type: "string",
22
+ description: "图像描述语料,用于检索匹配的照片(例如:'小狗的照片'、'带有键盘的图片'等)",
23
+ },
24
+ },
25
+ required: ["query"],
26
+ },
27
+ async execute(toolCallId, params) {
28
+ logger.log(`[SEARCH_PHOTO_TOOL] 🚀 Starting execution`);
29
+ logger.log(`[SEARCH_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
30
+ logger.log(`[SEARCH_PHOTO_TOOL] - params:`, JSON.stringify(params));
31
+ logger.log(`[SEARCH_PHOTO_TOOL] - timestamp: ${new Date().toISOString()}`);
32
+ // Validate parameters
33
+ if (!params.query) {
34
+ logger.error(`[SEARCH_PHOTO_TOOL] ❌ Missing required parameter: query`);
35
+ throw new Error("Missing required parameter: query is required");
36
+ }
37
+ // Get session context
38
+ logger.log(`[SEARCH_PHOTO_TOOL] 🔍 Attempting to get session context...`);
39
+ const sessionContext = getLatestSessionContext();
40
+ if (!sessionContext) {
41
+ logger.error(`[SEARCH_PHOTO_TOOL] ❌ FAILED: No active session found!`);
42
+ logger.error(`[SEARCH_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
43
+ throw new Error("No active XY session found. Search photo tool can only be used during an active conversation.");
44
+ }
45
+ logger.log(`[SEARCH_PHOTO_TOOL] ✅ Session context found`);
46
+ logger.log(`[SEARCH_PHOTO_TOOL] - sessionId: ${sessionContext.sessionId}`);
47
+ logger.log(`[SEARCH_PHOTO_TOOL] - taskId: ${sessionContext.taskId}`);
48
+ logger.log(`[SEARCH_PHOTO_TOOL] - messageId: ${sessionContext.messageId}`);
49
+ const { config, sessionId, taskId, messageId } = sessionContext;
50
+ // Get WebSocket manager
51
+ logger.log(`[SEARCH_PHOTO_TOOL] 🔌 Getting WebSocket manager...`);
52
+ const wsManager = getXYWebSocketManager(config);
53
+ logger.log(`[SEARCH_PHOTO_TOOL] ✅ WebSocket manager obtained`);
54
+ // Step 1: Search for photos
55
+ logger.log(`[SEARCH_PHOTO_TOOL] 📸 STEP 1: Searching for photos...`);
56
+ const mediaUris = await searchPhotos(wsManager, config, sessionId, taskId, messageId, params.query);
57
+ if (!mediaUris || mediaUris.length === 0) {
58
+ logger.warn(`[SEARCH_PHOTO_TOOL] ⚠️ No photos found for query: ${params.query}`);
59
+ return {
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: JSON.stringify({ imageUrls: [], message: "未找到匹配的照片" }),
64
+ },
65
+ ],
66
+ };
67
+ }
68
+ logger.log(`[SEARCH_PHOTO_TOOL] ✅ Found ${mediaUris.length} photos`);
69
+ logger.log(`[SEARCH_PHOTO_TOOL] - mediaUris:`, JSON.stringify(mediaUris));
70
+ // Step 2: Get public URLs for the photos
71
+ logger.log(`[SEARCH_PHOTO_TOOL] 🌐 STEP 2: Getting public URLs for photos...`);
72
+ const imageUrls = await getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris);
73
+ logger.log(`[SEARCH_PHOTO_TOOL] 🎉 Successfully retrieved ${imageUrls.length} photo URLs`);
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: JSON.stringify({ imageUrls }),
79
+ },
80
+ ],
81
+ };
82
+ },
83
+ };
84
+ /**
85
+ * Step 1: Search for photos using query description
86
+ * Returns array of mediaUri strings
87
+ */
88
+ async function searchPhotos(wsManager, config, sessionId, taskId, messageId, query) {
89
+ logger.log(`[SEARCH_PHOTO_TOOL] 📦 Building SearchPhotoVideo command...`);
90
+ const command = {
91
+ header: {
92
+ namespace: "Common",
93
+ name: "Action",
94
+ },
95
+ payload: {
96
+ cardParam: {},
97
+ executeParam: {
98
+ executeMode: "background",
99
+ intentName: "SearchPhotoVideo",
100
+ bundleName: "com.huawei.hmos.aidispatchservice",
101
+ needUnlock: true,
102
+ actionResponse: true,
103
+ appType: "OHOS_APP",
104
+ timeOut: 5,
105
+ intentParam: {
106
+ query: query,
107
+ },
108
+ permissionId: [],
109
+ achieveType: "INTENT",
110
+ },
111
+ responses: [
112
+ {
113
+ resultCode: "",
114
+ displayText: "",
115
+ ttsText: "",
116
+ },
117
+ ],
118
+ needUploadResult: true,
119
+ noHalfPage: false,
120
+ pageControlRelated: false,
121
+ },
122
+ };
123
+ return new Promise((resolve, reject) => {
124
+ const timeout = setTimeout(() => {
125
+ logger.error(`[SEARCH_PHOTO_TOOL] ⏰ Timeout: No response for SearchPhotoVideo within 60 seconds`);
126
+ wsManager.off("data-event", handler);
127
+ reject(new Error("搜索照片超时(60秒)"));
128
+ }, 60000);
129
+ const handler = (event) => {
130
+ logger.log(`[SEARCH_PHOTO_TOOL] 📨 Received data event (Step 1):`, JSON.stringify(event));
131
+ if (event.intentName === "SearchPhotoVideo") {
132
+ logger.log(`[SEARCH_PHOTO_TOOL] 🎯 SearchPhotoVideo event received`);
133
+ logger.log(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
134
+ clearTimeout(timeout);
135
+ wsManager.off("data-event", handler);
136
+ if (event.status === "success" && event.outputs) {
137
+ logger.log(`[SEARCH_PHOTO_TOOL] ✅ Photo search completed successfully`);
138
+ const result = event.outputs.result;
139
+ const items = result?.items || [];
140
+ // Extract mediaUri from each item
141
+ const mediaUris = items.map((item) => item.mediaUri).filter(Boolean);
142
+ logger.log(`[SEARCH_PHOTO_TOOL] 📊 Extracted ${mediaUris.length} mediaUris`);
143
+ resolve(mediaUris);
144
+ }
145
+ else {
146
+ logger.error(`[SEARCH_PHOTO_TOOL] ❌ Photo search failed`);
147
+ logger.error(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
148
+ reject(new Error(`搜索照片失败: ${event.status}`));
149
+ }
150
+ }
151
+ };
152
+ logger.log(`[SEARCH_PHOTO_TOOL] 📡 Registering data-event handler for SearchPhotoVideo`);
153
+ wsManager.on("data-event", handler);
154
+ logger.log(`[SEARCH_PHOTO_TOOL] 📤 Sending SearchPhotoVideo command...`);
155
+ sendCommand({
156
+ config,
157
+ sessionId,
158
+ taskId,
159
+ messageId,
160
+ command,
161
+ })
162
+ .then(() => {
163
+ logger.log(`[SEARCH_PHOTO_TOOL] ✅ SearchPhotoVideo command sent successfully`);
164
+ })
165
+ .catch((error) => {
166
+ logger.error(`[SEARCH_PHOTO_TOOL] ❌ Failed to send SearchPhotoVideo command:`, error);
167
+ clearTimeout(timeout);
168
+ wsManager.off("data-event", handler);
169
+ reject(error);
170
+ });
171
+ });
172
+ }
173
+ /**
174
+ * Step 2: Get public URLs for photos using mediaUris
175
+ * Returns array of publicly accessible image URLs
176
+ */
177
+ async function getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris) {
178
+ logger.log(`[SEARCH_PHOTO_TOOL] 📦 Building ImageUploadForClaw command...`);
179
+ // Build imageInfos array from mediaUris
180
+ const imageInfos = mediaUris.map(mediaUri => ({ mediaUri }));
181
+ const command = {
182
+ header: {
183
+ namespace: "Common",
184
+ name: "Action",
185
+ },
186
+ payload: {
187
+ cardParam: {},
188
+ executeParam: {
189
+ executeMode: "background",
190
+ intentName: "ImageUploadForClaw",
191
+ bundleName: "com.huawei.hmos.vassistant",
192
+ needUnlock: true,
193
+ actionResponse: true,
194
+ appType: "OHOS_APP",
195
+ timeOut: 5,
196
+ intentParam: {
197
+ imageInfos: imageInfos,
198
+ },
199
+ permissionId: [],
200
+ achieveType: "INTENT",
201
+ },
202
+ responses: [
203
+ {
204
+ resultCode: "",
205
+ displayText: "",
206
+ ttsText: "",
207
+ },
208
+ ],
209
+ needUploadResult: true,
210
+ noHalfPage: false,
211
+ pageControlRelated: false,
212
+ },
213
+ };
214
+ return new Promise((resolve, reject) => {
215
+ const timeout = setTimeout(() => {
216
+ logger.error(`[SEARCH_PHOTO_TOOL] ⏰ Timeout: No response for ImageUploadForClaw within 60 seconds`);
217
+ wsManager.off("data-event", handler);
218
+ reject(new Error("获取照片URL超时(60秒)"));
219
+ }, 60000);
220
+ const handler = (event) => {
221
+ logger.log(`[SEARCH_PHOTO_TOOL] 📨 Received data event (Step 2):`, JSON.stringify(event));
222
+ if (event.intentName === "ImageUploadForClaw") {
223
+ logger.log(`[SEARCH_PHOTO_TOOL] 🎯 ImageUploadForClaw event received`);
224
+ logger.log(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
225
+ clearTimeout(timeout);
226
+ wsManager.off("data-event", handler);
227
+ if (event.status === "success" && event.outputs) {
228
+ logger.log(`[SEARCH_PHOTO_TOOL] ✅ Image URL retrieval completed successfully`);
229
+ const result = event.outputs.result;
230
+ let imageUrls = result?.imageUrls || [];
231
+ // Decode Unicode escape sequences in URLs
232
+ // Replace \u003d with = and \u0026 with &
233
+ imageUrls = imageUrls.map((url) => {
234
+ const decodedUrl = url
235
+ .replace(/\\u003d/g, '=')
236
+ .replace(/\\u0026/g, '&');
237
+ logger.log(`[SEARCH_PHOTO_TOOL] 🔄 Decoded URL: ${url} -> ${decodedUrl}`);
238
+ return decodedUrl;
239
+ });
240
+ logger.log(`[SEARCH_PHOTO_TOOL] 📊 Retrieved and decoded ${imageUrls.length} image URLs`);
241
+ resolve(imageUrls);
242
+ }
243
+ else {
244
+ logger.error(`[SEARCH_PHOTO_TOOL] ❌ Image URL retrieval failed`);
245
+ logger.error(`[SEARCH_PHOTO_TOOL] - status: ${event.status}`);
246
+ reject(new Error(`获取照片URL失败: ${event.status}`));
247
+ }
248
+ }
249
+ };
250
+ logger.log(`[SEARCH_PHOTO_TOOL] 📡 Registering data-event handler for ImageUploadForClaw`);
251
+ wsManager.on("data-event", handler);
252
+ logger.log(`[SEARCH_PHOTO_TOOL] 📤 Sending ImageUploadForClaw command...`);
253
+ sendCommand({
254
+ config,
255
+ sessionId,
256
+ taskId,
257
+ messageId,
258
+ command,
259
+ })
260
+ .then(() => {
261
+ logger.log(`[SEARCH_PHOTO_TOOL] ✅ ImageUploadForClaw command sent successfully`);
262
+ })
263
+ .catch((error) => {
264
+ logger.error(`[SEARCH_PHOTO_TOOL] ❌ Failed to send ImageUploadForClaw command:`, error);
265
+ clearTimeout(timeout);
266
+ wsManager.off("data-event", handler);
267
+ reject(error);
268
+ });
269
+ });
270
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.11-beta",
3
+ "version": "0.0.12-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",