@ynhcj/xiaoyi-channel 0.0.159-beta → 0.0.161-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/src/cron-query-handler.js +1 -1
- package/dist/src/cspl/constants.d.ts +1 -0
- package/dist/src/cspl/constants.js +2 -0
- package/dist/src/cspl/sentinel_hook.js +16 -2
- package/dist/src/parser.js +31 -1
- package/dist/src/provider.js +19 -17
- package/dist/src/reply-dispatcher.js +6 -5
- package/dist/src/tools/discover-cross-devices-tool.js +1 -1
- package/dist/src/tools/send-cross-device-task-tool.js +18 -22
- 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/session-manager.d.ts +2 -2
- package/dist/src/tools/session-manager.js +29 -7
- package/dist/src/tools/upload-file-tool.d.ts +1 -1
- package/dist/src/tools/upload-file-tool.js +0 -5
- package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -1
- package/dist/src/types.d.ts +7 -2
- package/dist/src/websocket.js +29 -8
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@ export declare const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
|
29
29
|
export declare const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
|
30
30
|
export declare const REQUIRED_ENV_VARS: string[];
|
|
31
31
|
export declare const ALLOWED_TOOLS: string[];
|
|
32
|
+
export declare const STEER_ABORT_MESSAGE = "\u5F53\u524D\u884C\u4E3A\u5B58\u5728\u5B89\u5168\u9690\u60A3\uFF0C\u7EC8\u6B62\u6240\u6709\u64CD\u4F5C\uFF0C\u5E76\u4E14\u5728\u6700\u7EC8\u56DE\u590D\u4E2D\u8BF4\u660E\uFF0C\u8BC6\u522B\u5230\u5F53\u524D\u6D41\u7A0B\u4E2D\u5B58\u5728\u6F5C\u5728\u5B89\u5168\u98CE\u9669\uFF0C\u5DF2\u5E2E\u60A8\u4E2D\u6B62\u5F53\u524D\u6D41\u7A0B\uFF0C\u8BF7\u6E05\u7A7A\u4E0A\u4E0B\u6587\u540E\u91CD\u8BD5";
|
|
32
33
|
export declare const MAX_FILE_COUNT = 10;
|
|
33
34
|
export declare const MAX_COMMAND_LENGTH = 1024;
|
|
34
35
|
export declare const CODE_FILE_EXTENSIONS: string[];
|
|
@@ -28,6 +28,8 @@ export const API_URL_SUFFIX = '/celia-claw/v1/rest-api/skill/execute';
|
|
|
28
28
|
export const REQUIRED_ENV_VARS = ['PERSONAL-API-KEY', 'PERSONAL-UID', 'SERVICE_URL'];
|
|
29
29
|
// 工具白名单:除了web_fetch特殊处理外,只有这些工具才处理result文本
|
|
30
30
|
export const ALLOWED_TOOLS = ['web_fetch', 'read', 'pdf', 'exec', 'message'];
|
|
31
|
+
// Steer 注入话术:CSPL REJECT 时通过 steer 插话打断模型
|
|
32
|
+
export const STEER_ABORT_MESSAGE = '当前行为存在安全隐患,终止所有操作,并且在最终回复中说明,识别到当前流程中存在潜在安全风险,已帮您中止当前流程,请清空上下文后重试';
|
|
31
33
|
// 文件数量限制
|
|
32
34
|
export const MAX_FILE_COUNT = 10;
|
|
33
35
|
// 命令字符串截断长度(字节)
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { callApi } from './call_api.js';
|
|
6
6
|
import { processText, extractResultText, validateAndTruncateText, parseSecurityResult, handleExecToolInput, handleMessageToolInput, handleOtherToolInput } from './utils.js';
|
|
7
|
-
import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH } from './constants.js';
|
|
7
|
+
import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE } from './constants.js';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
|
+
import { getSessionContext } from '../tools/session-manager.js';
|
|
10
|
+
import { tryInjectSteer } from './steer-context.js';
|
|
9
11
|
// 主入口模块
|
|
10
12
|
export default function register(api) {
|
|
11
13
|
api.on("before_tool_call", async (event, ctx) => {
|
|
@@ -70,7 +72,19 @@ export default function register(api) {
|
|
|
70
72
|
const result = parseSecurityResult(response);
|
|
71
73
|
logger.log(`[SENTINEL HOOK] TOOL_OUTPUT response: status=${result.status}.`);
|
|
72
74
|
if (result.status === 'REJECT') {
|
|
73
|
-
logger.warn('[SENTINEL HOOK]
|
|
75
|
+
logger.warn('[SENTINEL HOOK] REJECT detected, attempting steer injection');
|
|
76
|
+
const sessionCtx = ctx.sessionKey ? getSessionContext(ctx.sessionKey) : null;
|
|
77
|
+
if (sessionCtx?.sessionId && sessionCtx?.taskId) {
|
|
78
|
+
await tryInjectSteer({
|
|
79
|
+
sessionId: sessionCtx.sessionId,
|
|
80
|
+
taskId: sessionCtx.taskId,
|
|
81
|
+
message: STEER_ABORT_MESSAGE,
|
|
82
|
+
source: 'cspl',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
logger.warn(`[SENTINEL HOOK] Cannot inject steer: sessionKey=${ctx.sessionKey}, sessionCtx found=${!!sessionCtx}`);
|
|
87
|
+
}
|
|
74
88
|
}
|
|
75
89
|
}
|
|
76
90
|
catch (error) {
|
package/dist/src/parser.js
CHANGED
|
@@ -46,6 +46,36 @@ export function extractDataEvents(parts) {
|
|
|
46
46
|
.filter((event) => event !== undefined);
|
|
47
47
|
}
|
|
48
48
|
export function extractRunCrossTaskContext(parts) {
|
|
49
|
+
const normalizeSentFiles = (value) => {
|
|
50
|
+
if (!Array.isArray(value)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
return value
|
|
54
|
+
.map((item) => {
|
|
55
|
+
if (!item || typeof item !== "object") {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const candidate = item;
|
|
59
|
+
const fileLocalUrls = Array.isArray(candidate.fileLocalUrls)
|
|
60
|
+
? candidate.fileLocalUrls.filter((url) => typeof url === "string" && url.length > 0)
|
|
61
|
+
: [];
|
|
62
|
+
const fileRemoteUrls = Array.isArray(candidate.fileRemoteUrls)
|
|
63
|
+
? candidate.fileRemoteUrls.filter((url) => typeof url === "string" && url.length > 0)
|
|
64
|
+
: [];
|
|
65
|
+
const fileNames = Array.isArray(candidate.fileNames)
|
|
66
|
+
? candidate.fileNames.filter((name) => typeof name === "string" && name.length > 0)
|
|
67
|
+
: [];
|
|
68
|
+
if (fileLocalUrls.length === 0 && fileRemoteUrls.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
|
|
73
|
+
...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
|
|
74
|
+
...(fileNames.length > 0 && fileNames.length === fileRemoteUrls.length ? { fileNames } : {}),
|
|
75
|
+
};
|
|
76
|
+
})
|
|
77
|
+
.filter((item) => item !== null);
|
|
78
|
+
};
|
|
49
79
|
for (const part of parts) {
|
|
50
80
|
if (part.kind !== "data" || !part.data) {
|
|
51
81
|
continue;
|
|
@@ -64,7 +94,7 @@ export function extractRunCrossTaskContext(parts) {
|
|
|
64
94
|
isDistributed: context.isDistributed === true,
|
|
65
95
|
networkId,
|
|
66
96
|
isSupportAgent: context.isSupportAgent !== false,
|
|
67
|
-
|
|
97
|
+
sentFiles: normalizeSentFiles(context.sentFiles),
|
|
68
98
|
rawContext: context,
|
|
69
99
|
};
|
|
70
100
|
}
|
package/dist/src/provider.js
CHANGED
|
@@ -536,26 +536,28 @@ export const xiaoyiProvider = {
|
|
|
536
536
|
const beforeLen = sp.length;
|
|
537
537
|
// 删除 ## Tooling 与 TOOLS.md 声明之间的内容
|
|
538
538
|
sp = sp.replace(/(## Tooling)[\s\S]*?(TOOLS\.md does not control tool availability; it is user guidance for how to use external tools\.)/, "$1\n\n$2");
|
|
539
|
-
// (1)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
539
|
+
// (1) Skills 部分:移动到 ## Runtime 之前
|
|
540
|
+
if (sp.includes('## Runtime')) {
|
|
541
|
+
// 提取 ## Skills (mandatory) 到 </available_skills> 作为第一部分
|
|
542
|
+
const skillsMatch = sp.match(/(## Skills \(mandatory\)[\s\S]*?<\/available_skills>)/);
|
|
543
|
+
if (skillsMatch) {
|
|
544
|
+
const part1 = skillsMatch[0];
|
|
545
|
+
sp = sp.replace(part1, '');
|
|
546
|
+
sp = sp.replace('## Runtime', part1 + '\n\n## Runtime');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// (2) SOUL.md 部分:移动到 ## Silent Replies 之前
|
|
550
|
+
if (sp.includes('## Silent Replies')) {
|
|
551
|
+
// 提取 ## /home/sandbox/.openclaw/workspace/SOUL.md 到 其特定脚注结束标志 的内容作为第二部分
|
|
552
|
+
const soulMatch = sp.match(/(## \/home\/sandbox\/\.openclaw\/workspace\/SOUL\.md[\s\S]*?_This file is yours to evolve\. As you learn who you are, update it\._)/);
|
|
553
|
+
if (soulMatch) {
|
|
554
|
+
const part2 = soulMatch[1].trim();
|
|
550
555
|
sp = sp.replace(soulMatch[1], '');
|
|
551
|
-
|
|
552
|
-
sp = sp.replace(/\n{3,}/g, '\n\n');
|
|
553
|
-
// (3) 将 第二部分 + 第一部分 插入到 ## Runtime 上面
|
|
554
|
-
const combined = (part2 + '\n\n' + part1).trim();
|
|
555
|
-
if (combined && sp.includes('## Runtime')) {
|
|
556
|
-
sp = sp.replace('## Runtime', combined + '\n\n## Runtime');
|
|
556
|
+
sp = sp.replace('## Silent Replies', part2 + '\n\n## Silent Replies');
|
|
557
557
|
}
|
|
558
558
|
}
|
|
559
|
+
// 清理多余空行
|
|
560
|
+
sp = sp.replace(/\n{3,}/g, '\n\n');
|
|
559
561
|
logger.log(`[xiaoyiprovider] system prompt optimized: ${beforeLen} -> ${sp.length}`);
|
|
560
562
|
context.systemPrompt = sp;
|
|
561
563
|
}
|
|
@@ -23,7 +23,7 @@ function buildDistributionStatusCommand(context) {
|
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
function buildCrossTaskExecuteResultCommand(code, message,
|
|
26
|
+
function buildCrossTaskExecuteResultCommand(code, message, sentFiles = []) {
|
|
27
27
|
return {
|
|
28
28
|
header: {
|
|
29
29
|
namespace: "DistributionInteraction",
|
|
@@ -32,15 +32,15 @@ function buildCrossTaskExecuteResultCommand(code, message, fileUrls = []) {
|
|
|
32
32
|
payload: {
|
|
33
33
|
code,
|
|
34
34
|
message,
|
|
35
|
-
|
|
35
|
+
sentFiles,
|
|
36
36
|
},
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
async function sendRunCrossTaskResult(params) {
|
|
40
40
|
const { config, sessionId, taskId, messageId, context, resultCode, resultMessage } = params;
|
|
41
|
-
const
|
|
41
|
+
const sentFiles = Array.isArray(context.sentFiles) ? context.sentFiles : [];
|
|
42
42
|
const statusCommand = buildDistributionStatusCommand(context);
|
|
43
|
-
const resultCommand = buildCrossTaskExecuteResultCommand(resultCode, resultMessage,
|
|
43
|
+
const resultCommand = buildCrossTaskExecuteResultCommand(resultCode, resultMessage, sentFiles);
|
|
44
44
|
await sendCommand({
|
|
45
45
|
config,
|
|
46
46
|
sessionId,
|
|
@@ -48,7 +48,7 @@ async function sendRunCrossTaskResult(params) {
|
|
|
48
48
|
messageId,
|
|
49
49
|
commands: [statusCommand, resultCommand],
|
|
50
50
|
});
|
|
51
|
-
logger.log(`${RUN_CROSS_TASK_LOG_TAG} sent cross-task result, sessionId=${sessionId}, taskId=${taskId}, code=${resultCode},
|
|
51
|
+
logger.log(`${RUN_CROSS_TASK_LOG_TAG} sent cross-task result, sessionId=${sessionId}, taskId=${taskId}, code=${resultCode}, sentFileCount=${sentFiles.length}, messageLength=${resultMessage.length}`);
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
@@ -321,6 +321,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
321
321
|
dispatcher,
|
|
322
322
|
replyOptions: {
|
|
323
323
|
...replyOptions,
|
|
324
|
+
suppressToolErrorWarnings: true,
|
|
324
325
|
onModelSelected: prefixContext.onModelSelected,
|
|
325
326
|
onToolStart: async ({ name, phase }) => {
|
|
326
327
|
// 🔑 steered dispatch不发送tool状态(让主dispatcher处理)
|
|
@@ -106,7 +106,7 @@ export function createDiscoverCrossDevicesTool(ctx) {
|
|
|
106
106
|
|
|
107
107
|
当用户明确表达要从另一台设备获取、查找、使用或操作内容时,必须优先调用本工具,例如从 PC、电脑、平板、手机等设备获取文件或查找内容。
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
本工具只做设备发现和目标设备推荐,不会读取副设备文件内容,不会上传文件,也不会真正下发跨端执行任务。下发跨端执行任务需要使用SendCrossDeviceTaskTool`,
|
|
110
110
|
parameters: {
|
|
111
111
|
type: "object",
|
|
112
112
|
properties: {
|
|
@@ -18,9 +18,9 @@ function buildResultText(result) {
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
function buildModelToolResult(result) {
|
|
21
|
-
const
|
|
21
|
+
const sentFiles = result.sentFiles;
|
|
22
22
|
const resultStatus = result.success
|
|
23
|
-
?
|
|
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
|
-
|
|
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
|
|
69
|
-
if (
|
|
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 ${
|
|
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
|
|
76
|
-
|
|
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},
|
|
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},
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
+
sentFiles: [],
|
|
297
293
|
rawEvent: null,
|
|
298
294
|
});
|
|
299
295
|
});
|
|
@@ -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,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;
|
|
@@ -76,5 +76,5 @@ export declare function cleanupStaleSessions(): number;
|
|
|
76
76
|
* Get the current number of active sessions (for diagnostics).
|
|
77
77
|
*/
|
|
78
78
|
export declare function getActiveSessionCount(): number;
|
|
79
|
-
export declare function
|
|
79
|
+
export declare function appendRunCrossTaskSentFiles(sentFiles: SentFileParams[], explicitRunCrossTaskContext?: RunCrossTaskContext): SentFileParams[];
|
|
80
80
|
export {};
|
|
@@ -237,18 +237,40 @@ export function cleanupStaleSessions() {
|
|
|
237
237
|
export function getActiveSessionCount() {
|
|
238
238
|
return activeSessions.size;
|
|
239
239
|
}
|
|
240
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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.
|
|
247
|
-
const merged =
|
|
248
|
-
runCrossTaskContext.
|
|
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.
|
|
273
|
+
sessionWithRef.runCrossTaskContext.sentFiles = merged;
|
|
252
274
|
}
|
|
253
275
|
return merged;
|
|
254
276
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { getXYWebSocketManager } from "../client.js";
|
|
2
2
|
import { sendCommand } from "../formatter.js";
|
|
3
|
-
import { appendRunCrossTaskFileUrls } from "./session-manager.js";
|
|
4
3
|
import { getCurrentTaskId } from "../task-manager.js";
|
|
5
4
|
import { logger } from "../utils/logger.js";
|
|
6
5
|
/**
|
|
@@ -108,10 +107,6 @@ export function createUploadFileTool(ctx) {
|
|
|
108
107
|
// Get public URLs for the files
|
|
109
108
|
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
110
109
|
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
|
-
}
|
|
115
110
|
return {
|
|
116
111
|
content: [
|
|
117
112
|
{
|
|
@@ -32,7 +32,7 @@ export function createXiaoyiAddCollectionTool(ctx) {
|
|
|
32
32
|
● title:非必填字段,数据类型为string,功能描述是标识文件类型数据的文件名称。适用于FILE类型。
|
|
33
33
|
说明:如果dataType为HYPER_LINK或TEXT,则content字段必填且不能为空;如果dataType为IMAGE或FILE,则uri字段必填且不能为空。当用户希望收藏海报、截图等图片类数据时,请将数据以图片IMAGE的形式存入到小艺帮记;当用户希望收藏电子书、笔记、报告、素材、文档、合同、协议、简历、证书、报表、日志、安装包、压缩包等描述的文件时,请将数据以文件FILE的形式存入到小艺帮记。
|
|
34
34
|
当你成功收藏这个数据到小艺帮记后,请在最后显示"已成功把数据添加到[小艺帮记](vassistant://voice/main?page=CollectionPage&jumpHomePageTab=myCollection)",
|
|
35
|
-
请额外注意,如果当前用户的设备是鸿蒙PC,则最后显示的内容需要额外定制,内容是"已成功把数据添加到[小艺帮记](vassistant://voice/main?agentId=celia_collection_share&agentPage=CollectionPage)"
|
|
35
|
+
请额外注意,如果当前用户的设备是鸿蒙PC,则最后显示的内容需要额外定制,内容是"已成功把数据添加到[小艺帮记](vassistant://voice/main?agentId=celia_collection_share&agentPage=CollectionPage&from=push&action=openFlashCardPage)"
|
|
36
36
|
注意:
|
|
37
37
|
a. 操作超时时间为60秒,请勿重复调用此工具
|
|
38
38
|
b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
|
package/dist/src/types.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface CrossDeviceTaskResultEvent {
|
|
|
66
66
|
sessionId: string;
|
|
67
67
|
code: string;
|
|
68
68
|
message: string;
|
|
69
|
-
|
|
69
|
+
sentFiles: SentFileParams[];
|
|
70
70
|
status: "success" | "failed";
|
|
71
71
|
rawEvent: any;
|
|
72
72
|
}
|
|
@@ -76,9 +76,14 @@ export interface RunCrossTaskContext {
|
|
|
76
76
|
isDistributed: boolean;
|
|
77
77
|
networkId: string;
|
|
78
78
|
isSupportAgent: boolean;
|
|
79
|
-
|
|
79
|
+
sentFiles: SentFileParams[];
|
|
80
80
|
rawContext: any;
|
|
81
81
|
}
|
|
82
|
+
export interface SentFileParams {
|
|
83
|
+
fileLocalUrls?: string[];
|
|
84
|
+
fileRemoteUrls?: string[];
|
|
85
|
+
fileNames?: string[];
|
|
86
|
+
}
|
|
82
87
|
export interface A2ATaskArtifactUpdateEvent {
|
|
83
88
|
taskId: string;
|
|
84
89
|
kind: "artifact-update";
|
package/dist/src/websocket.js
CHANGED
|
@@ -392,15 +392,36 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
392
392
|
}
|
|
393
393
|
const code = item?.payload?.code === undefined ? "" : String(item.payload.code);
|
|
394
394
|
const message = typeof item?.payload?.message === "string" ? item.payload.message : "";
|
|
395
|
-
const
|
|
396
|
-
? item.payload.
|
|
395
|
+
const sentFiles = Array.isArray(item?.payload?.sentFiles)
|
|
396
|
+
? item.payload.sentFiles.map((entry) => {
|
|
397
|
+
if (!entry || typeof entry !== "object") {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const fileLocalUrls = Array.isArray(entry.fileLocalUrls)
|
|
401
|
+
? entry.fileLocalUrls.filter((url) => typeof url === "string" && url.length > 0)
|
|
402
|
+
: [];
|
|
403
|
+
const fileRemoteUrls = Array.isArray(entry.fileRemoteUrls)
|
|
404
|
+
? entry.fileRemoteUrls.filter((url) => typeof url === "string" && url.length > 0)
|
|
405
|
+
: [];
|
|
406
|
+
const fileNames = Array.isArray(entry.fileNames)
|
|
407
|
+
? entry.fileNames.filter((name) => typeof name === "string" && name.length > 0)
|
|
408
|
+
: [];
|
|
409
|
+
if (fileLocalUrls.length === 0 && fileRemoteUrls.length === 0) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
|
|
414
|
+
...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
|
|
415
|
+
...(fileNames.length > 0 && fileNames.length === fileRemoteUrls.length ? { fileNames } : {}),
|
|
416
|
+
};
|
|
417
|
+
}).filter((entry) => entry !== null)
|
|
397
418
|
: [];
|
|
398
419
|
const status = code === "0" ? "success" : "failed";
|
|
399
420
|
const event = {
|
|
400
421
|
sessionId,
|
|
401
422
|
code,
|
|
402
423
|
message,
|
|
403
|
-
|
|
424
|
+
sentFiles,
|
|
404
425
|
status,
|
|
405
426
|
rawEvent: item,
|
|
406
427
|
};
|
|
@@ -444,7 +465,7 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
444
465
|
networkId,
|
|
445
466
|
isDistributed: true,
|
|
446
467
|
isSupportAgent: true,
|
|
447
|
-
|
|
468
|
+
sentFiles: [],
|
|
448
469
|
rawContext: parsed,
|
|
449
470
|
};
|
|
450
471
|
const request = {
|
|
@@ -602,8 +623,8 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
602
623
|
event: item,
|
|
603
624
|
});
|
|
604
625
|
}
|
|
605
|
-
else if (item.header?.namespace === "
|
|
606
|
-
log.log("[XY]
|
|
626
|
+
else if (item.header?.namespace === "AgentEvent" && item.header?.name === "CronQuery") {
|
|
627
|
+
log.log("[XY] AgentEvent.CronQuery detected, emitting cron-query-event");
|
|
607
628
|
this.emit("cron-query-event", {
|
|
608
629
|
...(item.payload ?? {}),
|
|
609
630
|
sessionId,
|
|
@@ -691,8 +712,8 @@ export class XYWebSocketManager extends EventEmitter {
|
|
|
691
712
|
event: item,
|
|
692
713
|
});
|
|
693
714
|
}
|
|
694
|
-
else if (item.header?.namespace === "
|
|
695
|
-
log.log("[XY]
|
|
715
|
+
else if (item.header?.namespace === "AgentEvent" && item.header?.name === "CronQuery") {
|
|
716
|
+
log.log("[XY] AgentEvent.CronQuery detected (wrapped format), emitting cron-query-event");
|
|
696
717
|
this.emit("cron-query-event", {
|
|
697
718
|
...(item.payload ?? {}),
|
|
698
719
|
sessionId: inboundMsg.sessionId || a2aRequest.params?.sessionId,
|