@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.
Files changed (41) hide show
  1. package/dist/index.js +141 -1
  2. package/dist/src/bot.js +60 -5
  3. package/dist/src/cron-command.d.ts +2 -0
  4. package/dist/src/cron-command.js +14 -8
  5. package/dist/src/cron-query-handler.js +45 -8
  6. package/dist/src/cspl/call_api.js +2 -2
  7. package/dist/src/cspl/sentinel_hook.js +9 -4
  8. package/dist/src/cspl/upload_file.js +2 -2
  9. package/dist/src/cspl/utils.d.ts +9 -3
  10. package/dist/src/cspl/utils.js +15 -9
  11. package/dist/src/file-upload.d.ts +5 -0
  12. package/dist/src/file-upload.js +102 -0
  13. package/dist/src/formatter.d.ts +29 -0
  14. package/dist/src/formatter.js +100 -2
  15. package/dist/src/monitor.js +35 -23
  16. package/dist/src/parser.d.ts +6 -0
  17. package/dist/src/parser.js +23 -13
  18. package/dist/src/provider.js +41 -1
  19. package/dist/src/reply-dispatcher.js +34 -16
  20. package/dist/src/self-evolution-handler.d.ts +1 -1
  21. package/dist/src/self-evolution-handler.js +12 -1
  22. package/dist/src/tools/check-plugin-privilege-tool.d.ts +6 -0
  23. package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
  24. package/dist/src/tools/create-all-tools.js +4 -0
  25. package/dist/src/tools/device-tool-map.d.ts +1 -1
  26. package/dist/src/tools/device-tool-map.js +8 -1
  27. package/dist/src/tools/modify-alarm-tool.js +17 -0
  28. package/dist/src/tools/send-cross-device-task-tool.js +84 -15
  29. package/dist/src/tools/send-file-to-user-tool.js +9 -11
  30. package/dist/src/tools/send-html-card-tool.d.ts +7 -0
  31. package/dist/src/tools/send-html-card-tool.js +113 -0
  32. package/dist/src/tools/session-manager.d.ts +11 -2
  33. package/dist/src/tools/session-manager.js +65 -18
  34. package/dist/src/tools/xiaoyi-gui-tool.js +1 -1
  35. package/dist/src/types.d.ts +9 -7
  36. package/dist/src/utils/config-manager.d.ts +3 -2
  37. package/dist/src/utils/config-manager.js +22 -2
  38. package/dist/src/utils/cron-push-map.d.ts +26 -0
  39. package/dist/src/utils/cron-push-map.js +131 -0
  40. package/dist/src/websocket.js +11 -13
  41. 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对端设备返回了文件,系统已自动通过 send_file_to_user 将文件卡片发送给用户。请你基于跨端任务结果生成最终回复,告知用户任务已完成且文件已发送。";
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
- logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto sending ${sentFiles.length} cross-device file(s) to user`);
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 sendFileTool = createSendFileToUserTool(ctx);
67
- const sendFileResult = await (async () => {
68
- const results = [];
69
- for (const sentFileParams of sentFiles) {
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 send_file_to_user failed, error=${errorMessage}`);
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}, sentFileCount=${result.sentFiles.length}`);
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}, sentFileCount=${event.sentFiles.length}`);
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, SentFileParams, XYChannelConfig } from "../types.js";
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: SentFileParams[], explicitRunCrossTaskContext?: RunCrossTaskContext): SentFileParams[];
88
+ export declare function appendRunCrossTaskSentFiles(sentFiles: SentFileCard[], explicitRunCrossTaskContext?: RunCrossTaskContext): SentFileCard[];
80
89
  export declare function clearRunCrossTaskSentFiles(explicitRunCrossTaskContext?: RunCrossTaskContext): void;
81
90
  export {};