@ynhcj/xiaoyi-channel 0.0.159-beta → 0.0.159-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 (70) hide show
  1. package/dist/src/bot.js +59 -5
  2. package/dist/src/cron-query-handler.d.ts +1 -11
  3. package/dist/src/cron-query-handler.js +89 -2
  4. package/dist/src/cspl/call_api.d.ts +1 -1
  5. package/dist/src/cspl/call_api.js +2 -2
  6. package/dist/src/cspl/config.js +30 -10
  7. package/dist/src/cspl/constants.d.ts +3 -0
  8. package/dist/src/cspl/constants.js +5 -0
  9. package/dist/src/cspl/sentinel_hook.js +26 -7
  10. package/dist/src/cspl/utils.d.ts +9 -3
  11. package/dist/src/cspl/utils.js +17 -11
  12. package/dist/src/file-upload.d.ts +5 -0
  13. package/dist/src/file-upload.js +102 -0
  14. package/dist/src/formatter.d.ts +30 -0
  15. package/dist/src/formatter.js +65 -10
  16. package/dist/src/monitor.js +35 -23
  17. package/dist/src/parser.d.ts +6 -0
  18. package/dist/src/parser.js +47 -1
  19. package/dist/src/provider.js +51 -17
  20. package/dist/src/reply-dispatcher.js +67 -21
  21. package/dist/src/self-evolution-handler.d.ts +1 -1
  22. package/dist/src/self-evolution-handler.js +12 -1
  23. package/dist/src/tools/calendar-tool.js +1 -1
  24. package/dist/src/tools/call-device-tool.js +0 -3
  25. package/dist/src/tools/call-phone-tool.js +1 -1
  26. package/dist/src/tools/check-plugin-privilege-tool.d.ts +6 -0
  27. package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
  28. package/dist/src/tools/create-alarm-tool.js +1 -1
  29. package/dist/src/tools/create-all-tools.js +8 -8
  30. package/dist/src/tools/delete-alarm-tool.js +1 -1
  31. package/dist/src/tools/device-tool-map.js +1 -0
  32. package/dist/src/tools/discover-cross-devices-tool.js +1 -1
  33. package/dist/src/tools/display-a2ui-card-tool.d.ts +2 -0
  34. package/dist/src/tools/display-a2ui-card-tool.js +85 -0
  35. package/dist/src/tools/get-device-file-tool-schema.js +2 -3
  36. package/dist/src/tools/location-tool.js +1 -1
  37. package/dist/src/tools/modify-alarm-tool.js +1 -1
  38. package/dist/src/tools/modify-note-tool.js +1 -1
  39. package/dist/src/tools/note-tool.js +1 -1
  40. package/dist/src/tools/query-app-message-tool.js +1 -1
  41. package/dist/src/tools/query-memory-data-tool.js +1 -1
  42. package/dist/src/tools/query-todo-task-tool.js +1 -1
  43. package/dist/src/tools/save-file-to-phone-tool.js +1 -1
  44. package/dist/src/tools/save-media-to-gallery-tool.js +1 -1
  45. package/dist/src/tools/schema-tool-factory.js +1 -1
  46. package/dist/src/tools/search-alarm-tool.js +1 -1
  47. package/dist/src/tools/search-calendar-tool.js +1 -1
  48. package/dist/src/tools/search-contact-tool.js +1 -1
  49. package/dist/src/tools/search-email-tool.js +1 -1
  50. package/dist/src/tools/search-file-tool.js +5 -10
  51. package/dist/src/tools/search-note-tool.js +1 -1
  52. package/dist/src/tools/search-photo-gallery-tool.js +1 -1
  53. package/dist/src/tools/send-cross-device-task-tool.js +18 -22
  54. package/dist/src/tools/send-email-tool.js +1 -1
  55. package/dist/src/tools/send-file-to-user-tool.d.ts +1 -1
  56. package/dist/src/tools/send-file-to-user-tool.js +35 -6
  57. package/dist/src/tools/send-html-card-tool.d.ts +7 -0
  58. package/dist/src/tools/send-html-card-tool.js +113 -0
  59. package/dist/src/tools/session-manager.d.ts +6 -2
  60. package/dist/src/tools/session-manager.js +42 -7
  61. package/dist/src/tools/upload-file-tool.d.ts +1 -1
  62. package/dist/src/tools/upload-file-tool.js +3 -17
  63. package/dist/src/tools/upload-photo-tool.js +1 -1
  64. package/dist/src/tools/xiaoyi-add-collection-tool.js +2 -2
  65. package/dist/src/tools/xiaoyi-collection-tool.js +1 -1
  66. package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -1
  67. package/dist/src/tools/xiaoyi-gui-tool.js +6 -1
  68. package/dist/src/types.d.ts +10 -3
  69. package/dist/src/websocket.js +29 -8
  70. package/package.json +1 -1
