@ynhcj/xiaoyi-channel 0.0.166-beta → 0.0.166-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.js +141 -1
- package/dist/src/bot.js +60 -5
- package/dist/src/cron-command.d.ts +2 -0
- package/dist/src/cron-command.js +14 -8
- package/dist/src/cron-query-handler.js +45 -8
- package/dist/src/cspl/call_api.js +2 -2
- package/dist/src/cspl/sentinel_hook.js +9 -4
- package/dist/src/cspl/upload_file.js +2 -2
- package/dist/src/cspl/utils.d.ts +9 -3
- package/dist/src/cspl/utils.js +15 -9
- package/dist/src/file-upload.d.ts +5 -0
- package/dist/src/file-upload.js +102 -0
- package/dist/src/formatter.d.ts +29 -0
- package/dist/src/formatter.js +100 -2
- package/dist/src/monitor.js +35 -23
- package/dist/src/parser.d.ts +6 -0
- package/dist/src/parser.js +23 -13
- package/dist/src/provider.js +41 -1
- package/dist/src/reply-dispatcher.js +34 -16
- package/dist/src/self-evolution-handler.d.ts +1 -1
- package/dist/src/self-evolution-handler.js +12 -1
- package/dist/src/tools/check-plugin-privilege-tool.d.ts +6 -0
- package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
- package/dist/src/tools/create-all-tools.js +4 -0
- package/dist/src/tools/device-tool-map.d.ts +1 -1
- package/dist/src/tools/device-tool-map.js +8 -1
- package/dist/src/tools/modify-alarm-tool.js +17 -0
- package/dist/src/tools/send-cross-device-task-tool.js +84 -15
- package/dist/src/tools/send-file-to-user-tool.js +9 -11
- package/dist/src/tools/send-html-card-tool.d.ts +7 -0
- package/dist/src/tools/send-html-card-tool.js +113 -0
- package/dist/src/tools/session-manager.d.ts +11 -2
- package/dist/src/tools/session-manager.js +65 -18
- package/dist/src/tools/xiaoyi-gui-tool.js +1 -1
- package/dist/src/types.d.ts +9 -7
- package/dist/src/utils/config-manager.d.ts +3 -2
- package/dist/src/utils/config-manager.js +22 -2
- package/dist/src/utils/cron-push-map.d.ts +26 -0
- package/dist/src/utils/cron-push-map.js +131 -0
- package/dist/src/websocket.js +11 -13
- package/package.json +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Check plugin privilege tool implementation
|
|
2
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
3
|
+
import { sendCommand } from "../formatter.js";
|
|
4
|
+
import { getCurrentTaskId } from "../task-manager.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
/**
|
|
7
|
+
* Mapping from intent name to required permission IDs.
|
|
8
|
+
*/
|
|
9
|
+
const INTENT_PERMISSION_MAP = {
|
|
10
|
+
GetCurrentLocation: ["ohos.permission.LOCATION", "ohos.permission.APPROXIMATELY_LOCATION"],
|
|
11
|
+
SearchCalendarEvent: ["ohos.permission.READ_WHOLE_CALENDAR"],
|
|
12
|
+
CreateCalendarEvent: ["ohos.permission.WRITE_WHOLE_CALENDAR"],
|
|
13
|
+
DeleteCalendarEvent: ["ohos.permission.WRITE_WHOLE_CALENDAR"],
|
|
14
|
+
ModifyCalendarEvent: ["ohos.permission.WRITE_WHOLE_CALENDAR"],
|
|
15
|
+
SearchNote: ["ohos.permission.READ_NOTE"],
|
|
16
|
+
CreateNote: ["ohos.permission.WRITE_NOTE"],
|
|
17
|
+
ModifyNote: ["ohos.permission.WRITE_NOTE"],
|
|
18
|
+
SearchContactLocal: ["ohos.permission.READ_CONTACTS"],
|
|
19
|
+
SearchPhotoVideo: ["ohos.permission.READ_IMAGEVIDEO"],
|
|
20
|
+
SaveMediaToGallery: ["ohos.permission.WRITE_IMAGEVIDEO"],
|
|
21
|
+
SearchFile: ["ohos.permission.FILE_ACCESS_MANAGER"],
|
|
22
|
+
SaveFileToFileManager: ["ohos.permission.FILE_SAVE_MANAGER"],
|
|
23
|
+
SearchAlarm: ["ohos.permission.READ_ALARM"],
|
|
24
|
+
CreateAlarm: ["ohos.permission.WRITE_ALARM"],
|
|
25
|
+
ModifyAlarm: ["ohos.permission.WRITE_ALARM"],
|
|
26
|
+
DeleteAlarm: ["ohos.permission.WRITE_ALARM"],
|
|
27
|
+
SearchMessage: ["ohos.permission.READ_SMS"],
|
|
28
|
+
SendShortMessage: ["ohos.permission.SEND_MESSAGES"],
|
|
29
|
+
StartCall: ["ohos.permission.PLACE_CALL"],
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* XY check plugin privilege tool - checks user authorization for device-side tools
|
|
33
|
+
* used in scheduled tasks.
|
|
34
|
+
*/
|
|
35
|
+
export function createCheckPluginPrivilegeTool(ctx) {
|
|
36
|
+
const { config, sessionId, taskId, messageId } = ctx;
|
|
37
|
+
return {
|
|
38
|
+
name: "check_plugin_privilege",
|
|
39
|
+
label: "Check Plugin Privilege",
|
|
40
|
+
description: "定时任务权限检查工具。" +
|
|
41
|
+
"【使用场景】仅在创建定时任务时使用,严禁在其他场景调用。当识别到定时任务中需要使用用户设备侧工具时,必须调用此工具进行权限检查。" +
|
|
42
|
+
"【调用前提】调用此工具前,必须确认用户定时任务中提到的工具在当前模型可使用的工具列表中存在。如果当前工具列表中不存在符合用户诉求的工具定义,则不要调用此工具,而是直接告知用户当前设备不支持该功能。" +
|
|
43
|
+
"【支持的意图名称及对应权限】" +
|
|
44
|
+
"GetCurrentLocation(获取用户位置), " +
|
|
45
|
+
"SearchCalendarEvent(搜索用户日程), " +
|
|
46
|
+
"CreateCalendarEvent(新建用户日程), " +
|
|
47
|
+
"DeleteCalendarEvent(删除用户日程), " +
|
|
48
|
+
"ModifyCalendarEvent(修改用户日程), " +
|
|
49
|
+
"SearchNote(搜索用户备忘录), " +
|
|
50
|
+
"CreateNote(新建用户备忘录), " +
|
|
51
|
+
"ModifyNote(修改用户备忘录), " +
|
|
52
|
+
"SearchContactLocal(搜索用户联系人), " +
|
|
53
|
+
"SearchPhotoVideo(搜索用户图库照片或视频), " +
|
|
54
|
+
"SaveMediaToGallery(保存图片/视频到图库), " +
|
|
55
|
+
"SearchFile(搜索用户文件管理里面的文件), " +
|
|
56
|
+
"SaveFileToFileManager(保存文件到文件管理), " +
|
|
57
|
+
"SearchAlarm(搜索闹钟), " +
|
|
58
|
+
"CreateAlarm(新建闹钟), " +
|
|
59
|
+
"ModifyAlarm(修改闹钟), " +
|
|
60
|
+
"DeleteAlarm(删除闹钟), " +
|
|
61
|
+
"SearchMessage(搜索短信), " +
|
|
62
|
+
"SendShortMessage(发送短信), " +
|
|
63
|
+
"StartCall(打电话)。" +
|
|
64
|
+
"【多次调用】如果用户的定时任务指令中涉及多个端侧工具,则依次分别调用此工具检查每个工具的权限。如果调用超时失败,最多重试一次。" +
|
|
65
|
+
"【回复约束】如果工具返回没有授权或其他报错,只需要完整描述没有授权或其他报错内容即可,不需要主动给用户提供解决方案。" +
|
|
66
|
+
"【使用约束1】只要是创建定时任务且涉及端插件的使用,则必须调用此工具检查权限" +
|
|
67
|
+
"【使用约束2】如果是定时任务执行过程中,禁止调用此工具,此工具仅在创建定时任务时按需调用",
|
|
68
|
+
parameters: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
checkIntentName: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "需要检查权限的意图名称,必须是支持的意图名称之一,例如:GetCurrentLocation、SearchCalendarEvent、CreateCalendarEvent 等。",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ["checkIntentName"],
|
|
77
|
+
},
|
|
78
|
+
async execute(toolCallId, params) {
|
|
79
|
+
const { checkIntentName } = params;
|
|
80
|
+
// Look up permission IDs for the given intent name
|
|
81
|
+
const permissionId = INTENT_PERMISSION_MAP[checkIntentName];
|
|
82
|
+
if (!permissionId) {
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `不支持的工具意图名称: ${checkIntentName}。请确认该意图名称在支持列表中。`,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Get WebSocket manager
|
|
93
|
+
const wsManager = getXYWebSocketManager(config);
|
|
94
|
+
// Build CheckPlugInPrivilege command
|
|
95
|
+
const command = {
|
|
96
|
+
header: {
|
|
97
|
+
namespace: "Common",
|
|
98
|
+
name: "Action",
|
|
99
|
+
},
|
|
100
|
+
payload: {
|
|
101
|
+
cardParam: {},
|
|
102
|
+
executeParam: {
|
|
103
|
+
achieveType: "INTENT",
|
|
104
|
+
actionResponse: true,
|
|
105
|
+
bundleName: "com.huawei.hmos.vassistant",
|
|
106
|
+
dimension: "",
|
|
107
|
+
executeMode: "background",
|
|
108
|
+
intentName: "CheckPlugInPrivilege",
|
|
109
|
+
intentParam: {
|
|
110
|
+
checkIntentName,
|
|
111
|
+
permissionId,
|
|
112
|
+
},
|
|
113
|
+
needUnlock: false,
|
|
114
|
+
permissionId: [],
|
|
115
|
+
timeOut: 5,
|
|
116
|
+
},
|
|
117
|
+
needUploadResult: true,
|
|
118
|
+
pageControlRelated: false,
|
|
119
|
+
responses: [
|
|
120
|
+
{
|
|
121
|
+
displayText: "",
|
|
122
|
+
resultCode: "",
|
|
123
|
+
ttsText: "",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
// Send command and wait for response (60 second timeout)
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const timeout = setTimeout(() => {
|
|
131
|
+
wsManager.off("data-event", handler);
|
|
132
|
+
logger.error("超时: 检查插件权限超时(60秒)", { toolCallId, checkIntentName });
|
|
133
|
+
reject(new Error(`检查插件权限超时(60秒), intentName: ${checkIntentName}`));
|
|
134
|
+
}, 60000);
|
|
135
|
+
// Listen for data events from WebSocket
|
|
136
|
+
const handler = (event) => {
|
|
137
|
+
if (event.intentName === "CheckPlugInPrivilege") {
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
wsManager.off("data-event", handler);
|
|
140
|
+
if (event.status === "success" && event.outputs) {
|
|
141
|
+
resolve({
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: JSON.stringify(event.outputs),
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
resolve({
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: JSON.stringify(event.outputs),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
// Register event handler
|
|
163
|
+
wsManager.on("data-event", handler);
|
|
164
|
+
// Send the command
|
|
165
|
+
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
166
|
+
sendCommand({
|
|
167
|
+
config,
|
|
168
|
+
sessionId,
|
|
169
|
+
taskId: currentTaskId,
|
|
170
|
+
messageId,
|
|
171
|
+
command,
|
|
172
|
+
toolCallId,
|
|
173
|
+
}).then(() => {
|
|
174
|
+
}).catch((error) => {
|
|
175
|
+
clearTimeout(timeout);
|
|
176
|
+
wsManager.off("data-event", handler);
|
|
177
|
+
reject(error);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createLocationTool } from "./location-tool.js";
|
|
2
2
|
import { createXiaoyiGuiTool } from "./xiaoyi-gui-tool.js";
|
|
3
3
|
import { createSendFileToUserTool } from "./send-file-to-user-tool.js";
|
|
4
|
+
import { createSendHtmlCardTool } from "./send-html-card-tool.js";
|
|
4
5
|
import { viewPushResultTool } from "./view-push-result-tool.js";
|
|
5
6
|
import { createImageReadingTool } from "./image-reading-tool.js";
|
|
6
7
|
import { timestampToUtc8Tool } from "./timestamp-to-utc8-tool.js";
|
|
@@ -19,6 +20,7 @@ import { createAgentAsSkillTool } from "./agent-as-skill-tool.js";
|
|
|
19
20
|
import { createDiscoverCrossDevicesTool } from "./discover-cross-devices-tool.js";
|
|
20
21
|
import { createSendCrossDeviceTaskTool } from "./send-cross-device-task-tool.js";
|
|
21
22
|
import { createDisplayA2UICardTool } from "./display-a2ui-card-tool.js";
|
|
23
|
+
import { createCheckPluginPrivilegeTool } from "./check-plugin-privilege-tool.js";
|
|
22
24
|
import { logger } from "../utils/logger.js";
|
|
23
25
|
/**
|
|
24
26
|
* Create all XY channel tools for the given session context.
|
|
@@ -47,6 +49,7 @@ export function createAllTools(ctx) {
|
|
|
47
49
|
createGetAlarmToolSchemaTool(ctx),
|
|
48
50
|
createGetCollectionToolSchemaTool(ctx),
|
|
49
51
|
createSendFileToUserTool(ctx),
|
|
52
|
+
createSendHtmlCardTool(ctx),
|
|
50
53
|
// createGetEmailToolSchemaTool(ctx),
|
|
51
54
|
viewPushResultTool,
|
|
52
55
|
createImageReadingTool(ctx),
|
|
@@ -54,5 +57,6 @@ export function createAllTools(ctx) {
|
|
|
54
57
|
createSaveSelfEvolutionSkillTool(ctx),
|
|
55
58
|
createLoginTokenTool(ctx),
|
|
56
59
|
createAgentAsSkillTool(ctx),
|
|
60
|
+
createCheckPluginPrivilegeTool(ctx),
|
|
57
61
|
];
|
|
58
62
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Known device type enum. */
|
|
2
|
-
export declare const DEVICE_TYPES: readonly ["car", "2in1", "phone", "web"];
|
|
2
|
+
export declare const DEVICE_TYPES: readonly ["car", "2in1", "phone", "web", "pad"];
|
|
3
3
|
export type DeviceType = (typeof DEVICE_TYPES)[number];
|
|
4
4
|
export declare function filterToolsByDevice(tools: any[], deviceType?: string): any[];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// - denylist: listed tools are blocked, everything else is available (used for permissive devices like pc)
|
|
5
5
|
// Tools NOT listed in any device entry → available to all devices (no restriction).
|
|
6
6
|
/** Known device type enum. */
|
|
7
|
-
export const DEVICE_TYPES = ["car", "2in1", "phone", "web"];
|
|
7
|
+
export const DEVICE_TYPES = ["car", "2in1", "phone", "web", "pad"];
|
|
8
8
|
const DEVICE_TOOL_POLICY = {
|
|
9
9
|
"2in1": {
|
|
10
10
|
allowlist: false,
|
|
@@ -15,6 +15,13 @@ const DEVICE_TOOL_POLICY = {
|
|
|
15
15
|
"search_message",
|
|
16
16
|
"search_contact",
|
|
17
17
|
"get_contact_tool_schema",
|
|
18
|
+
"send_html_card"
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
"pad": {
|
|
22
|
+
allowlist: false,
|
|
23
|
+
tools: [
|
|
24
|
+
"xiaoyi_gui_agent"
|
|
18
25
|
],
|
|
19
26
|
},
|
|
20
27
|
"web": {
|
|
@@ -75,6 +75,23 @@ export function createModifyAlarmTool(ctx) {
|
|
|
75
75
|
required: ["entityId"],
|
|
76
76
|
},
|
|
77
77
|
async execute(toolCallId, params) {
|
|
78
|
+
// Coerce numeric string params to actual numbers
|
|
79
|
+
// The model may produce "1" instead of 1, which would fail typeof checks
|
|
80
|
+
const numericParams = [
|
|
81
|
+
"alarmState",
|
|
82
|
+
"alarmSnoozeDuration",
|
|
83
|
+
"alarmSnoozeTotal",
|
|
84
|
+
"alarmRingDuration",
|
|
85
|
+
"daysOfWakeType",
|
|
86
|
+
];
|
|
87
|
+
for (const key of numericParams) {
|
|
88
|
+
if (typeof params[key] === "string") {
|
|
89
|
+
const num = Number(params[key]);
|
|
90
|
+
if (!isNaN(num)) {
|
|
91
|
+
params[key] = num;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
78
95
|
// ===== Validate required parameter: entityId =====
|
|
79
96
|
if (!params.entityId || typeof params.entityId !== "string") {
|
|
80
97
|
throw new Error("Missing required parameter: entityId must be a string obtained from search_alarm or create_alarm");
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { sendCommand, sendStatusUpdate } from "../formatter.js";
|
|
2
2
|
import { getXYWebSocketManager } from "../client.js";
|
|
3
3
|
import { getCurrentMessageId, getCurrentTaskId } from "../task-manager.js";
|
|
4
|
-
import { createSendFileToUserTool } from "./send-file-to-user-tool.js";
|
|
5
4
|
import { logger } from "../utils/logger.js";
|
|
6
5
|
const LOG_TAG = "[SendPcDeviceTask]";
|
|
7
6
|
const SEND_CROSS_RESULT_LOG_TAG = "[SendCrossResult]";
|
|
@@ -28,7 +27,7 @@ function buildModelToolResult(result) {
|
|
|
28
27
|
let message = `跨端任务执行结果:${baseMessage}`;
|
|
29
28
|
if (resultStatus === "对端设备执行任务成功且返回有文件") {
|
|
30
29
|
if (result.autoSendFileToUser?.success) {
|
|
31
|
-
message += "\n\n
|
|
30
|
+
message += "\n\n对端设备返回了文件,系统已自动将文件卡片发送给用户。请你基于跨端任务结果生成最终回复,告知用户任务已完成且文件已发送。";
|
|
32
31
|
}
|
|
33
32
|
else {
|
|
34
33
|
const errorMessage = result.autoSendFileToUser?.error || "未知错误";
|
|
@@ -56,22 +55,92 @@ function buildCrossDeviceResult(params) {
|
|
|
56
55
|
};
|
|
57
56
|
return result;
|
|
58
57
|
}
|
|
58
|
+
function collectSentFileCards(sentFiles) {
|
|
59
|
+
const cardsByFileId = new Map();
|
|
60
|
+
for (const card of sentFiles) {
|
|
61
|
+
const fileId = typeof card.fileId === "string" ? card.fileId.trim() : "";
|
|
62
|
+
const fileName = typeof card.fileName === "string" ? card.fileName.trim() : "";
|
|
63
|
+
const mimeType = typeof card.mimeType === "string" ? card.mimeType.trim() : "";
|
|
64
|
+
if (!fileId || !fileName || cardsByFileId.has(fileId)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
cardsByFileId.set(fileId, {
|
|
68
|
+
fileId,
|
|
69
|
+
fileName,
|
|
70
|
+
...(mimeType ? { mimeType } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return Array.from(cardsByFileId.values());
|
|
74
|
+
}
|
|
75
|
+
function countSentFileCards(sentFiles) {
|
|
76
|
+
return collectSentFileCards(sentFiles).length;
|
|
77
|
+
}
|
|
78
|
+
async function sendFileCardsToUser(ctx, fileCards) {
|
|
79
|
+
const { config, sessionId, taskId, messageId } = ctx;
|
|
80
|
+
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
81
|
+
const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
|
|
82
|
+
const wsManager = getXYWebSocketManager(config);
|
|
83
|
+
const sentFileCards = [];
|
|
84
|
+
for (const card of fileCards) {
|
|
85
|
+
const agentResponse = {
|
|
86
|
+
msgType: "agent_response",
|
|
87
|
+
agentId: config.agentId,
|
|
88
|
+
sessionId,
|
|
89
|
+
taskId: currentTaskId,
|
|
90
|
+
msgDetail: JSON.stringify({
|
|
91
|
+
jsonrpc: "2.0",
|
|
92
|
+
id: currentMessageId,
|
|
93
|
+
result: {
|
|
94
|
+
kind: "artifact-update",
|
|
95
|
+
append: true,
|
|
96
|
+
lastChunk: false,
|
|
97
|
+
final: false,
|
|
98
|
+
artifact: {
|
|
99
|
+
artifactId: currentTaskId,
|
|
100
|
+
parts: [
|
|
101
|
+
{
|
|
102
|
+
kind: "file",
|
|
103
|
+
file: {
|
|
104
|
+
name: card.fileName,
|
|
105
|
+
mimeType: card.mimeType,
|
|
106
|
+
fileId: card.fileId,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
error: { code: 0 },
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} sending file card by fileId, fileName=${card.fileName}`);
|
|
116
|
+
await wsManager.sendMessage(sessionId, agentResponse);
|
|
117
|
+
sentFileCards.push({ fileName: card.fileName, fileId: card.fileId });
|
|
118
|
+
}
|
|
119
|
+
return sentFileCards;
|
|
120
|
+
}
|
|
59
121
|
async function autoSendFileToUserIfNeeded(result, ctx) {
|
|
60
122
|
const sentFiles = Array.isArray(result.sentFiles) ? result.sentFiles : [];
|
|
61
123
|
if (sentFiles.length === 0) {
|
|
62
124
|
return result;
|
|
63
125
|
}
|
|
64
|
-
|
|
126
|
+
const fileCards = collectSentFileCards(sentFiles);
|
|
127
|
+
if (fileCards.length === 0) {
|
|
128
|
+
const errorMessage = "Cross-device result contains no valid fileCards.";
|
|
129
|
+
logger.error(`${SEND_CROSS_RESULT_LOG_TAG} auto file card send skipped, error=${errorMessage}`);
|
|
130
|
+
return {
|
|
131
|
+
...result,
|
|
132
|
+
autoSendFileToUser: {
|
|
133
|
+
success: false,
|
|
134
|
+
error: errorMessage,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto sending cross-device file cards, fileCardCount=${fileCards.length}`);
|
|
65
139
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
results.push(await sendFileTool.execute("auto_send_cross_device_file", sentFileParams));
|
|
71
|
-
}
|
|
72
|
-
return results;
|
|
73
|
-
})();
|
|
74
|
-
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto send_file_to_user completed`);
|
|
140
|
+
const sendFileResult = {
|
|
141
|
+
fileCards: await sendFileCardsToUser(ctx, fileCards),
|
|
142
|
+
};
|
|
143
|
+
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto file card send completed, fileCardCount=${sendFileResult.fileCards.length}`);
|
|
75
144
|
return {
|
|
76
145
|
...result,
|
|
77
146
|
autoSendFileToUser: {
|
|
@@ -82,7 +151,7 @@ async function autoSendFileToUserIfNeeded(result, ctx) {
|
|
|
82
151
|
}
|
|
83
152
|
catch (error) {
|
|
84
153
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
85
|
-
logger.error(`${SEND_CROSS_RESULT_LOG_TAG} auto
|
|
154
|
+
logger.error(`${SEND_CROSS_RESULT_LOG_TAG} auto file card send failed, error=${errorMessage}`);
|
|
86
155
|
return {
|
|
87
156
|
...result,
|
|
88
157
|
autoSendFileToUser: {
|
|
@@ -218,7 +287,7 @@ export function createSendCrossDeviceTaskTool(ctx) {
|
|
|
218
287
|
}
|
|
219
288
|
settled = true;
|
|
220
289
|
const modelResult = buildModelToolResult(result);
|
|
221
|
-
logger.log(`${LOG_TAG} completed, success=${result.success}, code=${result.code},
|
|
290
|
+
logger.log(`${LOG_TAG} completed, success=${result.success}, code=${result.code}, fileCardCount=${countSentFileCards(result.sentFiles)}`);
|
|
222
291
|
cleanup();
|
|
223
292
|
resolve(buildResultText(modelResult));
|
|
224
293
|
};
|
|
@@ -226,7 +295,7 @@ export function createSendCrossDeviceTaskTool(ctx) {
|
|
|
226
295
|
if (event.sessionId && event.sessionId !== sessionId && event.sessionId !== distributionSessionId) {
|
|
227
296
|
return;
|
|
228
297
|
}
|
|
229
|
-
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} received result, status=${event.status}, code=${event.code},
|
|
298
|
+
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} received result, status=${event.status}, code=${event.code}, fileCardCount=${countSentFileCards(event.sentFiles)}`);
|
|
230
299
|
void (async () => {
|
|
231
300
|
if (resultHandlingStarted) {
|
|
232
301
|
return;
|
|
@@ -174,16 +174,6 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
|
|
|
174
174
|
throw new Error(`fileNames length (${fileNames.length}) must match fileRemoteUrls length (${fileRemoteUrls.length})`);
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
-
if (ctx.runCrossTaskContext && (fileLocalUrls.length > 0 || fileRemoteUrls.length > 0)) {
|
|
178
|
-
const cachedSentFiles = appendRunCrossTaskSentFiles([
|
|
179
|
-
{
|
|
180
|
-
...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
|
|
181
|
-
...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
|
|
182
|
-
...(fileNames.length > 0 ? { fileNames } : {}),
|
|
183
|
-
},
|
|
184
|
-
], ctx.runCrossTaskContext);
|
|
185
|
-
logger.log(`[RunCrossTask] cached ${cachedSentFiles.length} send_file_to_user input(s) for cross-task result`);
|
|
186
|
-
}
|
|
187
177
|
// Get WebSocket manager
|
|
188
178
|
const wsManager = getXYWebSocketManager(config);
|
|
189
179
|
// Create upload service
|
|
@@ -237,6 +227,7 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
|
|
|
237
227
|
}
|
|
238
228
|
// Build and send agent_response messages for each file
|
|
239
229
|
const sentFiles = [];
|
|
230
|
+
let cachedSentFilesForReturn = [];
|
|
240
231
|
for (const uploadedFile of uploadedFiles) {
|
|
241
232
|
const { fileName, fileId, mimeType } = uploadedFile;
|
|
242
233
|
const agentResponse = {
|
|
@@ -273,6 +264,12 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
|
|
|
273
264
|
// Send WebSocket message
|
|
274
265
|
await wsManager.sendMessage(sessionId, agentResponse);
|
|
275
266
|
logger.log(`[SEND-FILE-TO-USER] send ${fileName} file to user success`);
|
|
267
|
+
if (ctx.runCrossTaskContext) {
|
|
268
|
+
const sentFileCard = { fileName, fileId, mimeType };
|
|
269
|
+
const cachedSentFiles = appendRunCrossTaskSentFiles([sentFileCard], ctx.runCrossTaskContext);
|
|
270
|
+
cachedSentFilesForReturn = cachedSentFiles;
|
|
271
|
+
logger.log(`[RunCrossTask] cached file card for cross-task result, fileName=${fileName}, cachedFileCardCount=${cachedSentFiles.length}`);
|
|
272
|
+
}
|
|
276
273
|
sentFiles.push({ fileName, fileId });
|
|
277
274
|
}
|
|
278
275
|
return {
|
|
@@ -282,7 +279,8 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
|
|
|
282
279
|
text: JSON.stringify({
|
|
283
280
|
sentFiles,
|
|
284
281
|
count: sentFiles.length,
|
|
285
|
-
message: `成功发送 ${sentFiles.length}
|
|
282
|
+
message: `成功发送 ${sentFiles.length} 个文件到用户设备`,
|
|
283
|
+
cachedSentFiles: cachedSentFilesForReturn
|
|
286
284
|
}),
|
|
287
285
|
},
|
|
288
286
|
],
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SessionContext } from "./session-manager.js";
|
|
2
|
+
/**
|
|
3
|
+
* XY send HTML card tool - sends HTML content as an H5 card to user's device.
|
|
4
|
+
* Prefer this tool over send_file_to_user when sending HTML files to users.
|
|
5
|
+
* Only use send_file_to_user for HTML files when the user explicitly requests the raw file.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createSendHtmlCardTool(ctx: SessionContext): any;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { XYFileUploadService } from "../file-upload.js";
|
|
2
|
+
import { sendCard } from "../formatter.js";
|
|
3
|
+
import { getCurrentTaskId } from "../task-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* XY send HTML card tool - sends HTML content as an H5 card to user's device.
|
|
7
|
+
* Prefer this tool over send_file_to_user when sending HTML files to users.
|
|
8
|
+
* Only use send_file_to_user for HTML files when the user explicitly requests the raw file.
|
|
9
|
+
*/
|
|
10
|
+
export function createSendHtmlCardTool(ctx) {
|
|
11
|
+
const { config, sessionId, taskId, messageId } = ctx;
|
|
12
|
+
return {
|
|
13
|
+
name: "send_html_card",
|
|
14
|
+
label: "Send HTML Card",
|
|
15
|
+
description: `工具能力描述:支持以H5卡片的形式展示HTML页面内容,用户可以直接在卡片中查看。
|
|
16
|
+
|
|
17
|
+
工具参数说明:
|
|
18
|
+
a. htmlUrl 和 htmlLocal 至少填写一个
|
|
19
|
+
b. htmlUrl 是在线链接,可以直接公网访问的HTML页面地址
|
|
20
|
+
c. htmlLocal 是本地HTML文件路径,会先上传获取预览链接再以卡片形式发送
|
|
21
|
+
|
|
22
|
+
注意事项:
|
|
23
|
+
a. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如果超时或失败,最多重试一次
|
|
24
|
+
b. 最后要把最终的html的公网地址作为工具执行结果返回回去,要以markdown超链接的形式返回给用户,必须严格保留完整的url,包含url的鉴权鉴权信息,返回给用户的url必须是完整的
|
|
25
|
+
c. 仅当用户或者skill中显示说明使用send_html_card工具时才调用此工具`,
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
htmlUrl: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "在线HTML页面链接,可直接公网访问的URL地址",
|
|
32
|
+
},
|
|
33
|
+
htmlLocal: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "本地HTML文件路径",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: [],
|
|
39
|
+
},
|
|
40
|
+
async execute(toolCallId, params) {
|
|
41
|
+
// Validate at least one parameter is provided
|
|
42
|
+
if (!params.htmlUrl && !params.htmlLocal) {
|
|
43
|
+
throw new Error("htmlUrl 和 htmlLocal 至少需要填写一个");
|
|
44
|
+
}
|
|
45
|
+
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
46
|
+
// Set timeout for the entire operation (2 minutes)
|
|
47
|
+
const TOOL_TIMEOUT = 120000;
|
|
48
|
+
let timeoutHandle = null;
|
|
49
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
50
|
+
timeoutHandle = setTimeout(() => {
|
|
51
|
+
reject(new Error("操作超时(2分钟)"));
|
|
52
|
+
}, TOOL_TIMEOUT);
|
|
53
|
+
});
|
|
54
|
+
const executionPromise = (async () => {
|
|
55
|
+
let url = params.htmlUrl;
|
|
56
|
+
// If htmlLocal is provided, upload it to get a preview URL
|
|
57
|
+
if (params.htmlLocal) {
|
|
58
|
+
const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
|
|
59
|
+
logger.log(`[SEND-HTML-CARD] Uploading local HTML file: ${params.htmlLocal}`);
|
|
60
|
+
const previewUrl = await uploadService.uploadFileAndGetPreviewUrl(params.htmlLocal);
|
|
61
|
+
logger.log(`[SEND-HTML-CARD] Upload complete, preview URL obtained`);
|
|
62
|
+
url = previewUrl;
|
|
63
|
+
}
|
|
64
|
+
if (!url) {
|
|
65
|
+
throw new Error("未能获取HTML页面的URL");
|
|
66
|
+
}
|
|
67
|
+
// Build card data
|
|
68
|
+
const cardsInfo = [
|
|
69
|
+
{
|
|
70
|
+
cardName: "clawH5",
|
|
71
|
+
cardData: {
|
|
72
|
+
url,
|
|
73
|
+
},
|
|
74
|
+
displayType: "DisplayFaCard",
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
// Send card via sendCard
|
|
78
|
+
await sendCard({
|
|
79
|
+
config,
|
|
80
|
+
sessionId,
|
|
81
|
+
taskId: currentTaskId,
|
|
82
|
+
messageId,
|
|
83
|
+
toolCallId,
|
|
84
|
+
cardsInfo,
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: JSON.stringify({
|
|
91
|
+
success: true,
|
|
92
|
+
message: `HTML卡片发送成功,html的在线链接如下,生成markdown超链接时与此url需保持完整一致 ${url}`,
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
})();
|
|
98
|
+
try {
|
|
99
|
+
const result = await Promise.race([executionPromise, timeoutPromise]);
|
|
100
|
+
if (timeoutHandle) {
|
|
101
|
+
clearTimeout(timeoutHandle);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (timeoutHandle) {
|
|
107
|
+
clearTimeout(timeoutHandle);
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
-
import type { RunCrossTaskContext,
|
|
2
|
+
import type { RunCrossTaskContext, SentFileCard, XYChannelConfig } from "../types.js";
|
|
3
3
|
export interface SessionContext {
|
|
4
4
|
config: XYChannelConfig;
|
|
5
5
|
sessionId: string;
|
|
@@ -8,6 +8,9 @@ export interface SessionContext {
|
|
|
8
8
|
messageId: string;
|
|
9
9
|
agentId: string;
|
|
10
10
|
deviceType?: string;
|
|
11
|
+
/** Model name extracted from A2A user variables (variables.clientVariables.modelName).
|
|
12
|
+
* When set, provider.ts replaces model.id in the OpenAI request body. */
|
|
13
|
+
modelName?: string;
|
|
11
14
|
runCrossTaskContext?: RunCrossTaskContext;
|
|
12
15
|
/** When true, this context was created for a cron/scheduled task execution.
|
|
13
16
|
* Tools should use the push channel instead of WebSocket sendCommand. */
|
|
@@ -24,6 +27,12 @@ export declare function markCronToolCall(toolCallId: string): void;
|
|
|
24
27
|
export declare function isCronToolCall(toolCallId?: string): boolean;
|
|
25
28
|
/** Clean up a cron tool call marker after use. */
|
|
26
29
|
export declare function clearCronToolCall(toolCallId: string): void;
|
|
30
|
+
/** 把 fire 期解析出的 jobId 绑定到当前 cron run 的合成 sessionId。 */
|
|
31
|
+
export declare function setCurrentCronJobId(cronSessionId: string, jobId: string): void;
|
|
32
|
+
/** 凭合成 cron sessionId 取本次 run 的 jobId(供 sendCommand 反查 pushId)。 */
|
|
33
|
+
export declare function getCurrentCronJobId(cronSessionId?: string): string | undefined;
|
|
34
|
+
/** cron run 结束后清理。 */
|
|
35
|
+
export declare function clearCronJobId(cronSessionId: string): void;
|
|
27
36
|
export declare const asyncLocalStorage: AsyncLocalStorage<SessionContext>;
|
|
28
37
|
/**
|
|
29
38
|
* Register a session context for tool access.
|
|
@@ -76,6 +85,6 @@ export declare function cleanupStaleSessions(): number;
|
|
|
76
85
|
* Get the current number of active sessions (for diagnostics).
|
|
77
86
|
*/
|
|
78
87
|
export declare function getActiveSessionCount(): number;
|
|
79
|
-
export declare function appendRunCrossTaskSentFiles(sentFiles:
|
|
88
|
+
export declare function appendRunCrossTaskSentFiles(sentFiles: SentFileCard[], explicitRunCrossTaskContext?: RunCrossTaskContext): SentFileCard[];
|
|
80
89
|
export declare function clearRunCrossTaskSentFiles(explicitRunCrossTaskContext?: RunCrossTaskContext): void;
|
|
81
90
|
export {};
|