ai-helper-agent 2.3.1 → 2.4.0
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/README.md +13 -3
- package/dist/assets/__vite-browser-external-CM3Eme-n.js +1 -0
- package/dist/assets/agent-store-Ce3f4Ya3.js +1 -0
- package/dist/assets/background.js +104 -80
- package/dist/assets/chat-export-BNCkL6fS.js +37 -0
- package/dist/assets/chunk-CMxvf4Kt.js +1 -0
- package/dist/assets/content.js +159 -156
- package/dist/assets/{dist-BQOFgIxk.js → dist-DhUTa_WT.js} +5 -6
- package/dist/assets/dist-LQPh1k0y.js +24 -0
- package/dist/assets/i18n-ouMgDZ2t.js +49 -0
- package/dist/assets/jszip.min-Bm_16osY.js +2 -0
- package/dist/assets/lib-33nIefF6.js +220 -0
- package/dist/assets/{marked.min.js-08SGy6CO.js → marked.min.js-D2rXgLAQ.js} +7 -8
- package/dist/assets/{marked.min.js-loader-4kRZ9iCP.js → marked.min.js-loader-tKH5SQVQ.js} +1 -1
- package/dist/assets/model-profiles-6dzdrSS4.js +24 -0
- package/dist/assets/network-scope-CNRYXk74.js +1 -0
- package/dist/assets/options-Cp1UMkjK.css +1 -0
- package/dist/assets/options.js +108 -102
- package/dist/assets/{page-snapshot-renderer-CVL6r6Kf.js → page-snapshot-renderer-j72q92KH.js} +2 -3
- package/dist/assets/pdf-DnVqBaAN.js +6 -0
- package/dist/assets/session-store-CmGs_rhh.js +76 -0
- package/dist/assets/side_panel.css +1 -1
- package/dist/assets/side_panel.js +958 -1124
- package/dist/assets/theme-BRAX5X2F.css +1 -0
- package/dist/assets/{modulepreload-polyfill-P2Xu9kJm.js → theme-CLwYsx9o.js} +1 -1
- package/dist/assets/utils-9i9g8vW8.js +112 -0
- package/dist/assets/version-t8HzLv3V.js +1 -0
- package/dist/assets/xlsx-DGkz7cnk.js +105 -0
- package/dist/manifest.json +2 -5
- package/dist/offscreen/offscreen.js +7 -7
- package/dist/options.html +151 -75
- package/dist/side_panel.html +167 -137
- package/package.json +1 -1
- package/src/config.js +3 -2
- package/src/executor.js +99 -18
- package/src/locales/en.json +16 -2
- package/src/locales/zh.json +16 -2
- package/src/memory/injection.js +12 -2
- package/src/memory/routes.js +6 -0
- package/src/memory/service.js +57 -2
- package/src/rag/manager.js +1 -1
- package/src/rag/url-guard.js +27 -12
- package/src/security.js +433 -31
- package/src/server.js +28 -8
- package/dist/assets/agent-store-DcAs6YyA.js +0 -2
- package/dist/assets/agent-store-DcAs6YyA.js.map +0 -1
- package/dist/assets/background.js.map +0 -1
- package/dist/assets/dist-BQOFgIxk.js.map +0 -1
- package/dist/assets/i18n-C4jSRoCH.js +0 -50
- package/dist/assets/i18n-C4jSRoCH.js.map +0 -1
- package/dist/assets/jszip.min-BXqW2-9v.js +0 -3
- package/dist/assets/jszip.min-BXqW2-9v.js.map +0 -1
- package/dist/assets/lib-Bi7BFuND.js +0 -221
- package/dist/assets/lib-Bi7BFuND.js.map +0 -1
- package/dist/assets/marked.min.js-08SGy6CO.js.map +0 -1
- package/dist/assets/model-profiles-Dl_MsXWa.js +0 -25
- package/dist/assets/model-profiles-Dl_MsXWa.js.map +0 -1
- package/dist/assets/options-8-nu3P5K.css +0 -1
- package/dist/assets/options.js.map +0 -1
- package/dist/assets/page-snapshot-renderer-CVL6r6Kf.js.map +0 -1
- package/dist/assets/pdf-HiJ0icvU.js +0 -7
- package/dist/assets/pdf-HiJ0icvU.js.map +0 -1
- package/dist/assets/rolldown-runtime-C0FnF6B9.js +0 -1
- package/dist/assets/session-store-CzVQCgQ_.js +0 -77
- package/dist/assets/session-store-CzVQCgQ_.js.map +0 -1
- package/dist/assets/side_panel.js.map +0 -1
- package/dist/assets/version-CNG2KGnJ.js +0 -2
- package/dist/assets/version-CNG2KGnJ.js.map +0 -1
- package/dist/assets/xlsx-BHKh7vjA.js +0 -106
- package/dist/assets/xlsx-BHKh7vjA.js.map +0 -1
- package/dist/icons/badges/license-MIT.svg +0 -21
- package/dist/icons/badges/platform.svg +0 -21
- package/dist/icons/badges/prs-welcome.svg +0 -21
- package/dist/icons/badges/version.svg +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-helper-agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "AI Helper Agent - Local file access and system command execution proxy for Chrome extension / AI Helper 代理服务,为 Chrome 扩展提供本地文件读写和系统命令执行",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server.js",
|
package/src/config.js
CHANGED
|
@@ -20,8 +20,9 @@ const DEFAULTS = {
|
|
|
20
20
|
pairCodeTTL: 30,
|
|
21
21
|
commandTimeout: 300000,
|
|
22
22
|
fileMaxSize: 50 * 1024 * 1024,
|
|
23
|
-
// RAG URL
|
|
24
|
-
//
|
|
23
|
+
// RAG URL 导入内网访问策略(B2 AND 闸,与扩展侧 network-scope.js 同序):
|
|
24
|
+
// allowPrivateNetwork 开启 **且** allowedPrivateHosts 命中才放行内网;关闸时白名单不生效,
|
|
25
|
+
// 开闸但白名单为空仍拒绝;缺省均关闭(默认拒绝内网),公网访问不受影响
|
|
25
26
|
ragUrlIngest: { allowPrivateNetwork: false, allowedPrivateHosts: [] },
|
|
26
27
|
// 长期记忆语义召回(hybrid recall)的 embedding 配置:缺省关闭 → recall 只走关键词通道(Tier A),
|
|
27
28
|
// 不引入任何新的默认网络调用。mode: 'openai-compat'(远端,零依赖)| 'local'(本地模型,需 RAG 依赖)。
|
package/src/executor.js
CHANGED
|
@@ -162,6 +162,29 @@ function generateExecId() {
|
|
|
162
162
|
return crypto.randomBytes(8).toString('hex');
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// ===== 超时策略(决策权威在 Agent 端:不受插件端 SW 生命周期影响) =====
|
|
166
|
+
export const MAX_TIMEOUT_EXTENSIONS = 5;
|
|
167
|
+
export const TOTAL_BUDGET_CAP_MS = 600000; // 与插件端 toolTimeout 上限对齐
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 命令总超时到期时的裁决纯函数(无副作用,便于单测)
|
|
171
|
+
* @param {{ outputInPeriod: boolean, extensions: number, elapsedMs: number, baseTimeout: number }} state
|
|
172
|
+
* outputInPeriod: 本预算期内是否收到过 stdout/stderr 输出(输出流 = 心跳)
|
|
173
|
+
* @param {number} [capMs] 总预算封顶
|
|
174
|
+
* @param {number} [maxExtensions] 最大延长次数
|
|
175
|
+
* @returns {{ action: 'extend'|'kill', extensions: number, nextTimeoutMs?: number }}
|
|
176
|
+
*/
|
|
177
|
+
export function decideTimeoutAction(state, capMs = TOTAL_BUDGET_CAP_MS, maxExtensions = MAX_TIMEOUT_EXTENSIONS) {
|
|
178
|
+
const { outputInPeriod, extensions, elapsedMs, baseTimeout } = state;
|
|
179
|
+
if (extensions >= maxExtensions) return { action: 'kill', extensions };
|
|
180
|
+
// 本预算期完全无输出 → 视为挂起/静默失败,到点即杀,不再放大等待窗口
|
|
181
|
+
if (!outputInPeriod) return { action: 'kill', extensions };
|
|
182
|
+
const currentBudget = Math.min(baseTimeout * (1 + extensions), capMs);
|
|
183
|
+
const nextBudget = Math.min(baseTimeout * (2 + extensions), capMs);
|
|
184
|
+
if (nextBudget <= currentBudget) return { action: 'kill', extensions }; // 已触及总封顶
|
|
185
|
+
return { action: 'extend', extensions: extensions + 1, nextTimeoutMs: nextBudget - elapsedMs };
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
/**
|
|
166
189
|
* 执行命令并通过 WebSocket 流式输出
|
|
167
190
|
* @param {string} command - 完整命令字符串
|
|
@@ -169,13 +192,18 @@ function generateExecId() {
|
|
|
169
192
|
* @param {Object} wsClient - 单个 WebSocket 客户端(用于流式输出)
|
|
170
193
|
* @param {Function} onComplete - 完成回调 ({ execId, exitCode, killed, stdout?, stderr? })
|
|
171
194
|
* @param {boolean} [collectOutput=false] - 是否收集完整 stdout/stderr 到内存
|
|
195
|
+
* @param {{ timeoutMs?: number }} [execOpts] - 受管模式协商:调用方显式传 timeoutMs 即接管超时权威
|
|
172
196
|
* @returns {string} execId
|
|
173
197
|
*/
|
|
174
|
-
function executeCommand(command, cwd, wsClient, onComplete, collectOutput = false) {
|
|
198
|
+
function executeCommand(command, cwd, wsClient, onComplete, collectOutput = false, execOpts = {}) {
|
|
175
199
|
const config = loadConfig();
|
|
176
200
|
const workdir = cwd || config.workdir;
|
|
177
201
|
const execId = generateExecId();
|
|
178
|
-
|
|
202
|
+
// 协议协商:显式 timeoutMs → 受管模式(定时器与 WS 连接数解耦,延长由策略函数裁决);
|
|
203
|
+
// 未传 → 旧语义(commandTimeout + 断连清定时器),保证新 agent × 旧插件行为不变
|
|
204
|
+
const requestedTimeout = Number(execOpts.timeoutMs) > 0 ? Math.round(Number(execOpts.timeoutMs)) : null;
|
|
205
|
+
const timeout = requestedTimeout || config.commandTimeout || 300000;
|
|
206
|
+
const managed = requestedTimeout != null;
|
|
179
207
|
|
|
180
208
|
const { shell, args } = getShellForExec();
|
|
181
209
|
// detached 的取舍(按平台 + shell 类型):
|
|
@@ -209,14 +237,42 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
|
|
|
209
237
|
// 存储到 entry 中供 clearTimers 读取
|
|
210
238
|
// stdoutBuf/stderrBuf 始终收集,用于回放给延迟连接的 WS 客户端
|
|
211
239
|
// command/startTime 用于状态详情面板展示运行中进程
|
|
212
|
-
const entry = {
|
|
240
|
+
const entry = {
|
|
241
|
+
process: proc, wsClients, stdoutBuf: '', stderrBuf: '',
|
|
242
|
+
truncStdout: false, truncStderr: false, command, startTime: Date.now(),
|
|
243
|
+
baseTimeout: timeout, managed, extensions: 0, outputInPeriod: false,
|
|
244
|
+
detached: false, timedOut: false,
|
|
245
|
+
};
|
|
213
246
|
runningProcesses.set(execId, entry);
|
|
214
247
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
248
|
+
// 超时控制:受管模式到期先由策略函数裁决(期内有输出才延长,静默即杀);旧模式到期直接杀
|
|
249
|
+
function armTimeout(ms) {
|
|
250
|
+
entry.timeoutId = setTimeout(() => {
|
|
251
|
+
entry.timeoutId = null;
|
|
252
|
+
if (!entry.managed) {
|
|
253
|
+
killed = true;
|
|
254
|
+
entry.timedOut = true;
|
|
255
|
+
killProcess(execId);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const d = decideTimeoutAction({
|
|
259
|
+
outputInPeriod: entry.outputInPeriod,
|
|
260
|
+
extensions: entry.extensions,
|
|
261
|
+
elapsedMs: Date.now() - entry.startTime,
|
|
262
|
+
baseTimeout: entry.baseTimeout,
|
|
263
|
+
});
|
|
264
|
+
if (d.action === 'extend') {
|
|
265
|
+
entry.extensions = d.extensions;
|
|
266
|
+
entry.outputInPeriod = false;
|
|
267
|
+
armTimeout(d.nextTimeoutMs);
|
|
268
|
+
} else {
|
|
269
|
+
killed = true;
|
|
270
|
+
entry.timedOut = true;
|
|
271
|
+
killProcess(execId);
|
|
272
|
+
}
|
|
273
|
+
}, ms);
|
|
274
|
+
}
|
|
275
|
+
armTimeout(timeout);
|
|
220
276
|
|
|
221
277
|
// 广播消息给所有连接的 WS 客户端
|
|
222
278
|
function broadcast(data) {
|
|
@@ -241,6 +297,7 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
|
|
|
241
297
|
// stdout 流式输出
|
|
242
298
|
proc.stdout.on('data', (chunk) => {
|
|
243
299
|
const str = chunk.toString();
|
|
300
|
+
entry.outputInPeriod = true;
|
|
244
301
|
if (collectOutput) stdoutCollected += str;
|
|
245
302
|
const truncState = { truncated: entry.truncStdout };
|
|
246
303
|
entry.stdoutBuf = appendWithLimit(entry.stdoutBuf, str, truncState);
|
|
@@ -254,6 +311,7 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
|
|
|
254
311
|
// stderr 流式输出
|
|
255
312
|
proc.stderr.on('data', (chunk) => {
|
|
256
313
|
const str = chunk.toString();
|
|
314
|
+
entry.outputInPeriod = true;
|
|
257
315
|
if (collectOutput) stderrCollected += str;
|
|
258
316
|
const truncState = { truncated: entry.truncStderr };
|
|
259
317
|
entry.stderrBuf = appendWithLimit(entry.stderrBuf, str, truncState);
|
|
@@ -270,7 +328,7 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
|
|
|
270
328
|
finished = true;
|
|
271
329
|
clearTimers();
|
|
272
330
|
const finalExitCode = typeof exitCode === 'number' ? exitCode : -1;
|
|
273
|
-
broadcast({ type: 'exit', exitCode: finalExitCode, execId, killed });
|
|
331
|
+
broadcast({ type: 'exit', exitCode: finalExitCode, execId, killed, timedOut: entry.timedOut });
|
|
274
332
|
// 移入已完成缓存,解决竞态条件:延迟连接的 WS 客户端可回放输出
|
|
275
333
|
const cleanupTimeout = setTimeout(() => {
|
|
276
334
|
completedProcesses.delete(execId);
|
|
@@ -280,6 +338,7 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
|
|
|
280
338
|
stderrBuf: entry.stderrBuf,
|
|
281
339
|
exitCode: finalExitCode,
|
|
282
340
|
killed,
|
|
341
|
+
timedOut: entry.timedOut,
|
|
283
342
|
cleanupTimeout
|
|
284
343
|
});
|
|
285
344
|
runningProcesses.delete(execId);
|
|
@@ -330,10 +389,11 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
|
|
|
330
389
|
* 同步执行命令(阻塞等待完成,返回完整输出)
|
|
331
390
|
* @returns {Promise<{execId, exitCode, stdout, stderr, killed, error?}>}
|
|
332
391
|
*/
|
|
333
|
-
function executeCommandSync(command, cwd) {
|
|
392
|
+
function executeCommandSync(command, cwd, timeoutMs) {
|
|
334
393
|
return new Promise((resolve) => {
|
|
335
394
|
const config = loadConfig();
|
|
336
|
-
const
|
|
395
|
+
const requestedTimeout = Number(timeoutMs) > 0 ? Math.round(Number(timeoutMs)) : null;
|
|
396
|
+
const timeout = requestedTimeout || config.commandTimeout || 300000;
|
|
337
397
|
|
|
338
398
|
let resolved = false;
|
|
339
399
|
let syncTimeoutId = null;
|
|
@@ -344,14 +404,19 @@ function executeCommandSync(command, cwd) {
|
|
|
344
404
|
resolved = true;
|
|
345
405
|
resolve(result);
|
|
346
406
|
}
|
|
347
|
-
}, true);
|
|
407
|
+
}, true, { timeoutMs });
|
|
348
408
|
|
|
349
|
-
//
|
|
409
|
+
// 兜底(超时后额外 5s 用于 kill 完成):内层策略失效时到点杀进程并回收已收集输出,不再返回空串
|
|
350
410
|
syncTimeoutId = setTimeout(() => {
|
|
351
411
|
if (!resolved) {
|
|
352
412
|
resolved = true;
|
|
353
413
|
killProcess(execId);
|
|
354
|
-
|
|
414
|
+
const e = runningProcesses.get(execId);
|
|
415
|
+
resolve({
|
|
416
|
+
execId, exitCode: -1, killed: true,
|
|
417
|
+
stdout: e ? e.stdoutBuf : '', stderr: e ? e.stderrBuf : '',
|
|
418
|
+
error: translate(currentLang, 'error.commandTimeout'),
|
|
419
|
+
});
|
|
355
420
|
}
|
|
356
421
|
}, timeout + 5000);
|
|
357
422
|
});
|
|
@@ -379,7 +444,7 @@ function addWsClient(execId, wsClient) {
|
|
|
379
444
|
if (completed.error) {
|
|
380
445
|
wsClient.send(JSON.stringify({ type: 'error', error: completed.error, execId }));
|
|
381
446
|
}
|
|
382
|
-
wsClient.send(JSON.stringify({ type: 'exit', exitCode: completed.exitCode, execId, killed: completed.killed }));
|
|
447
|
+
wsClient.send(JSON.stringify({ type: 'exit', exitCode: completed.exitCode, execId, killed: completed.killed, timedOut: completed.timedOut === true }));
|
|
383
448
|
clearTimeout(completed.cleanupTimeout);
|
|
384
449
|
completedProcesses.delete(execId);
|
|
385
450
|
return true;
|
|
@@ -397,8 +462,10 @@ function disconnectWsClient(execId, wsClient) {
|
|
|
397
462
|
|
|
398
463
|
entry.wsClients.delete(wsClient);
|
|
399
464
|
|
|
400
|
-
//
|
|
401
|
-
|
|
465
|
+
// 旧语义:所有客户端断开 → 取消超时,进程自由运行(挂起型服务依赖此行为)。
|
|
466
|
+
// 受管模式:断开可能只是插件端 SW 被 Chrome 杀掉/网络闪断,绝不可解除超时权威,
|
|
467
|
+
// 仅显式 detach(entry.detached)才豁免——连接状态不再反推用户意图。
|
|
468
|
+
if (entry.wsClients.size === 0 && (!entry.managed || entry.detached)) {
|
|
402
469
|
if (entry.timeoutId) {
|
|
403
470
|
clearTimeout(entry.timeoutId);
|
|
404
471
|
entry.timeoutId = null;
|
|
@@ -411,6 +478,20 @@ function disconnectWsClient(execId, wsClient) {
|
|
|
411
478
|
return true;
|
|
412
479
|
}
|
|
413
480
|
|
|
481
|
+
/**
|
|
482
|
+
* 显式声明 daemon 化:取消超时定时器,进程继续运行(插件端空闲挂起分支调用)
|
|
483
|
+
*/
|
|
484
|
+
function detachProcess(execId) {
|
|
485
|
+
const entry = runningProcesses.get(execId);
|
|
486
|
+
if (!entry) return false;
|
|
487
|
+
entry.detached = true;
|
|
488
|
+
if (entry.timeoutId) {
|
|
489
|
+
clearTimeout(entry.timeoutId);
|
|
490
|
+
entry.timeoutId = null;
|
|
491
|
+
}
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
414
495
|
/**
|
|
415
496
|
* 停止正在运行的命令(跨平台兼容,递归终止全部子进程)
|
|
416
497
|
* - macOS/Linux: SIGTERM to process group(-pid)→ 5s → SIGKILL to process group
|
|
@@ -486,4 +567,4 @@ function getRunningProcesses() {
|
|
|
486
567
|
return list;
|
|
487
568
|
}
|
|
488
569
|
|
|
489
|
-
export { executeCommand, executeCommandSync, addWsClient, disconnectWsClient, killProcess, getRunningProcesses, pickShell };
|
|
570
|
+
export { executeCommand, executeCommandSync, addWsClient, disconnectWsClient, killProcess, detachProcess, getRunningProcesses, pickShell };
|
package/src/locales/en.json
CHANGED
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"err_pathNotAllowed": "File path not allowed: {reason}",
|
|
108
108
|
"err_invalidUrl": "Invalid URL (http/https required)",
|
|
109
109
|
"err_urlFetchFailed": "URL fetch failed: HTTP {status}",
|
|
110
|
-
"err_urlPrivateBlocked": "Access to private network address blocked: {host} (
|
|
110
|
+
"err_urlPrivateBlocked": "Access to private network address blocked: {host} (requires the private-network switch enabled AND the host in the allowlist; either alone is not sufficient)",
|
|
111
111
|
"err_urlTooLarge": "URL response exceeds the size limit ({limit})",
|
|
112
112
|
"err_urlTooManyRedirects": "Too many URL redirects (limit: {max})",
|
|
113
113
|
"err_contentTooLarge": "Import content exceeds the size limit ({limit})",
|
|
@@ -168,6 +168,7 @@
|
|
|
168
168
|
"commandInvalid": "Invalid command",
|
|
169
169
|
"commandBlocked": "High-risk command blocked: \"{command}\"",
|
|
170
170
|
"cmdSudo": "Command requires administrator privileges (sudo)",
|
|
171
|
+
"cmdDoas": "Command requires privilege escalation (doas/runas)",
|
|
171
172
|
"cmdNpmGlobal": "Global npm package installation",
|
|
172
173
|
"cmdPip": "pip install/uninstall package",
|
|
173
174
|
"cmdChmod777": "Recursively change permissions to 777",
|
|
@@ -186,7 +187,18 @@
|
|
|
186
187
|
"cmdReg": "Modify registry (reg)",
|
|
187
188
|
"cmdNet": "User/group management (net)",
|
|
188
189
|
"cmdSc": "Service management (sc)",
|
|
189
|
-
"cmdTaskkillF": "Force kill process (taskkill /f)"
|
|
190
|
+
"cmdTaskkillF": "Force kill process (taskkill /f)",
|
|
191
|
+
"cmdPsRemoveItem": "PowerShell recursive/forced delete (Remove-Item)",
|
|
192
|
+
"cmdPsEncoded": "Execute base64-encoded PowerShell script",
|
|
193
|
+
"cmdCmdSubstitution": "Command contains command substitution ($( ) or backticks)",
|
|
194
|
+
"cmdDdWrite": "Write raw file content via dd",
|
|
195
|
+
"cmdShred": "Destroy file irrecoverably (shred)",
|
|
196
|
+
"cmdTruncate": "Truncate file content to zero (truncate)",
|
|
197
|
+
"cmdChmodDestroy": "Change permissions making file unusable (chmod 000/777/a-rwx)",
|
|
198
|
+
"cmdChown": "Change file owner (chown)",
|
|
199
|
+
"cmdNetwork": "Initiate outbound network connection",
|
|
200
|
+
"cmdKillProcess": "Terminate processes (kill/pkill/killall)",
|
|
201
|
+
"cmdSystemService": "Modify system services / cron / kernel parameters"
|
|
190
202
|
},
|
|
191
203
|
"trash": {
|
|
192
204
|
"moveFailed": "Move to trash failed: {message}",
|
|
@@ -305,6 +317,8 @@
|
|
|
305
317
|
"readFailed": "Failed to read memory file: {error}",
|
|
306
318
|
"writeFailed": "Failed to write memory file: {error}",
|
|
307
319
|
"parseFailed": "Memory file parse failed: {error}",
|
|
320
|
+
"injectionPatternBlocked": "Memory content appears to contain prompt-injection instructions (e.g. ignore previous instructions / override system prompt); write rejected",
|
|
321
|
+
"rateLimited": "Memory writes in this session are too frequent; anti-poisoning rate limit reached, please retry later",
|
|
308
322
|
"updatedDuplicate": "Updated existing memory: {id} (content identical, merged)",
|
|
309
323
|
"added": "Added memory: {id} ({type}){warning}",
|
|
310
324
|
"nearLimitWarning": "\n⚠️ Memory count approaching limit (facts: {facts}/{maxFacts}, summaries: {summaries}/{maxSummaries}). Consider calling agent_memory action=manage to review and clean up.",
|
package/src/locales/zh.json
CHANGED
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"err_pathNotAllowed": "文件路径不允许访问:{reason}",
|
|
108
108
|
"err_invalidUrl": "无效的 URL(须为 http/https)",
|
|
109
109
|
"err_urlFetchFailed": "URL 抓取失败: HTTP {status}",
|
|
110
|
-
"err_urlPrivateBlocked": "访问内网地址被拒绝:{host}
|
|
110
|
+
"err_urlPrivateBlocked": "访问内网地址被拒绝:{host}(需开启允许内网开关并将目标主机加入白名单,两者缺一不可)",
|
|
111
111
|
"err_urlTooLarge": "URL 响应体超过大小限制({limit})",
|
|
112
112
|
"err_urlTooManyRedirects": "URL 重定向次数超过上限({max})",
|
|
113
113
|
"err_contentTooLarge": "导入内容超过大小限制({limit})",
|
|
@@ -168,6 +168,7 @@
|
|
|
168
168
|
"commandInvalid": "命令无效",
|
|
169
169
|
"commandBlocked": "高危命令被拦截: \"{command}\"",
|
|
170
170
|
"cmdSudo": "命令需要管理员权限 (sudo)",
|
|
171
|
+
"cmdDoas": "命令需要提权 (doas/runas)",
|
|
171
172
|
"cmdNpmGlobal": "全局安装 npm 包",
|
|
172
173
|
"cmdPip": "pip 安装/卸载包",
|
|
173
174
|
"cmdChmod777": "递归修改权限为 777",
|
|
@@ -186,7 +187,18 @@
|
|
|
186
187
|
"cmdReg": "修改注册表 (reg)",
|
|
187
188
|
"cmdNet": "用户/用户组管理 (net)",
|
|
188
189
|
"cmdSc": "服务管理 (sc)",
|
|
189
|
-
"cmdTaskkillF": "强制结束进程 (taskkill /f)"
|
|
190
|
+
"cmdTaskkillF": "强制结束进程 (taskkill /f)",
|
|
191
|
+
"cmdPsRemoveItem": "PowerShell 递归/强制删除 (Remove-Item)",
|
|
192
|
+
"cmdPsEncoded": "执行 base64 编码的 PowerShell 脚本",
|
|
193
|
+
"cmdCmdSubstitution": "命令中包含命令替换($( … ) 或反引号)",
|
|
194
|
+
"cmdDdWrite": "使用 dd 裸写文件内容",
|
|
195
|
+
"cmdShred": "不可恢复地销毁文件 (shred)",
|
|
196
|
+
"cmdTruncate": "清空文件内容 (truncate)",
|
|
197
|
+
"cmdChmodDestroy": "修改权限致文件不可用 (chmod 000/777/a-rwx)",
|
|
198
|
+
"cmdChown": "修改文件属主 (chown)",
|
|
199
|
+
"cmdNetwork": "向外发起网络连接",
|
|
200
|
+
"cmdKillProcess": "结束进程 (kill/pkill/killall)",
|
|
201
|
+
"cmdSystemService": "修改系统服务/计划任务/内核参数"
|
|
190
202
|
},
|
|
191
203
|
"trash": {
|
|
192
204
|
"moveFailed": "移至回收站失败: {message}",
|
|
@@ -305,6 +317,8 @@
|
|
|
305
317
|
"readFailed": "读取记忆文件失败: {error}",
|
|
306
318
|
"writeFailed": "写入记忆文件失败: {error}",
|
|
307
319
|
"parseFailed": "记忆文件解析失败: {error}",
|
|
320
|
+
"injectionPatternBlocked": "记忆内容疑似包含提示注入指令(如忽略既有指令 / 覆盖系统提示),已拒绝写入",
|
|
321
|
+
"rateLimited": "本会话记忆写入过于频繁,已触发防批量投毒上限,请稍后再试",
|
|
308
322
|
"updatedDuplicate": "已更新已有记忆: {id}(内容相同,已合并)",
|
|
309
323
|
"added": "已添加记忆: {id} ({type}){warning}",
|
|
310
324
|
"nearLimitWarning": "\n⚠️ 记忆数量接近上限(事实: {facts}/{maxFacts}, 摘要: {summaries}/{maxSummaries}),建议调用 agent_memory action=manage 进行审查整理。",
|
package/src/memory/injection.js
CHANGED
|
@@ -11,6 +11,7 @@ export const MEMORY_INJECTION_CONFIG = {
|
|
|
11
11
|
itemOverheadChars: 50, // 每条渲染开销近似(序号、[重要性: N]、(标签: ...))
|
|
12
12
|
maxTopicTags: 8, // 索引提示最多展示的主题标签数
|
|
13
13
|
maxTagLength: 12, // 单个主题标签最大长度(超出截断)
|
|
14
|
+
tier1MaxChars: 500, // Tier1 常驻单条长度上限(P1-3:防大载荷永久驻留 system prompt)
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -85,9 +86,18 @@ export function selectMemoriesForInjection(facts, summaries, config = MEMORY_INJ
|
|
|
85
86
|
const summaryList = Array.isArray(summaries) ? summaries : [];
|
|
86
87
|
const threshold = config.alwaysInjectImportance;
|
|
87
88
|
|
|
88
|
-
// Tier1
|
|
89
|
+
// Tier1:常驻(不占预算),内部按价值降序。
|
|
90
|
+
// P1-3 来源分级:仅 origin==='user'(用户在面板手动新建/编辑)可进永久常驻;
|
|
91
|
+
// origin 缺失(旧数据)或 'model'(模型经工具/自动压缩写入)一律降入 Tier2/Tier3——
|
|
92
|
+
// 即便模型把投毒记忆的 importance 顶到 ≥8 也无法常驻,只能按需/预算召回。
|
|
93
|
+
// 单条长度 > tier1MaxChars 也不进 Tier1(防大载荷永久驻留);未配置该字段时不限制(自定义 config 兼容)。
|
|
94
|
+
const tier1MaxChars = config.tier1MaxChars ?? Infinity;
|
|
95
|
+
const isTier1Eligible = (m) =>
|
|
96
|
+
m.origin === 'user'
|
|
97
|
+
&& (m.importance || 5) >= threshold
|
|
98
|
+
&& (typeof m.content === 'string' ? m.content.length <= tier1MaxChars : false);
|
|
89
99
|
const tier1 = factList
|
|
90
|
-
.filter(
|
|
100
|
+
.filter(isTier1Eligible)
|
|
91
101
|
.sort((a, b) => calcMemoryValue(b, now) - calcMemoryValue(a, now));
|
|
92
102
|
const tier1Set = new Set(tier1);
|
|
93
103
|
|
package/src/memory/routes.js
CHANGED
|
@@ -160,6 +160,9 @@ export async function memoryDispatch(req, res, pathname, url, t, requestBody = {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const body = requestBody || {};
|
|
163
|
+
// P1-3:记忆来源以 HTTP header 为权威——模型只能控制 body(经 tool-memory.js 原样透传),
|
|
164
|
+
// 控制不了 background 发请求时的 header;仅 options 面板的 agentApi 会带 X-Memory-Caller: user。
|
|
165
|
+
const callerOrigin = String(req?.headers?.['x-memory-caller'] || '').toLowerCase() === 'user' ? 'user' : 'model';
|
|
163
166
|
|
|
164
167
|
try {
|
|
165
168
|
let data;
|
|
@@ -170,6 +173,9 @@ export async function memoryDispatch(req, res, pathname, url, t, requestBody = {
|
|
|
170
173
|
} else if (method === 'backfillVectors') {
|
|
171
174
|
// 不接受请求体:只做「按当前配置把全部记忆补进向量缓存」,t 用于渲染跳过原因
|
|
172
175
|
data = await service.backfillVectors(t);
|
|
176
|
+
} else if (method === 'store') {
|
|
177
|
+
// P1-3:store 传入可信 ctx(源于上面的 header 判定),忽略 body 里模型可伪造的 origin
|
|
178
|
+
data = await service.store(body, t, { origin: callerOrigin });
|
|
173
179
|
} else {
|
|
174
180
|
data = await service[method](body, t);
|
|
175
181
|
}
|
package/src/memory/service.js
CHANGED
|
@@ -68,6 +68,42 @@ function normalizeImportance(value) {
|
|
|
68
68
|
return Math.min(IMPORTANCE_MAX, Math.max(IMPORTANCE_MIN, Math.round(n)));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// P1-3 记忆来源分级:origin 记录每条记忆的可信来源,用于注入侧决定能否进 Tier1 常驻。
|
|
72
|
+
// 关键:origin 由「谁发的请求」在运行时判定,模型不能自封——agent_memory 的 body 经扩展
|
|
73
|
+
// tool-memory.js 以 { ...args } 原样透传,模型可往 args 塞任意字段,故 origin 绝不能读 body,
|
|
74
|
+
// 只认调用方透传进来的可信 ctx(源于 HTTP header)。缺失一律按 'model'(默认不可信 → 降出 Tier1)。
|
|
75
|
+
const MEMORY_ORIGIN_USER = 'user'; // 用户在面板(options)手动新建/编辑
|
|
76
|
+
const MEMORY_ORIGIN_MODEL = 'model'; // 模型经 agent_memory 工具或自动压缩写入
|
|
77
|
+
|
|
78
|
+
// 写入侧 fail-closed 拦截:命中提示注入短语的记忆直接拒写(中英常见越狱载荷形态)。
|
|
79
|
+
// 宁可少存一条,不可把可疑指令放进会回注 system prompt 的记忆库。
|
|
80
|
+
const MEMORY_INJECTION_PATTERNS = [
|
|
81
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
82
|
+
/ignore\s+the\s+(above|system)\s+prompt/i,
|
|
83
|
+
/disregard\s+(all\s+)?(previous|above)/i,
|
|
84
|
+
/system\s*(prompt|override)\s*[::]/i,
|
|
85
|
+
/you\s+are\s+now\s+(in\s+)?(developer|jailbreak|dan)\s*mode/i,
|
|
86
|
+
/忽略(以上|之前|上面)(的)?(所有)?指令/,
|
|
87
|
+
/无视.{0,6}(系统|之前).{0,6}(提示|指令)/,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
function detectInjectionPattern(content) {
|
|
91
|
+
if (typeof content !== 'string' || !content) return null;
|
|
92
|
+
return MEMORY_INJECTION_PATTERNS.find((re) => re.test(content)) || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 频率上限:同一 (origin:sessionId) 在进程存活期内的写入条数上限(防批量投毒)。
|
|
96
|
+
// 进程内计数、daemon 重启即清零——best-effort 兜底,不是持久配额。
|
|
97
|
+
const MEMORY_STORE_RATE_LIMIT = 20;
|
|
98
|
+
const storeRateCounter = new Map();
|
|
99
|
+
|
|
100
|
+
function bumpStoreRate(origin, sessionId) {
|
|
101
|
+
const key = `${origin}:${sessionId || 'unknown'}`;
|
|
102
|
+
const next = (storeRateCounter.get(key) || 0) + 1;
|
|
103
|
+
storeRateCounter.set(key, next);
|
|
104
|
+
return next;
|
|
105
|
+
}
|
|
106
|
+
|
|
71
107
|
/**
|
|
72
108
|
* 词面命中门槛:整词/整段命中可直接成命中;只碰碎块时需单元素 ≥ 2 才算。
|
|
73
109
|
* 这一条是碎 bigram 噪声的真正出口(旧实现里「写网」「个国」这类随机 2 字块也能凑出分)。
|
|
@@ -231,8 +267,10 @@ export function createMemoryService({
|
|
|
231
267
|
* store:新增/更新/删除记忆(迁移自 executeAgentMemoryStore tool-memory.js:215-347)
|
|
232
268
|
* args.action 为子动作 add/update/delete(扩展转发层已将 subAction 映射到 action)
|
|
233
269
|
*/
|
|
234
|
-
async function store(args, t) {
|
|
270
|
+
async function store(args, t, ctx = {}) {
|
|
235
271
|
const { action, type, category, content, title, tags, importance, memoryId, sourceSessionId } = args;
|
|
272
|
+
// P1-3:origin 只认调用方透传的可信 ctx(源于 HTTP header),绝不从 args 取——模型可伪造 body。
|
|
273
|
+
const origin = ctx && ctx.origin === MEMORY_ORIGIN_USER ? MEMORY_ORIGIN_USER : MEMORY_ORIGIN_MODEL;
|
|
236
274
|
|
|
237
275
|
if (!action) return { success: false, error: t('memory.missingAction') };
|
|
238
276
|
// delete 不需要 type(memoryId 全局唯一),add/update 需要
|
|
@@ -252,6 +290,11 @@ export function createMemoryService({
|
|
|
252
290
|
return { success: false, error: t('memory.invalidImportance', { value: String(importance) }) };
|
|
253
291
|
}
|
|
254
292
|
|
|
293
|
+
// P1-3 写入侧 fail-closed:命中提示注入短语的内容(add/update)直接拒写
|
|
294
|
+
if ((action === 'add' || action === 'update') && content !== undefined && detectInjectionPattern(content)) {
|
|
295
|
+
return { success: false, error: t('memory.injectionPatternBlocked'), code: 'injection_blocked' };
|
|
296
|
+
}
|
|
297
|
+
|
|
255
298
|
return withMemoryLock(async () => {
|
|
256
299
|
const readResult = await readMemoryFile();
|
|
257
300
|
if (!readResult.success) return { success: false, error: t('memory.readFailed', { error: readResult.error }) };
|
|
@@ -261,6 +304,11 @@ export function createMemoryService({
|
|
|
261
304
|
const targetArray = type === 'fact' ? memoryData.facts : memoryData.summaries;
|
|
262
305
|
|
|
263
306
|
if (action === 'add') {
|
|
307
|
+
// P1-3 频率上限:仅对带真实 sessionId 的会话计数限流(贴合“同一会话内”语义);
|
|
308
|
+
// 无 sessionId 的写入(如自动压缩)不归入会话桶,避免全局误伤。
|
|
309
|
+
if (sourceSessionId && bumpStoreRate(origin, sourceSessionId) > MEMORY_STORE_RATE_LIMIT) {
|
|
310
|
+
return { success: false, error: t('memory.rateLimited'), code: 'rate_limited' };
|
|
311
|
+
}
|
|
264
312
|
const newMemory = {
|
|
265
313
|
id: generateMemoryId(type),
|
|
266
314
|
type,
|
|
@@ -272,7 +320,8 @@ export function createMemoryService({
|
|
|
272
320
|
lastAccessAt: null,
|
|
273
321
|
createdAt: now,
|
|
274
322
|
updatedAt: now,
|
|
275
|
-
sourceSessionId: sourceSessionId || null
|
|
323
|
+
sourceSessionId: sourceSessionId || null,
|
|
324
|
+
origin
|
|
276
325
|
};
|
|
277
326
|
if (type === 'summary' && title) {
|
|
278
327
|
newMemory.title = title;
|
|
@@ -285,6 +334,9 @@ export function createMemoryService({
|
|
|
285
334
|
duplicate.tags = tags || duplicate.tags;
|
|
286
335
|
duplicate.importance = importanceValue ?? duplicate.importance;
|
|
287
336
|
duplicate.sourceSessionId = sourceSessionId || duplicate.sourceSessionId;
|
|
337
|
+
// P1-3:用户编辑可把来源升为 user;模型写入永不改判已有来源(缺失才补 model)
|
|
338
|
+
if (origin === MEMORY_ORIGIN_USER) duplicate.origin = MEMORY_ORIGIN_USER;
|
|
339
|
+
else if (duplicate.origin === undefined) duplicate.origin = MEMORY_ORIGIN_MODEL;
|
|
288
340
|
const writeResult = await writeMemoryFile(memoryData);
|
|
289
341
|
if (!writeResult.success) return { success: false, error: t('memory.writeFailed', { error: writeResult.error }) };
|
|
290
342
|
queueVectorBackfill(duplicate.id, duplicate);
|
|
@@ -342,6 +394,9 @@ export function createMemoryService({
|
|
|
342
394
|
if (importanceValue !== null) existing.importance = importanceValue;
|
|
343
395
|
if (category !== undefined) existing.category = category;
|
|
344
396
|
if (type === 'summary' && title !== undefined) existing.title = title;
|
|
397
|
+
// P1-3:用户编辑 → 升为 user;模型更新 → 保留原来源(缺失才补 model),绝不提权
|
|
398
|
+
if (origin === MEMORY_ORIGIN_USER) existing.origin = MEMORY_ORIGIN_USER;
|
|
399
|
+
else if (existing.origin === undefined) existing.origin = MEMORY_ORIGIN_MODEL;
|
|
345
400
|
existing.updatedAt = now;
|
|
346
401
|
|
|
347
402
|
const writeResult = await writeMemoryFile(memoryData);
|
package/src/rag/manager.js
CHANGED
|
@@ -799,7 +799,7 @@ export class RagManager {
|
|
|
799
799
|
|
|
800
800
|
/**
|
|
801
801
|
* 导入 URL 内容(HTML 提取正文 / 文本直取 / PDF 下载解析)
|
|
802
|
-
* 安全:逐跳重定向校验 +
|
|
802
|
+
* 安全:逐跳重定向校验 + 私网拦截(B2 AND 闸:开关开且白名单命中才放行,见 url-guard.js)+ 响应体大小上限
|
|
803
803
|
* @param {string} collectionId
|
|
804
804
|
* @param {{url: string, metadata?: object, chunkConfig?: object}} input
|
|
805
805
|
*/
|
package/src/rag/url-guard.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// agent/src/rag/url-guard.js - RAG URL 导入的 SSRF 防护
|
|
2
2
|
// 三层防线:
|
|
3
3
|
// 1. 目标地址干预:DNS 解析后按真实 IP 判断私网/保留段,决定是否放行
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// 内网放行采用「总闸 + 白名单」的 B2 AND 闸语义(与扩展侧 src/shared/network-scope.js 同序):
|
|
5
|
+
// 开关开 **且** 白名单命中才放行;关闸时白名单命中也拒绝;开闸但白名单为空仍拒绝
|
|
6
6
|
// 2. 重定向逐跳校验:redirect: manual 手动跟随,每一跳都重新过第 1 层(防合法地址 302 跳内网)
|
|
7
7
|
// 3. 响应体大小上限:流式累计,超限立即中断(防超大响应 OOM)
|
|
8
8
|
// 注:DNS 校验与实际连接存在二次解析的理论窗口(DNS rebinding),
|
|
@@ -66,16 +66,30 @@ export function matchesHostAllowlist(hostname, allowlist) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
* 私网目标放行决策(B2 AND 闸,判定顺序与扩展侧 src/shared/network-scope.js 的 decideNetworkAccess 一致):
|
|
70
|
+
* 1. 总开关关闭 → 拒绝(白名单命中也无效,白名单不被自动清空)
|
|
71
|
+
* 2. 开关开启 + 白名单为空(或仅空白条目)→ 拒绝("启用能力"不等于"放行全部内网")
|
|
72
|
+
* 3. 开关开启 + 白名单非空 → 仅放行命中条目
|
|
73
|
+
* @returns {{ allowed: boolean, reason: string }} reason: gateClosed / noAllowlistedHost / hostNotAllowlisted / allowlisted
|
|
72
74
|
*/
|
|
73
|
-
export function
|
|
75
|
+
export function decidePrivateAccess(hostname, policy) {
|
|
74
76
|
const p = policy && typeof policy === 'object' ? policy : {};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
// 规则 1:总闸未开一律拒绝,白名单不单独生效
|
|
78
|
+
if (p.allowPrivateNetwork !== true) return { allowed: false, reason: 'gateClosed' };
|
|
79
|
+
const allowlist = (Array.isArray(p.allowedPrivateHosts) ? p.allowedPrivateHosts : [])
|
|
80
|
+
.filter((entry) => String(entry || '').trim() !== '');
|
|
81
|
+
// 规则 2:开关开着但没配任何条目 → 仍拒绝,避免存在"整个内网全开"的状态
|
|
82
|
+
if (allowlist.length === 0) return { allowed: false, reason: 'noAllowlistedHost' };
|
|
83
|
+
// 规则 3:仅放行命中条目
|
|
84
|
+
if (matchesHostAllowlist(hostname, allowlist)) return { allowed: true, reason: 'allowlisted' };
|
|
85
|
+
return { allowed: false, reason: 'hostNotAllowlisted' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 私网目标是否放行(decidePrivateAccess 的布尔视图,保持既有调用方与用例形状)
|
|
90
|
+
*/
|
|
91
|
+
export function isPrivateTargetAllowed(hostname, policy) {
|
|
92
|
+
return decidePrivateAccess(hostname, policy).allowed;
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
/**
|
|
@@ -104,8 +118,9 @@ export async function assertUrlAllowed(urlStr, policy) {
|
|
|
104
118
|
? [hostname]
|
|
105
119
|
: (await lookup(hostname, { all: true })).map((r) => r.address);
|
|
106
120
|
if (!ips.some(isPrivateAddress)) return; // 公网目标直接放行
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
const decision = decidePrivateAccess(hostname, policy);
|
|
122
|
+
if (!decision.allowed) {
|
|
123
|
+
throw new RagError('urlPrivateBlocked', { host: hostname, reason: decision.reason });
|
|
109
124
|
}
|
|
110
125
|
}
|
|
111
126
|
|