@@ -2,14 +2,13 @@ import { createSchemaTool } from "./schema-tool-factory.js";
2
2
  import { createSearchFileTool } from "./search-file-tool.js";
3
3
  import { createUploadFileTool } from "./upload-file-tool.js";
4
4
  import { createSaveFileToPhoneTool } from "./save-file-to-phone-tool.js";
5
- import { createFindPcDevicesTool } from "./find-pc-devices-tool.js";
6
5
  export function createGetDeviceFileToolSchemaTool(ctx) {
7
6
  const searchFileTool = createSearchFileTool(ctx);
8
7
  const saveFileToPhoneTool = createSaveFileToPhoneTool(ctx);
9
8
  return createSchemaTool({
10
9
  name: "get_device_file_tool_schema",
11
10
  label: "Get Device File Tool Schema",
12
- description: "获取可在用户设备上搜索文件系统的文件、将用户设备本地文件上传到公网并获取链接、保存文件到文件管理器、查找PC设备的相关端工具列表。",
13
- tools: [searchFileTool, createUploadFileTool(ctx), saveFileToPhoneTool, createFindPcDevicesTool(ctx)],
11
+ description: "获取可在用户设备上搜索文件系统的文件、将用户设备本地文件上传到公网并获取链接、保存文件到文件管理器的相关端工具列表。",
12
+ tools: [searchFileTool, createUploadFileTool(ctx), saveFileToPhoneTool],
14
13
  });
15
14
  }
@@ -75,7 +75,7 @@ export function createLocationTool(ctx) {
75
75
  });
76
76
  }
77
77
  else {
78
- reject(new Error(`获取位置失败: ${event.status}`));
78
+ reject(new Error(`获取位置失败: ${JSON.stringify(event.outputs)}`));
79
79
  }
80
80
  }
81
81
  };
@@ -256,7 +256,7 @@ export function createModifyAlarmTool(ctx) {
256
256
  });
257
257
  }
258
258
  else {
259
- reject(new Error(`修改闹钟失败: ${event.status}`));
259
+ reject(new Error(`修改闹钟失败: ${JSON.stringify(event.outputs)}`));
260
260
  }
261
261
  }
262
262
  };
@@ -97,7 +97,7 @@ export function createModifyNoteTool(ctx) {
97
97
  });
98
98
  }
99
99
  else {
100
- reject(new Error(`修改备忘录失败: ${event.status}`));
100
+ reject(new Error(`修改备忘录失败: ${JSON.stringify(event.outputs)}`));
101
101
  }
102
102
  }
103
103
  };
@@ -114,7 +114,7 @@ export function createNoteTool(ctx) {
114
114
  });
115
115
  }
116
116
  else {
117
- reject(new Error(`创建备忘录失败: ${event.status}`));
117
+ reject(new Error(`创建备忘录失败: ${JSON.stringify(event.outputs)}`));
118
118
  }
119
119
  }
120
120
  };
@@ -114,7 +114,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
114
114
  });
115
115
  }
116
116
  else {
117
- reject(new Error(`查询通知消息失败: ${event.status}`));
117
+ reject(new Error(`查询通知消息失败: ${JSON.stringify(event.outputs)}`));
118
118
  }
119
119
  }
120
120
  };
@@ -130,7 +130,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
130
130
  });
131
131
  }
132
132
  else {
133
- reject(new Error(`查询记忆数据失败: ${event.status}`));
133
+ reject(new Error(`查询记忆数据失败: ${JSON.stringify(event.outputs)}`));
134
134
  }
