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/src/security.js
CHANGED
|
@@ -229,36 +229,92 @@ async function checkPath(pathStr, tFn) {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
// ==================== 命令分级管控 ====================
|
|
232
|
+
//
|
|
233
|
+
// 档位语义(见 checkCommand):
|
|
234
|
+
// - 黑名单 → level='deny':不给确认弹窗、force 也不可绕过,只留给「目标本身就该绝对禁止」的动作
|
|
235
|
+
// - 灰名单 → level='confirm':真实用户确认弹窗;force(用户点确认 / 关闭确认开关)可跳过
|
|
236
|
+
//
|
|
237
|
+
// 已知能力边界(不在本层解决,遇到时以注释说明而不是假装防住):
|
|
238
|
+
// 1. 命令通道不受 allowedPaths 路径沙箱约束(checkPath 只作用于文件类工具),
|
|
239
|
+
// 因此 `cat /etc/passwd` 这类「读取即外泄」的动作无法在本层拦截。
|
|
240
|
+
// 2. 变量间接执行(`S=rm; $S -rf x`)与脚本文件的正文内容静态不可判(另见 collectCommandScopes 注释)。
|
|
241
|
+
// 3. curl/wget/kill 等条目会给常规用法带来确认弹窗,属预期摩擦,可用「本次任务放行」降低频次。
|
|
242
|
+
//
|
|
243
|
+
// 豁免边界:下面 SUBSTITUTION_HARMLESS_HEADS 的只读豁免只适用于「命令替换」这一项
|
|
244
|
+
// 语法特征,黑名单与其余灰名单条目不享任何豁免。
|
|
245
|
+
|
|
246
|
+
// 命令替换特征用的两类只读名单:
|
|
247
|
+
// - WHITELIST_HEADS 与原黑名单同源(纯输出命令:结果只会被打印)
|
|
248
|
+
// - HARMLESS_HEADS 额外包含只读/不落盘的头命令(cd/true/test 等),
|
|
249
|
+
// 仅用于判定「链式命令的原始整串」要不要参与替换特征,避免整串一票否决
|
|
250
|
+
const SUBSTITUTION_WHITELIST_HEADS =
|
|
251
|
+
'(?:echo|printf|cat|head|tail|wc|ls|pwd|date|whoami|hostname|uname|id|env|printenv|which|type)';
|
|
252
|
+
const SUBSTITUTION_HARMLESS_HEADS =
|
|
253
|
+
'(?:echo|printf|cat|head|tail|wc|ls|pwd|date|whoami|hostname|uname|id|env|printenv|which|type|true|false|test|cd|dir)';
|
|
254
|
+
const SUBSTITUTION_BACKTICK = /`[^`\n]*`/;
|
|
255
|
+
const SUBSTITUTION_DOLLAR = /\$\s*\((?:[^()]|\([^)]*\))*\)/;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 命令替换特征的送检集合(档位纠偏后的误报控制)
|
|
259
|
+
* - 子段:只要段首不是纯输出命令就送检(`a=$(echo hi)`、剥壳后的内联载荷)
|
|
260
|
+
* - 原始整串:仅当整串头命令不在只读名单里才送检,否则 `pwd; echo $(date)`、
|
|
261
|
+
* `cd /tmp; echo $(date)` 会被整串一票否决(原始误报根源)
|
|
262
|
+
* @param {string} whole 原始整串
|
|
263
|
+
* @param {string[]} scopes 全部子命令范围
|
|
264
|
+
* @returns {string[]}
|
|
265
|
+
*/
|
|
266
|
+
function substitutionCheckScopes(whole, scopes) {
|
|
267
|
+
// 拆分器会整段保留 `$( … )`/反引号,extractSubstitutions 再把同一文本递回来一遍;
|
|
268
|
+
// 只看剥掉替换后的外层位置,否则 `echo $(date)` 会因内层 `date` 不自替换而被重复送检
|
|
269
|
+
const positions = new Set(splitShellSegments(whole).map(stripEnvAssignments));
|
|
270
|
+
const out = scopes.filter((s) => s !== whole && !positions.has(s));
|
|
271
|
+
const isPlainOutput = new RegExp(`^${SUBSTITUTION_WHITELIST_HEADS}\\s`, 'i').test(whole);
|
|
272
|
+
const isHarmlessHead = new RegExp(`^(${SUBSTITUTION_HARMLESS_HEADS})\\b`, 'i').test(whole);
|
|
273
|
+
if (!isHarmlessHead && !isPlainOutput) out.push(whole);
|
|
274
|
+
// 回落:替换藏在引号参数里时(`git commit -m "feat: $(date)"`),拆分器不产出携带它的子段,
|
|
275
|
+
// 头命令豁免也不送整串——此时只扫含替换的那段引号文本,不得扩大到整个串
|
|
276
|
+
if (out.length === 0 && !isPlainOutput) {
|
|
277
|
+
const quoted = (whole.match(/"[^"]*"|'[^']*'/g) || []).filter(
|
|
278
|
+
(frag) => SUBSTITUTION_BACKTICK.test(frag) || SUBSTITUTION_DOLLAR.test(frag)
|
|
279
|
+
);
|
|
280
|
+
out.push(...quoted);
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
232
284
|
|
|
233
285
|
const BLACKLIST_PATTERNS = [
|
|
234
286
|
// 高危写入系统目录
|
|
235
287
|
/^\s*rm\s+-rf\s+\/(\*|\s|$)/,
|
|
236
288
|
/^\s*rm\s+-rf\s+~(\*|\s|$)/,
|
|
237
289
|
/^\s*rm\s+-rf\s+\/dev\//,
|
|
238
|
-
/^\s*rm\s+-rf\s+\/etc\//,
|
|
239
290
|
/^\s*rm\s+-rf\s+\/proc\//,
|
|
240
291
|
/^\s*rm\s+-rf\s+\/sys\//,
|
|
292
|
+
// 递归删除关键系统目录:原表只列了 /dev /etc /proc /sys,/usr /var /bin /root 等同样删了就起不来
|
|
293
|
+
// 目标紧跟分隔符或结尾,因此 /tmp、/private/tmp 之类不会被误判成系统目录
|
|
294
|
+
/^\s*rm\s+-(?:[rRf]+|--recursive|--force)\s+(-\S+\s+)*(\/usr|\/var|\/bin|\/sbin|\/lib32|\/lib64|\/lib|\/etc|\/boot|\/opt|\/sys|\/dev|\/root|\/System|\/Library)(\/|\*|\s|$)/,
|
|
241
295
|
// 格式化/磁盘破坏
|
|
242
296
|
/^\s*mkfs\./,
|
|
243
297
|
/^\s*dd\s+if=.*of=\/dev\//,
|
|
244
298
|
/^\s*>\s*\/dev\/sd/,
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
299
|
+
// 覆盖系统文件:重定向可以出现在命令任意位置(`echo x > /etc/passwd`),只锁定行首 `^>` 会漏掉。
|
|
300
|
+
// 兼容 `>`/`>>`/`>&`/`&>` 与 `2>` 等 fd 形式,目标路径紧跟空白/结尾以免误伤 `/etc/passwd_xxx`。
|
|
301
|
+
/(?:^|[\s;|])\d*&?>>?&?\s*\/etc\/passwd(\s|$)/,
|
|
302
|
+
/(?:^|[\s;|])\d*&?>>?&?\s*\/etc\/shadow(\s|$)/,
|
|
303
|
+
/(?:^|[\s;|])\d*&?>>?&?\s*\/etc\/sudoers(\s|$)/,
|
|
249
304
|
// fork bomb
|
|
250
305
|
/:\(\)\s*{\s*:\s*\|\s*:\s*&\s*};\s*:/,
|
|
251
306
|
// shebang 直接执行(出现在命令开头)
|
|
252
307
|
/^\s*#!/,
|
|
253
|
-
//
|
|
254
|
-
/(curl|wget|lynx|links)\s+.*\|\s*(ba)?sh/,
|
|
255
|
-
/git\s+clone\s+.*\|\s*(ba)?sh/,
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
/^\s*(
|
|
261
|
-
|
|
308
|
+
// 恶意管道执行(管道右侧解释器允许带 sudo/doas 提权前缀,否则 `curl … | sudo sh` 只降级为 confirm)
|
|
309
|
+
/(curl|wget|lynx|links)\s+.*\|\s*(?:(?:sudo|doas|runas)\s+)*(?:[^|\s]+\s+)*(ba)?sh\b/,
|
|
310
|
+
/git\s+clone\s+.*\|\s*(?:(?:sudo|doas|runas)\s+)*(?:[^|\s]+\s+)*(ba)?sh\b/,
|
|
311
|
+
// 修改根目录权限/属主:只留给「目标本身就是根 /」的绝对禁止动作。
|
|
312
|
+
// `\/` 必须紧跟空白或结尾才命中,否则 `\s+\/` 会把 `chmod 777 /tmp/x` 这类普通绝对路径
|
|
313
|
+
// 也当根目录硬拒(force 不可越);非根目标改权限回落灰名单 confirm。
|
|
314
|
+
/^\s*chmod\s+(-R\s+)?(0?777|a\+rwx)\s+\/(\s|$)/,
|
|
315
|
+
/^\s*chown\s+-R\s+\S+\s+\/(\s|$)/,
|
|
316
|
+
// 注:命令替换($( … ) / 反引号)属「语法可疑」而非「效果危险」,已下调到灰名单,
|
|
317
|
+
// 否则 `a=$(echo hi)`、`ts=$(date +%s)` 这类零风险写法连确认机会都没有就被硬拒
|
|
262
318
|
// Windows 高危破坏性命令(大小写不敏感)
|
|
263
319
|
/^\s*format\s+[a-zA-Z]:/i, // 格式化磁盘
|
|
264
320
|
/^\s*diskpart\b/i, // 磁盘分区操作
|
|
@@ -270,15 +326,41 @@ const SCRIPT_INTERPRETERS = '(bash|sh|zsh|python3?|node|ruby|perl|php|lua)';
|
|
|
270
326
|
|
|
271
327
|
const GRAYLIST_PATTERNS = [
|
|
272
328
|
{ pattern: /^\s*sudo\s/, reasonKey: 'security.cmdSudo' },
|
|
329
|
+
// sudo 的同类替代品:不单独列进名单就成一个免费的提权绕道
|
|
330
|
+
{ pattern: /^\s*doas\s/, reasonKey: 'security.cmdDoas' },
|
|
331
|
+
{ pattern: /^\s*runas\s+\/user:/i, reasonKey: 'security.cmdDoas' },
|
|
273
332
|
{ pattern: /npm\s+(i|install)\s+-g\s/, reasonKey: 'security.cmdNpmGlobal' },
|
|
274
333
|
{ pattern: /pip\s+(install|uninstall)\s/, reasonKey: 'security.cmdPip' },
|
|
275
334
|
{ pattern: /^\s*chmod\s+-R\s+777\s/, reasonKey: 'security.cmdChmod777' },
|
|
276
|
-
|
|
335
|
+
// 递归强制删除:原表只拦了 `-rf` 这一种拼写,`rm -r -f x`、`rm --recursive --force x` 原本静默执行。
|
|
336
|
+
// 必须同时带递归与强删(`rm -f x` 不拦);黑名单只留给目标本身就该绝对禁止的系统目录
|
|
337
|
+
{ pattern: /^\s*rm\s+(?:-(?=[a-zA-Z]*[rR][a-zA-Z]*[fF])[a-zA-Z]+|-(?=[a-zA-Z]*[rR])[a-zA-Z]+\s*(?:-(?=[a-zA-Z]*[fF])[a-zA-Z]+|--force|--recursive)|--recursive\s*--force|--force\s*--recursive)\s/, reasonKey: 'security.cmdRmrf' },
|
|
277
338
|
{ pattern: /git\s+push\s+(-f|--force)/, reasonKey: 'security.cmdGitForcePush' },
|
|
278
339
|
{ pattern: /^\s*(shutdown|reboot|halt|poweroff)/, reasonKey: 'security.cmdShutdown' },
|
|
279
340
|
{ pattern: /^\s*>\s*\/etc\/hosts/, reasonKey: 'security.cmdHosts' },
|
|
280
341
|
{ pattern: /\beval\s+/, reasonKey: 'security.cmdEval' },
|
|
281
342
|
|
|
343
|
+
// 命令替换($( … ) / 反引号):从黑名单下调至此。只扫「替换确实落在有风险位置」的范围:
|
|
344
|
+
// 子段只看非纯输出段,整串只看非只读头命令(见 substitutionCheckScopes)
|
|
345
|
+
{ pattern: SUBSTITUTION_BACKTICK, substitutionOnly: true, reasonKey: 'security.cmdCmdSubstitution' },
|
|
346
|
+
{ pattern: SUBSTITUTION_DOLLAR, substitutionOnly: true, reasonKey: 'security.cmdCmdSubstitution' },
|
|
347
|
+
|
|
348
|
+
// 裸写/销毁文件内容:原表只拦了 `of=/dev/`,写普通文件同样能毁数据
|
|
349
|
+
{ pattern: /^\s*dd\s+.*\bof=/, reasonKey: 'security.cmdDdWrite' },
|
|
350
|
+
{ pattern: /^\s*shred\b/, reasonKey: 'security.cmdShred' },
|
|
351
|
+
{ pattern: /^\s*truncate\s+.*(?:-s\s*0\b|--size[=\s][+-]?0\b)/, reasonKey: 'security.cmdTruncate' },
|
|
352
|
+
|
|
353
|
+
// 权限/属主变更:原表只拦 777 与 +x,`chmod 000` 一样致文件不可用
|
|
354
|
+
{ pattern: /^\s*chmod\s+(-R\s+)?(0?00[0-7]|0?777|a-rwx)\b/, reasonKey: 'security.cmdChmodDestroy' },
|
|
355
|
+
{ pattern: /^\s*chown\s+/, reasonKey: 'security.cmdChown' },
|
|
356
|
+
|
|
357
|
+
// 网络外联:原表只拦 `curl … | sh`,而拉取/上传本身就是数据外泄面
|
|
358
|
+
{ pattern: /^\s*(curl|wget|nc|telnet|ssh|scp)\b/, reasonKey: 'security.cmdNetwork' },
|
|
359
|
+
|
|
360
|
+
// 进程操作与系统服务/计划任务
|
|
361
|
+
{ pattern: /^\s*(kill|pkill|killall)\s+/, reasonKey: 'security.cmdKillProcess' },
|
|
362
|
+
{ pattern: /^\s*(systemctl|launchctl|crontab|sysctl)\b/, reasonKey: 'security.cmdSystemService' },
|
|
363
|
+
|
|
282
364
|
// 脚本解释器执行外部文件
|
|
283
365
|
{ pattern: new RegExp(`^\\s*${SCRIPT_INTERPRETERS}(\\s+[^-]\\S*)*\\s+\\S*\\.${SCRIPT_EXTENSIONS}\\b`), reasonKey: 'security.cmdExecExternalScript' },
|
|
284
366
|
// 直接执行脚本
|
|
@@ -295,8 +377,311 @@ const GRAYLIST_PATTERNS = [
|
|
|
295
377
|
{ pattern: /^\s*net\s+(user|localgroup)\b/i, reasonKey: 'security.cmdNet' },
|
|
296
378
|
{ pattern: /^\s*sc\s+(stop|delete|config)\b/i, reasonKey: 'security.cmdSc' },
|
|
297
379
|
{ pattern: /^\s*taskkill\s+\/[a-zA-Z]*f/i, reasonKey: 'security.cmdTaskkillF' },
|
|
380
|
+
// PowerShell 侧的破坏性 cmdlet 与 base64 内联载荷:
|
|
381
|
+
// -EncodedCommand 的内容静态看不出是什么,安全网关上“看不出来”只能当成需要确认
|
|
382
|
+
{ pattern: /\bremove-item\b[\s\S]*-(?:recurse|force)/i, reasonKey: 'security.cmdPsRemoveItem' },
|
|
383
|
+
{ pattern: /^\s*(?:powershell|pwsh)\b.*\s-(?:e|enc|encodedcommand)\s/i, reasonKey: 'security.cmdPsEncoded' },
|
|
298
384
|
];
|
|
299
385
|
|
|
386
|
+
/**
|
|
387
|
+
* 链式命令拆分:黑白名单绝大多数模式锚定命令串开头,而模型很习惯写
|
|
388
|
+
* `cd /x && sudo rm -rf y`、`echo pw | sudo -S apt install`、`bash -c "sudo ..."`,
|
|
389
|
+
* 只检查整串开头会让后面几段完全绕过灰名单(用户表现为“点过一次后此后再也不弹框”)。
|
|
390
|
+
* 这里把一条命令拆成逐个“子命令范围”,逐段比对,只增不减判定强度。
|
|
391
|
+
* 已知能力边界:变量间接执行(`S=rm; $S -rf x`)无法静态判定,不在本层解决。
|
|
392
|
+
*/
|
|
393
|
+
// 会把内联脚本作为参数传入的解释器(只对它们解开内联载荷,避免 grep -c 之类误报)
|
|
394
|
+
const INLINE_EXEC_INTERPRETERS = /^(?:\.\/|\/usr\/(?:local\/)?bin\/)?(?:sh|bash|zsh|dash|ksh|fish|powershell|pwsh|cmd|python[0-9.]*|node|perl|ruby|php|lua)\b/i;
|
|
395
|
+
const INLINE_EXEC_FLAG = /\s-(?:c|e)\s+(.+)$/is;
|
|
396
|
+
// Windows 侧的内联载荷:cmd 用 /c /k,PowerShell 用 -Command 与 -EncodedCommand(base64/UTF-16LE)
|
|
397
|
+
const CMD_SLASH_FLAG = /\s\/([ck])\s+(.+)$/is;
|
|
398
|
+
const PS_INLINE_FLAG = /\s-(?:command|encodedcommand|enc)\s+(.+)$/i;
|
|
399
|
+
const PS_ENCODED_FLAG = /-(?:encodedcommand|enc)\b/i;
|
|
400
|
+
const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
401
|
+
// 前缀包装器:仅当整段以它们开头时才剥离(否则会把普通命令的参数误当成命令)。
|
|
402
|
+
// sudo/doas 是提权包装器:剥掉它们不是免检,而是让后面的 `rm -rf /usr`、`mkfs`、`dd of=/dev/`
|
|
403
|
+
// 这类「绝对禁止项」暴露到段首黑名单正则下——`sudo` 绝不能成为硬拒项的降级通行证。
|
|
404
|
+
const COMMAND_WRAPPERS = /^(?:nohup|time|timeout|nice|stdbuf|env|exec|command|xargs|sudo|doas)\b/i;
|
|
405
|
+
const OPTION_OR_SIZE_TOKEN = /^(?:[-+]|\d+(?:\.\d+)?[a-z%]*$)/i;
|
|
406
|
+
|
|
407
|
+
/** 去掉包裹引号 */
|
|
408
|
+
function unquote(text) {
|
|
409
|
+
const s = text.trim();
|
|
410
|
+
if (s.length >= 2 && ((s[0] === '"' && s.endsWith('"')) || (s[0] === "'" && s.endsWith("'")))) {
|
|
411
|
+
return s.slice(1, -1);
|
|
412
|
+
}
|
|
413
|
+
return s;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** 去掉开头的 `KEY=VAL` 环变量前缀(`FOO=1 sudo xxx` 依旧要看 sudo) */
|
|
417
|
+
function stripEnvAssignments(segment) {
|
|
418
|
+
const tokens = segment.trim().split(/\s+/);
|
|
419
|
+
let i = 0;
|
|
420
|
+
while (i < tokens.length && ENV_ASSIGNMENT.test(tokens[i])) i++;
|
|
421
|
+
return tokens.slice(i).join(' ');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* 逐层剥前缀包装器及其环变量/选项/数值参数,返回每一层剥壳后的中间形态:
|
|
426
|
+
* `nohup sudo …`、`env FOO=1 sudo …`、`timeout 10 -k 5 sudo …` 都得让后面的命令逐层暴露在段首。
|
|
427
|
+
* 保留中间形态而非一步剥到底,是因为 sudo/doas 同时是灰名单条目:
|
|
428
|
+
* `nohup sudo apt install foo` 必须既产出 `sudo apt install foo`(灰名单 sudo 仍可见),
|
|
429
|
+
* 又产出 `apt install foo`;而 `sudo rm -rf /usr` 要产出 `rm -rf /usr`(黑名单绝对禁止项)。
|
|
430
|
+
* @param {string} segment
|
|
431
|
+
* @returns {string[]} 每剥一层得到的中间形态(可能多个),段首不是包装器时为空数组
|
|
432
|
+
*/
|
|
433
|
+
function peelCommandWrappers(segment) {
|
|
434
|
+
const tokens = segment.trim().split(/\s+/);
|
|
435
|
+
const out = [];
|
|
436
|
+
// 段首不是包装器就不动:否则 `ls -la` 之类会被剥成光杆参数
|
|
437
|
+
while (tokens.length >= 2 && COMMAND_WRAPPERS.test(tokens[0])) {
|
|
438
|
+
tokens.shift(); // 去掉当前这一层包装器
|
|
439
|
+
// 连同它自己的环变量/选项/数值参数一起跳过(env FOO=1、timeout 10 -k 5、sudo -S)
|
|
440
|
+
while (tokens.length && (ENV_ASSIGNMENT.test(tokens[0]) || OPTION_OR_SIZE_TOKEN.test(tokens[0]))) {
|
|
441
|
+
tokens.shift();
|
|
442
|
+
}
|
|
443
|
+
const rest = tokens.join(' ');
|
|
444
|
+
if (rest) out.push(rest);
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 折叠 shell 绝对路径里的冗余分隔符与 `/.`、`/X/..` 段:`//usr`、`/.//usr`、`/tmp/../usr`
|
|
451
|
+
* 在 POSIX 下与 `/usr` 指向同一目录,不能靠换个写法绕过系统目录 / 设备黑名单。
|
|
452
|
+
* 同时覆盖 `of=//dev/sda`、`--path=/./usr` 这类 `key=/value` 形态(只归一化以 `/` 开头的值部分)。
|
|
453
|
+
* 只处理以 `/` 开头的路径(或其值),引号内容本就不进这一层,不会误伤 URL 里的 `://`。
|
|
454
|
+
* @param {string} segment
|
|
455
|
+
* @returns {string} 归一化后的串(与入参相同表示无可折叠)
|
|
456
|
+
*/
|
|
457
|
+
function normalizeShellPathTargets(segment) {
|
|
458
|
+
return segment
|
|
459
|
+
.split(/\s+/)
|
|
460
|
+
.map((tok) => {
|
|
461
|
+
// 拆开 `key=` 前缀(dd 的 if=/of=、长选项 --path= 等),只对以 / 开头的值做归一
|
|
462
|
+
const eq = tok.indexOf('=');
|
|
463
|
+
const prefix = eq > -1 ? tok.slice(0, eq + 1) : '';
|
|
464
|
+
const pathPart = eq > -1 ? tok.slice(eq + 1) : tok;
|
|
465
|
+
if (!pathPart.startsWith('/')) return tok;
|
|
466
|
+
let p = pathPart;
|
|
467
|
+
let prev;
|
|
468
|
+
do {
|
|
469
|
+
prev = p;
|
|
470
|
+
p = p
|
|
471
|
+
.replace(/\/\/+/g, '/')
|
|
472
|
+
.replace(/\/\.(?=\/|$)/g, '')
|
|
473
|
+
.replace(/\/[^/.][^/]*\/\.\.(?=\/|$)/g, '')
|
|
474
|
+
.replace(/^\/\.\.(?=\/|$)/, ''); // 根之上的 .. 等价根:/../x → /x
|
|
475
|
+
} while (p !== prev);
|
|
476
|
+
return prefix + p;
|
|
477
|
+
})
|
|
478
|
+
.join(' ');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* 取出解释器内联载荷(POSIX `-c/-e`、cmd `/c /k`、PowerShell `-Command/-EncodedCommand`)
|
|
483
|
+
* @param {string} segment
|
|
484
|
+
* @returns {string[]}
|
|
485
|
+
*/
|
|
486
|
+
function extractInlinePayloads(segment) {
|
|
487
|
+
if (!INLINE_EXEC_INTERPRETERS.test(segment)) return [];
|
|
488
|
+
const payloads = [];
|
|
489
|
+
|
|
490
|
+
const posix = segment.match(INLINE_EXEC_FLAG);
|
|
491
|
+
if (posix) payloads.push(unquote(posix[1]));
|
|
492
|
+
|
|
493
|
+
const cmd = segment.match(CMD_SLASH_FLAG);
|
|
494
|
+
if (cmd) payloads.push(unquote(cmd[2]));
|
|
495
|
+
|
|
496
|
+
const ps = segment.match(PS_INLINE_FLAG);
|
|
497
|
+
if (ps) {
|
|
498
|
+
const raw = unquote(ps[1]);
|
|
499
|
+
if (PS_ENCODED_FLAG.test(segment)) {
|
|
500
|
+
// -EncodedCommand 走 base64(UTF-16LE):解不出来就当作不命中,不当成放行依据
|
|
501
|
+
try {
|
|
502
|
+
payloads.push(Buffer.from(raw, 'base64').toString('utf16le'));
|
|
503
|
+
} catch {
|
|
504
|
+
/* 非法 base64:保留原文已由上面的 push 覆盖,不影响判定 */
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
payloads.push(raw);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return payloads.map((p) => String(p || '').trim()).filter(Boolean);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 取出 `$( … )` 与反引号内部的子命令
|
|
516
|
+
* 拆分器为了保留黑名单语义不拆这些位置,但里面的敏感段同样得过灰名单
|
|
517
|
+
* (如 `echo $(sudo id)`:黑名单对 echo 开头的替换网开一面,灰名单必须看到 sudo)
|
|
518
|
+
* @param {string} segment
|
|
519
|
+
* @returns {string[]}
|
|
520
|
+
*/
|
|
521
|
+
function extractSubstitutions(segment) {
|
|
522
|
+
const found = [];
|
|
523
|
+
const scan = (text) => {
|
|
524
|
+
for (let i = 0; i < text.length; i++) {
|
|
525
|
+
const ch = text[i];
|
|
526
|
+
if (ch === '`') {
|
|
527
|
+
const end = text.indexOf('`', i + 1);
|
|
528
|
+
if (end > i) {
|
|
529
|
+
found.push(text.slice(i + 1, end));
|
|
530
|
+
i = end;
|
|
531
|
+
}
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (ch === '$' && text[i + 1] === '(') {
|
|
535
|
+
let depth = 0;
|
|
536
|
+
let j = i + 1;
|
|
537
|
+
for (; j < text.length; j++) {
|
|
538
|
+
if (text[j] === '(') depth++;
|
|
539
|
+
else if (text[j] === ')') {
|
|
540
|
+
depth--;
|
|
541
|
+
if (depth === 0) break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (j < text.length) {
|
|
545
|
+
found.push(text.slice(i + 2, j));
|
|
546
|
+
i = j;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
scan(segment);
|
|
553
|
+
// 拆分器在引号内不扫替换(为了不把 `"a && b"` 误拆),于是被引号包住的替换内层会整个逃检:
|
|
554
|
+
// `bash -c "echo `rm -rf /`"` 曾直接判 allow。这里补扫引号片段内部,使内层命令照常进名单
|
|
555
|
+
for (const frag of segment.match(/"[^"]*"|'[^']*'/g) || []) {
|
|
556
|
+
scan(frag.slice(1, -1));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return found.map((s) => s.trim()).filter(Boolean);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* 按 shell 控制符拆分命令串(引号 / 反引号 / $( ) 内部不拆)
|
|
564
|
+
* @param {string} command
|
|
565
|
+
* @returns {string[]} 非空子命令段
|
|
566
|
+
*/
|
|
567
|
+
function splitShellSegments(command) {
|
|
568
|
+
const parts = [];
|
|
569
|
+
let buf = '';
|
|
570
|
+
let quote = null;
|
|
571
|
+
const push = () => {
|
|
572
|
+
const seg = buf.trim();
|
|
573
|
+
if (seg) parts.push(seg);
|
|
574
|
+
buf = '';
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
for (let i = 0; i < command.length; i++) {
|
|
578
|
+
const ch = command[i];
|
|
579
|
+
|
|
580
|
+
if (quote) {
|
|
581
|
+
buf += ch;
|
|
582
|
+
if (ch === '\\' && quote === '"' && i + 1 < command.length) {
|
|
583
|
+
buf += command[++i];
|
|
584
|
+
} else if (ch === quote) {
|
|
585
|
+
quote = null;
|
|
586
|
+
}
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (ch === "'" || ch === '"') {
|
|
591
|
+
quote = ch;
|
|
592
|
+
buf += ch;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (ch === '\\' && i + 1 < command.length) {
|
|
597
|
+
buf += ch + command[++i];
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 反引号命令替换:整段保留,不在此处拆分
|
|
602
|
+
if (ch === '`') {
|
|
603
|
+
buf += ch;
|
|
604
|
+
i++;
|
|
605
|
+
for (; i < command.length; i++) {
|
|
606
|
+
buf += command[i];
|
|
607
|
+
if (command[i] === '`') break;
|
|
608
|
+
}
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// $( ... ) 命令替换:朴素配对一层括号
|
|
613
|
+
if (ch === '$' && command[i + 1] === '(') {
|
|
614
|
+
let depth = 0;
|
|
615
|
+
for (; i < command.length; i++) {
|
|
616
|
+
buf += command[i];
|
|
617
|
+
if (command[i] === '(') depth++;
|
|
618
|
+
else if (command[i] === ')') {
|
|
619
|
+
depth--;
|
|
620
|
+
if (depth === 0) break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const two = command.slice(i, i + 2);
|
|
627
|
+
if (two === '&&' || two === '||') {
|
|
628
|
+
push();
|
|
629
|
+
i++;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (ch === ';' || ch === '|' || ch === '&' || ch === '\n') {
|
|
634
|
+
push();
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
buf += ch;
|
|
639
|
+
}
|
|
640
|
+
push();
|
|
641
|
+
return parts;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* 收集一条命令需要接受检查的全部“子命令范围”
|
|
646
|
+
* 始终包含原始整串(保留现有黑名单语义,例如 fork bomb 这种本身带控制符的模式),
|
|
647
|
+
* 再追加逐段拆分结果与解释器 -c/-e 内联载荷(递归一层层解开,深度封顶防死循环)。
|
|
648
|
+
* @param {string} command
|
|
649
|
+
* @param {number} [depth]
|
|
650
|
+
* @param {string[]} [out]
|
|
651
|
+
* @returns {string[]}
|
|
652
|
+
*/
|
|
653
|
+
function collectCommandScopes(command, depth = 3, out = []) {
|
|
654
|
+
const trimmed = String(command || '').trim();
|
|
655
|
+
if (!trimmed) return out;
|
|
656
|
+
if (!out.includes(trimmed)) out.push(trimmed);
|
|
657
|
+
if (depth <= 0) return out;
|
|
658
|
+
|
|
659
|
+
for (const seg of splitShellSegments(trimmed)) {
|
|
660
|
+
const normalized = stripEnvAssignments(seg);
|
|
661
|
+
if (normalized && !out.includes(normalized)) out.push(normalized);
|
|
662
|
+
|
|
663
|
+
// `nohup sudo …`、`env FOO=1 sudo …`、`timeout 10 sudo …`、`sudo rm -rf /usr`:
|
|
664
|
+
// 包装器/提权前缀只是外层,逐层剥壳后递归送检,不得把敏感命令藏到段首正则看不见的后面
|
|
665
|
+
for (const peeled of peelCommandWrappers(normalized)) {
|
|
666
|
+
collectCommandScopes(peeled, depth - 1, out);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 路径变形归一化:`rm -rf //usr`、`rm -rf /.//usr` 等价的系统目录不能因写法不同而降级为 confirm
|
|
670
|
+
const pathScope = normalizeShellPathTargets(normalized);
|
|
671
|
+
if (pathScope && !out.includes(pathScope)) out.push(pathScope);
|
|
672
|
+
|
|
673
|
+
// 解释器内联载荷:POSIX `-c/-e`、cmd `/c /k`、PowerShell `-Command/-EncodedCommand`
|
|
674
|
+
for (const payload of extractInlinePayloads(seg)) {
|
|
675
|
+
collectCommandScopes(payload, depth - 1, out);
|
|
676
|
+
}
|
|
677
|
+
// `$( … )` 与反引号内部:拆分器为了保留黑名单语义整段不动,这里单独取出送检
|
|
678
|
+
for (const sub of extractSubstitutions(seg)) {
|
|
679
|
+
collectCommandScopes(sub, depth - 1, out);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return out;
|
|
683
|
+
}
|
|
684
|
+
|
|
300
685
|
/**
|
|
301
686
|
* 检查命令安全性
|
|
302
687
|
* @param {boolean} [force=false] - 用户已确认(跳过灰名单但保留黑名单)
|
|
@@ -309,15 +694,27 @@ function checkCommand(command, force = false, tFn) {
|
|
|
309
694
|
}
|
|
310
695
|
|
|
311
696
|
const trimmed = command.trim();
|
|
697
|
+
// 只有空白字符的命令同样无法分类:安全网关上“无法分类”不得当成 allow
|
|
698
|
+
if (!trimmed) {
|
|
699
|
+
return { safe: false, level: 'deny', reason: tr('security.commandInvalid', undefined, tFn) };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// 0. 子命令范围:原始整串 + 链式拆分段 + 解释器内联载荷
|
|
703
|
+
// 黑名单与灰名单逐段判定,不得只看命令串开头
|
|
704
|
+
const scopes = collectCommandScopes(trimmed);
|
|
705
|
+
// 命令替换特征另有自己的送检集合(只读头命令豁免),不得复用整串
|
|
706
|
+
const substScopes = substitutionCheckScopes(trimmed, scopes);
|
|
312
707
|
|
|
313
708
|
// 1. 黑名单始终生效(即使是 force 模式也不可绕过)
|
|
314
|
-
for (const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
709
|
+
for (const scope of scopes) {
|
|
710
|
+
for (const pattern of BLACKLIST_PATTERNS) {
|
|
711
|
+
if (pattern.test(scope)) {
|
|
712
|
+
return {
|
|
713
|
+
safe: false,
|
|
714
|
+
level: 'deny',
|
|
715
|
+
reason: tr('security.commandBlocked', { command: trimmed.substring(0, 100) }, tFn)
|
|
716
|
+
};
|
|
717
|
+
}
|
|
321
718
|
}
|
|
322
719
|
}
|
|
323
720
|
|
|
@@ -340,14 +737,19 @@ function checkCommand(command, force = false, tFn) {
|
|
|
340
737
|
return { safe: true, level: 'allow' };
|
|
341
738
|
}
|
|
342
739
|
|
|
343
|
-
// 3.
|
|
344
|
-
for (const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
740
|
+
// 3. 检查灰名单(逐段:`cd x && sudo y` 里的 sudo 同样要用户确认)
|
|
741
|
+
for (const scope of scopes) {
|
|
742
|
+
for (const entry of GRAYLIST_PATTERNS) {
|
|
743
|
+
const candidates = entry.substitutionOnly ? substScopes : [scope];
|
|
744
|
+
for (const candidate of candidates) {
|
|
745
|
+
if (entry.pattern.test(candidate)) {
|
|
746
|
+
return {
|
|
747
|
+
safe: false,
|
|
748
|
+
level: 'confirm',
|
|
749
|
+
reason: tr(entry.reasonKey, undefined, tFn)
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
351
753
|
}
|
|
352
754
|
}
|
|
353
755
|
|
|
@@ -355,4 +757,4 @@ function checkCommand(command, force = false, tFn) {
|
|
|
355
757
|
return { safe: true, level: 'allow' };
|
|
356
758
|
}
|
|
357
759
|
|
|
358
|
-
export { checkPath, checkCommand, normalizePathFormat, isHardBlocked };
|
|
760
|
+
export { checkPath, checkCommand, normalizePathFormat, isHardBlocked, collectCommandScopes };
|
package/src/server.js
CHANGED
|
@@ -14,7 +14,7 @@ import { loadConfig, saveConfig, setConfigLang } from './config.js';
|
|
|
14
14
|
import { verifyToken, startPairCodeRotation, stopPairCodeRotation, handlePairRequest, setAuthLang } from './auth.js';
|
|
15
15
|
import { checkPath, checkCommand, normalizePathFormat, setSecurityLang } from './security.js';
|
|
16
16
|
import { moveToTrash, restoreFromTrash, listTrash, startPeriodicCleanup, stopPeriodicCleanup, setTrashLang } from './trash.js';
|
|
17
|
-
import { executeCommand, executeCommandSync, addWsClient, disconnectWsClient, killProcess, getRunningProcesses, setExecutorLang } from './executor.js';
|
|
17
|
+
import { executeCommand, executeCommandSync, addWsClient, disconnectWsClient, killProcess, detachProcess, getRunningProcesses, setExecutorLang } from './executor.js';
|
|
18
18
|
import { setConsoleOutput, setLoggerLocale, logAuth, logFs, logExec, logSecurity, logSystem, logError, queryLogs, getLogDates } from './logger.js';
|
|
19
19
|
import { initSearchTools, getSearchToolsAvailable, searchFiles, searchContent, setSearchLang } from './search.js';
|
|
20
20
|
import { detectRagAvailable, isRagAvailable, resetRagDetection, setRagLang, getLastRagProbeError } from './rag/detect.js';
|
|
@@ -253,7 +253,7 @@ function getAllowedOrigin(req) {
|
|
|
253
253
|
function jsonResponse(res, status, data) {
|
|
254
254
|
const headers = {
|
|
255
255
|
'Content-Type': 'application/json; charset=utf-8',
|
|
256
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
256
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Memory-Caller',
|
|
257
257
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS'
|
|
258
258
|
};
|
|
259
259
|
const allowedOrigin = getAllowedOrigin(res.req);
|
|
@@ -444,7 +444,7 @@ export function startServer() {
|
|
|
444
444
|
// CORS 预检(仅放行 Chrome 扩展来源)
|
|
445
445
|
if (req.method === 'OPTIONS') {
|
|
446
446
|
const optHeaders = {
|
|
447
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
447
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Memory-Caller',
|
|
448
448
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS'
|
|
449
449
|
};
|
|
450
450
|
const allowedOrigin = getAllowedOrigin(req);
|
|
@@ -1603,7 +1603,7 @@ export function startServer() {
|
|
|
1603
1603
|
// === 命令执行 ===
|
|
1604
1604
|
|
|
1605
1605
|
if (pathname === '/api/exec') {
|
|
1606
|
-
const { command, cwd, wait, force } = body;
|
|
1606
|
+
const { command, cwd, wait, force, timeoutMs } = body;
|
|
1607
1607
|
if (!command || typeof command !== 'string') return jsonResponse(res, 400, { success: false, error: t('error.missingCommand') });
|
|
1608
1608
|
|
|
1609
1609
|
// 校验 cwd
|
|
@@ -1628,7 +1628,7 @@ export function startServer() {
|
|
|
1628
1628
|
// 同步等待模式
|
|
1629
1629
|
if (wait) {
|
|
1630
1630
|
try {
|
|
1631
|
-
const result = await executeCommandSync(command, resolvedCwd);
|
|
1631
|
+
const result = await executeCommandSync(command, resolvedCwd, timeoutMs);
|
|
1632
1632
|
logExec('completed', {
|
|
1633
1633
|
command,
|
|
1634
1634
|
cwd: resolvedCwd,
|
|
@@ -1654,7 +1654,7 @@ export function startServer() {
|
|
|
1654
1654
|
}
|
|
1655
1655
|
}
|
|
1656
1656
|
|
|
1657
|
-
//
|
|
1657
|
+
// 异步模式(透传 timeoutMs → 受管模式;未传则维持旧语义)
|
|
1658
1658
|
const execId = executeCommand(command, resolvedCwd, null, (result) => {
|
|
1659
1659
|
logExec('completed', {
|
|
1660
1660
|
command,
|
|
@@ -1663,7 +1663,7 @@ export function startServer() {
|
|
|
1663
1663
|
exitCode: result.exitCode,
|
|
1664
1664
|
killed: result.killed
|
|
1665
1665
|
});
|
|
1666
|
-
});
|
|
1666
|
+
}, false, { timeoutMs });
|
|
1667
1667
|
logExec('started', { command, cwd: resolvedCwd, execId });
|
|
1668
1668
|
return jsonResponse(res, 200, {
|
|
1669
1669
|
success: true,
|
|
@@ -1680,6 +1680,14 @@ export function startServer() {
|
|
|
1680
1680
|
return jsonResponse(res, 200, { success: stopped, execId: body.execId });
|
|
1681
1681
|
}
|
|
1682
1682
|
|
|
1683
|
+
// 显式 daemon 化:豁免超时(插件端空闲挂起分支调用;
|
|
1684
|
+
// 替代"WS 全断开即清定时器"的隐式推断,通讯故障不再意外保活)
|
|
1685
|
+
if (pathname === '/api/exec/detach') {
|
|
1686
|
+
const detached = detachProcess(body.execId);
|
|
1687
|
+
logExec('detached', { execId: body.execId, success: detached });
|
|
1688
|
+
return jsonResponse(res, 200, { success: detached, execId: body.execId });
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1683
1691
|
// === Skill 操作(需要 body) ===
|
|
1684
1692
|
|
|
1685
1693
|
// 回收站恢复
|
|
@@ -2122,8 +2130,20 @@ export function startServer() {
|
|
|
2122
2130
|
ws.close();
|
|
2123
2131
|
return;
|
|
2124
2132
|
}
|
|
2125
|
-
//
|
|
2133
|
+
// 控制面心跳:检测半开连接(TCP 假死)。浏览器对 ping 自动回 pong,
|
|
2134
|
+
// 两拍未回视为连接已死 → terminate → 走 disconnect 收敛路径(受管模式定时器仍在)
|
|
2135
|
+
ws.isAlive = true;
|
|
2136
|
+
ws.on('pong', () => { ws.isAlive = true; });
|
|
2137
|
+
const heartbeat = setInterval(() => {
|
|
2138
|
+
if (ws.readyState !== 1) { clearInterval(heartbeat); return; }
|
|
2139
|
+
if (!ws.isAlive) { try { ws.terminate(); } catch {} clearInterval(heartbeat); return; }
|
|
2140
|
+
ws.isAlive = false;
|
|
2141
|
+
ws.ping();
|
|
2142
|
+
}, 30000);
|
|
2143
|
+
heartbeat.unref();
|
|
2144
|
+
// 客户端断开时,从监听列表中移除;所有客户端都断开后按受管/旧语义分别处理超时
|
|
2126
2145
|
ws.on('close', () => {
|
|
2146
|
+
clearInterval(heartbeat);
|
|
2127
2147
|
disconnectWsClient(execId, ws);
|
|
2128
2148
|
});
|
|
2129
2149
|
});
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{r as e}from"./rolldown-runtime-C0FnF6B9.js";import{a as t,c as n}from"./i18n-C4jSRoCH.js";import{_ as r,v as i}from"./session-store-CzVQCgQ_.js";var a=e({createAgent:()=>m,deleteAgent:()=>g,getActiveAgentId:()=>_,getAgent:()=>p,getAllAgents:()=>d,getCustomAgents:()=>f,setActiveAgentId:()=>v,updateAgent:()=>h});t(`zh`,{agentStore:{unnamedAgent:`未命名Agent`}}),t(`en`,{agentStore:{unnamedAgent:`Unnamed Agent`}});var o=`customAgents`,s=`activeAssistantId`,c=`activeAgentId`,l=!1;async function u(){if(l)return;l=!0;let e=await chrome.storage.local.get([c,s]),t=e[c],n=e[s];t&&!n&&!t.startsWith(`pa_`)&&(await chrome.storage.local.set({[s]:t}),await chrome.storage.local.remove(c))}async function d(){let e=(await chrome.storage.local.get([o]))[o]||[];return[...r,...e]}async function f(){return(await chrome.storage.local.get([o]))[o]||[]}async function p(e){return e&&(await d()).find(t=>t.id===e)||null}async function m(e){let t=await f(),r={id:i(),name:e.name||n(`agentStore.unnamedAgent`),description:e.description||``,icon:e.icon||`🤖`,systemPrompt:e.systemPrompt||``,toolIds:e.toolIds||null,skillIds:e.skillIds||null,mcpExcludedServerIds:e.mcpExcludedServerIds??null,isBuiltin:!1,allowSubDispatch:e.allowSubDispatch!==void 0&&e.allowSubDispatch,model:e.model||null,temperature:e.temperature===void 0?null:e.temperature,topP:e.topP===void 0?null:e.topP,createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};return t.push(r),await chrome.storage.local.set({[o]:t}),r}async function h(e,t){let n=await f(),r=n.findIndex(t=>t.id===e);return r===-1?null:(n[r]={...n[r],...t,id:e,isBuiltin:!1,updatedAt:new Date().toISOString()},await chrome.storage.local.set({[o]:n}),n[r])}async function g(e){let t=await f(),n=t.findIndex(t=>t.id===e);return n!==-1&&(t.splice(n,1),await chrome.storage.local.set({[o]:t}),(await chrome.storage.local.get([s]))[s]===e&&await chrome.storage.local.remove(s),!0)}async function _(){return await u(),(await chrome.storage.local.get([s]))[s]||null}async function v(e){e?await chrome.storage.local.set({[s]:e}):await chrome.storage.local.remove(s)}export{p as a,h as c,_ as i,m as n,d as o,g as r,v as s,a as t};
|
|
2
|
-
//# sourceMappingURL=agent-store-DcAs6YyA.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"agent-store-DcAs6YyA.js","names":[],"sources":["../../src/side_panel/agent-store.js"],"sourcesContent":["// side_panel/agent-store.js - Agent 数据持久化\nimport { BUILTIN_AGENTS, generateAgentId } from '../shared/agent-defaults.js';\nimport { t, registerTranslations } from '../shared/i18n.js';\n\nregisterTranslations('zh', {\n agentStore: {\n unnamedAgent: '未命名Agent',\n },\n});\nregisterTranslations('en', {\n agentStore: {\n unnamedAgent: 'Unnamed Agent',\n },\n});\n\nconst STORAGE_KEY = 'customAgents';\nconst ACTIVE_KEY = 'activeAssistantId';\nconst LEGACY_ACTIVE_KEY = 'activeAgentId';\n\nlet migratedActiveKey = false;\n\nasync function migrateLegacyActiveKey() {\n if (migratedActiveKey) return;\n migratedActiveKey = true;\n\n const result = await chrome.storage.local.get([LEGACY_ACTIVE_KEY, ACTIVE_KEY]);\n const legacyValue = result[LEGACY_ACTIVE_KEY];\n const newValue = result[ACTIVE_KEY];\n\n if (legacyValue && !newValue && !legacyValue.startsWith('pa_')) {\n await chrome.storage.local.set({ [ACTIVE_KEY]: legacyValue });\n await chrome.storage.local.remove(LEGACY_ACTIVE_KEY);\n }\n}\n\n/**\n * 获取所有 Agent(内置 + 用户自定义)\n * @returns {Promise<Array>}\n */\nexport async function getAllAgents() {\n const result = await chrome.storage.local.get([STORAGE_KEY]);\n const customAgents = result[STORAGE_KEY] || [];\n return [...BUILTIN_AGENTS, ...customAgents];\n}\n\n/**\n * 仅获取用户自定义的 Agent\n * @returns {Promise<Array>}\n */\nexport async function getCustomAgents() {\n const result = await chrome.storage.local.get([STORAGE_KEY]);\n return result[STORAGE_KEY] || [];\n}\n\n/**\n * 根据 ID 获取单个 Agent\n * @param {string} agentId\n * @returns {Promise<Object|null>}\n */\nexport async function getAgent(agentId) {\n if (!agentId) return null;\n const all = await getAllAgents();\n return all.find(a => a.id === agentId) || null;\n}\n\n/**\n * 创建新的自定义 Agent\n * @param {Object} agentData - 不含 id 的 Agent 数据\n * @returns {Promise<Object>}\n */\nexport async function createAgent(agentData) {\n const agents = await getCustomAgents();\n const newAgent = {\n id: generateAgentId(),\n name: agentData.name || t('agentStore.unnamedAgent'),\n description: agentData.description || '',\n icon: agentData.icon || '🤖',\n systemPrompt: agentData.systemPrompt || '',\n toolIds: agentData.toolIds || null, // null = 继承全局\n skillIds: agentData.skillIds || null, // null = 继承全部启用技能\n mcpExcludedServerIds: agentData.mcpExcludedServerIds ?? null, // MCP 服务排除列表(服务级 deny-list)\n isBuiltin: false,\n allowSubDispatch: agentData.allowSubDispatch !== undefined ? agentData.allowSubDispatch : false,\n model: agentData.model || null,\n temperature: agentData.temperature !== undefined ? agentData.temperature : null,\n topP: agentData.topP !== undefined ? agentData.topP : null,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n agents.push(newAgent);\n await chrome.storage.local.set({ [STORAGE_KEY]: agents });\n return newAgent;\n}\n\n/**\n * 更新已有的自定义 Agent\n * @param {string} agentId\n * @param {Object} updates\n * @returns {Promise<Object|null>}\n */\nexport async function updateAgent(agentId, updates) {\n const agents = await getCustomAgents();\n const idx = agents.findIndex(a => a.id === agentId);\n if (idx === -1) return null;\n \n agents[idx] = {\n ...agents[idx],\n ...updates,\n id: agentId, // id 不可变\n isBuiltin: false, // 不可变\n updatedAt: new Date().toISOString(),\n };\n await chrome.storage.local.set({ [STORAGE_KEY]: agents });\n return agents[idx];\n}\n\n/**\n * 删除自定义 Agent\n * @param {string} agentId\n * @returns {Promise<boolean>}\n */\nexport async function deleteAgent(agentId) {\n const agents = await getCustomAgents();\n const idx = agents.findIndex(a => a.id === agentId);\n if (idx === -1) return false;\n \n agents.splice(idx, 1);\n await chrome.storage.local.set({ [STORAGE_KEY]: agents });\n\n // 如果删除的 Agent 是当前选中的,重置为默认\n const result = await chrome.storage.local.get([ACTIVE_KEY]);\n if (result[ACTIVE_KEY] === agentId) {\n await chrome.storage.local.remove(ACTIVE_KEY);\n }\n \n return true;\n}\n\n/**\n * 获取当前选中的 Agent ID\n * @returns {Promise<string|null>}\n */\nexport async function getActiveAgentId() {\n await migrateLegacyActiveKey();\n const result = await chrome.storage.local.get([ACTIVE_KEY]);\n return result[ACTIVE_KEY] || null;\n}\n\n/**\n * 设置当前选中的 Agent ID\n * @param {string|null} agentId\n */\nexport async function setActiveAgentId(agentId) {\n if (agentId) {\n await chrome.storage.local.set({ [ACTIVE_KEY]: agentId });\n } else {\n await chrome.storage.local.remove(ACTIVE_KEY);\n }\n}\n\n/**\n * 从模板创建 Agent\n * @param {Object} template\n * @returns {Promise<Object>}\n */\nexport async function createAgentFromTemplate(template) {\n return createAgent({\n name: template.name,\n icon: template.icon,\n description: template.description,\n systemPrompt: template.systemPrompt,\n toolIds: template.toolIds ? [...template.toolIds] : null,\n allowSubDispatch: template.allowSubDispatch || false,\n });\n}\n"],"mappings":"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"}
|