@ynhcj/xiaoyi-channel 1.1.26 → 1.1.28
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 +26 -69
- package/dist/src/approval-bridge.d.ts +48 -0
- package/dist/src/approval-bridge.js +382 -0
- package/dist/src/bot.js +132 -73
- package/dist/src/channel.js +59 -5
- package/dist/src/client.js +13 -23
- 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 +189 -0
- package/dist/src/cspl/call_api.d.ts +2 -0
- package/dist/src/cspl/call_api.js +107 -0
- package/dist/src/cspl/config.d.ts +4 -17
- package/dist/src/cspl/config.js +100 -70
- package/dist/src/cspl/configs.json +10 -0
- package/dist/src/cspl/constants.d.ts +49 -24
- package/dist/src/cspl/constants.js +46 -16
- package/dist/src/cspl/sentinel_hook.d.ts +2 -0
- package/dist/src/cspl/sentinel_hook.js +103 -0
- package/dist/src/cspl/steer-context.js +1 -1
- package/dist/src/cspl/upload_file.d.ts +1 -0
- package/dist/src/cspl/upload_file.js +211 -0
- package/dist/src/cspl/utils.d.ts +17 -2
- package/dist/src/cspl/utils.js +271 -15
- package/dist/src/file-upload.d.ts +5 -0
- package/dist/src/file-upload.js +102 -0
- package/dist/src/formatter.d.ts +43 -1
- package/dist/src/formatter.js +171 -41
- package/dist/src/monitor.js +64 -43
- package/dist/src/outbound.js +8 -9
- package/dist/src/parser.d.ts +8 -1
- package/dist/src/parser.js +71 -0
- package/dist/src/provider.js +51 -17
- package/dist/src/push.d.ts +11 -1
- package/dist/src/push.js +101 -17
- package/dist/src/reply-dispatcher.js +152 -59
- package/dist/src/self-evolution-handler.d.ts +1 -1
- package/dist/src/self-evolution-handler.js +14 -3
- package/dist/src/sensitive-redactor.d.ts +4 -0
- package/dist/src/sensitive-redactor.js +364 -0
- package/dist/src/task-manager.js +6 -10
- package/dist/src/tools/agent-as-skill-tool.d.ts +7 -0
- package/dist/src/tools/agent-as-skill-tool.js +190 -0
- package/dist/src/tools/calendar-tool.js +3 -2
- package/dist/src/tools/call-phone-tool.js +3 -2
- package/dist/src/tools/check-plugin-privilege-tool.d.ts +6 -0
- package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
- package/dist/src/tools/create-alarm-tool.js +3 -2
- package/dist/src/tools/create-all-tools.js +11 -3
- package/dist/src/tools/delete-alarm-tool.js +3 -2
- 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.d.ts +2 -1
- package/dist/src/tools/find-pc-devices-tool.js +85 -88
- package/dist/src/tools/get-collection-tool-schema.js +1 -1
- package/dist/src/tools/location-tool.js +3 -2
- package/dist/src/tools/modify-alarm-tool.js +3 -2
- package/dist/src/tools/modify-note-tool.js +3 -2
- package/dist/src/tools/note-tool.js +3 -2
- 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 +3 -2
- package/dist/src/tools/save-media-to-gallery-tool.js +3 -2
- package/dist/src/tools/schema-tool-factory.js +1 -1
- package/dist/src/tools/search-alarm-tool.js +3 -2
- package/dist/src/tools/search-calendar-tool.js +3 -2
- package/dist/src/tools/search-contact-tool.js +3 -2
- package/dist/src/tools/search-email-tool.js +4 -3
- package/dist/src/tools/search-file-tool.js +8 -9
- package/dist/src/tools/search-message-tool.js +2 -1
- package/dist/src/tools/search-note-tool.js +3 -2
- package/dist/src/tools/search-photo-gallery-tool.js +5 -4
- 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 +37 -8
- package/dist/src/tools/send-html-card-tool.d.ts +7 -0
- package/dist/src/tools/send-html-card-tool.js +113 -0
- package/dist/src/tools/send-message-tool.js +2 -1
- package/dist/src/tools/session-manager.d.ts +17 -1
- package/dist/src/tools/session-manager.js +87 -1
- package/dist/src/tools/upload-file-tool.js +9 -7
- package/dist/src/tools/upload-photo-tool.js +5 -4
- package/dist/src/tools/xiaoyi-add-collection-tool.js +5 -3
- package/dist/src/tools/xiaoyi-collection-tool.js +4 -3
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +4 -3
- package/dist/src/tools/xiaoyi-gui-tool.js +8 -2
- package/dist/src/trigger-handler.js +4 -7
- package/dist/src/types.d.ts +25 -1
- package/dist/src/utils/config-manager.js +3 -6
- package/dist/src/utils/logger.d.ts +8 -0
- package/dist/src/utils/logger.js +69 -34
- package/dist/src/utils/pushdata-manager.js +1 -5
- package/dist/src/utils/pushid-manager.js +1 -2
- package/dist/src/utils/runtime-manager.js +1 -4
- package/dist/src/websocket.d.ts +3 -0
- package/dist/src/websocket.js +242 -38
- package/package.json +1 -1
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// 一键开关 Xiaoyi channel 的敏感信息脱敏与检测能力。
|
|
4
|
+
// false 时:
|
|
5
|
+
// 1. 不再读取 openclaw.json / .xiaoyienv 提取敏感值;
|
|
6
|
+
// 2. 不再对消息内容做脱敏替换;
|
|
7
|
+
// 3. 不再做 Base64 绕过检测;
|
|
8
|
+
// 4. containsSensitiveInfo 一律返回 false。
|
|
9
|
+
const ENABLE_SENSITIVE_REDACTION = true;
|
|
10
|
+
let sensitiveValues = null;
|
|
11
|
+
let lastLoadTime = 0;
|
|
12
|
+
const CACHE_TTL_MS = 60 * 1000;
|
|
13
|
+
const REDACTED_MARKER = "[execution-validator已脱敏]";
|
|
14
|
+
const BASE64_CANDIDATE_RE = /(?<![A-Za-z0-9+/_-])([A-Za-z0-9+/]{12,}={0,2}|[A-Za-z0-9_-]{12,})(?![A-Za-z0-9+/_-])/g;
|
|
15
|
+
const SECRET_KEYWORDS = [
|
|
16
|
+
"apikey",
|
|
17
|
+
"token",
|
|
18
|
+
"secret",
|
|
19
|
+
"password",
|
|
20
|
+
"passwd",
|
|
21
|
+
"pwd",
|
|
22
|
+
"authorization",
|
|
23
|
+
"cookie",
|
|
24
|
+
"session",
|
|
25
|
+
"signature",
|
|
26
|
+
"sign",
|
|
27
|
+
];
|
|
28
|
+
const ID_KEYWORDS = [
|
|
29
|
+
"agentid",
|
|
30
|
+
"apiid",
|
|
31
|
+
"uid",
|
|
32
|
+
"pushid",
|
|
33
|
+
"userid",
|
|
34
|
+
"accountid",
|
|
35
|
+
"clientid",
|
|
36
|
+
"appid",
|
|
37
|
+
];
|
|
38
|
+
const URL_KEYWORDS = [
|
|
39
|
+
"url",
|
|
40
|
+
"endpoint",
|
|
41
|
+
];
|
|
42
|
+
const PLACEHOLDER_VALUES = new Set([
|
|
43
|
+
"apikey",
|
|
44
|
+
"api-key",
|
|
45
|
+
"api_key",
|
|
46
|
+
"token",
|
|
47
|
+
"secret",
|
|
48
|
+
"password",
|
|
49
|
+
"passwd",
|
|
50
|
+
"pwd",
|
|
51
|
+
"authorization",
|
|
52
|
+
"bearer",
|
|
53
|
+
"openclaw",
|
|
54
|
+
"text/event-stream",
|
|
55
|
+
]);
|
|
56
|
+
function getConfigRoot() {
|
|
57
|
+
return process.env.HOME || "/home/sandbox";
|
|
58
|
+
}
|
|
59
|
+
function normalizeKeyName(key) {
|
|
60
|
+
return String(key ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
61
|
+
}
|
|
62
|
+
function normalizeValue(value) {
|
|
63
|
+
return String(value ?? "").trim();
|
|
64
|
+
}
|
|
65
|
+
function matchesKeyword(normalizedKey, keywords) {
|
|
66
|
+
return keywords.some(keyword => normalizedKey.includes(keyword));
|
|
67
|
+
}
|
|
68
|
+
function isPlaceholderValue(value) {
|
|
69
|
+
const normalized = normalizeValue(value).toLowerCase();
|
|
70
|
+
if (!normalized) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (PLACEHOLDER_VALUES.has(normalized)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (/^(my|your|demo|test)?(apikey|token|secret|password)$/.test(normalized)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function looksLikeToken(value) {
|
|
82
|
+
const trimmed = normalizeValue(value);
|
|
83
|
+
if (trimmed.length < 8 || isPlaceholderValue(trimmed)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (/^(sk-|sk_)[a-z0-9]/i.test(trimmed)) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (/^[a-f0-9]{24,}$/i.test(trimmed)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (/^[A-Za-z0-9._/+\\=-]{16,}$/.test(trimmed) && /[A-Za-z]/.test(trimmed) && /\d/.test(trimmed)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
function looksLikeIdentifier(value) {
|
|
98
|
+
const trimmed = normalizeValue(value);
|
|
99
|
+
if (trimmed.length < 6 || isPlaceholderValue(trimmed)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (/^agent[a-z0-9]{8,}$/i.test(trimmed)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (/^webhook[a-z0-9]{6,}$/i.test(trimmed)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
if (/^\d{6,}$/.test(trimmed)) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (/^[A-Za-z][A-Za-z0-9_-]{10,}$/.test(trimmed) && /\d/.test(trimmed)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
function looksLikeSensitiveUrl(value) {
|
|
117
|
+
const trimmed = normalizeValue(value);
|
|
118
|
+
try {
|
|
119
|
+
const parsed = new URL(trimmed);
|
|
120
|
+
if (parsed.username || parsed.password) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
for (const [name, paramValue] of parsed.searchParams.entries()) {
|
|
124
|
+
const normalizedName = normalizeKeyName(name);
|
|
125
|
+
if ((matchesKeyword(normalizedName, SECRET_KEYWORDS) || matchesKeyword(normalizedName, ID_KEYWORDS))
|
|
126
|
+
&& normalizeValue(paramValue).length > 5) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
131
|
+
return segments.some(segment => looksLikeToken(segment));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function shouldKeepSensitiveValue(key, value) {
|
|
138
|
+
const trimmed = normalizeValue(value);
|
|
139
|
+
if (trimmed.length <= 5 || isPlaceholderValue(trimmed)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const normalizedKey = normalizeKeyName(key);
|
|
143
|
+
if (matchesKeyword(normalizedKey, SECRET_KEYWORDS)) {
|
|
144
|
+
return looksLikeToken(trimmed) || looksLikeSensitiveUrl(trimmed);
|
|
145
|
+
}
|
|
146
|
+
if (matchesKeyword(normalizedKey, ID_KEYWORDS)) {
|
|
147
|
+
return looksLikeIdentifier(trimmed) || looksLikeToken(trimmed);
|
|
148
|
+
}
|
|
149
|
+
if (matchesKeyword(normalizedKey, URL_KEYWORDS)) {
|
|
150
|
+
return looksLikeSensitiveUrl(trimmed);
|
|
151
|
+
}
|
|
152
|
+
return looksLikeToken(trimmed);
|
|
153
|
+
}
|
|
154
|
+
function addSensitiveValue(values, key, value) {
|
|
155
|
+
if (shouldKeepSensitiveValue(key, value)) {
|
|
156
|
+
values.push(String(value));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function extractSensitiveValues() {
|
|
160
|
+
const values = [];
|
|
161
|
+
try {
|
|
162
|
+
const openclawJsonPath = path.join(getConfigRoot(), ".openclaw", "openclaw.json");
|
|
163
|
+
if (fs.existsSync(openclawJsonPath)) {
|
|
164
|
+
const content = fs.readFileSync(openclawJsonPath, "utf-8");
|
|
165
|
+
const config = JSON.parse(content);
|
|
166
|
+
if (config.channels) {
|
|
167
|
+
for (const channelName of Object.keys(config.channels)) {
|
|
168
|
+
const channel = config.channels[channelName];
|
|
169
|
+
addSensitiveValue(values, "apiKey", channel.apiKey);
|
|
170
|
+
addSensitiveValue(values, "agentId", channel.agentId);
|
|
171
|
+
addSensitiveValue(values, "apiId", channel.apiId);
|
|
172
|
+
addSensitiveValue(values, "uid", channel.uid);
|
|
173
|
+
addSensitiveValue(values, "pushId", channel.pushId);
|
|
174
|
+
addSensitiveValue(values, "wsUrl1", channel.wsUrl1);
|
|
175
|
+
addSensitiveValue(values, "wsUrl2", channel.wsUrl2);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (config.models?.providers) {
|
|
179
|
+
for (const providerName of Object.keys(config.models.providers)) {
|
|
180
|
+
const provider = config.models.providers[providerName];
|
|
181
|
+
addSensitiveValue(values, "apiKey", provider.apiKey);
|
|
182
|
+
addSensitiveValue(values, "baseUrl", provider.baseUrl);
|
|
183
|
+
if (provider.headers) {
|
|
184
|
+
for (const headerKey of Object.keys(provider.headers)) {
|
|
185
|
+
addSensitiveValue(values, headerKey, provider.headers[headerKey]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
addSensitiveValue(values, "token", config.gateway?.auth?.token);
|
|
191
|
+
console.log(`[SENSITIVE_REDACTOR] Extracted ${values.length} sensitive values from openclaw.json`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
console.error("[SENSITIVE_REDACTOR] Failed to read openclaw.json:", err);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const xiaoyienvPath = path.join(getConfigRoot(), ".openclaw", ".xiaoyienv");
|
|
199
|
+
if (fs.existsSync(xiaoyienvPath)) {
|
|
200
|
+
const content = fs.readFileSync(xiaoyienvPath, "utf-8");
|
|
201
|
+
try {
|
|
202
|
+
const envConfig = JSON.parse(content);
|
|
203
|
+
for (const key of Object.keys(envConfig)) {
|
|
204
|
+
addSensitiveValue(values, key, envConfig[key]);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
const lines = content.split("\n");
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
const trimmed = line.trim();
|
|
211
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
212
|
+
const eqIndex = trimmed.indexOf("=");
|
|
213
|
+
if (eqIndex > 0) {
|
|
214
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
215
|
+
const value = trimmed.slice(eqIndex + 1).trim();
|
|
216
|
+
addSensitiveValue(values, key, value);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
console.log(`[SENSITIVE_REDACTOR] Total sensitive values after .xiaoyienv: ${values.length}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
console.error("[SENSITIVE_REDACTOR] Failed to read .xiaoyienv:", err);
|
|
226
|
+
}
|
|
227
|
+
const uniqueValues = [...new Set(values)].filter(value => typeof value === "string" && value.length > 5);
|
|
228
|
+
uniqueValues.sort((a, b) => b.length - a.length);
|
|
229
|
+
return uniqueValues;
|
|
230
|
+
}
|
|
231
|
+
function getSensitiveValues() {
|
|
232
|
+
if (!ENABLE_SENSITIVE_REDACTION) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
if (sensitiveValues === null || now - lastLoadTime > CACHE_TTL_MS) {
|
|
237
|
+
sensitiveValues = extractSensitiveValues();
|
|
238
|
+
lastLoadTime = now;
|
|
239
|
+
}
|
|
240
|
+
return sensitiveValues;
|
|
241
|
+
}
|
|
242
|
+
export function refreshSensitivePatterns() {
|
|
243
|
+
sensitiveValues = null;
|
|
244
|
+
lastLoadTime = 0;
|
|
245
|
+
if (ENABLE_SENSITIVE_REDACTION) {
|
|
246
|
+
getSensitiveValues();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function maskSensitiveValue(value) {
|
|
250
|
+
return REDACTED_MARKER;
|
|
251
|
+
}
|
|
252
|
+
function escapeRegExp(value) {
|
|
253
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
254
|
+
}
|
|
255
|
+
function containsSensitiveValue(text, values) {
|
|
256
|
+
const normalizedText = String(text).toLowerCase();
|
|
257
|
+
for (const sensitiveValue of values) {
|
|
258
|
+
if (sensitiveValue && normalizedText.includes(String(sensitiveValue).toLowerCase())) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
function replaceSensitiveValues(text, values) {
|
|
265
|
+
let result = text;
|
|
266
|
+
for (const sensitiveValue of values) {
|
|
267
|
+
if (sensitiveValue && containsSensitiveValue(result, [sensitiveValue])) {
|
|
268
|
+
const masked = maskSensitiveValue(sensitiveValue);
|
|
269
|
+
result = result.replace(new RegExp(escapeRegExp(sensitiveValue), "gi"), masked);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
// 从文本里提取“看起来像 Base64”的连续片段。
|
|
275
|
+
// 这里只做候选收集,是否真的需要脱敏由解码后的匹配结果决定。
|
|
276
|
+
function extractBase64Candidates(text) {
|
|
277
|
+
return [...text.matchAll(BASE64_CANDIDATE_RE)].map(match => match[1]);
|
|
278
|
+
}
|
|
279
|
+
// 尝试把候选片段按 Base64 / Base64URL 解码。
|
|
280
|
+
// 解码失败时返回 null,交给上层继续处理其他候选。
|
|
281
|
+
function tryDecodeBase64(input) {
|
|
282
|
+
try {
|
|
283
|
+
let normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
284
|
+
const mod = normalized.length % 4;
|
|
285
|
+
if (mod) {
|
|
286
|
+
normalized += "=".repeat(4 - mod);
|
|
287
|
+
}
|
|
288
|
+
const decoded = Buffer.from(normalized, "base64").toString("utf8");
|
|
289
|
+
if (!decoded) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return decoded;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export function redactSensitiveText(text) {
|
|
299
|
+
if (!ENABLE_SENSITIVE_REDACTION) {
|
|
300
|
+
return text;
|
|
301
|
+
}
|
|
302
|
+
if (!text || typeof text !== "string") {
|
|
303
|
+
return text;
|
|
304
|
+
}
|
|
305
|
+
const values = getSensitiveValues();
|
|
306
|
+
let result = replaceSensitiveValues(text, values);
|
|
307
|
+
// 先按原文直接匹配;只有没命中时才继续尝试 Base64 绕过检测。
|
|
308
|
+
if (result !== text) {
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
const candidates = extractBase64Candidates(text);
|
|
312
|
+
for (const candidate of candidates) {
|
|
313
|
+
const decoded = tryDecodeBase64(candidate);
|
|
314
|
+
if (!decoded) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (containsSensitiveValue(decoded, values)) {
|
|
318
|
+
result = result.split(candidate).join(REDACTED_MARKER);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
export function redactSensitiveObject(obj) {
|
|
324
|
+
if (!ENABLE_SENSITIVE_REDACTION) {
|
|
325
|
+
return obj;
|
|
326
|
+
}
|
|
327
|
+
if (obj === null || obj === undefined) {
|
|
328
|
+
return obj;
|
|
329
|
+
}
|
|
330
|
+
if (typeof obj === "string") {
|
|
331
|
+
return redactSensitiveText(obj);
|
|
332
|
+
}
|
|
333
|
+
if (Array.isArray(obj)) {
|
|
334
|
+
return obj.map(item => redactSensitiveObject(item));
|
|
335
|
+
}
|
|
336
|
+
if (typeof obj === "object") {
|
|
337
|
+
const result = {};
|
|
338
|
+
for (const key of Object.keys(obj)) {
|
|
339
|
+
result[key] = redactSensitiveObject(obj[key]);
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
return obj;
|
|
344
|
+
}
|
|
345
|
+
export function containsSensitiveInfo(text) {
|
|
346
|
+
if (!ENABLE_SENSITIVE_REDACTION) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
if (!text || typeof text !== "string") {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
const values = getSensitiveValues();
|
|
353
|
+
if (containsSensitiveValue(text, values)) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
const candidates = extractBase64Candidates(text);
|
|
357
|
+
for (const candidate of candidates) {
|
|
358
|
+
const decoded = tryDecodeBase64(candidate);
|
|
359
|
+
if (decoded && containsSensitiveValue(decoded, values)) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
}
|
package/dist/src/task-manager.js
CHANGED
|
@@ -17,26 +17,22 @@ const activeTaskIds = _g.__xyActiveTaskIds;
|
|
|
17
17
|
* Returns true if this was an update (session already had an active task).
|
|
18
18
|
*/
|
|
19
19
|
export function registerTaskId(sessionId, taskId, messageId) {
|
|
20
|
-
logger.log(`[TASK_MANAGER] 📝 Registering/Updating taskId for session: ${sessionId}`);
|
|
21
|
-
logger.log(`[TASK_MANAGER] - taskId: ${taskId}`);
|
|
22
20
|
const existing = activeTaskIds.get(sessionId);
|
|
23
21
|
if (existing) {
|
|
24
|
-
logger.log(`[TASK_MANAGER]
|
|
25
|
-
logger.log(`[TASK_MANAGER] - 🔄 Updating taskId`);
|
|
22
|
+
logger.log(`[TASK_MANAGER] Updating taskId: ${existing.currentTaskId} → ${taskId}`);
|
|
26
23
|
existing.currentTaskId = taskId;
|
|
27
24
|
existing.currentMessageId = messageId;
|
|
28
25
|
existing.updatedAt = Date.now();
|
|
29
26
|
return true; // isUpdate
|
|
30
27
|
}
|
|
31
28
|
else {
|
|
32
|
-
|
|
29
|
+
activeTaskIds.set(sessionId, {
|
|
33
30
|
sessionId,
|
|
34
31
|
currentTaskId: taskId,
|
|
35
32
|
currentMessageId: messageId,
|
|
36
33
|
updatedAt: Date.now(),
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
logger.log(`[TASK_MANAGER] - ✅ TaskId registered (new)`);
|
|
34
|
+
});
|
|
35
|
+
logger.log(`[TASK_MANAGER] Registered new taskId: ${taskId}`);
|
|
40
36
|
return false;
|
|
41
37
|
}
|
|
42
38
|
}
|
|
@@ -44,7 +40,7 @@ export function registerTaskId(sessionId, taskId, messageId) {
|
|
|
44
40
|
* 移除session的活跃taskId(消息处理完成时调用)。
|
|
45
41
|
*/
|
|
46
42
|
export function decrementTaskIdRef(sessionId) {
|
|
47
|
-
logger.log(`[TASK_MANAGER]
|
|
43
|
+
logger.log(`[TASK_MANAGER] Removing taskId`);
|
|
48
44
|
activeTaskIds.delete(sessionId);
|
|
49
45
|
}
|
|
50
46
|
/**
|
|
@@ -77,6 +73,6 @@ export function getAllActiveTaskBindings() {
|
|
|
77
73
|
* 强制清理(错误恢复用)
|
|
78
74
|
*/
|
|
79
75
|
export function forceCleanTaskId(sessionId) {
|
|
80
|
-
logger.log(`[TASK_MANAGER]
|
|
76
|
+
logger.log(`[TASK_MANAGER] Force clearing taskId`);
|
|
81
77
|
activeTaskIds.delete(sessionId);
|
|
82
78
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SessionContext } from "./session-manager.js";
|
|
2
|
+
/**
|
|
3
|
+
* Agent-as-skill tool - invokes a registered agent by agentId as a skill.
|
|
4
|
+
* The tool receives the agentId, query, and optional file attachments,
|
|
5
|
+
* forwards the request to the target agent via WebSocket, and returns the result.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createAgentAsSkillTool(ctx: SessionContext): any;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Agent-as-skill tool implementation - invokes another agent as a skill
|
|
2
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
3
|
+
import { sendCommand } from "../formatter.js";
|
|
4
|
+
import { getCurrentTaskId } from "../task-manager.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
import { XYFileUploadService } from "../file-upload.js";
|
|
7
|
+
/**
|
|
8
|
+
* Agent-as-skill tool - invokes a registered agent by agentId as a skill.
|
|
9
|
+
* The tool receives the agentId, query, and optional file attachments,
|
|
10
|
+
* forwards the request to the target agent via WebSocket, and returns the result.
|
|
11
|
+
*/
|
|
12
|
+
export function createAgentAsSkillTool(ctx) {
|
|
13
|
+
const { config, sessionId, taskId, messageId } = ctx;
|
|
14
|
+
return {
|
|
15
|
+
name: "agent_as_a_tool",
|
|
16
|
+
label: "Agent as Skill Tool",
|
|
17
|
+
description: `智能体作为skill的执行元工具。当需要调用其他已注册的Agent来执行特定任务时使用此工具。
|
|
18
|
+
该工具会将用户请求和可选的附件文件转发给目标Agent执行,并返回执行结果。
|
|
19
|
+
|
|
20
|
+
使用场景:
|
|
21
|
+
- 需要调用其他Agent完成特定领域的任务
|
|
22
|
+
- 需要将文件/图片交给专门的Agent处理
|
|
23
|
+
- 需要组合多个Agent的能力来完成复杂任务
|
|
24
|
+
|
|
25
|
+
注意事项:
|
|
26
|
+
- 操作超时时间为5分钟
|
|
27
|
+
- 该工具执行期间必须严格等待结果返回,不要执行其他操作
|
|
28
|
+
- 如果超时或失败,最多重试一次`,
|
|
29
|
+
parameters: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
agentId: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "待执行的AgentId,精准匹配系统注册的AgentId",
|
|
35
|
+
},
|
|
36
|
+
query: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "用户原始请求文本,原样转发给目标Agent执行",
|
|
39
|
+
},
|
|
40
|
+
filesInfo: {
|
|
41
|
+
description: "附件文件/图片信息列表,无文件时可传null或空数组,支持传入数组或JSON字符串",
|
|
42
|
+
items: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
fileType: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "文件类型:file 或 image",
|
|
48
|
+
enum: ["file", "image"],
|
|
49
|
+
},
|
|
50
|
+
fileId: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "文件全局唯一标识",
|
|
53
|
+
},
|
|
54
|
+
fileUrl: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "文件可访问下载链接(完整HTTP/HTTPS地址)",
|
|
57
|
+
},
|
|
58
|
+
fileUrlLocal: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "文件本地路径,如果提供此字段,工具会自动上传文件并将公网URL填入fileUrl",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
required: ["agentId", "query"],
|
|
67
|
+
},
|
|
68
|
+
async execute(toolCallId, params) {
|
|
69
|
+
// Dynamic lookup: use latest taskId from task-manager (handles steer/interrupt)
|
|
70
|
+
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
71
|
+
// Validate parameters
|
|
72
|
+
if (!params.agentId || typeof params.agentId !== "string") {
|
|
73
|
+
throw new Error("Missing or invalid required parameter: agentId must be a non-empty string");
|
|
74
|
+
}
|
|
75
|
+
if (!params.query || typeof params.query !== "string") {
|
|
76
|
+
throw new Error("Missing or invalid required parameter: query must be a non-empty string");
|
|
77
|
+
}
|
|
78
|
+
// Robust parsing: normalize filesInfo from array or JSON string
|
|
79
|
+
let filesInfo = null;
|
|
80
|
+
if (params.filesInfo) {
|
|
81
|
+
if (Array.isArray(params.filesInfo)) {
|
|
82
|
+
filesInfo = params.filesInfo;
|
|
83
|
+
}
|
|
84
|
+
else if (typeof params.filesInfo === 'string') {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(params.filesInfo);
|
|
87
|
+
if (Array.isArray(parsed)) {
|
|
88
|
+
filesInfo = parsed;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
throw new Error("filesInfo must be an array or a JSON string representing an array");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (parseError) {
|
|
95
|
+
throw new Error(`filesInfo JSON解析失败: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
filesInfo = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Upload local files and fill fileUrl
|
|
103
|
+
if (filesInfo && filesInfo.length > 0) {
|
|
104
|
+
const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
|
|
105
|
+
for (const fileInfo of filesInfo) {
|
|
106
|
+
if (fileInfo.fileUrlLocal && !fileInfo.fileUrl) {
|
|
107
|
+
try {
|
|
108
|
+
const publicUrl = await uploadService.uploadFileAndGetUrl(fileInfo.fileUrlLocal, "TEMPORARY_MATERIAL_DOC");
|
|
109
|
+
if (publicUrl) {
|
|
110
|
+
fileInfo.fileUrl = publicUrl;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
logger.warn("[AGENT-AS-SKILL] 上传文件未返回公网URL", { fileUrlLocal: fileInfo.fileUrlLocal });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (uploadError) {
|
|
117
|
+
logger.error("[AGENT-AS-SKILL] 上传本地文件失败", { fileUrlLocal: fileInfo.fileUrlLocal, error: uploadError });
|
|
118
|
+
throw new Error(`上传本地文件失败 (${fileInfo.fileUrlLocal}): ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Remove fileUrlLocal from the final payload
|
|
122
|
+
delete fileInfo.fileUrlLocal;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Get WebSocket manager
|
|
126
|
+
const wsManager = getXYWebSocketManager(config);
|
|
127
|
+
// Build ExecuteAgentAsSkill command
|
|
128
|
+
const command = {
|
|
129
|
+
header: {
|
|
130
|
+
namespace: "System",
|
|
131
|
+
name: "ExecuteAgentAsSkill",
|
|
132
|
+
},
|
|
133
|
+
payload: {
|
|
134
|
+
agentId: params.agentId,
|
|
135
|
+
query: params.query,
|
|
136
|
+
filesInfo: filesInfo || null,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
// Send command and wait for response (5 minute timeout)
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const timeout = setTimeout(() => {
|
|
142
|
+
wsManager.off("agent-as-skill-response", handler);
|
|
143
|
+
logger.error("超时: Agent-as-Skill 操作超时(5分钟)", { agentId: params.agentId, toolCallId });
|
|
144
|
+
reject(new Error("Agent-as-Skill 操作超时(5分钟)"));
|
|
145
|
+
}, 300000); // 5 minutes timeout
|
|
146
|
+
// Listen for Agent-as-Skill response events
|
|
147
|
+
const handler = (event) => {
|
|
148
|
+
// Check if this is the ExecuteAgentAsSkillResponse we're waiting for
|
|
149
|
+
if (event.header?.namespace === "System" &&
|
|
150
|
+
event.header?.name === "ExecuteAgentAsSkillResponse") {
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
wsManager.off("agent-as-skill-response", handler);
|
|
153
|
+
// Return the payload directly as the tool result
|
|
154
|
+
const payload = event.payload;
|
|
155
|
+
if (payload) {
|
|
156
|
+
resolve({
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: typeof payload === "string" ? payload : JSON.stringify(payload),
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
reject(new Error("Agent-as-Skill 响应格式错误:缺少 payload"));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
// Register event handler
|
|
171
|
+
wsManager.on("agent-as-skill-response", handler);
|
|
172
|
+
// Send the command
|
|
173
|
+
sendCommand({
|
|
174
|
+
config,
|
|
175
|
+
sessionId,
|
|
176
|
+
taskId: currentTaskId,
|
|
177
|
+
messageId,
|
|
178
|
+
command,
|
|
179
|
+
toolCallId,
|
|
180
|
+
}).then(() => {
|
|
181
|
+
logger.log("[AGENT-AS-SKILL] Command sent successfully", { agentId: params.agentId });
|
|
182
|
+
}).catch((error) => {
|
|
183
|
+
clearTimeout(timeout);
|
|
184
|
+
wsManager.off("agent-as-skill-response", handler);
|
|
185
|
+
reject(error);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -87,7 +87,7 @@ export function createCalendarTool(ctx) {
|
|
|
87
87
|
return new Promise((resolve, reject) => {
|
|
88
88
|
const timeout = setTimeout(() => {
|
|
89
89
|
wsManager.off("data-event", handler);
|
|
90
|
-
logger.error("超时: 创建日程超时(60秒)", {
|
|
90
|
+
logger.error("超时: 创建日程超时(60秒)", { toolCallId });
|
|
91
91
|
reject(new Error("创建日程超时(60秒)"));
|
|
92
92
|
}, 60000);
|
|
93
93
|
// Listen for data events from WebSocket
|
|
@@ -106,7 +106,7 @@ export function createCalendarTool(ctx) {
|
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
109
|
-
reject(new Error(`创建日程失败: ${event.
|
|
109
|
+
reject(new Error(`创建日程失败: ${JSON.stringify(event.outputs)}`));
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
};
|
|
@@ -120,6 +120,7 @@ export function createCalendarTool(ctx) {
|
|
|
120
120
|
taskId: currentTaskId,
|
|
121
121
|
messageId,
|
|
122
122
|
command,
|
|
123
|
+
toolCallId,
|
|
123
124
|
})
|
|
124
125
|
.then(() => {
|
|
125
126
|
})
|
|
@@ -79,7 +79,7 @@ export function createCallPhoneTool(ctx) {
|
|
|
79
79
|
return new Promise((resolve, reject) => {
|
|
80
80
|
const timeout = setTimeout(() => {
|
|
81
81
|
wsManager.off("data-event", handler);
|
|
82
|
-
logger.error("超时: 拨打电话超时(60秒)", {
|
|
82
|
+
logger.error("超时: 拨打电话超时(60秒)", { toolCallId });
|
|
83
83
|
reject(new Error("拨打电话超时(60秒)"));
|
|
84
84
|
}, 60000);
|
|
85
85
|
// Listen for data events from WebSocket
|
|
@@ -98,7 +98,7 @@ export function createCallPhoneTool(ctx) {
|
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
else {
|
|
101
|
-
reject(new Error(`拨打电话失败: ${event.
|
|
101
|
+
reject(new Error(`拨打电话失败: ${JSON.stringify(event.outputs)}`));
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
};
|
|
@@ -112,6 +112,7 @@ export function createCallPhoneTool(ctx) {
|
|
|
112
112
|
taskId: currentTaskId,
|
|
113
113
|
messageId,
|
|
114
114
|
command,
|
|
115
|
+
toolCallId,
|
|
115
116
|
})
|
|
116
117
|
.then(() => {
|
|
117
118
|
})
|