@ynhcj/xiaoyi-channel 0.0.2 → 0.0.3-next
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/bot.js +115 -35
- package/dist/src/channel.js +22 -1
- package/dist/src/client.d.ts +15 -0
- package/dist/src/client.js +97 -2
- package/dist/src/file-download.js +10 -1
- package/dist/src/file-upload.js +1 -1
- package/dist/src/formatter.d.ts +17 -0
- package/dist/src/formatter.js +86 -6
- package/dist/src/heartbeat.d.ts +2 -1
- package/dist/src/heartbeat.js +9 -1
- package/dist/src/monitor.d.ts +5 -0
- package/dist/src/monitor.js +131 -25
- package/dist/src/onboarding.js +7 -7
- package/dist/src/outbound.js +150 -71
- package/dist/src/parser.d.ts +5 -0
- package/dist/src/parser.js +15 -0
- package/dist/src/push.d.ts +7 -1
- package/dist/src/push.js +110 -19
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +210 -57
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/calendar-tool.d.ts +6 -0
- package/dist/src/tools/calendar-tool.js +167 -0
- package/dist/src/tools/call-phone-tool.d.ts +5 -0
- package/dist/src/tools/call-phone-tool.js +183 -0
- package/dist/src/tools/create-alarm-tool.d.ts +7 -0
- package/dist/src/tools/create-alarm-tool.js +444 -0
- package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
- package/dist/src/tools/delete-alarm-tool.js +238 -0
- package/dist/src/tools/location-tool.js +48 -9
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +474 -0
- 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/note-tool.d.ts +5 -0
- package/dist/src/tools/note-tool.js +146 -0
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +389 -0
- package/dist/src/tools/search-calendar-tool.d.ts +12 -0
- package/dist/src/tools/search-calendar-tool.js +259 -0
- package/dist/src/tools/search-contact-tool.d.ts +5 -0
- package/dist/src/tools/search-contact-tool.js +168 -0
- package/dist/src/tools/search-file-tool.d.ts +5 -0
- package/dist/src/tools/search-file-tool.js +185 -0
- package/dist/src/tools/search-message-tool.d.ts +5 -0
- package/dist/src/tools/search-message-tool.js +173 -0
- package/dist/src/tools/search-note-tool.d.ts +5 -0
- package/dist/src/tools/search-note-tool.js +130 -0
- package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
- package/dist/src/tools/search-photo-gallery-tool.js +184 -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/send-file-to-user-tool.d.ts +5 -0
- package/dist/src/tools/send-file-to-user-tool.js +318 -0
- package/dist/src/tools/send-message-tool.d.ts +5 -0
- package/dist/src/tools/send-message-tool.js +189 -0
- package/dist/src/tools/session-manager.d.ts +15 -0
- package/dist/src/tools/session-manager.js +126 -6
- package/dist/src/tools/upload-file-tool.d.ts +13 -0
- package/dist/src/tools/upload-file-tool.js +265 -0
- package/dist/src/tools/upload-photo-tool.d.ts +9 -0
- package/dist/src/tools/upload-photo-tool.js +223 -0
- package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
- package/dist/src/types.d.ts +5 -9
- package/dist/src/utils/config-manager.d.ts +26 -0
- package/dist/src/utils/config-manager.js +56 -0
- package/dist/src/websocket.d.ts +41 -0
- package/dist/src/websocket.js +192 -9
- package/package.json +1 -2
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* XY search calendar event tool - searches calendar events on user's device.
|
|
7
|
+
* Returns matching events based on time range and optional title filter.
|
|
8
|
+
*
|
|
9
|
+
* Time range guidelines:
|
|
10
|
+
* - For a specific day: use 00:00:00 to 23:59:59 of that day
|
|
11
|
+
* - For morning: 06:00:00 to 12:00:00
|
|
12
|
+
* - For afternoon: 12:00:00 to 18:00:00
|
|
13
|
+
* - For evening: 18:00:00 to 24:00:00
|
|
14
|
+
* - For a specific time: use ±1 hour range (e.g., for 3PM, use 14:00:00 to 16:00:00)
|
|
15
|
+
*/
|
|
16
|
+
export const searchCalendarTool = {
|
|
17
|
+
name: "search_calendar_event",
|
|
18
|
+
label: "Search Calendar Event",
|
|
19
|
+
description: `检索用户日历中的日程安排。根据时间范围和可选的日程标题进行检索。时间格式必须为:YYYYMMDD hhmmss(例如:20240115 143000)。
|
|
20
|
+
|
|
21
|
+
时间范围说明:
|
|
22
|
+
- 查询某一天的日程:使用该天的 00:00:00 到 23:59:59(例如:20240115 000000 到 20240115 235959)
|
|
23
|
+
- 查询上午的日程:使用 06:00:00 到 12:00:00
|
|
24
|
+
- 查询下午的日程:使用 12:00:00 到 18:00:00
|
|
25
|
+
- 查询晚上的日程:使用 18:00:00 到 23:59:59
|
|
26
|
+
- 查询某个时刻附近的日程:使用该时刻前后1小时的区间(例如:查询3点左右的日程,使用 14:00:00 到 16:00:00)
|
|
27
|
+
|
|
28
|
+
注意:该工具执行时间较长(最多60秒),请勿重复调用,超时或失败时最多重试一次。`,
|
|
29
|
+
parameters: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
startTime: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "日程起始时间,格式必须为:YYYYMMDD hhmmss(例如:20240115 143000 表示 2024年1月15日 14:30:00)",
|
|
35
|
+
},
|
|
36
|
+
endTime: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "日程结束时间,格式必须为:YYYYMMDD hhmmss(例如:20240115 173000 表示 2024年1月15日 17:30:00)",
|
|
39
|
+
},
|
|
40
|
+
title: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "日程标题/类型(可选),用于过滤特定类型的日程",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["startTime", "endTime"],
|
|
46
|
+
},
|
|
47
|
+
async execute(toolCallId, params) {
|
|
48
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🚀 Starting execution`);
|
|
49
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - toolCallId: ${toolCallId}`);
|
|
50
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - params:`, JSON.stringify(params));
|
|
51
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
52
|
+
// Validate parameters
|
|
53
|
+
if (!params.startTime || !params.endTime) {
|
|
54
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Missing required parameters`);
|
|
55
|
+
throw new Error("Missing required parameters: startTime and endTime are required");
|
|
56
|
+
}
|
|
57
|
+
// Convert time strings to millisecond timestamps
|
|
58
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🕒 Converting time strings to timestamps...`);
|
|
59
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - startTime input: ${params.startTime}`);
|
|
60
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - endTime input: ${params.endTime}`);
|
|
61
|
+
// Parse YYYYMMDD hhmmss format
|
|
62
|
+
const parseTimeString = (timeStr) => {
|
|
63
|
+
// Remove any extra spaces and split
|
|
64
|
+
const cleaned = timeStr.trim().replace(/\s+/g, ' ');
|
|
65
|
+
const parts = cleaned.split(' ');
|
|
66
|
+
if (parts.length !== 2) {
|
|
67
|
+
throw new Error(`Invalid time format: ${timeStr}. Expected format: YYYYMMDD hhmmss`);
|
|
68
|
+
}
|
|
69
|
+
const datePart = parts[0]; // YYYYMMDD
|
|
70
|
+
const timePart = parts[1]; // hhmmss
|
|
71
|
+
if (datePart.length !== 8 || timePart.length !== 6) {
|
|
72
|
+
throw new Error(`Invalid time format: ${timeStr}. Expected format: YYYYMMDD hhmmss`);
|
|
73
|
+
}
|
|
74
|
+
const year = parseInt(datePart.substring(0, 4), 10);
|
|
75
|
+
const month = parseInt(datePart.substring(4, 6), 10) - 1; // Month is 0-indexed
|
|
76
|
+
const day = parseInt(datePart.substring(6, 8), 10);
|
|
77
|
+
const hours = parseInt(timePart.substring(0, 2), 10);
|
|
78
|
+
const minutes = parseInt(timePart.substring(2, 4), 10);
|
|
79
|
+
const seconds = parseInt(timePart.substring(4, 6), 10);
|
|
80
|
+
const date = new Date(year, month, day, hours, minutes, seconds);
|
|
81
|
+
return date.getTime();
|
|
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
|
+
};
|
|
94
|
+
let startTimeMs;
|
|
95
|
+
let endTimeMs;
|
|
96
|
+
try {
|
|
97
|
+
startTimeMs = parseTimeString(params.startTime);
|
|
98
|
+
endTimeMs = parseTimeString(params.endTime);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Time parsing error:`, error);
|
|
102
|
+
throw new Error(`Invalid time format. Required format: YYYYMMDD hhmmss (e.g., 20240115 143000). Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
103
|
+
}
|
|
104
|
+
if (isNaN(startTimeMs) || isNaN(endTimeMs)) {
|
|
105
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Invalid time format`);
|
|
106
|
+
throw new Error("Invalid time format. Required format: YYYYMMDD hhmmss (e.g., 20240115 143000)");
|
|
107
|
+
}
|
|
108
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Time conversion successful`);
|
|
109
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - startTime timestamp: ${startTimeMs}`);
|
|
110
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - endTime timestamp: ${endTimeMs}`);
|
|
111
|
+
// Get session context
|
|
112
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🔍 Attempting to get session context...`);
|
|
113
|
+
const sessionContext = getCurrentSessionContext();
|
|
114
|
+
if (!sessionContext) {
|
|
115
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ FAILED: No active session found!`);
|
|
116
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] - toolCallId: ${toolCallId}`);
|
|
117
|
+
throw new Error("No active XY session found. Search calendar tool can only be used during an active conversation.");
|
|
118
|
+
}
|
|
119
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Session context found`);
|
|
120
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
121
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
122
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
123
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
124
|
+
// Get WebSocket manager
|
|
125
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🔌 Getting WebSocket manager...`);
|
|
126
|
+
const wsManager = getXYWebSocketManager(config);
|
|
127
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ WebSocket manager obtained`);
|
|
128
|
+
// Build SearchCalendarEvent command
|
|
129
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📦 Building SearchCalendarEvent command...`);
|
|
130
|
+
// Build intentParam with timeInterval and optional title
|
|
131
|
+
const intentParam = {
|
|
132
|
+
timeInterval: [startTimeMs, endTimeMs],
|
|
133
|
+
};
|
|
134
|
+
if (params.title) {
|
|
135
|
+
intentParam.title = params.title;
|
|
136
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - Including title filter: ${params.title}`);
|
|
137
|
+
}
|
|
138
|
+
const command = {
|
|
139
|
+
header: {
|
|
140
|
+
namespace: "Common",
|
|
141
|
+
name: "Action",
|
|
142
|
+
},
|
|
143
|
+
payload: {
|
|
144
|
+
cardParam: {},
|
|
145
|
+
executeParam: {
|
|
146
|
+
executeMode: "background",
|
|
147
|
+
intentName: "SearchCalendarEvent",
|
|
148
|
+
bundleName: "com.huawei.hmos.calendardata",
|
|
149
|
+
dimension: "",
|
|
150
|
+
needUnlock: true,
|
|
151
|
+
actionResponse: true,
|
|
152
|
+
appType: "OHOS_APP",
|
|
153
|
+
timeOut: 5,
|
|
154
|
+
intentParam,
|
|
155
|
+
permissionId: [],
|
|
156
|
+
achieveType: "INTENT",
|
|
157
|
+
},
|
|
158
|
+
responses: [
|
|
159
|
+
{
|
|
160
|
+
resultCode: "",
|
|
161
|
+
displayText: "",
|
|
162
|
+
ttsText: "",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
needUploadResult: true,
|
|
166
|
+
noHalfPage: false,
|
|
167
|
+
pageControlRelated: false,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
// Send command and wait for response (60 second timeout)
|
|
171
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ⏳ Setting up promise to wait for calendar search response...`);
|
|
172
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - Timeout: 60 seconds`);
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const timeout = setTimeout(() => {
|
|
175
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ⏰ Timeout: No response received within 60 seconds`);
|
|
176
|
+
wsManager.off("data-event", handler);
|
|
177
|
+
reject(new Error("检索日程超时(60秒)"));
|
|
178
|
+
}, 60000);
|
|
179
|
+
// Listen for data events from WebSocket
|
|
180
|
+
const handler = (event) => {
|
|
181
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
182
|
+
if (event.intentName === "SearchCalendarEvent") {
|
|
183
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 🎯 SearchCalendarEvent event received`);
|
|
184
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] - status: ${event.status}`);
|
|
185
|
+
clearTimeout(timeout);
|
|
186
|
+
wsManager.off("data-event", handler);
|
|
187
|
+
if (event.status === "success" && event.outputs) {
|
|
188
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Calendar events response received`);
|
|
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
|
+
}
|
|
198
|
+
// Return the result directly as requested
|
|
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
|
+
}
|
|
220
|
+
resolve({
|
|
221
|
+
content: [
|
|
222
|
+
{
|
|
223
|
+
type: "text",
|
|
224
|
+
text: result !== undefined ? JSON.stringify(result) : "[]",
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Calendar event search failed`);
|
|
231
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] - status: ${event.status}`);
|
|
232
|
+
reject(new Error(`检索日程失败: ${event.status}`));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
// Register event handler
|
|
237
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📡 Registering data-event handler on WebSocket manager`);
|
|
238
|
+
wsManager.on("data-event", handler);
|
|
239
|
+
// Send the command
|
|
240
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] 📤 Sending SearchCalendarEvent command...`);
|
|
241
|
+
sendCommand({
|
|
242
|
+
config,
|
|
243
|
+
sessionId,
|
|
244
|
+
taskId,
|
|
245
|
+
messageId,
|
|
246
|
+
command,
|
|
247
|
+
})
|
|
248
|
+
.then(() => {
|
|
249
|
+
logger.log(`[SEARCH_CALENDAR_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
250
|
+
})
|
|
251
|
+
.catch((error) => {
|
|
252
|
+
logger.error(`[SEARCH_CALENDAR_TOOL] ❌ Failed to send command:`, error);
|
|
253
|
+
clearTimeout(timeout);
|
|
254
|
+
wsManager.off("data-event", handler);
|
|
255
|
+
reject(error);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } 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 = getCurrentSessionContext();
|
|
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
|
+
// 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
|
|
129
|
+
resolve({
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: JSON.stringify(result),
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ❌ Contact search failed`);
|
|
140
|
+
logger.error(`[SEARCH_CONTACT_TOOL] - status: ${event.status}`);
|
|
141
|
+
reject(new Error(`搜索联系人失败: ${event.status}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
// Register event handler
|
|
146
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📡 Registering data-event handler on WebSocket manager`);
|
|
147
|
+
wsManager.on("data-event", handler);
|
|
148
|
+
// Send the command
|
|
149
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📤 Sending SearchContactLocal command...`);
|
|
150
|
+
sendCommand({
|
|
151
|
+
config,
|
|
152
|
+
sessionId,
|
|
153
|
+
taskId,
|
|
154
|
+
messageId,
|
|
155
|
+
command,
|
|
156
|
+
})
|
|
157
|
+
.then(() => {
|
|
158
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
159
|
+
})
|
|
160
|
+
.catch((error) => {
|
|
161
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ❌ Failed to send command:`, error);
|
|
162
|
+
clearTimeout(timeout);
|
|
163
|
+
wsManager.off("data-event", handler);
|
|
164
|
+
reject(error);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* XY search file tool - searches files on user's device file system.
|
|
7
|
+
* Returns matching files based on keyword search in file name or content.
|
|
8
|
+
*/
|
|
9
|
+
export const searchFileTool = {
|
|
10
|
+
name: "search_file",
|
|
11
|
+
label: "Search File",
|
|
12
|
+
description: `搜索手机文件系统的文件。
|
|
13
|
+
|
|
14
|
+
【重要】使用约束:此工具仅在用户显著说明要从手机搜索时才执行,例如:
|
|
15
|
+
- "从我手机里面搜索xxxx"
|
|
16
|
+
- "从手机文件系统找一下xxxx"
|
|
17
|
+
- "在手机上查找文件xxxx"
|
|
18
|
+
- "搜索手机里的文件"
|
|
19
|
+
|
|
20
|
+
如果用户没有明确说明从手机搜索(如仅说"搜索文件"、"找一下xxxx"),应默认从 openclaw 本地的文件系统查询,不要调用此工具。
|
|
21
|
+
|
|
22
|
+
功能说明:根据关键词搜索文件名称或内容,返回匹配的文件列表(包括文件名、路径、大小、修改时间等信息)。
|
|
23
|
+
|
|
24
|
+
注意事项:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。`,
|
|
25
|
+
parameters: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
query: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "搜索关键词,用于匹配文件名称、后缀名或文件内容",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
required: ["query"],
|
|
34
|
+
},
|
|
35
|
+
async execute(toolCallId, params) {
|
|
36
|
+
logger.log(`[SEARCH_FILE_TOOL] 🚀 Starting execution`);
|
|
37
|
+
logger.log(`[SEARCH_FILE_TOOL] - toolCallId: ${toolCallId}`);
|
|
38
|
+
logger.log(`[SEARCH_FILE_TOOL] - params:`, JSON.stringify(params));
|
|
39
|
+
logger.log(`[SEARCH_FILE_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
40
|
+
// Validate query parameter
|
|
41
|
+
if (!params.query || typeof params.query !== "string" || params.query.trim() === "") {
|
|
42
|
+
logger.error(`[SEARCH_FILE_TOOL] ❌ Missing or invalid query parameter`);
|
|
43
|
+
throw new Error("Missing required parameter: query must be a non-empty string");
|
|
44
|
+
}
|
|
45
|
+
logger.log(`[SEARCH_FILE_TOOL] 🔍 Searching for files with keyword: ${params.query}`);
|
|
46
|
+
// Get session context
|
|
47
|
+
logger.log(`[SEARCH_FILE_TOOL] 🔍 Attempting to get session context...`);
|
|
48
|
+
const sessionContext = getCurrentSessionContext();
|
|
49
|
+
if (!sessionContext) {
|
|
50
|
+
logger.error(`[SEARCH_FILE_TOOL] ❌ FAILED: No active session found!`);
|
|
51
|
+
logger.error(`[SEARCH_FILE_TOOL] - toolCallId: ${toolCallId}`);
|
|
52
|
+
throw new Error("No active XY session found. Search file tool can only be used during an active conversation.");
|
|
53
|
+
}
|
|
54
|
+
logger.log(`[SEARCH_FILE_TOOL] ✅ Session context found`);
|
|
55
|
+
logger.log(`[SEARCH_FILE_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
56
|
+
logger.log(`[SEARCH_FILE_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
57
|
+
logger.log(`[SEARCH_FILE_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
58
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
59
|
+
// Get WebSocket manager
|
|
60
|
+
logger.log(`[SEARCH_FILE_TOOL] 🔌 Getting WebSocket manager...`);
|
|
61
|
+
const wsManager = getXYWebSocketManager(config);
|
|
62
|
+
logger.log(`[SEARCH_FILE_TOOL] ✅ WebSocket manager obtained`);
|
|
63
|
+
// Build SearchFile command
|
|
64
|
+
logger.log(`[SEARCH_FILE_TOOL] 📦 Building SearchFile command...`);
|
|
65
|
+
const command = {
|
|
66
|
+
header: {
|
|
67
|
+
namespace: "Common",
|
|
68
|
+
name: "Action",
|
|
69
|
+
},
|
|
70
|
+
payload: {
|
|
71
|
+
cardParam: {},
|
|
72
|
+
executeParam: {
|
|
73
|
+
executeMode: "background",
|
|
74
|
+
intentName: "SearchFile",
|
|
75
|
+
bundleName: "com.huawei.hmos.aidispatchservice",
|
|
76
|
+
needUnlock: true,
|
|
77
|
+
actionResponse: true,
|
|
78
|
+
appType: "OHOS_APP",
|
|
79
|
+
timeOut: 5,
|
|
80
|
+
intentParam: {
|
|
81
|
+
query: params.query.trim(),
|
|
82
|
+
},
|
|
83
|
+
permissionId: [],
|
|
84
|
+
achieveType: "INTENT",
|
|
85
|
+
},
|
|
86
|
+
responses: [
|
|
87
|
+
{
|
|
88
|
+
resultCode: "",
|
|
89
|
+
displayText: "",
|
|
90
|
+
ttsText: "",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
needUploadResult: true,
|
|
94
|
+
noHalfPage: false,
|
|
95
|
+
pageControlRelated: false,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
logger.log(`[SEARCH_FILE_TOOL] 📋 Command details:`, JSON.stringify(command, null, 2));
|
|
99
|
+
// Send command and wait for response (60 second timeout)
|
|
100
|
+
logger.log(`[SEARCH_FILE_TOOL] ⏳ Setting up promise to wait for file search response...`);
|
|
101
|
+
logger.log(`[SEARCH_FILE_TOOL] - Timeout: 60 seconds`);
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const timeout = setTimeout(() => {
|
|
104
|
+
logger.error(`[SEARCH_FILE_TOOL] ⏰ Timeout: No response received within 60 seconds`);
|
|
105
|
+
wsManager.off("data-event", handler);
|
|
106
|
+
reject(new Error("搜索文件超时(60秒)"));
|
|
107
|
+
}, 60000);
|
|
108
|
+
// Listen for data events from WebSocket
|
|
109
|
+
const handler = (event) => {
|
|
110
|
+
logger.log(`[SEARCH_FILE_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
111
|
+
if (event.intentName === "SearchFile") {
|
|
112
|
+
logger.log(`[SEARCH_FILE_TOOL] 🎯 SearchFile event received`);
|
|
113
|
+
logger.log(`[SEARCH_FILE_TOOL] - status: ${event.status}`);
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
wsManager.off("data-event", handler);
|
|
116
|
+
if (event.status === "success" && event.outputs) {
|
|
117
|
+
logger.log(`[SEARCH_FILE_TOOL] ✅ File search response received`);
|
|
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}`);
|
|
144
|
+
resolve({
|
|
145
|
+
content: [
|
|
146
|
+
{
|
|
147
|
+
type: "text",
|
|
148
|
+
text: JSON.stringify(items),
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
logger.error(`[SEARCH_FILE_TOOL] ❌ File search failed`);
|
|
155
|
+
logger.error(`[SEARCH_FILE_TOOL] - status: ${event.status}`);
|
|
156
|
+
logger.error(`[SEARCH_FILE_TOOL] - outputs:`, JSON.stringify(event.outputs || {}));
|
|
157
|
+
const errorDetail = event.outputs ? JSON.stringify(event.outputs) : event.status;
|
|
158
|
+
reject(new Error(`搜索文件失败: ${errorDetail}`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
// Register event handler
|
|
163
|
+
logger.log(`[SEARCH_FILE_TOOL] 📡 Registering data-event handler on WebSocket manager`);
|
|
164
|
+
wsManager.on("data-event", handler);
|
|
165
|
+
// Send the command
|
|
166
|
+
logger.log(`[SEARCH_FILE_TOOL] 📤 Sending SearchFile command...`);
|
|
167
|
+
sendCommand({
|
|
168
|
+
config,
|
|
169
|
+
sessionId,
|
|
170
|
+
taskId,
|
|
171
|
+
messageId,
|
|
172
|
+
command,
|
|
173
|
+
})
|
|
174
|
+
.then(() => {
|
|
175
|
+
logger.log(`[SEARCH_FILE_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
176
|
+
})
|
|
177
|
+
.catch((error) => {
|
|
178
|
+
logger.error(`[SEARCH_FILE_TOOL] ❌ Failed to send command:`, error);
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
wsManager.off("data-event", handler);
|
|
181
|
+
reject(error);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
};
|