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