135
135
  }
136
136
  };
@@ -109,7 +109,7 @@ d. 当只传入 startTime 时,返回该时间点之后的所有任务;当只
109
109
  });
110
110
  }
111
111
  else {
112
- reject(new Error(`查询待办任务失败: ${event.status}`));
112
+ reject(new Error(`查询待办任务失败: ${JSON.stringify(event.outputs)}`));
113
113
  }
114
114
  }
115
115
  };
@@ -138,7 +138,7 @@ export function createSaveFileToPhoneTool(ctx) {
138
138
  });
139
139
  }
140
140
  else {
141
- reject(new Error(`保存文件到手机失败: ${event.status}`));
141
+ reject(new Error(`保存文件到手机失败: ${JSON.stringify(event.outputs)}`));
142
142
  }
143
143
  }
144
144
  };
@@ -146,7 +146,7 @@ export function createSaveMediaToGalleryTool(ctx) {
146
146
  });
147
147
  }
148
148
  else {
149
- reject(new Error(`保存媒体到图库失败: ${event.status}`));
149
+ reject(new Error(`保存媒体到图库失败: ${JSON.stringify(event.outputs)}`));
150
150
  }
151
151
  }
152
152
  };
@@ -23,7 +23,7 @@ export function createSchemaTool(options) {
23
23
  content: [
24
24
  {
25
25
  type: "text",
26
- text: JSON.stringify(schemas, null, 2),
26
+ text: "以下为工具列表。请仔细阅读,使用call_device_tool进行调用:\n" + JSON.stringify(schemas, null, 2),
27
27
  },
28
28
  ],
29
29
  };
@@ -188,7 +188,7 @@ b. 使用该工具之前需获取当前真实时间
188
188
  });
189
189
  }
190
190
  else {
191
- reject(new Error(`检索闹钟失败: ${event.status}`));
191
+ reject(new Error(`检索闹钟失败: ${JSON.stringify(event.outputs)}`));
192
192
  }
193
193
  }
194
194
  };
@@ -159,7 +159,7 @@ d. 如果查询结果返回-303,代表查询结果为空
159
159
  });
160
160
  }
161
161
  else {
162
- reject(new Error(`检索日程失败: ${event.status}`));
162
+ reject(new Error(`检索日程失败: ${JSON.stringify(event.outputs)}`));
163
163
  }
164
164
  }
165
165
  };
@@ -87,7 +87,7 @@ export function createSearchContactTool(ctx) {
87
87
  });
88
88
  }
89
89
  else {
90
- reject(new Error(`搜索联系人失败: ${event.status}`));
90
+ reject(new Error(`搜索联系人失败: ${JSON.stringify(event.outputs)}`));
91
91
  }
92
92
  }
93
93
  };
@@ -110,7 +110,7 @@ b. 使用该工具之前需获取当前真实时间
110
110
  });
111
111
  }
112
112
  else {
113
- reject(new Error(`检索邮件失败: ${event.status}`));
113
+ reject(new Error(`检索邮件失败: ${JSON.stringify(event.outputs)}`));
114
114
  }
115
115
  }
116
116
  };
