@ynhcj/xiaoyi-channel 0.0.152-beta → 0.0.154-beta
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 +16 -0
- package/dist/src/cron-command.js +64 -0
- package/dist/src/formatter.d.ts +11 -1
- package/dist/src/formatter.js +22 -2
- package/dist/src/parser.d.ts +2 -1
- package/dist/src/parser.js +25 -0
- package/dist/src/reply-dispatcher.js +73 -1
- package/dist/src/tools/agent-as-skill-tool.js +1 -0
- package/dist/src/tools/calendar-tool.js +1 -0
- package/dist/src/tools/call-phone-tool.js +1 -0
- package/dist/src/tools/create-alarm-tool.js +1 -0
- package/dist/src/tools/create-all-tools.js +4 -0
- package/dist/src/tools/delete-alarm-tool.js +1 -0
- package/dist/src/tools/device-tool-map.d.ts +1 -1
- package/dist/src/tools/device-tool-map.js +11 -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/find-pc-devices-tool.js +1 -0
- package/dist/src/tools/location-tool.js +1 -0
- package/dist/src/tools/modify-alarm-tool.js +1 -0
- package/dist/src/tools/modify-note-tool.js +1 -0
- package/dist/src/tools/note-tool.js +1 -0
- package/dist/src/tools/query-app-message-tool.js +3 -2
- package/dist/src/tools/query-memory-data-tool.js +3 -2
- package/dist/src/tools/query-todo-task-tool.js +3 -2
- package/dist/src/tools/save-file-to-phone-tool.js +1 -0
- package/dist/src/tools/save-media-to-gallery-tool.js +1 -0
- package/dist/src/tools/search-alarm-tool.js +1 -0
- package/dist/src/tools/search-calendar-tool.js +1 -0
- package/dist/src/tools/search-contact-tool.js +1 -0
- package/dist/src/tools/search-email-tool.js +3 -2
- package/dist/src/tools/search-file-tool.js +1 -0
- package/dist/src/tools/search-message-tool.js +1 -0
- package/dist/src/tools/search-note-tool.js +1 -0
- package/dist/src/tools/search-photo-gallery-tool.js +3 -2
- package/dist/src/tools/send-cross-device-task-tool.d.ts +2 -0
- package/dist/src/tools/send-cross-device-task-tool.js +303 -0
- package/dist/src/tools/send-email-tool.js +3 -2
- package/dist/src/tools/send-message-tool.js +1 -0
- package/dist/src/tools/session-manager.d.ts +13 -1
- package/dist/src/tools/session-manager.js +38 -0
- package/dist/src/tools/upload-file-tool.d.ts +1 -1
- package/dist/src/tools/upload-file-tool.js +8 -2
- package/dist/src/tools/upload-photo-tool.js +3 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +1 -0
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +1 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/websocket.d.ts +3 -0
- package/dist/src/websocket.js +168 -15
- package/package.json +2 -1
|
@@ -0,0 +1,303 @@
|
|
|
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 fileUrls = result.fileUrls;
|
|
22
|
+
const resultStatus = result.success
|
|
23
|
+
? fileUrls.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 extractFileUrl(message) {
|
|
50
|
+
const matches = message.match(/https?:\/\/[^\s<>"']+/g);
|
|
51
|
+
const firstUrl = matches?.[0] ?? "";
|
|
52
|
+
return firstUrl.replace(/[,。;、!?,.!?;:)\]}]+$/u, "");
|
|
53
|
+
}
|
|
54
|
+
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
|
+
const result = {
|
|
59
|
+
success: params.success,
|
|
60
|
+
code: params.code,
|
|
61
|
+
message: params.message,
|
|
62
|
+
fileUrls,
|
|
63
|
+
rawEvent: params.rawEvent,
|
|
64
|
+
};
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
async function autoSendFileToUserIfNeeded(result, ctx) {
|
|
68
|
+
const fileUrls = result.fileUrls;
|
|
69
|
+
if (fileUrls.length === 0) {
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto sending ${fileUrls.length} cross-device file(s) to user`);
|
|
73
|
+
try {
|
|
74
|
+
const sendFileTool = createSendFileToUserTool(ctx);
|
|
75
|
+
const sendFileResult = await sendFileTool.execute("auto_send_cross_device_file", {
|
|
76
|
+
fileRemoteUrls: fileUrls,
|
|
77
|
+
});
|
|
78
|
+
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} auto send_file_to_user completed`);
|
|
79
|
+
return {
|
|
80
|
+
...result,
|
|
81
|
+
autoSendFileToUser: {
|
|
82
|
+
success: true,
|
|
83
|
+
result: sendFileResult,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
89
|
+
logger.error(`${SEND_CROSS_RESULT_LOG_TAG} auto send_file_to_user failed, error=${errorMessage}`);
|
|
90
|
+
return {
|
|
91
|
+
...result,
|
|
92
|
+
autoSendFileToUser: {
|
|
93
|
+
success: false,
|
|
94
|
+
error: errorMessage,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function normalizeTargetDeviceInfo(value) {
|
|
100
|
+
if (!value || typeof value !== "object") {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const candidate = value;
|
|
104
|
+
const networkId = typeof candidate.networkId === "string"
|
|
105
|
+
? candidate.networkId.trim()
|
|
106
|
+
: typeof candidate.deviceId === "string"
|
|
107
|
+
? candidate.deviceId.trim()
|
|
108
|
+
: "";
|
|
109
|
+
const deviceName = typeof candidate.deviceName === "string" ? candidate.deviceName.trim() : "";
|
|
110
|
+
const deviceTypeId = typeof candidate.deviceTypeId === "string"
|
|
111
|
+
? candidate.deviceTypeId.trim()
|
|
112
|
+
: typeof candidate.deviceType === "string"
|
|
113
|
+
? candidate.deviceType.trim()
|
|
114
|
+
: "";
|
|
115
|
+
if (!networkId || !deviceName || !deviceTypeId) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
networkId,
|
|
120
|
+
deviceName,
|
|
121
|
+
deviceTypeId,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function buildUnifiedDistributeCommand(query, targetDeviceInfo, distributionSessionId) {
|
|
125
|
+
return {
|
|
126
|
+
header: {
|
|
127
|
+
namespace: "DistributionInteraction",
|
|
128
|
+
name: "UnifiedDistribute",
|
|
129
|
+
},
|
|
130
|
+
payload: {
|
|
131
|
+
targetDeviceInfo,
|
|
132
|
+
crossDeviceContent: {
|
|
133
|
+
query,
|
|
134
|
+
contexts: {
|
|
135
|
+
agentClientContext: {
|
|
136
|
+
header: {
|
|
137
|
+
namespace: "System",
|
|
138
|
+
name: "ClientContext",
|
|
139
|
+
},
|
|
140
|
+
payload: {
|
|
141
|
+
agentId: "",
|
|
142
|
+
isSupportAgent: true,
|
|
143
|
+
distributionSessionId,
|
|
144
|
+
localNetworkId: targetDeviceInfo.networkId,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export function createSendCrossDeviceTaskTool(ctx) {
|
|
153
|
+
const { config, sessionId, taskId, messageId } = ctx;
|
|
154
|
+
return {
|
|
155
|
+
name: "send_cross_device_task",
|
|
156
|
+
label: "下发跨设备协作任务",
|
|
157
|
+
description: `向用户已经选定的目标设备下发跨设备协作任务。
|
|
158
|
+
|
|
159
|
+
使用流程:
|
|
160
|
+
1. 必须先调用 discover_cross_devices 获取设备列表。
|
|
161
|
+
2. 根据用户原始需求选择唯一目标设备。
|
|
162
|
+
3. 如果存在多个同类型候选设备,或无法判断目标设备,必须先询问用户选择设备,不要调用本工具。
|
|
163
|
+
4. 只有当 targetDeviceInfo 中的 networkId、deviceName、deviceTypeId 都已明确时,才调用本工具。
|
|
164
|
+
5. 传入的query必须是用户原始query,不要做任何更改。`,
|
|
165
|
+
parameters: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
query: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "用户原始跨设备任务需求,例如:从 PC 获取某文件。",
|
|
171
|
+
},
|
|
172
|
+
targetDeviceInfo: {
|
|
173
|
+
type: "object",
|
|
174
|
+
description: "模型从 discover_cross_devices 返回列表中选定的唯一目标设备。",
|
|
175
|
+
properties: {
|
|
176
|
+
networkId: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description: "目标设备标识 networkId。",
|
|
179
|
+
},
|
|
180
|
+
deviceName: {
|
|
181
|
+
type: "string",
|
|
182
|
+
description: "目标设备名称。",
|
|
183
|
+
},
|
|
184
|
+
deviceTypeId: {
|
|
185
|
+
type: "string",
|
|
186
|
+
description: "目标设备类型编号 deviceTypeId,例如 14、17、131、2607。",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
required: ["networkId", "deviceName", "deviceTypeId"],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
required: ["query", "targetDeviceInfo"],
|
|
193
|
+
},
|
|
194
|
+
async execute(_toolCallId, params) {
|
|
195
|
+
const query = typeof params.query === "string" ? params.query.trim() : "";
|
|
196
|
+
const targetDeviceInfo = normalizeTargetDeviceInfo(params.targetDeviceInfo);
|
|
197
|
+
if (!query || !targetDeviceInfo) {
|
|
198
|
+
return buildResultText({
|
|
199
|
+
message: "Missing required parameters: query and targetDeviceInfo.networkId/deviceName/deviceTypeId.",
|
|
200
|
+
resultStatus: "对端设备任务失败",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
204
|
+
const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
|
|
205
|
+
const wsManager = getXYWebSocketManager(config);
|
|
206
|
+
const distributionSessionId = ctx.distributionSessionId || sessionId;
|
|
207
|
+
const command = buildUnifiedDistributeCommand(query, targetDeviceInfo, distributionSessionId);
|
|
208
|
+
const statusText = `正在调用${targetDeviceInfo.deviceName}执行“${query}”跨设备任务...`;
|
|
209
|
+
logger.log(`${LOG_TAG} sending task to ${targetDeviceInfo.deviceName}, distributionSessionId=${distributionSessionId}`);
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
let timeout;
|
|
212
|
+
let handler;
|
|
213
|
+
let settled = false;
|
|
214
|
+
let resultHandlingStarted = false;
|
|
215
|
+
const cleanup = () => {
|
|
216
|
+
clearTimeout(timeout);
|
|
217
|
+
wsManager.off("cross-device-task-result", handler);
|
|
218
|
+
};
|
|
219
|
+
const finish = (result) => {
|
|
220
|
+
if (settled) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
settled = true;
|
|
224
|
+
const modelResult = buildModelToolResult(result);
|
|
225
|
+
logger.log(`${LOG_TAG} completed, success=${result.success}, code=${result.code}, fileUrlCount=${result.fileUrls.length}`);
|
|
226
|
+
cleanup();
|
|
227
|
+
resolve(buildResultText(modelResult));
|
|
228
|
+
};
|
|
229
|
+
handler = (event) => {
|
|
230
|
+
if (event.sessionId && event.sessionId !== sessionId && event.sessionId !== distributionSessionId) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
logger.log(`${SEND_CROSS_RESULT_LOG_TAG} received result, status=${event.status}, code=${event.code}, fileUrlCount=${event.fileUrls.length}`);
|
|
234
|
+
void (async () => {
|
|
235
|
+
if (resultHandlingStarted) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
resultHandlingStarted = true;
|
|
239
|
+
clearTimeout(timeout);
|
|
240
|
+
try {
|
|
241
|
+
await sendStatusUpdate({
|
|
242
|
+
config,
|
|
243
|
+
sessionId,
|
|
244
|
+
taskId: currentTaskId,
|
|
245
|
+
messageId: currentMessageId,
|
|
246
|
+
text: PEER_TASK_COMPLETED_STATUS_TEXT,
|
|
247
|
+
state: "working",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
logger.error(`${SEND_CROSS_RESULT_LOG_TAG} failed to send peer task completed status update: ${error instanceof Error ? error.message : String(error)}`);
|
|
252
|
+
}
|
|
253
|
+
const result = buildCrossDeviceResult({
|
|
254
|
+
success: event.status === "success",
|
|
255
|
+
code: event.code,
|
|
256
|
+
message: event.message,
|
|
257
|
+
fileUrls: event.fileUrls,
|
|
258
|
+
rawEvent: event.rawEvent,
|
|
259
|
+
});
|
|
260
|
+
const resultWithFileSend = await autoSendFileToUserIfNeeded(result, ctx);
|
|
261
|
+
finish(resultWithFileSend);
|
|
262
|
+
})();
|
|
263
|
+
};
|
|
264
|
+
timeout = setTimeout(() => {
|
|
265
|
+
logger.log(`${LOG_TAG} timeout waiting cross-device result after ${CROSS_DEVICE_TASK_TIMEOUT_MS}ms`);
|
|
266
|
+
finish({
|
|
267
|
+
success: false,
|
|
268
|
+
code: "",
|
|
269
|
+
message: `Cross-device task timed out after ${CROSS_DEVICE_TASK_TIMEOUT_MS / 1000} seconds.`,
|
|
270
|
+
fileUrls: [],
|
|
271
|
+
rawEvent: null,
|
|
272
|
+
});
|
|
273
|
+
}, CROSS_DEVICE_TASK_TIMEOUT_MS);
|
|
274
|
+
wsManager.on("cross-device-task-result", handler);
|
|
275
|
+
sendStatusUpdate({
|
|
276
|
+
config,
|
|
277
|
+
sessionId,
|
|
278
|
+
taskId: currentTaskId,
|
|
279
|
+
messageId: currentMessageId,
|
|
280
|
+
text: statusText,
|
|
281
|
+
state: "working",
|
|
282
|
+
})
|
|
283
|
+
.then(() => sendCommand({
|
|
284
|
+
config,
|
|
285
|
+
sessionId,
|
|
286
|
+
taskId: currentTaskId,
|
|
287
|
+
messageId: currentMessageId,
|
|
288
|
+
command,
|
|
289
|
+
}))
|
|
290
|
+
.catch((error) => {
|
|
291
|
+
logger.error(`${LOG_TAG} failed to send cross-device task command: ${error instanceof Error ? error.message : String(error)}`);
|
|
292
|
+
finish({
|
|
293
|
+
success: false,
|
|
294
|
+
code: "",
|
|
295
|
+
message: `Failed to send cross-device task command: ${error instanceof Error ? error.message : String(error)}`,
|
|
296
|
+
fileUrls: [],
|
|
297
|
+
rawEvent: null,
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -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) => {
|
|
@@ -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,18 +1,29 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
-
import type { XYChannelConfig } from "../types.js";
|
|
2
|
+
import type { RunCrossTaskContext, 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,5 @@ 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 appendRunCrossTaskFileUrls(fileUrls: string[], explicitRunCrossTaskContext?: RunCrossTaskContext): string[];
|
|
68
80
|
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,21 @@ export function cleanupStaleSessions() {
|
|
|
214
237
|
export function getActiveSessionCount() {
|
|
215
238
|
return activeSessions.size;
|
|
216
239
|
}
|
|
240
|
+
export function appendRunCrossTaskFileUrls(fileUrls, explicitRunCrossTaskContext) {
|
|
241
|
+
const context = asyncLocalStorage.getStore() ?? null;
|
|
242
|
+
const runCrossTaskContext = explicitRunCrossTaskContext ?? context?.runCrossTaskContext;
|
|
243
|
+
if (!runCrossTaskContext || fileUrls.length === 0) {
|
|
244
|
+
return runCrossTaskContext?.fileUrls ?? [];
|
|
245
|
+
}
|
|
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;
|
|
249
|
+
const sessionWithRef = Array.from(activeSessions.values()).find((session) => session.runCrossTaskContext === runCrossTaskContext);
|
|
250
|
+
if (sessionWithRef?.runCrossTaskContext) {
|
|
251
|
+
sessionWithRef.runCrossTaskContext.fileUrls = merged;
|
|
252
|
+
}
|
|
253
|
+
return merged;
|
|
254
|
+
}
|
|
217
255
|
/**
|
|
218
256
|
* Enrich a base session context with the latest taskId/messageId
|
|
219
257
|
* from task-manager (supports interruption scenarios).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getXYWebSocketManager } from "../client.js";
|
|
2
2
|
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { appendRunCrossTaskFileUrls } from "./session-manager.js";
|
|
3
4
|
import { getCurrentTaskId } from "../task-manager.js";
|
|
4
5
|
import { logger } from "../utils/logger.js";
|
|
5
6
|
/**
|
|
@@ -106,7 +107,11 @@ export function createUploadFileTool(ctx) {
|
|
|
106
107
|
const wsManager = getXYWebSocketManager(config);
|
|
107
108
|
// Get public URLs for the files
|
|
108
109
|
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
109
|
-
const fileUrls = await getFileUrls(wsManager, config, sessionId, currentTaskId, messageId, fileInfos, params.udid);
|
|
110
|
+
const fileUrls = await getFileUrls(wsManager, config, sessionId, currentTaskId, messageId, toolCallId, fileInfos, params.udid);
|
|
111
|
+
if (ctx.runCrossTaskContext && fileUrls.length > 0) {
|
|
112
|
+
const cachedFileUrls = appendRunCrossTaskFileUrls(fileUrls, ctx.runCrossTaskContext);
|
|
113
|
+
logger.log(`[RunCrossTask] cached ${fileUrls.length} upload_file URL(s) for cross-task result, total=${cachedFileUrls.length}`);
|
|
114
|
+
}
|
|
110
115
|
return {
|
|
111
116
|
content: [
|
|
112
117
|
{
|
|
@@ -126,7 +131,7 @@ export function createUploadFileTool(ctx) {
|
|
|
126
131
|
* Get public URLs for files using fileInfos
|
|
127
132
|
* Returns array of publicly accessible file URLs
|
|
128
133
|
*/
|
|
129
|
-
async function getFileUrls(wsManager, config, sessionId, taskId, messageId, fileInfos, udid) {
|
|
134
|
+
async function getFileUrls(wsManager, config, sessionId, taskId, messageId, toolCallId, fileInfos, udid) {
|
|
130
135
|
const command = {
|
|
131
136
|
header: {
|
|
132
137
|
namespace: "Common",
|
|
@@ -214,6 +219,7 @@ async function getFileUrls(wsManager, config, sessionId, taskId, messageId, file
|
|
|
214
219
|
taskId,
|
|
215
220
|
messageId,
|
|
216
221
|
command,
|
|
222
|
+
toolCallId,
|
|
217
223
|
})
|
|
218
224
|
.then(() => {
|
|
219
225
|
})
|
|
@@ -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 = {
|
|
@@ -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
|
})
|
package/dist/src/types.d.ts
CHANGED
|
@@ -62,6 +62,23 @@ export interface A2ADataEvent {
|
|
|
62
62
|
outputs: Record<string, any>;
|
|
63
63
|
status: "success" | "failed";
|
|
64
64
|
}
|
|
65
|
+
export interface CrossDeviceTaskResultEvent {
|
|
66
|
+
sessionId: string;
|
|
67
|
+
code: string;
|
|
68
|
+
message: string;
|
|
69
|
+
fileUrls: string[];
|
|
70
|
+
status: "success" | "failed";
|
|
71
|
+
rawEvent: any;
|
|
72
|
+
}
|
|
73
|
+
export interface RunCrossTaskContext {
|
|
74
|
+
agentId: string;
|
|
75
|
+
sessionId: string;
|
|
76
|
+
isDistributed: boolean;
|
|
77
|
+
networkId: string;
|
|
78
|
+
isSupportAgent: boolean;
|
|
79
|
+
fileUrls?: string[];
|
|
80
|
+
rawContext: any;
|
|
81
|
+
}
|
|
65
82
|
export interface A2ATaskArtifactUpdateEvent {
|
|
66
83
|
taskId: string;
|
|
67
84
|
kind: "artifact-update";
|
package/dist/src/websocket.d.ts
CHANGED
|
@@ -102,6 +102,9 @@ export declare class XYWebSocketManager extends EventEmitter {
|
|
|
102
102
|
* Start heartbeat.
|
|
103
103
|
*/
|
|
104
104
|
private startHeartbeat;
|
|
105
|
+
private toUploadExeDataEvent;
|
|
106
|
+
private toCrossDeviceTaskResultEvent;
|
|
107
|
+
private toRunCrossTaskA2ARequest;
|
|
105
108
|
/**
|
|
106
109
|
* Handle incoming message from server.
|
|
107
110
|
*/
|