@ynhcj/xiaoyi-channel 0.0.52-beta → 0.0.52-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/index.d.ts +0 -2
- package/dist/index.js +7 -6
- package/dist/src/bot.d.ts +1 -0
- package/dist/src/bot.js +44 -32
- package/dist/src/channel.js +32 -4
- package/dist/src/client.js +0 -6
- package/dist/src/cspl/call-api.js +19 -9
- package/dist/src/cspl/config.js +3 -3
- package/dist/src/cspl/constants.d.ts +2 -1
- package/dist/src/cspl/constants.js +1 -1
- package/dist/src/file-download.js +1 -1
- package/dist/src/file-upload.js +1 -11
- package/dist/src/formatter.d.ts +2 -0
- package/dist/src/formatter.js +11 -6
- package/dist/src/monitor.js +8 -10
- package/dist/src/onboarding.d.ts +3 -4
- package/dist/src/onboarding.js +2 -2
- package/dist/src/outbound.d.ts +2 -1
- package/dist/src/outbound.js +1 -19
- package/dist/src/parser.d.ts +6 -0
- package/dist/src/parser.js +16 -0
- package/dist/src/provider.d.ts +2 -0
- package/dist/src/provider.js +124 -0
- package/dist/src/push.js +0 -21
- package/dist/src/reply-dispatcher.d.ts +4 -0
- package/dist/src/reply-dispatcher.js +21 -13
- package/dist/src/thread-bindings.d.ts +54 -0
- package/dist/src/thread-bindings.js +214 -0
- package/dist/src/tools/call-phone-tool.js +2 -18
- package/dist/src/tools/create-alarm-tool.js +3 -7
- package/dist/src/tools/device-tool-map.d.ts +4 -0
- package/dist/src/tools/device-tool-map.js +35 -0
- package/dist/src/tools/find-pc-devices-tool.d.ts +5 -0
- package/dist/src/tools/find-pc-devices-tool.js +98 -0
- package/dist/src/tools/image-reading-tool.js +3 -3
- package/dist/src/tools/modify-alarm-tool.js +3 -7
- package/dist/src/tools/save-file-to-phone-tool.d.ts +5 -0
- package/dist/src/tools/save-file-to-phone-tool.js +170 -0
- package/dist/src/tools/save-media-to-gallery-tool.d.ts +5 -0
- package/dist/src/tools/save-media-to-gallery-tool.js +178 -0
- package/dist/src/tools/send-command-to-car-tool.d.ts +5 -0
- package/dist/src/tools/send-command-to-car-tool.js +85 -0
- package/dist/src/tools/send-file-to-user-tool.js +1 -0
- package/dist/src/tools/session-manager.d.ts +1 -0
- package/dist/src/tools/timestamp-to-utc8-tool.d.ts +12 -0
- package/dist/src/tools/timestamp-to-utc8-tool.js +104 -0
- package/dist/src/tools/upload-file-tool.js +2 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +4 -0
- package/dist/src/tools/xiaoyi-add-collection-tool.js +188 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +42 -7
- package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +4 -0
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +163 -0
- package/dist/src/utils/runtime-manager.d.ts +7 -0
- package/dist/src/utils/runtime-manager.js +42 -0
- package/dist/src/websocket.js +15 -9
- package/openclaw.plugin.json +1 -0
- package/package.json +3 -4
- package/dist/src/tools/search-photo-tool.d.ts +0 -9
- package/dist/src/tools/search-photo-tool.js +0 -270
- package/dist/src/utils/session.d.ts +0 -34
- package/dist/src/utils/session.js +0 -50
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
import { XYFileUploadService } from "../file-upload.js";
|
|
5
|
+
/**
|
|
6
|
+
* Duck-typed ToolInputError: openclaw 按 .name 字段匹配,不用 instanceof。
|
|
7
|
+
* 抛出此错误会让 openclaw 返回 HTTP 400 而非 500,
|
|
8
|
+
* LLM 会将其识别为参数错误而非瞬时故障,不会触发重试。
|
|
9
|
+
*/
|
|
10
|
+
class ToolInputError extends Error {
|
|
11
|
+
status = 400;
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ToolInputError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* XY save media to gallery tool - saves image or video files to user's device gallery.
|
|
19
|
+
* Supports local file paths (auto-uploaded to get public URL) and public URLs.
|
|
20
|
+
*/
|
|
21
|
+
export const saveMediaToGalleryTool = {
|
|
22
|
+
name: "save_media_to_gallery",
|
|
23
|
+
label: "Save Media to Gallery",
|
|
24
|
+
description: `将图片文件或者视频文件保存到手机图库。
|
|
25
|
+
工具参数说明:
|
|
26
|
+
a. mediaType:非必填,string类型,不传端侧默认为pic。支持传 pic(图片) 或 video(视频)。
|
|
27
|
+
b. fileName:非必填,string类型,文件名称,不传手机侧默认生成随机uuid。
|
|
28
|
+
c. url:必填,string类型,支持本地路径或者公网url路径。如果是本地路径,会先上传获取公网url再保存到图库。
|
|
29
|
+
|
|
30
|
+
注意:
|
|
31
|
+
a. 操作超时时间为60秒,请勿重复调用此工具
|
|
32
|
+
b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
|
|
33
|
+
c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
34
|
+
|
|
35
|
+
回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。
|
|
36
|
+
`,
|
|
37
|
+
parameters: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
mediaType: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "非必填,不传默认为pic。支持 pic(图片) 或 video(视频)。",
|
|
43
|
+
},
|
|
44
|
+
fileName: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "非必填,文件名称,不传手机侧默认生成随机uuid。",
|
|
47
|
+
},
|
|
48
|
+
url: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "必填,支持本地路径或者公网url路径。如果是本地路径会先上传获取公网url。",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ["url"],
|
|
54
|
+
},
|
|
55
|
+
async execute(toolCallId, params) {
|
|
56
|
+
// Validate parameters
|
|
57
|
+
const { mediaType, fileName, url } = params;
|
|
58
|
+
if (!url || typeof url !== "string") {
|
|
59
|
+
throw new ToolInputError("缺少必填参数: url");
|
|
60
|
+
}
|
|
61
|
+
if (mediaType && !["pic", "video"].includes(mediaType)) {
|
|
62
|
+
throw new ToolInputError(`mediaType只支持 pic 或 video,当前值: ${mediaType}`);
|
|
63
|
+
}
|
|
64
|
+
// Strip file extension from fileName if present
|
|
65
|
+
let sanitizedFileName = fileName;
|
|
66
|
+
if (sanitizedFileName && typeof sanitizedFileName === "string") {
|
|
67
|
+
const lastDot = sanitizedFileName.lastIndexOf(".");
|
|
68
|
+
if (lastDot > 0) {
|
|
69
|
+
sanitizedFileName = sanitizedFileName.substring(0, lastDot);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Get session context
|
|
73
|
+
const sessionContext = getCurrentSessionContext();
|
|
74
|
+
if (!sessionContext) {
|
|
75
|
+
throw new Error("No active XY session found. SaveMediaToGallery tool can only be used during an active conversation.");
|
|
76
|
+
}
|
|
77
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
78
|
+
// Get WebSocket manager
|
|
79
|
+
const wsManager = getXYWebSocketManager(config);
|
|
80
|
+
// Determine the URL: if it's a local path, upload first to get public URL
|
|
81
|
+
let publicUrl = url;
|
|
82
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
83
|
+
// Local file path - upload to get public URL
|
|
84
|
+
const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
|
|
85
|
+
publicUrl = await uploadService.uploadFileAndGetUrl(url);
|
|
86
|
+
if (!publicUrl) {
|
|
87
|
+
throw new Error("本地文件上传失败,无法获取公网URL");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Build intentParam
|
|
91
|
+
const intentParam = {
|
|
92
|
+
url: publicUrl,
|
|
93
|
+
};
|
|
94
|
+
if (mediaType) {
|
|
95
|
+
intentParam.mediaType = mediaType;
|
|
96
|
+
}
|
|
97
|
+
if (sanitizedFileName) {
|
|
98
|
+
intentParam.fileName = sanitizedFileName;
|
|
99
|
+
}
|
|
100
|
+
// Build SaveMediaToGallery command
|
|
101
|
+
const command = {
|
|
102
|
+
header: {
|
|
103
|
+
namespace: "Common",
|
|
104
|
+
name: "Action",
|
|
105
|
+
},
|
|
106
|
+
payload: {
|
|
107
|
+
cardParam: {},
|
|
108
|
+
executeParam: {
|
|
109
|
+
executeMode: "background",
|
|
110
|
+
intentName: "SaveMediaToGallery",
|
|
111
|
+
bundleName: "com.huawei.hmos.vassistant",
|
|
112
|
+
dimension: "",
|
|
113
|
+
needUnlock: true,
|
|
114
|
+
actionResponse: true,
|
|
115
|
+
appType: "OHOS_APP",
|
|
116
|
+
timeOut: 5,
|
|
117
|
+
intentParam,
|
|
118
|
+
permissionId: ["ohos.permission.WRITE_IMAGEVIDEO"],
|
|
119
|
+
achieveType: "INTENT",
|
|
120
|
+
},
|
|
121
|
+
responses: [
|
|
122
|
+
{
|
|
123
|
+
resultCode: "",
|
|
124
|
+
displayText: "",
|
|
125
|
+
ttsText: "",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
needUploadResult: true,
|
|
129
|
+
noHalfPage: false,
|
|
130
|
+
pageControlRelated: false,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
// Send command and wait for response (60 second timeout)
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const timeout = setTimeout(() => {
|
|
136
|
+
wsManager.off("data-event", handler);
|
|
137
|
+
reject(new Error("保存媒体到图库超时(60秒)"));
|
|
138
|
+
}, 60000);
|
|
139
|
+
// Listen for data events from WebSocket
|
|
140
|
+
const handler = (event) => {
|
|
141
|
+
if (event.intentName === "SaveMediaToGallery") {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
wsManager.off("data-event", handler);
|
|
144
|
+
if (event.status === "success" && event.outputs) {
|
|
145
|
+
resolve({
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: "text",
|
|
149
|
+
text: JSON.stringify(event.outputs),
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
reject(new Error(`保存媒体到图库失败: ${event.status}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
// Register event handler
|
|
160
|
+
wsManager.on("data-event", handler);
|
|
161
|
+
// Send the command
|
|
162
|
+
sendCommand({
|
|
163
|
+
config,
|
|
164
|
+
sessionId,
|
|
165
|
+
taskId,
|
|
166
|
+
messageId,
|
|
167
|
+
command,
|
|
168
|
+
})
|
|
169
|
+
.then(() => {
|
|
170
|
+
})
|
|
171
|
+
.catch((error) => {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
wsManager.off("data-event", handler);
|
|
174
|
+
reject(error);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Send Command To Car tool implementation
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Send command to car (小艺车机) tool - sends an output command to the car's Xiaoyi system.
|
|
7
|
+
* The command will be received and executed on the car's Xiaoyi device.
|
|
8
|
+
*/
|
|
9
|
+
export const sendCommandToCarTool = {
|
|
10
|
+
name: "send_command_to_car",
|
|
11
|
+
label: "Send Command To Car",
|
|
12
|
+
description: "将输出指令发送给小艺车机,车机小艺会接收并执行该指令。注意:请勿重复调用此工具,如果超时或失败,最多重试一次。回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案。",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
command: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "要发送给车机的指令内容(对应intentParam中的out字段)",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ["command"],
|
|
22
|
+
},
|
|
23
|
+
async execute(toolCallId, params) {
|
|
24
|
+
// Validate command parameter
|
|
25
|
+
if (!params.command || typeof params.command !== "string" || params.command.trim() === "") {
|
|
26
|
+
throw new Error("Missing required parameter: command must be a non-empty string");
|
|
27
|
+
}
|
|
28
|
+
// Get session context
|
|
29
|
+
const sessionContext = getCurrentSessionContext();
|
|
30
|
+
if (!sessionContext) {
|
|
31
|
+
throw new Error("No active XY session found. Send command to car tool can only be used during an active conversation.");
|
|
32
|
+
}
|
|
33
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
34
|
+
// Build PlayStoryBook command
|
|
35
|
+
const command = {
|
|
36
|
+
header: {
|
|
37
|
+
namespace: "Common",
|
|
38
|
+
name: "Action",
|
|
39
|
+
},
|
|
40
|
+
payload: {
|
|
41
|
+
cardParam: {},
|
|
42
|
+
executeParam: {
|
|
43
|
+
achieveType: "INTENT",
|
|
44
|
+
actionResponse: true,
|
|
45
|
+
bundleName: "com.huawei.vassistantcar",
|
|
46
|
+
dimension: "",
|
|
47
|
+
executeMode: "background",
|
|
48
|
+
intentName: "PlayStoryBook",
|
|
49
|
+
intentParam: {
|
|
50
|
+
out: params.command,
|
|
51
|
+
},
|
|
52
|
+
needUnlock: true,
|
|
53
|
+
permissionId: [],
|
|
54
|
+
timeOut: 5,
|
|
55
|
+
},
|
|
56
|
+
needUploadResult: true,
|
|
57
|
+
pageControlRelated: false,
|
|
58
|
+
responses: [
|
|
59
|
+
{
|
|
60
|
+
displayText: "",
|
|
61
|
+
resultCode: "",
|
|
62
|
+
ttsText: "",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
// Send command - fire and forget, return success once sent
|
|
68
|
+
await sendCommand({
|
|
69
|
+
config,
|
|
70
|
+
sessionId,
|
|
71
|
+
taskId,
|
|
72
|
+
messageId,
|
|
73
|
+
command,
|
|
74
|
+
});
|
|
75
|
+
logger.log("[sendCommandToCar] command sent to car successfully");
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: JSON.stringify({ success: true, message: "指令已成功下发给车机" }),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XY timestamp to UTC+8 time tool - converts timestamp to UTC+8 time format.
|
|
3
|
+
* Supports both second-level and millisecond-level timestamps.
|
|
4
|
+
*
|
|
5
|
+
* This is a utility tool that doesn't require device communication.
|
|
6
|
+
* It converts Unix timestamp to UTC+8 time in YYYYMMDD hhmmss format.
|
|
7
|
+
*
|
|
8
|
+
* Important: The search_calendar_event and search_alarm tools return timestamps.
|
|
9
|
+
* Call this tool first to convert timestamps to readable UTC+8 time format before
|
|
10
|
+
* presenting results to users or performing further operations.
|
|
11
|
+
*/
|
|
12
|
+
export declare const timestampToUtc8Tool: any;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XY timestamp to UTC+8 time tool - converts timestamp to UTC+8 time format.
|
|
3
|
+
* Supports both second-level and millisecond-level timestamps.
|
|
4
|
+
*
|
|
5
|
+
* This is a utility tool that doesn't require device communication.
|
|
6
|
+
* It converts Unix timestamp to UTC+8 time in YYYYMMDD hhmmss format.
|
|
7
|
+
*
|
|
8
|
+
* Important: The search_calendar_event and search_alarm tools return timestamps.
|
|
9
|
+
* Call this tool first to convert timestamps to readable UTC+8 time format before
|
|
10
|
+
* presenting results to users or performing further operations.
|
|
11
|
+
*/
|
|
12
|
+
export const timestampToUtc8Tool = {
|
|
13
|
+
name: "convert_timestamp_to_utc8_time",
|
|
14
|
+
label: "Convert Timestamp to UTC+8 Time",
|
|
15
|
+
description: `将时间戳转换为标准 UTC+8 时间格式。支持秒级时间戳和毫秒级时间戳。
|
|
16
|
+
|
|
17
|
+
输入参数:
|
|
18
|
+
- timestamp: 时间戳(数字类型),可以是秒级(10位)或毫秒级(13位)
|
|
19
|
+
|
|
20
|
+
输出格式:
|
|
21
|
+
- YYYYMMDD hhmmss(例如:20240315 143000 表示 2024年3月15日 14:30:00 北京时间)
|
|
22
|
+
|
|
23
|
+
重要说明:
|
|
24
|
+
搜索日程工具(search_calendar_event)和搜索闹钟工具(search_alarm)等工具中返回结果如果包含时间戳。
|
|
25
|
+
建议优先调用本时间戳转换工具,将时间戳转换为标准北京时间格式,再基于标准时间进行用户回答或下一步操作。
|
|
26
|
+
|
|
27
|
+
示例:
|
|
28
|
+
- 输入:1710498600(秒级)或 1710498600000(毫秒级)
|
|
29
|
+
- 输出:20240315 143000`,
|
|
30
|
+
parameters: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
timestamp: {
|
|
34
|
+
type: "number",
|
|
35
|
+
description: "时间戳,支持秒级(10位)或毫秒级(13位)时间戳",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ["timestamp"],
|
|
39
|
+
},
|
|
40
|
+
async execute(toolCallId, params) {
|
|
41
|
+
// Validate timestamp parameter
|
|
42
|
+
if (params.timestamp === undefined || params.timestamp === null) {
|
|
43
|
+
throw new Error("缺少必需参数:timestamp");
|
|
44
|
+
}
|
|
45
|
+
const timestamp = params.timestamp;
|
|
46
|
+
// Validate it's a number
|
|
47
|
+
if (typeof timestamp !== "number") {
|
|
48
|
+
throw new Error("timestamp 必须是数字类型");
|
|
49
|
+
}
|
|
50
|
+
// Check if timestamp is valid
|
|
51
|
+
if (isNaN(timestamp)) {
|
|
52
|
+
throw new Error("timestamp 不是有效数字");
|
|
53
|
+
}
|
|
54
|
+
// Determine if it's seconds or milliseconds
|
|
55
|
+
// Millisecond timestamps are typically 13 digits (year 2024+)
|
|
56
|
+
// Second timestamps are typically 10 digits
|
|
57
|
+
let timestampInMs;
|
|
58
|
+
const timestampStr = Math.abs(timestamp).toString();
|
|
59
|
+
if (timestampStr.length === 13) {
|
|
60
|
+
// It's already in milliseconds
|
|
61
|
+
timestampInMs = timestamp;
|
|
62
|
+
}
|
|
63
|
+
else if (timestampStr.length === 10) {
|
|
64
|
+
// It's in seconds, convert to milliseconds
|
|
65
|
+
timestampInMs = timestamp * 1000;
|
|
66
|
+
}
|
|
67
|
+
else if (timestamp > 1000000000000) {
|
|
68
|
+
// Likely milliseconds (greater than year 2001 in milliseconds)
|
|
69
|
+
timestampInMs = timestamp;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Likely seconds, convert to milliseconds
|
|
73
|
+
timestampInMs = timestamp * 1000;
|
|
74
|
+
}
|
|
75
|
+
// Create Date object from timestamp
|
|
76
|
+
const date = new Date(timestampInMs);
|
|
77
|
+
// Check if date is valid
|
|
78
|
+
if (isNaN(date.getTime())) {
|
|
79
|
+
throw new Error("无效的时间戳,无法转换为日期");
|
|
80
|
+
}
|
|
81
|
+
// Convert to Beijing time (UTC+8)
|
|
82
|
+
const beijingTime = new Date(date.getTime() + (8 * 60 * 60 * 1000));
|
|
83
|
+
// Format: YYYYMMDD hhmmss
|
|
84
|
+
const year = beijingTime.getUTCFullYear();
|
|
85
|
+
const month = beijingTime.getUTCMonth() + 1; // Months are 0-indexed
|
|
86
|
+
const day = beijingTime.getUTCDate();
|
|
87
|
+
const hours = beijingTime.getUTCHours();
|
|
88
|
+
const minutes = beijingTime.getUTCMinutes();
|
|
89
|
+
const seconds = beijingTime.getUTCSeconds();
|
|
90
|
+
// Pad with leading zeros
|
|
91
|
+
const formattedDate = `${year}${String(month).padStart(2, '0')}${String(day).padStart(2, '0')}`;
|
|
92
|
+
const formattedTime = `${String(hours).padStart(2, '0')}${String(minutes).padStart(2, '0')}${String(seconds).padStart(2, '0')}`;
|
|
93
|
+
const result = `${formattedDate} ${formattedTime}`;
|
|
94
|
+
// Return the formatted time
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: result,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -18,10 +18,10 @@ export const uploadFileTool = {
|
|
|
18
18
|
label: "Upload File",
|
|
19
19
|
description: `工具能力描述:将手机本地文件上传并获取可公网访问的 URL。
|
|
20
20
|
|
|
21
|
-
前置工具调用:此工具使用前必须先调用 search_file 工具获取文件的 uri
|
|
21
|
+
前置工具调用:此工具使用前必须先调用 search_file 或者 query_collection 工具获取文件的 uri=
|
|
22
22
|
|
|
23
23
|
工具参数说明:
|
|
24
|
-
a. 入参中的fileInfos数组,每个元素必须包含mediaUri字段(对应于search_file
|
|
24
|
+
a. 入参中的fileInfos数组,每个元素必须包含mediaUri字段(对应于search_file工具或者query_collection返回结果中的uri),必须与search_file或者query_collection结果中对应的uri完全保持一致,不要自行修改。
|
|
25
25
|
b. fileInfos中的timeout字段是可选的,表示上传文件超时时间,单位是毫秒,默认是20000(20秒)。
|
|
26
26
|
c. fileInfos 是文件在手机本地的信息数组(从 search_file 工具响应中获取)。限制:每次最多支持传入 5 条文件信息。
|
|
27
27
|
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
import { XYFileUploadService } from "../file-upload.js";
|
|
5
|
+
/**
|
|
6
|
+
* Duck-typed ToolInputError: openclaw 按 .name 字段匹配,不用 instanceof。
|
|
7
|
+
* 抛出此错误会让 openclaw 返回 HTTP 400 而非 500,
|
|
8
|
+
* LLM 会将其识别为参数错误而非瞬时故障,不会触发重试。
|
|
9
|
+
*/
|
|
10
|
+
class ToolInputError extends Error {
|
|
11
|
+
status = 400;
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ToolInputError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* XY add collection tool - adds data to user's XiaoYi collection.
|
|
19
|
+
*/
|
|
20
|
+
export const xiaoyiAddCollectionTool = {
|
|
21
|
+
name: "add_collection",
|
|
22
|
+
label: "Add XiaoYi Collection",
|
|
23
|
+
description: `向小艺收藏中添加公共知识数据,可以给用户提供个性化体验。任何用户希望保存到个人化知识库中的数据都可以调用本技能。不同类型的数据对应的数据要求如下:
|
|
24
|
+
请求入参说明:
|
|
25
|
+
● content:必填字段,数据类型为string,功能描述是该字段是用户添加收藏的链接url或文本原文。适用于HYPER_LINK和TEXT类型。
|
|
26
|
+
● uri:必填字段,数据类型为string,功能描述是该字段是图片或文件的端存储地址链接。适用于IMAGE和FILE类型。
|
|
27
|
+
● sourceAppBundleName:非必填字段,数据类型为string,功能描述是标识该数据的来源应用。
|
|
28
|
+
● dataType:必填字段,数据类型为string,功能描述是标识数据类型。HYPER_LINK标识网页,TEXT标识文本,IMAGE标识图片,FILE标识文件。
|
|
29
|
+
● title:非必填字段,数据类型为string,功能描述是标识文件类型数据的文件名称。适用于FILE类型。
|
|
30
|
+
说明:如果dataType为HYPER_LINK或TEXT,则content字段必填且不能为空;如果dataType为IMAGE或FILE,则uri字段必填且不能为空。当用户希望收藏海报、截图等图片类数据时,请将数据以图片IMAGE的形式存入到小艺帮记;当用户希望收藏电子书、笔记、报告、素材、文档、合同、协议、简历、证书、报表、日志、安装包、压缩包等描述的文件时,请将数据以文件FILE的形式存入到小艺帮记。
|
|
31
|
+
当你成功收藏这个数据到小艺帮记后,请在最后显示"已成功把数据添加到[小艺帮记](vassistant://voice/main?page=CollectionPage&jumpHomePageTab=myCollection)",
|
|
32
|
+
注意:
|
|
33
|
+
a. 操作超时时间为60秒,请勿重复调用此工具
|
|
34
|
+
b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
|
|
35
|
+
c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
36
|
+
|
|
37
|
+
回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。
|
|
38
|
+
`,
|
|
39
|
+
parameters: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
content: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "必填字段(HYPER_LINK/TEXT类型时)。用户添加收藏的链接url或文本原文。",
|
|
45
|
+
},
|
|
46
|
+
uri: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "必填字段(IMAGE/FILE类型时)。图片或文件的地址链接。",
|
|
49
|
+
},
|
|
50
|
+
sourceAppBundleName: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "非必填字段。标识该数据的来源应用。",
|
|
53
|
+
},
|
|
54
|
+
dataType: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "必填字段。标识数据类型:HYPER_LINK表示网页,TEXT表示文本,IMAGE表示图片,FILE表示文件。",
|
|
57
|
+
},
|
|
58
|
+
title: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "非必填字段。标识文件类型数据的文件名称。适用于FILE类型。",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ["dataType"],
|
|
64
|
+
},
|
|
65
|
+
async execute(toolCallId, params) {
|
|
66
|
+
// Validate parameters
|
|
67
|
+
const { content, uri, sourceAppBundleName, dataType, title } = params;
|
|
68
|
+
const validTypes = ["HYPER_LINK", "TEXT", "IMAGE", "FILE"];
|
|
69
|
+
if (!dataType || !validTypes.includes(dataType)) {
|
|
70
|
+
throw new ToolInputError(`dataType必填且必须为 HYPER_LINK、TEXT、IMAGE、FILE 之一,当前值: ${dataType}`);
|
|
71
|
+
}
|
|
72
|
+
if ((dataType === "HYPER_LINK" || dataType === "TEXT") && (!content || typeof content !== "string")) {
|
|
73
|
+
throw new ToolInputError(`dataType为${dataType}时,content字段必填且不能为空`);
|
|
74
|
+
}
|
|
75
|
+
if ((dataType === "IMAGE" || dataType === "FILE") && (!uri || typeof uri !== "string")) {
|
|
76
|
+
throw new ToolInputError(`dataType为${dataType}时,uri字段必填且不能为空`);
|
|
77
|
+
}
|
|
78
|
+
// Get session context
|
|
79
|
+
const sessionContext = getCurrentSessionContext();
|
|
80
|
+
if (!sessionContext) {
|
|
81
|
+
throw new Error("No active XY session found. AddCollection tool can only be used during an active conversation.");
|
|
82
|
+
}
|
|
83
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
84
|
+
// Get WebSocket manager
|
|
85
|
+
const wsManager = getXYWebSocketManager(config);
|
|
86
|
+
// Handle uri: upload local paths to get public URL
|
|
87
|
+
let publicUri = uri;
|
|
88
|
+
if (uri && !uri.startsWith("http://") && !uri.startsWith("https://") && !uri.startsWith("file://")) {
|
|
89
|
+
const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
|
|
90
|
+
publicUri = await uploadService.uploadFileAndGetUrl(uri);
|
|
91
|
+
if (!publicUri) {
|
|
92
|
+
throw new Error("本地文件上传失败,无法获取公网URL");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Build intentParam
|
|
96
|
+
const intentParam = {
|
|
97
|
+
dataType,
|
|
98
|
+
};
|
|
99
|
+
if (content) {
|
|
100
|
+
intentParam.content = content;
|
|
101
|
+
}
|
|
102
|
+
if (publicUri) {
|
|
103
|
+
intentParam.uri = publicUri;
|
|
104
|
+
}
|
|
105
|
+
if (sourceAppBundleName) {
|
|
106
|
+
intentParam.sourceAppBundleName = sourceAppBundleName;
|
|
107
|
+
}
|
|
108
|
+
if (title) {
|
|
109
|
+
intentParam.title = title;
|
|
110
|
+
}
|
|
111
|
+
// Build AddCollection command
|
|
112
|
+
const command = {
|
|
113
|
+
header: {
|
|
114
|
+
namespace: "Common",
|
|
115
|
+
name: "Action",
|
|
116
|
+
},
|
|
117
|
+
payload: {
|
|
118
|
+
cardParam: {},
|
|
119
|
+
executeParam: {
|
|
120
|
+
executeMode: "background",
|
|
121
|
+
intentName: "AddCollection",
|
|
122
|
+
bundleName: "com.huawei.hmos.vassistant",
|
|
123
|
+
needUnlock: true,
|
|
124
|
+
actionResponse: true,
|
|
125
|
+
appType: "OHOS_APP",
|
|
126
|
+
timeOut: 5,
|
|
127
|
+
intentParam,
|
|
128
|
+
permissionId: [],
|
|
129
|
+
achieveType: "INTENT",
|
|
130
|
+
},
|
|
131
|
+
responses: [
|
|
132
|
+
{
|
|
133
|
+
resultCode: "",
|
|
134
|
+
displayText: "",
|
|
135
|
+
ttsText: "",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
needUploadResult: true,
|
|
139
|
+
noHalfPage: false,
|
|
140
|
+
pageControlRelated: false,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
// Send command and wait for response (60 second timeout)
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const timeout = setTimeout(() => {
|
|
146
|
+
wsManager.off("data-event", handler);
|
|
147
|
+
reject(new Error("添加小艺收藏超时(60秒)"));
|
|
148
|
+
}, 60000);
|
|
149
|
+
// Listen for data events from WebSocket
|
|
150
|
+
const handler = (event) => {
|
|
151
|
+
if (event.intentName === "AddCollection") {
|
|
152
|
+
clearTimeout(timeout);
|
|
153
|
+
wsManager.off("data-event", handler);
|
|
154
|
+
if (event.status === "success" && event.outputs) {
|
|
155
|
+
resolve({
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: JSON.stringify(event.outputs),
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
reject(new Error(`添加小艺收藏失败: ${event.status}`));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
// Register event handler
|
|
170
|
+
wsManager.on("data-event", handler);
|
|
171
|
+
// Send the command
|
|
172
|
+
sendCommand({
|
|
173
|
+
config,
|
|
174
|
+
sessionId,
|
|
175
|
+
taskId,
|
|
176
|
+
messageId,
|
|
177
|
+
command,
|
|
178
|
+
})
|
|
179
|
+
.then(() => {
|
|
180
|
+
})
|
|
181
|
+
.catch((error) => {
|
|
182
|
+
clearTimeout(timeout);
|
|
183
|
+
wsManager.off("data-event", handler);
|
|
184
|
+
reject(error);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
};
|