@@ -11,13 +11,12 @@ export function createSearchFileTool(ctx) {
11
11
  return {
12
12
  name: "search_file",
13
13
  label: "Search File",
14
- description: `搜索手机或PC/电脑文件系统的文件。
14
+ description: `搜索用户设备的文件系统的文件,用户设备可以是手机或者鸿蒙PC等。
15
15
 
16
- 【重要】使用场景与调用流程:
17
- 1. 搜索手机文件:当用户明确说明要从手机搜索时(如"从手机里面搜索xxxx"、"在手机上查找文件xxxx"),直接调用此工具,无需传入 udid。
18
- 2. 搜索PC/电脑文件:当用户要求搜索PC/电脑上的文件时(如"帮我找一下PC上的xxx文件"、"搜索电脑上的xxx"),必须先调用 find_pc_devices 工具获取设备ID(udid),然后将 udid 传入此工具进行搜索。
16
+ 使用场景与调用流程:
17
+ 当用户明确说明要从设备搜索时(如"从手机里面搜索xxxx"、"在手机上查找文件xxxx","查找我电脑上的xxxx文件","查找我鸿蒙PC上的xxx文件"),直接调用此工具。
19
18
 
20
- 如果用户没有明确说明从手机或PC搜索(如仅说"搜索文件"、"找一下xxxx"),应默认从当前runtime运行环境的本地文件系统查询,不要调用此工具。
19
+ 如果用户没有明确说明从设备搜索(如仅说"搜索文件"、"找一下xxxx"),应默认从当前runtime运行环境的本地文件系统查询,不要调用此工具。
21
20
 
22
21
  功能说明:根据关键词搜索文件名称或内容,返回匹配的文件列表(包括文件名、路径、大小、修改时间等信息)。
23
22
 
@@ -28,11 +27,7 @@ export function createSearchFileTool(ctx) {
28
27
  query: {
29
28
  type: "string",
30
29
  description: "搜索关键词,用于匹配文件名称、后缀名或文件内容",
31
- },
32
- udid: {
33
- type: "string",
34
- description: "PC/电脑设备ID。当搜索PC/电脑上的文件时,需要先通过 find_pc_devices 工具获取设备ID后传入。搜索手机文件时不需要传入此参数。",
35
- },
30
+ }
36
31
  },
37
32
  required: ["query"],
38
33
  },
@@ -86,7 +86,7 @@ export function createSearchNoteTool(ctx) {
86
86
  });
87
87
  }
88
88
  else {
89
- reject(new Error(`搜索备忘录失败: ${event.status}`));
89
+ reject(new Error(`搜索备忘录失败: ${JSON.stringify(event.outputs)}`));
90
90
  }
91
91
  }
92
92
  };
@@ -140,7 +140,7 @@ async function searchPhotos(wsManager, config, sessionId, taskId, messageId, too
140
140
  resolve(event.outputs);
141
141
  }
142
142
  else {
143
- reject(new Error(`搜索照片失败: ${event.status}`));
143
+ reject(new Error(`搜索照片失败: ${JSON.stringify(event.outputs)}`));
144
144
  }
145
145
  }
146
146
  };
@@ -18,9 +18,9 @@ function buildResultText(result) {
18
18
  };
19
19
  }
20
20
  function buildModelToolResult(result) {
21
- const fileUrls = result.fileUrls;
21
+ const sentFiles = result.sentFiles;
22
22
  const resultStatus = result.success
23
- ? fileUrls.length > 0
23
+ ? sentFiles.length > 0
24
24
  ? "对端设备执行任务成功且返回有文件"
25
25
  : "对端设备执行任务成功且返回无文件"
26
26
  : "对端设备任务失败";
@@ -46,35 +46,31 @@ function buildModelToolResult(result) {
46
46
  resultStatus,
47
47
  };
48
48
  }
49
- function extractFileUrl(message) {
50
- const matches = message.match(/https?:\/\/[^\s<>"']+/g);
51
- const firstUrl = matches?.[0] ?? "";
52
- return firstUrl.replace(/[,。;、!?,.!?;:)\]}]+$/u, "");
53
- }
54
49
  function buildCrossDeviceResult(params) {
55
- const payloadFileUrls = params.success ? params.fileUrls.filter((url) => typeof url === "string" && url.length > 0) : [];
56
- const messageFileUrl = params.success ? extractFileUrl(params.message) : "";
57
- const fileUrls = Array.from(new Set([...payloadFileUrls, ...(messageFileUrl ? [messageFileUrl] : [])]));
58
50
  const result = {
59
51
  success: params.success,
60
52
  code: params.code,
61
53
  message: params.message,
62
- fileUrls,
54
+ sentFiles: params.sentFiles,
63
55
  rawEvent: params.rawEvent,
64
56
  };
65
57
  return result;
66
58
  }
