@ynhcj/xiaoyi-channel 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/channel.js +5 -1
- package/dist/src/reply-dispatcher.js +0 -5
- package/dist/src/tools/modify-note-tool.d.ts +9 -0
- package/dist/src/tools/modify-note-tool.js +163 -0
- package/dist/src/tools/search-calendar-tool.js +41 -2
- package/dist/src/tools/search-contact-tool.d.ts +5 -0
- package/dist/src/tools/search-contact-tool.js +147 -0
- package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
- package/dist/src/tools/search-photo-gallery-tool.js +175 -0
- package/dist/src/tools/search-photo-tool.d.ts +9 -0
- package/dist/src/tools/search-photo-tool.js +270 -0
- package/dist/src/tools/upload-photo-tool.d.ts +9 -0
- package/dist/src/tools/upload-photo-tool.js +213 -0
- package/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -5,8 +5,12 @@ import { xyOnboardingAdapter } from "./onboarding.js";
|
|
|
5
5
|
import { locationTool } from "./tools/location-tool.js";
|
|
6
6
|
import { noteTool } from "./tools/note-tool.js";
|
|
7
7
|
import { searchNoteTool } from "./tools/search-note-tool.js";
|
|
8
|
+
import { modifyNoteTool } from "./tools/modify-note-tool.js";
|
|
8
9
|
import { calendarTool } from "./tools/calendar-tool.js";
|
|
9
10
|
import { searchCalendarTool } from "./tools/search-calendar-tool.js";
|
|
11
|
+
import { searchContactTool } from "./tools/search-contact-tool.js";
|
|
12
|
+
import { searchPhotoGalleryTool } from "./tools/search-photo-gallery-tool.js";
|
|
13
|
+
import { uploadPhotoTool } from "./tools/upload-photo-tool.js";
|
|
10
14
|
/**
|
|
11
15
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
12
16
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -46,7 +50,7 @@ export const xyPlugin = {
|
|
|
46
50
|
},
|
|
47
51
|
outbound: xyOutbound,
|
|
48
52
|
onboarding: xyOnboardingAdapter,
|
|
49
|
-
agentTools: [locationTool, noteTool, searchNoteTool, calendarTool, searchCalendarTool],
|
|
53
|
+
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool],
|
|
50
54
|
messaging: {
|
|
51
55
|
normalizeTarget: (raw) => {
|
|
52
56
|
const trimmed = raw.trim();
|
|
@@ -273,11 +273,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
273
273
|
const text = payload.text ?? "";
|
|
274
274
|
const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
|
|
275
275
|
log(`[PARTIAL REPLY] 📝 Partial reply chunk received: session=${sessionId}, taskId=${taskId}`);
|
|
276
|
-
log(`[PARTIAL REPLY] - text.length=${text.length}`);
|
|
277
|
-
log(`[PARTIAL REPLY] - hasMedia=${hasMedia}`);
|
|
278
|
-
if (text.length > 0) {
|
|
279
|
-
log(`[PARTIAL REPLY] - text preview: "${text.slice(0, 200)}"`);
|
|
280
|
-
}
|
|
281
276
|
try {
|
|
282
277
|
if (text.length > 0) {
|
|
283
278
|
await sendReasoningTextUpdate({
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XY modify note tool - appends content to an existing note on user's device.
|
|
3
|
+
* Requires entityId from search_notes tool as prerequisite.
|
|
4
|
+
*
|
|
5
|
+
* Prerequisites:
|
|
6
|
+
* 1. Call search_notes tool first to get the entityId of target note
|
|
7
|
+
* 2. Use the entityId to append content to that note
|
|
8
|
+
*/
|
|
9
|
+
export declare const modifyNoteTool: any;
|
|
@@ -0,0 +1,163 @@
|
|
|
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 modify note tool - appends content to an existing note on user's device.
|
|
7
|
+
* Requires entityId from search_notes tool as prerequisite.
|
|
8
|
+
*
|
|
9
|
+
* Prerequisites:
|
|
10
|
+
* 1. Call search_notes tool first to get the entityId of target note
|
|
11
|
+
* 2. Use the entityId to append content to that note
|
|
12
|
+
*/
|
|
13
|
+
export const modifyNoteTool = {
|
|
14
|
+
name: "modify_note",
|
|
15
|
+
label: "Modify Note",
|
|
16
|
+
description: "在指定备忘录中追加新内容。使用前必须先调用 search_notes 工具获取备忘录的 entityId。参数说明:entityId 是备忘录的唯一标识符(从 search_notes 工具获取),text 是要追加的文本内容。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
|
|
17
|
+
parameters: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
entityId: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "备忘录的唯一标识符,必须先通过 search_notes 工具获取",
|
|
23
|
+
},
|
|
24
|
+
text: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "要追加到备忘录的文本内容",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ["entityId", "text"],
|
|
30
|
+
},
|
|
31
|
+
async execute(toolCallId, params) {
|
|
32
|
+
logger.log(`[MODIFY_NOTE_TOOL] 🚀 Starting execution`);
|
|
33
|
+
logger.log(`[MODIFY_NOTE_TOOL] - toolCallId: ${toolCallId}`);
|
|
34
|
+
logger.log(`[MODIFY_NOTE_TOOL] - params:`, JSON.stringify(params));
|
|
35
|
+
logger.log(`[MODIFY_NOTE_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
36
|
+
// Validate parameters
|
|
37
|
+
if (!params.entityId || !params.text) {
|
|
38
|
+
logger.error(`[MODIFY_NOTE_TOOL] ❌ Missing required parameters`);
|
|
39
|
+
throw new Error("Missing required parameters: entityId and text are required");
|
|
40
|
+
}
|
|
41
|
+
// Get session context
|
|
42
|
+
logger.log(`[MODIFY_NOTE_TOOL] 🔍 Attempting to get session context...`);
|
|
43
|
+
const sessionContext = getLatestSessionContext();
|
|
44
|
+
if (!sessionContext) {
|
|
45
|
+
logger.error(`[MODIFY_NOTE_TOOL] ❌ FAILED: No active session found!`);
|
|
46
|
+
logger.error(`[MODIFY_NOTE_TOOL] - toolCallId: ${toolCallId}`);
|
|
47
|
+
throw new Error("No active XY session found. Modify note tool can only be used during an active conversation.");
|
|
48
|
+
}
|
|
49
|
+
logger.log(`[MODIFY_NOTE_TOOL] ✅ Session context found`);
|
|
50
|
+
logger.log(`[MODIFY_NOTE_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
51
|
+
logger.log(`[MODIFY_NOTE_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
52
|
+
logger.log(`[MODIFY_NOTE_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
53
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
54
|
+
// Get WebSocket manager
|
|
55
|
+
logger.log(`[MODIFY_NOTE_TOOL] 🔌 Getting WebSocket manager...`);
|
|
56
|
+
const wsManager = getXYWebSocketManager(config);
|
|
57
|
+
logger.log(`[MODIFY_NOTE_TOOL] ✅ WebSocket manager obtained`);
|
|
58
|
+
// Build ModifyNote command
|
|
59
|
+
logger.log(`[MODIFY_NOTE_TOOL] 📦 Building ModifyNote command...`);
|
|
60
|
+
const command = {
|
|
61
|
+
header: {
|
|
62
|
+
namespace: "Common",
|
|
63
|
+
name: "Action",
|
|
64
|
+
},
|
|
65
|
+
payload: {
|
|
66
|
+
cardParam: {},
|
|
67
|
+
executeParam: {
|
|
68
|
+
executeMode: "background",
|
|
69
|
+
intentName: "ModifyNote",
|
|
70
|
+
bundleName: "com.huawei.hmos.notepad",
|
|
71
|
+
needUnlock: true,
|
|
72
|
+
actionResponse: true,
|
|
73
|
+
appType: "OHOS_APP",
|
|
74
|
+
timeOut: 5,
|
|
75
|
+
intentParam: {
|
|
76
|
+
contentType: "1", // 1 = append mode (追加模式)
|
|
77
|
+
text: params.text,
|
|
78
|
+
entityId: params.entityId,
|
|
79
|
+
},
|
|
80
|
+
permissionId: [],
|
|
81
|
+
achieveType: "INTENT",
|
|
82
|
+
},
|
|
83
|
+
responses: [
|
|
84
|
+
{
|
|
85
|
+
resultCode: "",
|
|
86
|
+
displayText: "",
|
|
87
|
+
ttsText: "",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
needUploadResult: true,
|
|
91
|
+
noHalfPage: false,
|
|
92
|
+
pageControlRelated: false,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
logger.log(`[MODIFY_NOTE_TOOL] - entityId: ${params.entityId}`);
|
|
96
|
+
logger.log(`[MODIFY_NOTE_TOOL] - contentType: 1 (append mode)`);
|
|
97
|
+
logger.log(`[MODIFY_NOTE_TOOL] - text length: ${params.text.length} characters`);
|
|
98
|
+
// Send command and wait for response (60 second timeout)
|
|
99
|
+
logger.log(`[MODIFY_NOTE_TOOL] ⏳ Setting up promise to wait for note modification response...`);
|
|
100
|
+
logger.log(`[MODIFY_NOTE_TOOL] - Timeout: 60 seconds`);
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const timeout = setTimeout(() => {
|
|
103
|
+
logger.error(`[MODIFY_NOTE_TOOL] ⏰ Timeout: No response received within 60 seconds`);
|
|
104
|
+
wsManager.off("data-event", handler);
|
|
105
|
+
reject(new Error("修改备忘录超时(60秒)"));
|
|
106
|
+
}, 60000);
|
|
107
|
+
// Listen for data events from WebSocket
|
|
108
|
+
const handler = (event) => {
|
|
109
|
+
logger.log(`[MODIFY_NOTE_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
110
|
+
if (event.intentName === "ModifyNote") {
|
|
111
|
+
logger.log(`[MODIFY_NOTE_TOOL] 🎯 ModifyNote event received`);
|
|
112
|
+
logger.log(`[MODIFY_NOTE_TOOL] - status: ${event.status}`);
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
wsManager.off("data-event", handler);
|
|
115
|
+
if (event.status === "success" && event.outputs) {
|
|
116
|
+
logger.log(`[MODIFY_NOTE_TOOL] ✅ Note modified successfully`);
|
|
117
|
+
logger.log(`[MODIFY_NOTE_TOOL] - outputs:`, JSON.stringify(event.outputs));
|
|
118
|
+
// Return the result directly as requested
|
|
119
|
+
const result = event.outputs.result;
|
|
120
|
+
logger.log(`[MODIFY_NOTE_TOOL] 📝 Note updated:`);
|
|
121
|
+
logger.log(`[MODIFY_NOTE_TOOL] - entityId: ${result?.entityId}`);
|
|
122
|
+
logger.log(`[MODIFY_NOTE_TOOL] - title: ${result?.title}`);
|
|
123
|
+
logger.log(`[MODIFY_NOTE_TOOL] - modifiedDate: ${result?.modifiedDate}`);
|
|
124
|
+
resolve({
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: "text",
|
|
128
|
+
text: JSON.stringify(result),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
logger.error(`[MODIFY_NOTE_TOOL] ❌ Note modification failed`);
|
|
135
|
+
logger.error(`[MODIFY_NOTE_TOOL] - status: ${event.status}`);
|
|
136
|
+
reject(new Error(`修改备忘录失败: ${event.status}`));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
// Register event handler
|
|
141
|
+
logger.log(`[MODIFY_NOTE_TOOL] 📡 Registering data-event handler on WebSocket manager`);
|
|
142
|
+
wsManager.on("data-event", handler);
|
|
143
|
+
// Send the command
|
|
144
|
+
logger.log(`[MODIFY_NOTE_TOOL] 📤 Sending ModifyNote command...`);
|
|
145
|
+
sendCommand({
|
|
146
|
+
config,
|
|
147
|
+
sessionId,
|
|
148
|
+
taskId,
|
|
149
|
+
messageId,
|
|
150
|
+
command,
|
|
151
|
+
})
|
|
152
|
+
.then(() => {
|
|
153
|
+
logger.log(`[MODIFY_NOTE_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
154
|
+
})
|
|
155
|
+
.catch((error) => {
|
|
156
|
+
logger.error(`[MODIFY_NOTE_TOOL] ❌ Failed to send command:`, error);
|
|
157
|
+
clearTimeout(timeout);
|
|
158
|
+
wsManager.off("data-event", handler);
|
|
159
|
+
reject(error);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
};
|
|
@@ -80,6 +80,17 @@ export const searchCalendarTool = {
|
|
|
80
80
|
const date = new Date(year, month, day, hours, minutes, seconds);
|
|
81
81
|
return date.getTime();
|
|
82
82
|
};
|
|
83
|
+
// Helper function to convert timestamp to YYYYMMDD hhmmss format
|
|
84
|
+
const formatTimestamp = (timestamp) => {
|
|
85
|
+
const date = new Date(timestamp);
|
|
86
|
+
const year = date.getFullYear();
|
|
87
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
88
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
89
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
90
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
91
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
92
|
+
return `${year}${month}${day} ${hours}${minutes}${seconds}`;
|
|
93
|
+
};
|
|
83
94
|
let startTimeMs;
|
|
84
95
|
let endTimeMs;
|
|
85
96
|
try {
|
|
@@ -174,15 +185,43 @@ export const searchCalendarTool = {
|
|
|
174
185
|
clearTimeout(timeout);
|
|
175
186
|
wsManager.off("data-event", handler);
|
|
176
187
|
if (event.status === "success" && event.outputs) {
|
|
177
|
-
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Calendar events
|
|
188
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Calendar events response received`);
|
|
178
189
|
logger.log(`[SEARCH_CALENDAR_TOOL] - outputs:`, JSON.stringify(event.outputs));
|
|
190
|
+
// Check for error code in outputs
|
|
191
|
+
if (event.outputs.retErrCode && event.outputs.retErrCode !== "0") {
|
|
192
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Device returned error`);
|
|
193
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] - retErrCode: ${event.outputs.retErrCode}`);
|
|
194
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] - errMsg: ${event.outputs.errMsg || "Unknown error"}`);
|
|
195
|
+
reject(new Error(`检索日程失败: ${event.outputs.errMsg || "未知错误"} (错误代码: ${event.outputs.retErrCode})`));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
179
198
|
// Return the result directly as requested
|
|
180
199
|
const result = event.outputs.result;
|
|
200
|
+
// Ensure result is not undefined
|
|
201
|
+
if (result === undefined) {
|
|
202
|
+
logger.warn(`[SEARCH_CALENDAR_TOOL] ⚠️ Result is undefined, returning empty result`);
|
|
203
|
+
}
|
|
204
|
+
// Convert dtStart and dtEnd from timestamps to YYYYMMDD hhmmss format
|
|
205
|
+
if (result && result.items && Array.isArray(result.items)) {
|
|
206
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🔄 Converting timestamps to formatted dates...`);
|
|
207
|
+
result.items = result.items.map((item) => {
|
|
208
|
+
const formattedItem = { ...item };
|
|
209
|
+
if (item.dtStart) {
|
|
210
|
+
formattedItem.dtStart = formatTimestamp(item.dtStart);
|
|
211
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - dtStart: ${item.dtStart} -> ${formattedItem.dtStart}`);
|
|
212
|
+
}
|
|
213
|
+
if (item.dtEnd) {
|
|
214
|
+
formattedItem.dtEnd = formatTimestamp(item.dtEnd);
|
|
215
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - dtEnd: ${item.dtEnd} -> ${formattedItem.dtEnd}`);
|
|
216
|
+
}
|
|
217
|
+
return formattedItem;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
181
220
|
resolve({
|
|
182
221
|
content: [
|
|
183
222
|
{
|
|
184
223
|
type: "text",
|
|
185
|
-
text: JSON.stringify(result),
|
|
224
|
+
text: result !== undefined ? JSON.stringify(result) : "[]",
|
|
186
225
|
},
|
|
187
226
|
],
|
|
188
227
|
});
|
|
@@ -0,0 +1,147 @@
|
|
|
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 contact tool - searches contacts on user's device.
|
|
7
|
+
* Returns matching contact information based on name.
|
|
8
|
+
*/
|
|
9
|
+
export const searchContactTool = {
|
|
10
|
+
name: "search_contact",
|
|
11
|
+
label: "Search Contact",
|
|
12
|
+
description: "搜索用户设备上的联系人信息。根据姓名在通讯录中检索联系人详细信息(包括姓名、电话号码、邮箱、组织、职位等)。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
name: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "联系人姓名,用于在通讯录中检索联系人信息",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ["name"],
|
|
22
|
+
},
|
|
23
|
+
async execute(toolCallId, params) {
|
|
24
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 🚀 Starting execution`);
|
|
25
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - toolCallId: ${toolCallId}`);
|
|
26
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - params:`, JSON.stringify(params));
|
|
27
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
28
|
+
// Validate parameters
|
|
29
|
+
if (!params.name) {
|
|
30
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ❌ Missing required parameter: name`);
|
|
31
|
+
throw new Error("Missing required parameter: name is required");
|
|
32
|
+
}
|
|
33
|
+
// Get session context
|
|
34
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 🔍 Attempting to get session context...`);
|
|
35
|
+
const sessionContext = getLatestSessionContext();
|
|
36
|
+
if (!sessionContext) {
|
|
37
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ❌ FAILED: No active session found!`);
|
|
38
|
+
logger.error(`[SEARCH_CONTACT_TOOL] - toolCallId: ${toolCallId}`);
|
|
39
|
+
throw new Error("No active XY session found. Search contact tool can only be used during an active conversation.");
|
|
40
|
+
}
|
|
41
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ✅ Session context found`);
|
|
42
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
43
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
44
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
45
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
46
|
+
// Get WebSocket manager
|
|
47
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 🔌 Getting WebSocket manager...`);
|
|
48
|
+
const wsManager = getXYWebSocketManager(config);
|
|
49
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ✅ WebSocket manager obtained`);
|
|
50
|
+
// Build SearchContactLocal command
|
|
51
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📦 Building SearchContactLocal command...`);
|
|
52
|
+
const command = {
|
|
53
|
+
header: {
|
|
54
|
+
namespace: "Common",
|
|
55
|
+
name: "Action",
|
|
56
|
+
},
|
|
57
|
+
payload: {
|
|
58
|
+
cardParam: {},
|
|
59
|
+
executeParam: {
|
|
60
|
+
executeMode: "background",
|
|
61
|
+
intentName: "SearchContactLocal",
|
|
62
|
+
bundleName: "com.huawei.hmos.aidispatchservice",
|
|
63
|
+
needUnlock: true,
|
|
64
|
+
actionResponse: true,
|
|
65
|
+
appType: "OHOS_APP",
|
|
66
|
+
timeOut: 5,
|
|
67
|
+
intentParam: {
|
|
68
|
+
name: params.name,
|
|
69
|
+
},
|
|
70
|
+
permissionId: [],
|
|
71
|
+
achieveType: "INTENT",
|
|
72
|
+
},
|
|
73
|
+
responses: [
|
|
74
|
+
{
|
|
75
|
+
resultCode: "",
|
|
76
|
+
displayText: "",
|
|
77
|
+
ttsText: "",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
needUploadResult: true,
|
|
81
|
+
noHalfPage: false,
|
|
82
|
+
pageControlRelated: false,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
// Send command and wait for response (60 second timeout)
|
|
86
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ⏳ Setting up promise to wait for contact search response...`);
|
|
87
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - Timeout: 60 seconds`);
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const timeout = setTimeout(() => {
|
|
90
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ⏰ Timeout: No response received within 60 seconds`);
|
|
91
|
+
wsManager.off("data-event", handler);
|
|
92
|
+
reject(new Error("搜索联系人超时(60秒)"));
|
|
93
|
+
}, 60000);
|
|
94
|
+
// Listen for data events from WebSocket
|
|
95
|
+
const handler = (event) => {
|
|
96
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
97
|
+
if (event.intentName === "SearchContactLocal") {
|
|
98
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 🎯 SearchContactLocal event received`);
|
|
99
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - status: ${event.status}`);
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
wsManager.off("data-event", handler);
|
|
102
|
+
if (event.status === "success" && event.outputs) {
|
|
103
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ✅ Contact search completed successfully`);
|
|
104
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - outputs:`, JSON.stringify(event.outputs));
|
|
105
|
+
// Return the result directly as requested
|
|
106
|
+
const result = event.outputs.result;
|
|
107
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📊 Contacts found: ${result?.items?.length || 0} results for name "${params.name}"`);
|
|
108
|
+
resolve({
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: JSON.stringify(result),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ❌ Contact search failed`);
|
|
119
|
+
logger.error(`[SEARCH_CONTACT_TOOL] - status: ${event.status}`);
|
|
120
|
+
reject(new Error(`搜索联系人失败: ${event.status}`));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
// Register event handler
|
|
125
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📡 Registering data-event handler on WebSocket manager`);
|
|
126
|
+
wsManager.on("data-event", handler);
|
|
127
|
+
// Send the command
|
|
128
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📤 Sending SearchContactLocal command...`);
|
|
129
|
+
sendCommand({
|
|
130
|
+
config,
|
|
131
|
+
sessionId,
|
|
132
|
+
taskId,
|
|
133
|
+
messageId,
|
|
134
|
+
command,
|
|
135
|
+
})
|
|
136
|
+
.then(() => {
|
|
137
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
138
|
+
})
|
|
139
|
+
.catch((error) => {
|
|
140
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ❌ Failed to send command:`, error);
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
wsManager.off("data-event", handler);
|
|
143
|
+
reject(error);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XY search photo gallery tool - searches photos in user's gallery.
|
|
3
|
+
* Returns local mediaUri strings that can be used with upload_photo tool.
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: The returned mediaUris are LOCAL URIs that cannot be downloaded directly.
|
|
6
|
+
* To get publicly accessible URLs, use the upload_photo tool with these URIs.
|
|
7
|
+
*/
|
|
8
|
+
export declare const searchPhotoGalleryTool: any;
|
|
@@ -0,0 +1,175 @@
|
|
|
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 gallery tool - searches photos in user's gallery.
|
|
7
|
+
* Returns local mediaUri strings that can be used with upload_photo tool.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: The returned mediaUris are LOCAL URIs that cannot be downloaded directly.
|
|
10
|
+
* To get publicly accessible URLs, use the upload_photo tool with these URIs.
|
|
11
|
+
*/
|
|
12
|
+
export const searchPhotoGalleryTool = {
|
|
13
|
+
name: "search_photo_gallery",
|
|
14
|
+
label: "Search Photo Gallery",
|
|
15
|
+
description: "搜索用户手机图库中的照片。根据图像描述语料检索匹配的照片,返回照片在手机本地的 mediaUri。注意:返回的 mediaUri 是本地路径,无法直接下载或访问。如果需要下载、查看、使用或展示照片,请使用 upload_photo 工具将 mediaUri 转换为可访问的公网 URL。操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
|
|
16
|
+
parameters: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
query: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "图像描述语料,用于检索匹配的照片(例如:'小狗的照片'、'带有键盘的图片'等)",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ["query"],
|
|
25
|
+
},
|
|
26
|
+
async execute(toolCallId, params) {
|
|
27
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 🚀 Starting execution`);
|
|
28
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - toolCallId: ${toolCallId}`);
|
|
29
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - params:`, JSON.stringify(params));
|
|
30
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
31
|
+
// Validate parameters
|
|
32
|
+
if (!params.query) {
|
|
33
|
+
logger.error(`[SEARCH_PHOTO_GALLERY_TOOL] ❌ Missing required parameter: query`);
|
|
34
|
+
throw new Error("Missing required parameter: query is required");
|
|
35
|
+
}
|
|
36
|
+
// Get session context
|
|
37
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 🔍 Attempting to get session context...`);
|
|
38
|
+
const sessionContext = getLatestSessionContext();
|
|
39
|
+
if (!sessionContext) {
|
|
40
|
+
logger.error(`[SEARCH_PHOTO_GALLERY_TOOL] ❌ FAILED: No active session found!`);
|
|
41
|
+
logger.error(`[SEARCH_PHOTO_GALLERY_TOOL] - toolCallId: ${toolCallId}`);
|
|
42
|
+
throw new Error("No active XY session found. Search photo gallery tool can only be used during an active conversation.");
|
|
43
|
+
}
|
|
44
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] ✅ Session context found`);
|
|
45
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
46
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
47
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
48
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
49
|
+
// Get WebSocket manager
|
|
50
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 🔌 Getting WebSocket manager...`);
|
|
51
|
+
const wsManager = getXYWebSocketManager(config);
|
|
52
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] ✅ WebSocket manager obtained`);
|
|
53
|
+
// Search for photos
|
|
54
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 📸 Searching for photos...`);
|
|
55
|
+
const mediaUris = await searchPhotos(wsManager, config, sessionId, taskId, messageId, params.query);
|
|
56
|
+
if (!mediaUris || mediaUris.length === 0) {
|
|
57
|
+
logger.warn(`[SEARCH_PHOTO_GALLERY_TOOL] ⚠️ No photos found for query: ${params.query}`);
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: JSON.stringify({
|
|
63
|
+
mediaUris: [],
|
|
64
|
+
count: 0,
|
|
65
|
+
message: "未找到匹配的照片"
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] ✅ Found ${mediaUris.length} photos`);
|
|
72
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - mediaUris:`, JSON.stringify(mediaUris));
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: "text",
|
|
77
|
+
text: JSON.stringify({
|
|
78
|
+
mediaUris,
|
|
79
|
+
count: mediaUris.length,
|
|
80
|
+
message: `找到 ${mediaUris.length} 张照片。注意:这些是本地 URI,无法直接访问。如需下载或查看,请使用 upload_photo 工具。`
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Search for photos using query description
|
|
89
|
+
* Returns array of mediaUri strings
|
|
90
|
+
*/
|
|
91
|
+
async function searchPhotos(wsManager, config, sessionId, taskId, messageId, query) {
|
|
92
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 📦 Building SearchPhotoVideo command...`);
|
|
93
|
+
const command = {
|
|
94
|
+
header: {
|
|
95
|
+
namespace: "Common",
|
|
96
|
+
name: "Action",
|
|
97
|
+
},
|
|
98
|
+
payload: {
|
|
99
|
+
cardParam: {},
|
|
100
|
+
executeParam: {
|
|
101
|
+
executeMode: "background",
|
|
102
|
+
intentName: "SearchPhotoVideo",
|
|
103
|
+
bundleName: "com.huawei.hmos.aidispatchservice",
|
|
104
|
+
needUnlock: true,
|
|
105
|
+
actionResponse: true,
|
|
106
|
+
appType: "OHOS_APP",
|
|
107
|
+
timeOut: 5,
|
|
108
|
+
intentParam: {
|
|
109
|
+
query: query,
|
|
110
|
+
},
|
|
111
|
+
permissionId: [],
|
|
112
|
+
achieveType: "INTENT",
|
|
113
|
+
},
|
|
114
|
+
responses: [
|
|
115
|
+
{
|
|
116
|
+
resultCode: "",
|
|
117
|
+
displayText: "",
|
|
118
|
+
ttsText: "",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
needUploadResult: true,
|
|
122
|
+
noHalfPage: false,
|
|
123
|
+
pageControlRelated: false,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const timeout = setTimeout(() => {
|
|
128
|
+
logger.error(`[SEARCH_PHOTO_GALLERY_TOOL] ⏰ Timeout: No response for SearchPhotoVideo within 60 seconds`);
|
|
129
|
+
wsManager.off("data-event", handler);
|
|
130
|
+
reject(new Error("搜索照片超时(60秒)"));
|
|
131
|
+
}, 60000);
|
|
132
|
+
const handler = (event) => {
|
|
133
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
134
|
+
if (event.intentName === "SearchPhotoVideo") {
|
|
135
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 🎯 SearchPhotoVideo event received`);
|
|
136
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] - status: ${event.status}`);
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
wsManager.off("data-event", handler);
|
|
139
|
+
if (event.status === "success" && event.outputs) {
|
|
140
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] ✅ Photo search completed successfully`);
|
|
141
|
+
const result = event.outputs.result;
|
|
142
|
+
const items = result?.items || [];
|
|
143
|
+
// Extract mediaUri from each item
|
|
144
|
+
const mediaUris = items.map((item) => item.mediaUri).filter(Boolean);
|
|
145
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 📊 Extracted ${mediaUris.length} mediaUris`);
|
|
146
|
+
resolve(mediaUris);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
logger.error(`[SEARCH_PHOTO_GALLERY_TOOL] ❌ Photo search failed`);
|
|
150
|
+
logger.error(`[SEARCH_PHOTO_GALLERY_TOOL] - status: ${event.status}`);
|
|
151
|
+
reject(new Error(`搜索照片失败: ${event.status}`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 📡 Registering data-event handler for SearchPhotoVideo`);
|
|
156
|
+
wsManager.on("data-event", handler);
|
|
157
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] 📤 Sending SearchPhotoVideo command...`);
|
|
158
|
+
sendCommand({
|
|
159
|
+
config,
|
|
160
|
+
sessionId,
|
|
161
|
+
taskId,
|
|
162
|
+
messageId,
|
|
163
|
+
command,
|
|
164
|
+
})
|
|
165
|
+
.then(() => {
|
|
166
|
+
logger.log(`[SEARCH_PHOTO_GALLERY_TOOL] ✅ SearchPhotoVideo command sent successfully`);
|
|
167
|
+
})
|
|
168
|
+
.catch((error) => {
|
|
169
|
+
logger.error(`[SEARCH_PHOTO_GALLERY_TOOL] ❌ Failed to send SearchPhotoVideo command:`, error);
|
|
170
|
+
clearTimeout(timeout);
|
|
171
|
+
wsManager.off("data-event", handler);
|
|
172
|
+
reject(error);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XY upload photo tool - uploads local photos to get publicly accessible URLs.
|
|
3
|
+
* Requires mediaUris from search_photo_gallery tool as prerequisite.
|
|
4
|
+
*
|
|
5
|
+
* Prerequisites:
|
|
6
|
+
* 1. Call search_photo_gallery tool first to get mediaUris of photos
|
|
7
|
+
* 2. Use the mediaUris (maximum 5 at a time) to get public URLs
|
|
8
|
+
*/
|
|
9
|
+
export declare const uploadPhotoTool: any;
|
|
@@ -0,0 +1,213 @@
|
|
|
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 upload photo tool - uploads local photos to get publicly accessible URLs.
|
|
7
|
+
* Requires mediaUris from search_photo_gallery tool as prerequisite.
|
|
8
|
+
*
|
|
9
|
+
* Prerequisites:
|
|
10
|
+
* 1. Call search_photo_gallery tool first to get mediaUris of photos
|
|
11
|
+
* 2. Use the mediaUris (maximum 5 at a time) to get public URLs
|
|
12
|
+
*/
|
|
13
|
+
export const uploadPhotoTool = {
|
|
14
|
+
name: "upload_photo",
|
|
15
|
+
label: "Upload Photo",
|
|
16
|
+
description: "将手机本地照片回传并获取可公网访问的 URL。使用前必须先调用 search_photo_gallery 工具获取照片的 mediaUri。参数说明:mediaUris 是照片在手机本地的 URI 数组或 JSON 字符串数组(从 search_photo_gallery 工具获取)。限制:每次最多支持传入 5 条 mediaUri。操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
|
|
17
|
+
parameters: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
mediaUris: {
|
|
21
|
+
// 不指定 type,允许传入数组或 JSON 字符串
|
|
22
|
+
// 具体的类型验证和转换在 execute 函数内部进行
|
|
23
|
+
description: "照片在手机本地的 URI 数组(或 JSON 字符串形式的数组),必须先通过 search_photo_gallery 工具获取。每次最多支持 5 条 URI。支持传入数组 [\"uri1\", \"uri2\"] 或 JSON 字符串 '[\"uri1\", \"uri2\"]'。",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ["mediaUris"],
|
|
27
|
+
},
|
|
28
|
+
async execute(toolCallId, params) {
|
|
29
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🚀 Starting execution`);
|
|
30
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
|
|
31
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - params (raw):`, JSON.stringify(params));
|
|
32
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - params.mediaUris type:`, typeof params.mediaUris);
|
|
33
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
34
|
+
// ===== 参数规范化:兼容数组和 JSON 字符串 =====
|
|
35
|
+
let mediaUris = null;
|
|
36
|
+
if (!params.mediaUris) {
|
|
37
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Missing parameter: mediaUris`);
|
|
38
|
+
throw new Error("Missing required parameter: mediaUris");
|
|
39
|
+
}
|
|
40
|
+
// 情况1: 已经是数组
|
|
41
|
+
if (Array.isArray(params.mediaUris)) {
|
|
42
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ mediaUris is already an array`);
|
|
43
|
+
mediaUris = params.mediaUris;
|
|
44
|
+
}
|
|
45
|
+
// 情况2: 是字符串,尝试解析为 JSON 数组
|
|
46
|
+
else if (typeof params.mediaUris === 'string') {
|
|
47
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🔄 mediaUris is a string, attempting to parse as JSON...`);
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(params.mediaUris);
|
|
50
|
+
if (Array.isArray(parsed)) {
|
|
51
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ Successfully parsed JSON string to array`);
|
|
52
|
+
mediaUris = parsed;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Parsed JSON is not an array:`, typeof parsed);
|
|
56
|
+
throw new Error("mediaUris must be an array or a JSON string representing an array");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (parseError) {
|
|
60
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Failed to parse mediaUris as JSON:`, parseError);
|
|
61
|
+
throw new Error(`mediaUris must be a valid JSON array string. Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// 情况3: 其他类型,报错
|
|
65
|
+
else {
|
|
66
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Invalid mediaUris type:`, typeof params.mediaUris);
|
|
67
|
+
throw new Error(`mediaUris must be an array or a JSON string, got ${typeof params.mediaUris}`);
|
|
68
|
+
}
|
|
69
|
+
// 验证数组非空
|
|
70
|
+
if (!mediaUris || mediaUris.length === 0) {
|
|
71
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ mediaUris array is empty`);
|
|
72
|
+
throw new Error("mediaUris array cannot be empty");
|
|
73
|
+
}
|
|
74
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ Normalized mediaUris:`, JSON.stringify(mediaUris));
|
|
75
|
+
// Validate maximum 5 URIs
|
|
76
|
+
if (mediaUris.length > 5) {
|
|
77
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Too many mediaUris: ${mediaUris.length}`);
|
|
78
|
+
throw new Error(`最多支持 5 条 mediaUri,当前提供了 ${mediaUris.length} 条。请分批处理。`);
|
|
79
|
+
}
|
|
80
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - mediaUris count: ${mediaUris.length}`);
|
|
81
|
+
// Get session context
|
|
82
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🔍 Attempting to get session context...`);
|
|
83
|
+
const sessionContext = getLatestSessionContext();
|
|
84
|
+
if (!sessionContext) {
|
|
85
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ FAILED: No active session found!`);
|
|
86
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] - toolCallId: ${toolCallId}`);
|
|
87
|
+
throw new Error("No active XY session found. Upload photo tool can only be used during an active conversation.");
|
|
88
|
+
}
|
|
89
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ Session context found`);
|
|
90
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
91
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
92
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
93
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
94
|
+
// Get WebSocket manager
|
|
95
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🔌 Getting WebSocket manager...`);
|
|
96
|
+
const wsManager = getXYWebSocketManager(config);
|
|
97
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ WebSocket manager obtained`);
|
|
98
|
+
// Get public URLs for the photos
|
|
99
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🌐 Getting public URLs for photos...`);
|
|
100
|
+
const imageUrls = await getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris);
|
|
101
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🎉 Successfully retrieved ${imageUrls.length} photo URLs`);
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: JSON.stringify({
|
|
107
|
+
imageUrls,
|
|
108
|
+
count: imageUrls.length,
|
|
109
|
+
message: `成功获取 ${imageUrls.length} 张照片的公网访问 URL`
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Get public URLs for photos using mediaUris
|
|
118
|
+
* Returns array of publicly accessible image URLs
|
|
119
|
+
*/
|
|
120
|
+
async function getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris) {
|
|
121
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 📦 Building ImageUploadForClaw command...`);
|
|
122
|
+
// Build imageInfos array from mediaUris
|
|
123
|
+
const imageInfos = mediaUris.map(mediaUri => ({ mediaUri }));
|
|
124
|
+
const command = {
|
|
125
|
+
header: {
|
|
126
|
+
namespace: "Common",
|
|
127
|
+
name: "Action",
|
|
128
|
+
},
|
|
129
|
+
payload: {
|
|
130
|
+
cardParam: {},
|
|
131
|
+
executeParam: {
|
|
132
|
+
executeMode: "background",
|
|
133
|
+
intentName: "ImageUploadForClaw",
|
|
134
|
+
bundleName: "com.huawei.hmos.vassistant",
|
|
135
|
+
needUnlock: true,
|
|
136
|
+
actionResponse: true,
|
|
137
|
+
appType: "OHOS_APP",
|
|
138
|
+
timeOut: 5,
|
|
139
|
+
intentParam: {
|
|
140
|
+
imageInfos: imageInfos,
|
|
141
|
+
},
|
|
142
|
+
permissionId: [],
|
|
143
|
+
achieveType: "INTENT",
|
|
144
|
+
},
|
|
145
|
+
responses: [
|
|
146
|
+
{
|
|
147
|
+
resultCode: "",
|
|
148
|
+
displayText: "",
|
|
149
|
+
ttsText: "",
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
needUploadResult: true,
|
|
153
|
+
noHalfPage: false,
|
|
154
|
+
pageControlRelated: false,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const timeout = setTimeout(() => {
|
|
159
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ⏰ Timeout: No response for ImageUploadForClaw within 60 seconds`);
|
|
160
|
+
wsManager.off("data-event", handler);
|
|
161
|
+
reject(new Error("获取照片URL超时(60秒)"));
|
|
162
|
+
}, 60000);
|
|
163
|
+
const handler = (event) => {
|
|
164
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
165
|
+
if (event.intentName === "ImageUploadForClaw") {
|
|
166
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🎯 ImageUploadForClaw event received`);
|
|
167
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] - status: ${event.status}`);
|
|
168
|
+
clearTimeout(timeout);
|
|
169
|
+
wsManager.off("data-event", handler);
|
|
170
|
+
if (event.status === "success" && event.outputs) {
|
|
171
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ Image URL retrieval completed successfully`);
|
|
172
|
+
const result = event.outputs.result;
|
|
173
|
+
let imageUrls = result?.imageUrls || [];
|
|
174
|
+
// Decode Unicode escape sequences in URLs
|
|
175
|
+
// Replace \u003d with = and \u0026 with &
|
|
176
|
+
imageUrls = imageUrls.map((url) => {
|
|
177
|
+
const decodedUrl = url
|
|
178
|
+
.replace(/\\u003d/g, '=')
|
|
179
|
+
.replace(/\\u0026/g, '&');
|
|
180
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 🔄 Decoded URL: ${url} -> ${decodedUrl}`);
|
|
181
|
+
return decodedUrl;
|
|
182
|
+
});
|
|
183
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 📊 Retrieved and decoded ${imageUrls.length} image URLs`);
|
|
184
|
+
resolve(imageUrls);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Image URL retrieval failed`);
|
|
188
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] - status: ${event.status}`);
|
|
189
|
+
reject(new Error(`获取照片URL失败: ${event.status}`));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 📡 Registering data-event handler for ImageUploadForClaw`);
|
|
194
|
+
wsManager.on("data-event", handler);
|
|
195
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] 📤 Sending ImageUploadForClaw command...`);
|
|
196
|
+
sendCommand({
|
|
197
|
+
config,
|
|
198
|
+
sessionId,
|
|
199
|
+
taskId,
|
|
200
|
+
messageId,
|
|
201
|
+
command,
|
|
202
|
+
})
|
|
203
|
+
.then(() => {
|
|
204
|
+
logger.log(`[UPLOAD_PHOTO_TOOL] ✅ ImageUploadForClaw command sent successfully`);
|
|
205
|
+
})
|
|
206
|
+
.catch((error) => {
|
|
207
|
+
logger.error(`[UPLOAD_PHOTO_TOOL] ❌ Failed to send ImageUploadForClaw command:`, error);
|
|
208
|
+
clearTimeout(timeout);
|
|
209
|
+
wsManager.off("data-event", handler);
|
|
210
|
+
reject(error);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|