@ynhcj/xiaoyi-channel 0.0.151-beta → 0.0.151-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 +23 -0
- package/dist/src/bot.js +9 -1
- package/dist/src/channel.js +59 -5
- package/dist/src/cron-command.d.ts +15 -0
- package/dist/src/cron-command.js +49 -0
- package/dist/src/cron-query-handler.d.ts +7 -0
- package/dist/src/cron-query-handler.js +188 -0
- package/dist/src/cspl/call_api.d.ts +1 -1
- package/dist/src/cspl/call_api.js +2 -2
- package/dist/src/cspl/config.js +30 -10
- package/dist/src/cspl/constants.d.ts +3 -0
- package/dist/src/cspl/constants.js +5 -0
- package/dist/src/cspl/sentinel_hook.js +17 -3
- package/dist/src/cspl/utils.js +2 -2
- package/dist/src/formatter.d.ts +14 -1
- package/dist/src/formatter.js +31 -8
- package/dist/src/monitor.js +13 -1
- package/dist/src/parser.d.ts +2 -1
- package/dist/src/parser.js +55 -0
- package/dist/src/provider.js +19 -17
- package/dist/src/push.d.ts +11 -1
- package/dist/src/push.js +93 -2
- package/dist/src/reply-dispatcher.js +113 -14
- package/dist/src/self-evolution-handler.js +1 -1
- package/dist/src/tools/agent-as-skill-tool.js +56 -4
- package/dist/src/tools/calendar-tool.js +2 -1
- package/dist/src/tools/call-device-tool.js +0 -3
- package/dist/src/tools/call-phone-tool.js +2 -1
- package/dist/src/tools/create-alarm-tool.js +2 -1
- package/dist/src/tools/create-all-tools.js +8 -4
- package/dist/src/tools/delete-alarm-tool.js +2 -1
- package/dist/src/tools/device-tool-map.d.ts +1 -1
- package/dist/src/tools/device-tool-map.js +12 -5
- package/dist/src/tools/discover-cross-devices-tool.d.ts +2 -0
- package/dist/src/tools/discover-cross-devices-tool.js +235 -0
- package/dist/src/tools/display-a2ui-card-tool.d.ts +2 -0
- package/dist/src/tools/display-a2ui-card-tool.js +85 -0
- package/dist/src/tools/find-pc-devices-tool.js +1 -0
- package/dist/src/tools/get-collection-tool-schema.js +1 -1
- package/dist/src/tools/get-device-file-tool-schema.js +2 -3
- package/dist/src/tools/location-tool.js +2 -1
- package/dist/src/tools/modify-alarm-tool.js +2 -1
- package/dist/src/tools/modify-note-tool.js +2 -1
- package/dist/src/tools/note-tool.js +2 -1
- package/dist/src/tools/query-app-message-tool.js +4 -3
- package/dist/src/tools/query-memory-data-tool.js +4 -3
- package/dist/src/tools/query-todo-task-tool.js +4 -3
- package/dist/src/tools/save-file-to-phone-tool.js +2 -1
- package/dist/src/tools/save-media-to-gallery-tool.js +2 -1
- package/dist/src/tools/schema-tool-factory.js +1 -1
- package/dist/src/tools/search-alarm-tool.js +2 -1
- package/dist/src/tools/search-calendar-tool.js +2 -1
- package/dist/src/tools/search-contact-tool.js +2 -1
- package/dist/src/tools/search-email-tool.js +4 -3
- package/dist/src/tools/search-file-tool.js +6 -10
- package/dist/src/tools/search-message-tool.js +1 -0
- package/dist/src/tools/search-note-tool.js +2 -1
- package/dist/src/tools/search-photo-gallery-tool.js +4 -3
- package/dist/src/tools/send-cross-device-task-tool.d.ts +2 -0
- package/dist/src/tools/send-cross-device-task-tool.js +299 -0
- package/dist/src/tools/send-email-tool.js +4 -3
- package/dist/src/tools/send-file-to-user-tool.d.ts +1 -1
- package/dist/src/tools/send-file-to-user-tool.js +35 -6
- package/dist/src/tools/send-message-tool.js +1 -0
- package/dist/src/tools/session-manager.d.ts +14 -1
- package/dist/src/tools/session-manager.js +73 -0
- package/dist/src/tools/upload-file-tool.js +6 -14
- package/dist/src/tools/upload-photo-tool.js +4 -3
- package/dist/src/tools/xiaoyi-add-collection-tool.js +4 -2
- package/dist/src/tools/xiaoyi-collection-tool.js +3 -2
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +3 -2
- package/dist/src/tools/xiaoyi-gui-tool.js +6 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/websocket.d.ts +3 -0
- package/dist/src/websocket.js +207 -15
- package/package.json +1 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { sendCommand, sendStatusUpdate } from "../formatter.js";
|
|
2
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
3
|
+
import { getCurrentMessageId, getCurrentTaskId } from "../task-manager.js";
|
|
4
|
+
import { createSendFileToUserTool } from "./send-file-to-user-tool.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
const LOG_TAG = "[SendPcDeviceTask]";
|
|
7
|
+
const SEND_CROSS_RESULT_LOG_TAG = "[SendCrossResult]";
|
|
8
|
+
const CROSS_DEVICE_TASK_TIMEOUT_MS = 5 * 60_000;
|
|
9
|
+
const PEER_TASK_COMPLETED_STATUS_TEXT = "对端设备已完成当前任务,正在处理中 ...";
|
|
10
|
+
function buildResultText(result) {
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: JSON.stringify(result),
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function buildModelToolResult(result) {
|
|
21
|
+
const sentFiles = result.sentFiles;
|
|
22
|
+
const resultStatus = result.success
|
|
23
|
+
? sentFiles.length > 0
|
|
24
|
+
? "对端设备执行任务成功且返回有文件"
|
|
25
|
+
: "对端设备执行任务成功且返回无文件"
|
|
26
|
+
: "对端设备任务失败";
|
|
27
|
+
const baseMessage = result.message || "对端设备未返回具体结果。";
|
|
28
|
+
let message = `跨端任务执行结果:${baseMessage}`;
|
|
29
|
+
if (resultStatus === "对端设备执行任务成功且返回有文件") {
|
|
30
|
+
if (result.autoSendFileToUser?.success) {
|
|
31
|
+
message += "\n\n对端设备返回了文件,系统已自动通过 send_file_to_user 将文件卡片发送给用户。请你基于跨端任务结果生成最终回复,告知用户任务已完成且文件已发送。";
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const errorMessage = result.autoSendFileToUser?.error || "未知错误";
|
|
35
|
+
message += `\n\n对端设备返回了文件,但系统自动发送文件卡片失败:${errorMessage}。请你向用户说明任务已完成但文件发送失败。`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (resultStatus === "对端设备执行任务成功且返回无文件") {
|
|
39
|
+
message += "\n\n对端设备未返回文件。请你直接根据跨端任务结果向用户总结完成情况。";
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
message += "\n\n对端设备任务失败。请你向用户说明失败情况,并给出可重试或调整任务描述的建议。";
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
message,
|
|
46
|
+
resultStatus,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function buildCrossDeviceResult(params) {
|
|
50
|
+
const result = {
|
|
51
|
+
success: params.success,
|
|
52
|
+
code: params.code,
|
|
53
|
+
message: params.message,
|
|
54
|
+
sentFiles: params.sentFiles,
|
|
55
|
+
rawEvent: params.rawEvent,
|
|
56
|
+
};
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
async function autoSendFileToUserIfNeeded(result, ctx) {
|
|
60
|
+
const sentFiles = Array.isArray(result.sentFiles) ? result.sentFiles : [];
|
|
61
|
+
if (sentFiles.length === 0) {
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto sending ${sentFiles.length} cross-device file(s) to user`);
|
|
65
|
+
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`);
|
|
75
|
+
return {
|
|
76
|
+
...result,
|
|
77
|
+
autoSendFileToUser: {
|
|
78
|
+
success: true,
|
|
79
|
+
result: sendFileResult,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
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}`);
|
|
86
|
+
return {
|
|
87
|
+
...result,
|
|
88
|
+
autoSendFileToUser: {
|
|
89
|
+
success: false,
|
|
90
|
+
error: errorMessage,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function normalizeTargetDeviceInfo(value) {
|
|
96
|
+
if (!value || typeof value !== "object") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const candidate = value;
|
|
100
|
+
const networkId = typeof candidate.networkId === "string"
|
|
101
|
+
? candidate.networkId.trim()
|
|
102
|
+
: typeof candidate.deviceId === "string"
|
|
103
|
+
? candidate.deviceId.trim()
|
|
104
|
+
: "";
|
|
105
|
+
const deviceName = typeof candidate.deviceName === "string" ? candidate.deviceName.trim() : "";
|
|
106
|
+
const deviceTypeId = typeof candidate.deviceTypeId === "string"
|
|
107
|
+
? candidate.deviceTypeId.trim()
|
|
108
|
+
: typeof candidate.deviceType === "string"
|
|
109
|
+
? candidate.deviceType.trim()
|
|
110
|
+
: "";
|
|
111
|
+
if (!networkId || !deviceName || !deviceTypeId) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
networkId,
|
|
116
|
+
deviceName,
|
|
117
|
+
deviceTypeId,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function buildUnifiedDistributeCommand(query, targetDeviceInfo, distributionSessionId) {
|
|
121
|
+
return {
|
|
122
|
+
header: {
|
|
123
|
+
namespace: "DistributionInteraction",
|
|
124
|
+
name: "UnifiedDistribute",
|
|
125
|
+
},
|
|
126
|
+
payload: {
|
|
127
|
+
targetDeviceInfo,
|
|
128
|
+
crossDeviceContent: {
|
|
129
|
+
query,
|
|
130
|
+
contexts: {
|
|
131
|
+
agentClientContext: {
|
|
132
|
+
header: {
|
|
133
|
+
namespace: "System",
|
|
134
|
+
name: "ClientContext",
|
|
135
|
+
},
|
|
136
|
+
payload: {
|
|
137
|
+
agentId: "",
|
|
138
|
+
isSupportAgent: true,
|
|
139
|
+
distributionSessionId,
|
|
140
|
+
localNetworkId: targetDeviceInfo.networkId,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function createSendCrossDeviceTaskTool(ctx) {
|
|
149
|
+
const { config, sessionId, taskId, messageId } = ctx;
|
|
150
|
+
return {
|
|
151
|
+
name: "send_cross_device_task",
|
|
152
|
+
label: "下发跨设备协作任务",
|
|
153
|
+
description: `向用户已经选定的目标设备下发跨设备协作任务。
|
|
154
|
+
|
|
155
|
+
使用流程:
|
|
156
|
+
1. 必须先调用 discover_cross_devices 获取设备列表。
|
|
157
|
+
2. 根据用户原始需求选择唯一目标设备。
|
|
158
|
+
3. 如果存在多个同类型候选设备,或无法判断目标设备,必须先询问用户选择设备,不要调用本工具。
|
|
159
|
+
4. 只有当 targetDeviceInfo 中的 networkId、deviceName、deviceTypeId 都已明确时,才调用本工具。
|
|
160
|
+
5. 传入的query必须是用户原始query,不要做任何更改。`,
|
|
161
|
+
parameters: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
query: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "用户原始跨设备任务需求,例如:从 PC 获取某文件。",
|
|
167
|
+
},
|
|
168
|
+
targetDeviceInfo: {
|
|
169
|
+
type: "object",
|
|
170
|
+
description: "模型从 discover_cross_devices 返回列表中选定的唯一目标设备。",
|
|
171
|
+
properties: {
|
|
172
|
+
networkId: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "目标设备标识 networkId。",
|
|
175
|
+
},
|
|
176
|
+
deviceName: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description: "目标设备名称。",
|
|
179
|
+
},
|
|
180
|
+
deviceTypeId: {
|
|
181
|
+
type: "string",
|
|
182
|
+
description: "目标设备类型编号 deviceTypeId,例如 14、17、131、2607。",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
required: ["networkId", "deviceName", "deviceTypeId"],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
required: ["query", "targetDeviceInfo"],
|
|
189
|
+
},
|
|
190
|
+
async execute(_toolCallId, params) {
|
|
191
|
+
const query = typeof params.query === "string" ? params.query.trim() : "";
|
|
192
|
+
const targetDeviceInfo = normalizeTargetDeviceInfo(params.targetDeviceInfo);
|
|
193
|
+
if (!query || !targetDeviceInfo) {
|
|
194
|
+
return buildResultText({
|
|
195
|
+
message: "Missing required parameters: query and targetDeviceInfo.networkId/deviceName/deviceTypeId.",
|
|
196
|
+
resultStatus: "对端设备任务失败",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
200
|
+
const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
|
|
201
|
+
const wsManager = getXYWebSocketManager(config);
|
|
202
|
+
const distributionSessionId = ctx.distributionSessionId || sessionId;
|
|
203
|
+
const command = buildUnifiedDistributeCommand(query, targetDeviceInfo, distributionSessionId);
|
|
204
|
+
const statusText = `正在调用${targetDeviceInfo.deviceName}执行“${query}”跨设备任务...`;
|
|
205
|
+
logger.log(`${LOG_TAG} sending task to ${targetDeviceInfo.deviceName}, distributionSessionId=${distributionSessionId}`);
|
|
206
|
+
return new Promise((resolve) => {
|
|
207
|
+
let timeout;
|
|
208
|
+
let handler;
|
|
209
|
+
let settled = false;
|
|
210
|
+
let resultHandlingStarted = false;
|
|
211
|
+
const cleanup = () => {
|
|
212
|
+
clearTimeout(timeout);
|
|
213
|
+
wsManager.off("cross-device-task-result", handler);
|
|
214
|
+
};
|
|
215
|
+
const finish = (result) => {
|
|
216
|
+
if (settled) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
settled = true;
|
|
220
|
+
const modelResult = buildModelToolResult(result);
|
|
221
|
+
logger.log(`${LOG_TAG} completed, success=${result.success}, code=${result.code}, sentFileCount=${result.sentFiles.length}`);
|
|
222
|
+
cleanup();
|
|
223
|
+
resolve(buildResultText(modelResult));
|
|
224
|
+
};
|
|
225
|
+
handler = (event) => {
|
|
226
|
+
if (event.sessionId && event.sessionId !== sessionId && event.sessionId !== distributionSessionId) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} received result, status=${event.status}, code=${event.code}, sentFileCount=${event.sentFiles.length}`);
|
|
230
|
+
void (async () => {
|
|
231
|
+
if (resultHandlingStarted) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
resultHandlingStarted = true;
|
|
235
|
+
clearTimeout(timeout);
|
|
236
|
+
try {
|
|
237
|
+
await sendStatusUpdate({
|
|
238
|
+
config,
|
|
239
|
+
sessionId,
|
|
240
|
+
taskId: currentTaskId,
|
|
241
|
+
messageId: currentMessageId,
|
|
242
|
+
text: PEER_TASK_COMPLETED_STATUS_TEXT,
|
|
243
|
+
state: "working",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
logger.error(`${SEND_CROSS_RESULT_LOG_TAG} failed to send peer task completed status update: ${error instanceof Error ? error.message : String(error)}`);
|
|
248
|
+
}
|
|
249
|
+
const result = buildCrossDeviceResult({
|
|
250
|
+
success: event.status === "success",
|
|
251
|
+
code: event.code,
|
|
252
|
+
message: event.message,
|
|
253
|
+
sentFiles: event.sentFiles,
|
|
254
|
+
rawEvent: event.rawEvent,
|
|
255
|
+
});
|
|
256
|
+
const resultWithFileSend = await autoSendFileToUserIfNeeded(result, ctx);
|
|
257
|
+
finish(resultWithFileSend);
|
|
258
|
+
})();
|
|
259
|
+
};
|
|
260
|
+
timeout = setTimeout(() => {
|
|
261
|
+
logger.log(`${LOG_TAG} timeout waiting cross-device result after ${CROSS_DEVICE_TASK_TIMEOUT_MS}ms`);
|
|
262
|
+
finish({
|
|
263
|
+
success: false,
|
|
264
|
+
code: "",
|
|
265
|
+
message: `Cross-device task timed out after ${CROSS_DEVICE_TASK_TIMEOUT_MS / 1000} seconds.`,
|
|
266
|
+
sentFiles: [],
|
|
267
|
+
rawEvent: null,
|
|
268
|
+
});
|
|
269
|
+
}, CROSS_DEVICE_TASK_TIMEOUT_MS);
|
|
270
|
+
wsManager.on("cross-device-task-result", handler);
|
|
271
|
+
sendStatusUpdate({
|
|
272
|
+
config,
|
|
273
|
+
sessionId,
|
|
274
|
+
taskId: currentTaskId,
|
|
275
|
+
messageId: currentMessageId,
|
|
276
|
+
text: statusText,
|
|
277
|
+
state: "working",
|
|
278
|
+
})
|
|
279
|
+
.then(() => sendCommand({
|
|
280
|
+
config,
|
|
281
|
+
sessionId,
|
|
282
|
+
taskId: currentTaskId,
|
|
283
|
+
messageId: currentMessageId,
|
|
284
|
+
command,
|
|
285
|
+
}))
|
|
286
|
+
.catch((error) => {
|
|
287
|
+
logger.error(`${LOG_TAG} failed to send cross-device task command: ${error instanceof Error ? error.message : String(error)}`);
|
|
288
|
+
finish({
|
|
289
|
+
success: false,
|
|
290
|
+
code: "",
|
|
291
|
+
message: `Failed to send cross-device task command: ${error instanceof Error ? error.message : String(error)}`,
|
|
292
|
+
sentFiles: [],
|
|
293
|
+
rawEvent: null,
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -43,7 +43,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
|
43
43
|
},
|
|
44
44
|
required: ["subject", "to", "body"],
|
|
45
45
|
},
|
|
46
|
-
async execute(
|
|
46
|
+
async execute(toolCallId, params) {
|
|
47
47
|
if (typeof params.subject !== "string" || !params.subject.trim()) {
|
|
48
48
|
throw new ToolInputError("缺少必填参数 subject(邮件主题)");
|
|
49
49
|
}
|
|
@@ -92,7 +92,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
|
92
92
|
return new Promise((resolve, reject) => {
|
|
93
93
|
const timeout = setTimeout(() => {
|
|
94
94
|
wsManager.off("data-event", handler);
|
|
95
|
-
logger.error("超时: 发送邮件超时(60秒)", { toolCallId:
|
|
95
|
+
logger.error("超时: 发送邮件超时(60秒)", { toolCallId: toolCallId });
|
|
96
96
|
reject(new Error("发送邮件超时(60秒)"));
|
|
97
97
|
}, 60000);
|
|
98
98
|
const handler = (event) => {
|
|
@@ -110,7 +110,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
else {
|
|
113
|
-
reject(new Error(`发送邮件失败: ${event.
|
|
113
|
+
reject(new Error(`发送邮件失败: ${JSON.stringify(event.outputs)}`));
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
};
|
|
@@ -122,6 +122,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
|
122
122
|
taskId: currentTaskId,
|
|
123
123
|
messageId,
|
|
124
124
|
command,
|
|
125
|
+
toolCallId,
|
|
125
126
|
})
|
|
126
127
|
.then(() => { })
|
|
127
128
|
.catch((error) => {
|
|
@@ -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
|
-
//
|
|
74
|
-
let filename
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
}
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
-
import type { 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;
|
|
6
|
+
distributionSessionId?: string;
|
|
6
7
|
taskId: string;
|
|
7
8
|
messageId: string;
|
|
8
9
|
agentId: string;
|
|
9
10
|
deviceType?: string;
|
|
11
|
+
runCrossTaskContext?: RunCrossTaskContext;
|
|
12
|
+
/** When true, this context was created for a cron/scheduled task execution.
|
|
13
|
+
* Tools should use the push channel instead of WebSocket sendCommand. */
|
|
14
|
+
isCron?: boolean;
|
|
10
15
|
}
|
|
11
16
|
interface SessionContextWithRef extends SessionContext {
|
|
12
17
|
refCount: number;
|
|
13
18
|
createdAt: number;
|
|
14
19
|
}
|
|
15
20
|
export declare const activeSessions: Map<string, SessionContextWithRef>;
|
|
21
|
+
/** Mark a toolCallId as originating from a cron trigger. */
|
|
22
|
+
export declare function markCronToolCall(toolCallId: string): void;
|
|
23
|
+
/** Check whether a toolCallId is from a cron trigger. */
|
|
24
|
+
export declare function isCronToolCall(toolCallId?: string): boolean;
|
|
25
|
+
/** Clean up a cron tool call marker after use. */
|
|
26
|
+
export declare function clearCronToolCall(toolCallId: string): void;
|
|
16
27
|
export declare const asyncLocalStorage: AsyncLocalStorage<SessionContext>;
|
|
17
28
|
/**
|
|
18
29
|
* Register a session context for tool access.
|
|
@@ -65,4 +76,6 @@ export declare function cleanupStaleSessions(): number;
|
|
|
65
76
|
* Get the current number of active sessions (for diagnostics).
|
|
66
77
|
*/
|
|
67
78
|
export declare function getActiveSessionCount(): number;
|
|
79
|
+
export declare function appendRunCrossTaskSentFiles(sentFiles: SentFileParams[], explicitRunCrossTaskContext?: RunCrossTaskContext): SentFileParams[];
|
|
80
|
+
export declare function clearRunCrossTaskSentFiles(explicitRunCrossTaskContext?: RunCrossTaskContext): void;
|
|
68
81
|
export {};
|
|
@@ -25,6 +25,29 @@ if (!_g.__xyLastRegisteredSessionKey) {
|
|
|
25
25
|
}
|
|
26
26
|
const getLastRegisteredKey = () => _g.__xyLastRegisteredSessionKey;
|
|
27
27
|
const setLastRegisteredKey = (key) => { _g.__xyLastRegisteredSessionKey = key; };
|
|
28
|
+
// ── Cron tool-call tracking ─────────────────────────────────────────
|
|
29
|
+
// Global Map keyed by toolCallId to track whether a specific tool call
|
|
30
|
+
// originated from a cron/scheduled task. Populated by the
|
|
31
|
+
// `before_tool_call` hook (which receives openclaw's sessionKey with
|
|
32
|
+
// "cron:" prefix), consumed by sendCommand() to route through push channel.
|
|
33
|
+
if (!_g.__xyCronToolCallMap) {
|
|
34
|
+
_g.__xyCronToolCallMap = new Map();
|
|
35
|
+
}
|
|
36
|
+
const cronToolCallMap = _g.__xyCronToolCallMap;
|
|
37
|
+
/** Mark a toolCallId as originating from a cron trigger. */
|
|
38
|
+
export function markCronToolCall(toolCallId) {
|
|
39
|
+
cronToolCallMap.set(toolCallId, true);
|
|
40
|
+
}
|
|
41
|
+
/** Check whether a toolCallId is from a cron trigger. */
|
|
42
|
+
export function isCronToolCall(toolCallId) {
|
|
43
|
+
if (!toolCallId)
|
|
44
|
+
return false;
|
|
45
|
+
return cronToolCallMap.get(toolCallId) === true;
|
|
46
|
+
}
|
|
47
|
+
/** Clean up a cron tool call marker after use. */
|
|
48
|
+
export function clearCronToolCall(toolCallId) {
|
|
49
|
+
cronToolCallMap.delete(toolCallId);
|
|
50
|
+
}
|
|
28
51
|
// AsyncLocalStorage for thread-safe session context isolation
|
|
29
52
|
export const asyncLocalStorage = new AsyncLocalStorage();
|
|
30
53
|
// Export AsyncLocalStorage to globalThis so logger.ts can access it
|
|
@@ -214,6 +237,56 @@ export function cleanupStaleSessions() {
|
|
|
214
237
|
export function getActiveSessionCount() {
|
|
215
238
|
return activeSessions.size;
|
|
216
239
|
}
|
|
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) {
|
|
260
|
+
const context = asyncLocalStorage.getStore() ?? null;
|
|
261
|
+
const runCrossTaskContext = explicitRunCrossTaskContext ?? context?.runCrossTaskContext;
|
|
262
|
+
const normalizedSentFiles = sentFiles
|
|
263
|
+
.map((params) => normalizeSentFileParams(params))
|
|
264
|
+
.filter((params) => params !== null);
|
|
265
|
+
if (!runCrossTaskContext || normalizedSentFiles.length === 0) {
|
|
266
|
+
return runCrossTaskContext?.sentFiles ?? [];
|
|
267
|
+
}
|
|
268
|
+
const existing = Array.isArray(runCrossTaskContext.sentFiles) ? runCrossTaskContext.sentFiles : [];
|
|
269
|
+
const merged = [...existing, ...normalizedSentFiles];
|
|
270
|
+
runCrossTaskContext.sentFiles = merged;
|
|
271
|
+
const sessionWithRef = Array.from(activeSessions.values()).find((session) => session.runCrossTaskContext === runCrossTaskContext);
|
|
272
|
+
if (sessionWithRef?.runCrossTaskContext) {
|
|
273
|
+
sessionWithRef.runCrossTaskContext.sentFiles = merged;
|
|
274
|
+
}
|
|
275
|
+
return merged;
|
|
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
|
+
}
|
|
217
290
|
/**
|
|
218
291
|
* Enrich a base session context with the latest taskId/messageId
|
|
219
292
|
* from task-manager (supports interruption scenarios).
|
|
@@ -19,19 +19,14 @@ export function createUploadFileTool(ctx) {
|
|
|
19
19
|
return {
|
|
20
20
|
name: "upload_file",
|
|
21
21
|
label: "Upload File",
|
|
22
|
-
description:
|
|
22
|
+
description: `工具能力描述:将用户设备(可以使手机或者鸿蒙PC等)本地文件上传并获取可公网访问的 URL。
|
|
23
23
|
|
|
24
24
|
前置工具调用:此工具使用前必须先通过call_device_tool调用 search_file 或者 query_collection 工具获取文件的 uri
|
|
25
25
|
|
|
26
|
-
使用场景与调用流程:
|
|
27
|
-
1. 上传手机文件:直接调用此工具,无需传入 udid。
|
|
28
|
-
2. 上传PC/电脑文件:当用户要求上传PC/电脑上的文件时,需要先通过 find_pc_devices 工具获取设备ID(udid),然后将 udid 传入此工具。
|
|
29
|
-
|
|
30
26
|
工具参数说明:
|
|
31
27
|
a. 入参中的fileInfos数组,每个元素必须包含mediaUri字段(对应于search_file工具或者query_collection返回结果中的uri),必须与search_file或者query_collection结果中对应的uri完全保持一致,不要自行修改。
|
|
32
28
|
b. fileInfos中的timeout字段是可选的,表示上传文件超时时间,单位是毫秒,默认是20000(20秒)。
|
|
33
29
|
c. fileInfos 是文件在本地的信息数组(从 search_file 工具或者query_collection 工具响应中获取)。限制:每次最多支持传入 5 条文件信息。
|
|
34
|
-
d. udid 是PC/电脑设备ID,仅在上传PC/电脑文件时需要传入(通过 find_pc_devices 工具获取)。
|
|
35
30
|
|
|
36
31
|
注意事项:
|
|
37
32
|
a. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。
|
|
@@ -43,11 +38,7 @@ export function createUploadFileTool(ctx) {
|
|
|
43
38
|
// 不指定 type,允许传入数组或 JSON 字符串
|
|
44
39
|
// 具体的类型验证和转换在 execute 函数内部进行
|
|
45
40
|
description: "文件信息数组,每个元素包含mediaUri(必需)和timeout(可选,默认20000)。必须先通过 search_file 工具获取。每次最多支持 5 条文件信息。",
|
|
46
|
-
}
|
|
47
|
-
udid: {
|
|
48
|
-
type: "string",
|
|
49
|
-
description: "PC/电脑设备ID。当上传PC/电脑上的文件时,需要先通过 find_pc_devices 工具获取设备ID后传入。上传手机文件时不需要传入此参数。",
|
|
50
|
-
},
|
|
41
|
+
}
|
|
51
42
|
},
|
|
52
43
|
required: ["fileInfos"],
|
|
53
44
|
},
|
|
@@ -106,7 +97,7 @@ export function createUploadFileTool(ctx) {
|
|
|
106
97
|
const wsManager = getXYWebSocketManager(config);
|
|
107
98
|
// Get public URLs for the files
|
|
108
99
|
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
109
|
-
const fileUrls = await getFileUrls(wsManager, config, sessionId, currentTaskId, messageId, fileInfos, params.udid);
|
|
100
|
+
const fileUrls = await getFileUrls(wsManager, config, sessionId, currentTaskId, messageId, toolCallId, fileInfos, params.udid);
|
|
110
101
|
return {
|
|
111
102
|
content: [
|
|
112
103
|
{
|
|
@@ -126,7 +117,7 @@ export function createUploadFileTool(ctx) {
|
|
|
126
117
|
* Get public URLs for files using fileInfos
|
|
127
118
|
* Returns array of publicly accessible file URLs
|
|
128
119
|
*/
|
|
129
|
-
async function getFileUrls(wsManager, config, sessionId, taskId, messageId, fileInfos, udid) {
|
|
120
|
+
async function getFileUrls(wsManager, config, sessionId, taskId, messageId, toolCallId, fileInfos, udid) {
|
|
130
121
|
const command = {
|
|
131
122
|
header: {
|
|
132
123
|
namespace: "Common",
|
|
@@ -203,7 +194,7 @@ async function getFileUrls(wsManager, config, sessionId, taskId, messageId, file
|
|
|
203
194
|
resolve(fileUrls);
|
|
204
195
|
}
|
|
205
196
|
else {
|
|
206
|
-
reject(new Error(`获取文件URL失败: ${event.
|
|
197
|
+
reject(new Error(`获取文件URL失败: ${JSON.stringify(event.outputs)}`));
|
|
207
198
|
}
|
|
208
199
|
}
|
|
209
200
|
};
|
|
@@ -214,6 +205,7 @@ async function getFileUrls(wsManager, config, sessionId, taskId, messageId, file
|
|
|
214
205
|
taskId,
|
|
215
206
|
messageId,
|
|
216
207
|
command,
|
|
208
|
+
toolCallId,
|
|
217
209
|
})
|
|
218
210
|
.then(() => {
|
|
219
211
|
})
|
|
@@ -78,7 +78,7 @@ export function createUploadPhotoTool(ctx) {
|
|
|
78
78
|
const wsManager = getXYWebSocketManager(config);
|
|
79
79
|
// Get public URLs for the photos
|
|
80
80
|
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
81
|
-
const imageUrls = await getPhotoUrls(wsManager, config, sessionId, currentTaskId, messageId, mediaUris);
|
|
81
|
+
const imageUrls = await getPhotoUrls(wsManager, config, sessionId, currentTaskId, messageId, toolCallId, mediaUris);
|
|
82
82
|
return {
|
|
83
83
|
content: [
|
|
84
84
|
{
|
|
@@ -98,7 +98,7 @@ export function createUploadPhotoTool(ctx) {
|
|
|
98
98
|
* Get public URLs for photos using mediaUris
|
|
99
99
|
* Returns array of publicly accessible image URLs
|
|
100
100
|
*/
|
|
101
|
-
async function getPhotoUrls(wsManager, config, sessionId, taskId, messageId, mediaUris) {
|
|
101
|
+
async function getPhotoUrls(wsManager, config, sessionId, taskId, messageId, toolCallId, mediaUris) {
|
|
102
102
|
// Build imageInfos array from mediaUris
|
|
103
103
|
const imageInfos = mediaUris.map(mediaUri => ({ mediaUri }));
|
|
104
104
|
const command = {
|
|
@@ -158,7 +158,7 @@ async function getPhotoUrls(wsManager, config, sessionId, taskId, messageId, med
|
|
|
158
158
|
resolve(imageUrls);
|
|
159
159
|
}
|
|
160
160
|
else {
|
|
161
|
-
reject(new Error(`获取照片URL失败: ${event.
|
|
161
|
+
reject(new Error(`获取照片URL失败: ${JSON.stringify(event.outputs)}`));
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
};
|
|
@@ -169,6 +169,7 @@ async function getPhotoUrls(wsManager, config, sessionId, taskId, messageId, med
|
|
|
169
169
|
taskId,
|
|
170
170
|
messageId,
|
|
171
171
|
command,
|
|
172
|
+
toolCallId,
|
|
172
173
|
})
|
|
173
174
|
.then(() => {
|
|
174
175
|
})
|