67
59
  async function autoSendFileToUserIfNeeded(result, ctx) {
68
- const fileUrls = result.fileUrls;
69
- if (fileUrls.length === 0) {
60
+ const sentFiles = Array.isArray(result.sentFiles) ? result.sentFiles : [];
61
+ if (sentFiles.length === 0) {
70
62
  return result;
71
63
  }
72
- logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto sending ${fileUrls.length} cross-device file(s) to user`);
64
+ logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto sending ${sentFiles.length} cross-device file(s) to user`);
73
65
  try {
74
66
  const sendFileTool = createSendFileToUserTool(ctx);
75
- const sendFileResult = await sendFileTool.execute("auto_send_cross_device_file", {
76
- fileRemoteUrls: fileUrls,
77
- });
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
+ })();
78
74
  logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto send_file_to_user completed`);
79
75
  return {
80
76
  ...result,
@@ -222,7 +218,7 @@ export function createSendCrossDeviceTaskTool(ctx) {
222
218
  }
223
219
  settled = true;
224
220
  const modelResult = buildModelToolResult(result);
225
- logger.log(`${LOG_TAG} completed, success=${result.success}, code=${result.code}, fileUrlCount=${result.fileUrls.length}`);
221
+ logger.log(`${LOG_TAG} completed, success=${result.success}, code=${result.code}, sentFileCount=${result.sentFiles.length}`);
226
222
  cleanup();
227
223
  resolve(buildResultText(modelResult));
228
224
  };
@@ -230,7 +226,7 @@ export function createSendCrossDeviceTaskTool(ctx) {
230
226
  if (event.sessionId && event.sessionId !== sessionId && event.sessionId !== distributionSessionId) {
231
227
  return;
232
228
  }
233
- logger.log(`${SEND_CROSS_RESULT_LOG_TAG} received result, status=${event.status}, code=${event.code}, fileUrlCount=${event.fileUrls.length}`);
229
+ logger.log(`${SEND_CROSS_RESULT_LOG_TAG} received result, status=${event.status}, code=${event.code}, sentFileCount=${event.sentFiles.length}`);
234
230
  void (async () => {
235
231
  if (resultHandlingStarted) {
236
232
  return;
@@ -254,7 +250,7 @@ export function createSendCrossDeviceTaskTool(ctx) {
254
250
  success: event.status === "success",
255
251
  code: event.code,
256
252
  message: event.message,
257
- fileUrls: event.fileUrls,
253
+ sentFiles: event.sentFiles,
258
254
  rawEvent: event.rawEvent,
259
255
  });
260
256
  const resultWithFileSend = await autoSendFileToUserIfNeeded(result, ctx);
@@ -267,7 +263,7 @@ export function createSendCrossDeviceTaskTool(ctx) {
267
263
  success: false,
268
264
  code: "",
269
265
  message: `Cross-device task timed out after ${CROSS_DEVICE_TASK_TIMEOUT_MS / 1000} seconds.`,
270
- fileUrls: [],
266
+ sentFiles: [],
271
267
  rawEvent: null,
272
268
  });
273
269
  }, CROSS_DEVICE_TASK_TIMEOUT_MS);
@@ -293,7 +289,7 @@ export function createSendCrossDeviceTaskTool(ctx) {
293
289
  success: false,
294
290
  code: "",
295
291
  message: `Failed to send cross-device task command: ${error instanceof Error ? error.message : String(error)}`,
296
- fileUrls: [],
292
+ sentFiles: [],
297
293
  rawEvent: null,
298
294
  });
299
295
  });
@@ -110,7 +110,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
110
110
  });
111
111
  }
112
112
  else {
113
- reject(new Error(`发送邮件失败: ${event.status}`));
113
+ reject(new Error(`发送邮件失败: ${JSON.stringify(event.outputs)}`));
114
114
  }
115
115
  }
116
116
  };
@@ -1,4 +1,4 @@
1
- import type { SessionContext } from "./session-manager.js";
1
+ import { type SessionContext } from "./session-manager.js";
2
2
  /**
3
3
  * XY send file to user tool - sends local files or remote files to user's device.
4
4
  * Supports both local file paths and remote URLs.
@@ -1,5 +1,6 @@
1
1
  import { getXYWebSocketManager } from "../client.js";
2
2
  import { XYFileUploadService } from "../file-upload.js";
3
+ import { appendRunCrossTaskSentFiles } from "./session-manager.js";
3
4
  import { logger } from "../utils/logger.js";
4
5
  import { getCurrentTaskId, getCurrentMessageId } from "../task-manager.js";
5
6
  import fetch from "node-fetch";
@@ -64,16 +65,22 @@ function normalizeToArray(param) {
64
65
  /**
65
66
  * Download remote file to local temp directory
66
67
  */
67
- async function downloadRemoteFile(url) {
68
+ async function downloadRemoteFile(url, desiredFilename) {
68
69
  try {
69
70
  const response = await fetch(url);
70
71
  if (!response.ok) {
71
72
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
72
73
  }
73
- // Get filename from URL or use default
74
- let filename = url.split("/").pop() || "downloaded_file";
75
- // Remove query parameters if present
76
- filename = filename.split("?")[0];
74
+ // Use desired filename if provided, otherwise extract from URL
75
+ let filename;
76
+ if (desiredFilename) {
77
+ filename = desiredFilename;
78
+ }
79
+ else {
80
+ filename = url.split("/").pop() || "downloaded_file";
81
+ // Remove query parameters if present
82
+ filename = filename.split("?")[0];
83
+ }
77
84
  // Ensure temp directory exists
78
85
  const tempDir = "/tmp/xy_channel";
79
86
  await fs.mkdir(tempDir, { recursive: true });
@@ -119,6 +126,9 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
119
126
  fileRemoteUrls: {
120
127
  description: "公网地址数组,包含用户需要回传的文件的公网地址(会先下载到本地再发送),注意不要对原始url做任何截断(例如裁减掉链接后面的鉴权信息或者修改域名后缀),必须使用上下文中完整的文件地址",
121
128
  },
129
+ fileNames: {
130
+ description: "文件名数组,与 fileRemoteUrls 一一对应,用于指定下载后的文件名。必须从 search_file 工具返回结果中提取每个文件的原始文件名,根据uoploadfile工具的顺序传入。",
131
+ },
122
132
  },
123
133
  },
124
134
  async execute(toolCallId, params) {
@@ -156,6 +166,24 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
156
166
  throw new Error("fileRemoteUrls array cannot be empty");
157
167
  }
158
168
  }
169
+ // Normalize fileNames parameter
170
+ let fileNames = [];
171
+ if (params.fileNames) {
172
+ fileNames = normalizeToArray(params.fileNames);
173
+ if (fileNames.length > 0 && fileNames.length !== fileRemoteUrls.length) {
174
+ throw new Error(`fileNames length (${fileNames.length}) must match fileRemoteUrls length (${fileRemoteUrls.length})`);
175
+ }
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
+ }
159
187
  // Get WebSocket manager
160
188
  const wsManager = getXYWebSocketManager(config);
161
189
  // Create upload service
@@ -168,7 +196,8 @@ b. 操作超时时间为2分钟(120秒),请勿重复调用此工具,如
168
196
  for (let i = 0; i < fileRemoteUrls.length; i++) {
169
197
  const remoteUrl = fileRemoteUrls[i];
170
198
  try {
171
- const localPath = await downloadRemoteFile(remoteUrl);
199
+ const desiredName = fileNames[i] || undefined;
200
+ const localPath = await downloadRemoteFile(remoteUrl, desiredName);
172
201
  allLocalPaths.push(localPath);
173
202
  downloadedFiles.push(localPath);
174
203
  }
@@ -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, XYChannelConfig } from "../types.js";
2
+ import type { RunCrossTaskContext, SentFileParams, 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. */
@@ -76,5 +79,6 @@ export declare function cleanupStaleSessions(): number;
76
79
  * Get the current number of active sessions (for diagnostics).
77
80
  */
78
81
  export declare function getActiveSessionCount(): number;
79
- export declare function appendRunCrossTaskFileUrls(fileUrls: string[], explicitRunCrossTaskContext?: RunCrossTaskContext): string[];
82
+ export declare function appendRunCrossTaskSentFiles(sentFiles: SentFileParams[], explicitRunCrossTaskContext?: RunCrossTaskContext): SentFileParams[];
83
+ export declare function clearRunCrossTaskSentFiles(explicitRunCrossTaskContext?: RunCrossTaskContext): void;
80
84
  export {};
@@ -237,21 +237,56 @@ export function cleanupStaleSessions() {
237
237
  export function getActiveSessionCount() {
238
238
  return activeSessions.size;
239
239
  }
240
- export function appendRunCrossTaskFileUrls(fileUrls, explicitRunCrossTaskContext) {
240
+ function normalizeSentFileParams(params) {
241
+ const fileLocalUrls = Array.isArray(params.fileLocalUrls)
242
+ ? params.fileLocalUrls.filter((url) => typeof url === "string" && url.length > 0)
243
+ : [];
244
+ const fileRemoteUrls = Array.isArray(params.fileRemoteUrls)
245
+ ? params.fileRemoteUrls.filter((url) => typeof url === "string" && url.length > 0)
246
+ : [];
247
+ const fileNames = Array.isArray(params.fileNames)
248
+ ? params.fileNames.filter((name) => typeof name === "string" && name.length > 0)
249
+ : [];
250
+ if (fileLocalUrls.length === 0 && fileRemoteUrls.length === 0) {
251
+ return null;
252
+ }
253
+ return {
254
+ ...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
255
+ ...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
256
+ ...(fileNames.length > 0 && fileNames.length === fileRemoteUrls.length ? { fileNames } : {}),
257
+ };
258
+ }
259
+ export function appendRunCrossTaskSentFiles(sentFiles, explicitRunCrossTaskContext) {
241
260
  const context = asyncLocalStorage.getStore() ?? null;
242
261
  const runCrossTaskContext = explicitRunCrossTaskContext ?? context?.runCrossTaskContext;
243
- if (!runCrossTaskContext || fileUrls.length === 0) {
244
- return runCrossTaskContext?.fileUrls ?? [];
262
+ const normalizedSentFiles = sentFiles
263
+ .map((params) => normalizeSentFileParams(params))
264
+ .filter((params) => params !== null);
265
+ if (!runCrossTaskContext || normalizedSentFiles.length === 0) {
266
+ return runCrossTaskContext?.sentFiles ?? [];
245
267
  }
246
- const existing = Array.isArray(runCrossTaskContext.fileUrls) ? runCrossTaskContext.fileUrls : [];
247
- const merged = Array.from(new Set([...existing, ...fileUrls.filter((url) => typeof url === "string" && url.length > 0)]));
248
- runCrossTaskContext.fileUrls = merged;
268
+ const existing = Array.isArray(runCrossTaskContext.sentFiles) ? runCrossTaskContext.sentFiles : [];
269
+ const merged = [...existing, ...normalizedSentFiles];
270
+ runCrossTaskContext.sentFiles = merged;
249
271
  const sessionWithRef = Array.from(activeSessions.values()).find((session) => session.runCrossTaskContext === runCrossTaskContext);
250
272
  if (sessionWithRef?.runCrossTaskContext) {
251
- sessionWithRef.runCrossTaskContext.fileUrls = merged;
273
+ sessionWithRef.runCrossTaskContext.sentFiles = merged;
252
274
  }
253
275
  return merged;
254
276
  }
277
+ export function clearRunCrossTaskSentFiles(explicitRunCrossTaskContext) {
278
+ const context = asyncLocalStorage.getStore() ?? null;
279
+ const runCrossTaskContext = explicitRunCrossTaskContext ?? context?.runCrossTaskContext;
280
+ if (!runCrossTaskContext) {
281
+ return;
282
+ }
283
+ runCrossTaskContext.sentFiles = [];
284
+ for (const sessionWithRef of activeSessions.values()) {
285
+ if (sessionWithRef.runCrossTaskContext === runCrossTaskContext) {
286
+ sessionWithRef.runCrossTaskContext.sentFiles = [];
287
+ }
288
+ }
289
+ }
255
290
  /**
256
291
  * Enrich a base session context with the latest taskId/messageId
257
292
  * from task-manager (supports interruption scenarios).
@@ -1,4 +1,4 @@
1
- import { type SessionContext } from "./session-manager.js";
1
+ import type { SessionContext } from "./session-manager.js";
2
2
  /**
3
3
  * XY upload file tool - uploads local files to get publicly accessible URLs.
4
4
  * Requires file URIs from search_file tool as prerequisite.