@zqagent/core 0.3.13 → 0.3.16
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/{TurnEngine-BZjUGic_.d.ts → TurnEngine-C8qqbilC.d.ts} +18 -3
- package/dist/{TurnEngine-CMGdcSdp.d.cts → TurnEngine-DKSczYaA.d.cts} +18 -3
- package/dist/{conflictGuard-BBOOIxxK.d.cts → conflictGuard-BuICnWEI.d.cts} +8 -0
- package/dist/{conflictGuard-BBOOIxxK.d.ts → conflictGuard-BuICnWEI.d.ts} +8 -0
- package/dist/index.cjs +3372 -1136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +221 -6
- package/dist/index.d.ts +221 -6
- package/dist/index.js +3391 -1155
- package/dist/index.js.map +1 -1
- package/dist/internal/engine/FileLogHook.d.cts +1 -1
- package/dist/internal/engine/FileLogHook.d.ts +1 -1
- package/dist/internal/update/controller.cjs +1 -1
- package/dist/internal/update/controller.js +1 -1
- package/dist/internal/update/platform.cjs +1 -1
- package/dist/internal/update/platform.js +1 -1
- package/dist/tools/diff.d.cts +1 -1
- package/dist/tools/diff.d.ts +1 -1
- package/dist/tools/explore.cjs +504 -127
- package/dist/tools/explore.cjs.map +1 -1
- package/dist/tools/explore.d.cts +1 -1
- package/dist/tools/explore.d.ts +1 -1
- package/dist/tools/explore.js +504 -127
- package/dist/tools/explore.js.map +1 -1
- package/dist/tools/files.cjs +267 -47
- package/dist/tools/files.cjs.map +1 -1
- package/dist/tools/files.d.cts +1 -1
- package/dist/tools/files.d.ts +1 -1
- package/dist/tools/files.js +267 -47
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/search.cjs +103 -24
- package/dist/tools/search.cjs.map +1 -1
- package/dist/tools/search.d.cts +8 -2
- package/dist/tools/search.d.ts +8 -2
- package/dist/tools/search.js +103 -24
- package/dist/tools/search.js.map +1 -1
- package/package.json +6 -5
package/dist/tools/search.js
CHANGED
|
@@ -513,12 +513,61 @@ function resolveRipgrep() {
|
|
|
513
513
|
}
|
|
514
514
|
|
|
515
515
|
// src/tools/search.ts
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
516
|
+
var DEFAULT_MAX_RESULTS = 50;
|
|
517
|
+
var MAX_RESULTS_CAP = 200;
|
|
518
|
+
var MAX_TEXT_CHARS = 200;
|
|
519
|
+
var RG_TIMEOUT_MS = 1e4;
|
|
520
|
+
function parseRgOutput(result, limit) {
|
|
521
|
+
const out = [];
|
|
522
|
+
for (const raw of result.split("\n")) {
|
|
523
|
+
if (out.length >= limit) break;
|
|
524
|
+
const line = raw.replace(/\r$/, "");
|
|
525
|
+
if (!line) continue;
|
|
526
|
+
const m = /^(.+?)[:-](\d+)[:-](.*)$/.exec(line);
|
|
527
|
+
if (!m) continue;
|
|
528
|
+
out.push({
|
|
529
|
+
file: m[1],
|
|
530
|
+
line: Number(m[2]),
|
|
531
|
+
text: m[3].trim().slice(0, MAX_TEXT_CHARS)
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
function parseRgFiles(result, limit) {
|
|
537
|
+
const files = [];
|
|
538
|
+
const seen = /* @__PURE__ */ new Set();
|
|
539
|
+
for (const raw of result.split("\n")) {
|
|
540
|
+
if (files.length >= limit) break;
|
|
541
|
+
const f = raw.replace(/\r$/, "").trim();
|
|
542
|
+
if (!f || seen.has(f)) continue;
|
|
543
|
+
seen.add(f);
|
|
544
|
+
files.push(f);
|
|
545
|
+
}
|
|
546
|
+
return files;
|
|
547
|
+
}
|
|
548
|
+
function rgArgs(opts) {
|
|
549
|
+
const args = [];
|
|
550
|
+
if (opts.filesOnly) {
|
|
551
|
+
args.push("--files-with-matches");
|
|
552
|
+
} else {
|
|
553
|
+
args.push("--with-filename", "--line-number", "--no-heading");
|
|
554
|
+
args.push("--max-count", String(Math.max(opts.maxResults, 1)));
|
|
555
|
+
}
|
|
556
|
+
if (opts.ignoreCase) args.push("--ignore-case");
|
|
557
|
+
if (opts.glob) args.push("--glob", opts.glob);
|
|
558
|
+
if (opts.contextLines > 0 && !opts.filesOnly) {
|
|
559
|
+
args.push("--context", String(opts.contextLines));
|
|
560
|
+
}
|
|
561
|
+
args.push("--", opts.pattern, opts.path);
|
|
562
|
+
return args;
|
|
563
|
+
}
|
|
564
|
+
function rgFailureHint(res) {
|
|
565
|
+
if (res.timedOut) {
|
|
566
|
+
return `\u641C\u7D22\u8D85\u65F6\uFF08${RG_TIMEOUT_MS / 1e3}s\uFF09\u3002\u5927\u4ED3\u5E93\u5168\u91CF\u641C\u7D22\u5F88\u5BB9\u6613\u8D85\u65F6\uFF1A\u8BF7\u7F29\u5C0F path \u5230\u5177\u4F53\u5B50\u76EE\u5F55\uFF0C\u6216\u7528 glob \u53C2\u6570\u9650\u5236\u6587\u4EF6\u7C7B\u578B\uFF08\u5982 "*.ts"\uFF09\u540E\u91CD\u8BD5`;
|
|
567
|
+
}
|
|
568
|
+
if (res.aborted) return "\u641C\u7D22\u5DF2\u4E2D\u65AD";
|
|
569
|
+
const stderr = decodeOutput(res.stderr).trim().slice(0, 300);
|
|
570
|
+
return `\u641C\u7D22\u5931\u8D25\uFF08rg \u9000\u51FA\u7801 ${String(res.status)}\uFF09${stderr ? `\uFF1A${stderr}` : ""}`;
|
|
522
571
|
}
|
|
523
572
|
function makeGrep(workspace) {
|
|
524
573
|
return async function grep(args) {
|
|
@@ -527,37 +576,50 @@ function makeGrep(workspace) {
|
|
|
527
576
|
if (!pattern.trim()) {
|
|
528
577
|
return { error: "\u641C\u7D22\u6A21\u5F0F\u4E0D\u80FD\u4E3A\u7A7A" };
|
|
529
578
|
}
|
|
579
|
+
const filesOnly = args.files_only === true;
|
|
580
|
+
const ignoreCase = args.ignore_case === true;
|
|
581
|
+
const glob = typeof args.glob === "string" && args.glob.trim() ? args.glob.trim() : void 0;
|
|
582
|
+
const contextLines = Math.max(
|
|
583
|
+
0,
|
|
584
|
+
Math.min(Number(args.context_lines ?? 0) || 0, 5)
|
|
585
|
+
);
|
|
586
|
+
const maxResults = Math.max(
|
|
587
|
+
1,
|
|
588
|
+
Math.min(Number(args.max_results ?? DEFAULT_MAX_RESULTS) || DEFAULT_MAX_RESULTS, MAX_RESULTS_CAP)
|
|
589
|
+
);
|
|
530
590
|
const rg = resolveRipgrep();
|
|
531
591
|
if (rg) {
|
|
532
592
|
const res = await execAsync(
|
|
533
593
|
rg,
|
|
534
|
-
|
|
535
|
-
// --with-filename:单文件模式下 rg 默认不输出文件名前缀,会导致
|
|
536
|
-
// "行号:内容" 被误当成 "文件:行号:内容" 解析而错位;显式要求带文件名。
|
|
537
|
-
"--with-filename",
|
|
538
|
-
"--line-number",
|
|
539
|
-
"--no-heading",
|
|
540
|
-
"--max-count",
|
|
541
|
-
"50",
|
|
542
|
-
pattern,
|
|
543
|
-
filePath
|
|
544
|
-
],
|
|
594
|
+
rgArgs({ pattern, path: filePath, filesOnly, ignoreCase, glob, contextLines, maxResults }),
|
|
545
595
|
{
|
|
546
596
|
cwd: workspace,
|
|
547
|
-
timeoutMs:
|
|
597
|
+
timeoutMs: RG_TIMEOUT_MS,
|
|
548
598
|
maxBuffer: 1024 * 1024 * 2
|
|
549
599
|
}
|
|
550
600
|
);
|
|
551
601
|
if (res.status === 0) {
|
|
552
|
-
const
|
|
553
|
-
return { query: pattern, engine: "ripgrep",
|
|
602
|
+
const out = decodeOutput(res.stdout);
|
|
603
|
+
return filesOnly ? { query: pattern, engine: "ripgrep", files: parseRgFiles(out, maxResults) } : (() => {
|
|
604
|
+
const matches = parseRgOutput(out, maxResults);
|
|
605
|
+
return { query: pattern, engine: "ripgrep", matches, count: matches.length };
|
|
606
|
+
})();
|
|
554
607
|
}
|
|
555
608
|
if (res.status === 1) {
|
|
556
|
-
return { query: pattern, engine: "ripgrep", matches: [], count: 0 };
|
|
609
|
+
return filesOnly ? { query: pattern, engine: "ripgrep", files: [], count: 0 } : { query: pattern, engine: "ripgrep", matches: [], count: 0 };
|
|
557
610
|
}
|
|
611
|
+
if (!res.error) {
|
|
612
|
+
return { query: pattern, error: rgFailureHint(res) };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (filesOnly || glob || ignoreCase || contextLines > 0) {
|
|
616
|
+
return {
|
|
617
|
+
query: pattern,
|
|
618
|
+
error: "\u5F53\u524D\u8282\u70B9\u672A\u5B89\u88C5 ripgrep\uFF0Cfiles_only / glob / ignore_case / context_lines \u9700\u8981 ripgrep \u624D\u80FD\u751F\u6548\uFF1B\u8BF7\u53BB\u6389\u8FD9\u4E9B\u53C2\u6570\u540E\u91CD\u8BD5\uFF0C\u6216\u5148\u5B89\u88C5 ripgrep"
|
|
619
|
+
};
|
|
558
620
|
}
|
|
559
621
|
try {
|
|
560
|
-
const matches = nodeGrep({ pattern, target: filePath, workspace });
|
|
622
|
+
const matches = nodeGrep({ pattern, target: filePath, workspace, maxMatches: maxResults });
|
|
561
623
|
return { query: pattern, engine: "node", matches, count: matches.length };
|
|
562
624
|
} catch (exc) {
|
|
563
625
|
return { error: `\u641C\u7D22\u5931\u8D25: ${String(exc instanceof Error ? exc.message : exc)}` };
|
|
@@ -573,12 +635,29 @@ function registerSearchTool(registry, workspace) {
|
|
|
573
635
|
type: "function",
|
|
574
636
|
function: {
|
|
575
637
|
name: "grep",
|
|
576
|
-
description: "\u5728\u6587\u4EF6\u4E2D\u641C\u7D22\u6587\u672C\u6A21\u5F0F\uFF08\u652F\u6301\u5B8C\u6574 JS \u6B63\u5219\u8BED\u6CD5\uFF09\u3002\u8FD4\u56DE\u5339\u914D\u7684\u6587\u4EF6\u540D\u3001\u884C\u53F7\u548C\u5185\u5BB9\
|
|
638
|
+
description: "\u5728\u6587\u4EF6\u4E2D\u641C\u7D22\u6587\u672C\u6A21\u5F0F\uFF08\u652F\u6301\u5B8C\u6574 JS \u6B63\u5219\u8BED\u6CD5\uFF09\u3002\u9ED8\u8BA4\u8FD4\u56DE\u5339\u914D\u7684\u6587\u4EF6\u540D\u3001\u884C\u53F7\u548C\u5185\u5BB9\uFF08\u6700\u591A 50 \u6761\uFF09\u3002\u5B9A\u4F4D\u4EE3\u7801\u4F4D\u7F6E\u65F6\u4F18\u5148\u7528 files_only=true \u53EA\u53D6\u6587\u4EF6\u5217\u8868\uFF08\u4F53\u79EF\u5C0F\u4E00\u4E2A\u6570\u91CF\u7EA7\uFF09\uFF0C\u518D\u7528 read_file \u7CBE\u51C6\u8BFB\u53D6\uFF1B\u5927\u4ED3\u5E93\u8BF7\u914D\u5408 path \u7F29\u5C0F\u8303\u56F4\u6216 glob \u9650\u5236\u6587\u4EF6\u7C7B\u578B\uFF0C\u907F\u514D\u5168\u91CF\u626B\u63CF\u8D85\u65F6\u3002",
|
|
577
639
|
parameters: {
|
|
578
640
|
type: "object",
|
|
579
641
|
properties: {
|
|
580
642
|
pattern: { type: "string", description: "\u641C\u7D22\u6A21\u5F0F (\u6587\u672C\u6216\u6B63\u5219\u8868\u8FBE\u5F0F)" },
|
|
581
|
-
path: { type: "string", description: "\u641C\u7D22\u8DEF\u5F84 (\u6587\u4EF6\u6216\u76EE\u5F55\uFF0C\u9ED8\u8BA4 '.')" }
|
|
643
|
+
path: { type: "string", description: "\u641C\u7D22\u8DEF\u5F84 (\u6587\u4EF6\u6216\u76EE\u5F55\uFF0C\u9ED8\u8BA4 '.'\uFF1B\u5927\u4ED3\u5E93\u5EFA\u8BAE\u5148\u7F29\u5C0F\u5230\u5B50\u76EE\u5F55)" },
|
|
644
|
+
files_only: {
|
|
645
|
+
type: "boolean",
|
|
646
|
+
description: "\u53EA\u8FD4\u56DE\u5339\u914D\u7684\u6587\u4EF6\u8DEF\u5F84\uFF0C\u4E0D\u8FD4\u56DE\u884C\u5185\u5BB9\uFF08\u63A8\u8350\u7528\u4E8E\u5B9A\u4F4D\uFF0C\u663E\u8457\u8282\u7701\u4E0A\u4E0B\u6587\uFF09"
|
|
647
|
+
},
|
|
648
|
+
glob: {
|
|
649
|
+
type: "string",
|
|
650
|
+
description: '\u6587\u4EF6\u8FC7\u6EE4\uFF0C\u5982 "*.ts" \u6216 "!*.min.js"\uFF08\u53EF\u591A\u6B21\u6392\u9664\u9700\u81EA\u884C\u7EC4\u5408\u4E3A {a,b} \u5F62\u5F0F\uFF09'
|
|
651
|
+
},
|
|
652
|
+
ignore_case: { type: "boolean", description: "\u5FFD\u7565\u5927\u5C0F\u5199" },
|
|
653
|
+
context_lines: {
|
|
654
|
+
type: "integer",
|
|
655
|
+
description: "\u6BCF\u6761\u547D\u4E2D\u9644\u5E26\u7684\u524D\u540E\u4E0A\u4E0B\u6587\u884C\u6570\uFF080-5\uFF0C\u9ED8\u8BA4 0\uFF09"
|
|
656
|
+
},
|
|
657
|
+
max_results: {
|
|
658
|
+
type: "integer",
|
|
659
|
+
description: `\u6700\u591A\u8FD4\u56DE\u6761\u6570\uFF08\u9ED8\u8BA4 ${DEFAULT_MAX_RESULTS}\uFF0C\u4E0A\u9650 ${MAX_RESULTS_CAP}\uFF09`
|
|
660
|
+
}
|
|
582
661
|
},
|
|
583
662
|
required: ["pattern"]
|
|
584
663
|
}
|
package/dist/tools/search.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tools/execAsync.ts","../../src/tools/killTree.ts","../../src/tools/shellRuntime.ts","../../src/tools/shellBackground.ts","../../src/tools/sandbox.ts","../../src/tools/shell.ts","../../src/tools/nodeGrep.ts","../../src/tools/ripgrep.ts","../../src/tools/search.ts"],"sourcesContent":["/**\n * 异步子进程执行(spawnSync 的异步替身)\n *\n * 为什么需要:spawnSync 期间 Node 事件循环被整体阻塞,单进程节点上多个会话\n * 并发跑命令时会互相拖死——一条长命令(run_shell / ffmpeg 可达数百秒)就让\n * server 停止响应,SSE 心跳、HTTP 请求、其他会话的工具调用全部排队。\n * 换成异步 spawn 后,等待子进程的时间可以正常处理事件循环。\n *\n * 与 spawnSync 的差异(异步 spawn 不自带的语义在这里补齐):\n * - 超时:自行计时 + SIGKILL,结果里给出 `timedOut` 布尔\n * (spawnSync 是 ETIMEDOUT 错误码);\n * - 中断:监听 opts.signal,abort 时终止整棵进程树,结果里给出 `aborted` 布尔;\n * - 结算兜底:kill 后若仍有后代持有管道(`close` 不触发),宽限期到点强制结算,\n * 保证 Promise 一定有界,绝不永久挂起;\n * - 输出上限:自行累积 + 截断,结果里给出 `truncated` 布尔\n * (spawnSync 是 ENOBUFS 错误);\n * - stdout/stderr 保留原始 Buffer,交调用方的 decodeOutput 做编码回退。\n *\n * 参数一律用数组传递,不经 shell 解释,保持与原先一致的注入防护。\n */\nimport { spawn } from \"node:child_process\";\nimport { killTree } from \"./killTree.js\";\n\nexport interface ExecAsyncOptions {\n cwd?: string;\n /** 超时毫秒数;到点 SIGKILL 子进程并置 timedOut。不传则不限时 */\n timeoutMs?: number;\n /** stdout / stderr 各自的累积上限(字节);超出部分丢弃并置 truncated */\n maxBuffer?: number;\n env?: NodeJS.ProcessEnv;\n /** 需要向 stdin 写入的内容(如 git apply 的补丁);不传则 stdin 关闭 */\n input?: string;\n /**\n * 中断信号:abort 时终止整棵进程树并置 aborted。\n * 用于把 TurnEngine 的用户中断传导到正在执行的命令。\n */\n signal?: AbortSignal;\n}\n\nexport interface ExecAsyncResult {\n /** 退出码;被信号杀死或启动失败时为 null */\n status: number | null;\n stdout: Buffer;\n stderr: Buffer;\n /** 仅启动失败(命令不存在 ENOENT、无执行权限等)时存在 */\n error?: NodeJS.ErrnoException;\n timedOut: boolean;\n truncated: boolean;\n /** 是否因 signal abort 被终止 */\n aborted?: boolean;\n}\n\n/**\n * 存活的子进程(pid)。子进程以 detached 启动(自成进程组,便于整组终止),\n * 因此终端 Ctrl+C 不再波及它们;进程退出时统一收掉,避免遗留孤儿。\n */\nconst liveChildren = new Set<number>();\nlet exitCleanupInstalled = false;\n\nfunction trackChild(pid: number | undefined): void {\n if (!pid) return;\n liveChildren.add(pid);\n if (exitCleanupInstalled) return;\n exitCleanupInstalled = true;\n process.once(\"exit\", () => {\n for (const p of liveChildren) killTree(p);\n });\n}\n\nconst DEFAULT_MAX_BUFFER = 1024 * 1024 * 5;\n\n/**\n * 终止进程树后等待 stdio 关闭的宽限期(毫秒)。\n *\n * 结算挂在 `close` 上,而 `close` 要求子进程的 stdout/stderr 全部关闭。子进程虽以\n * detached 启动(自成进程组,killTree 的 `-pid` 覆盖整组),但仍有后代存活的可能:\n * detached 未生效、`process.kill(-pid)` 抛错走了只杀直接 pid 的兜底分支、或后代\n * 自行 setsid / 被 reparent 脱离进程组。此时管道写端不关,`close` 永不触发,\n * Promise 永久挂起(会话卡在 running,interrupt 也因等不到安全点而无效)。\n */\nconst KILL_GRACE_MS = 1000;\n\n/** 累积器:到上限后丢弃后续数据,但仍消费流避免子进程写阻塞 */\nfunction makeSink(maxBuffer: number): {\n push: (chunk: Buffer) => void;\n done: () => Buffer;\n over: () => boolean;\n} {\n const chunks: Buffer[] = [];\n let len = 0;\n let over = false;\n return {\n push(chunk: Buffer) {\n if (len >= maxBuffer) {\n over = true;\n return;\n }\n const room = maxBuffer - len;\n if (chunk.length > room) {\n chunks.push(chunk.subarray(0, room));\n len = maxBuffer;\n over = true;\n return;\n }\n chunks.push(chunk);\n len += chunk.length;\n },\n done: () => Buffer.concat(chunks),\n over: () => over,\n };\n}\n\nexport function execAsync(\n file: string,\n argv: string[],\n opts: ExecAsyncOptions = {},\n): Promise<ExecAsyncResult> {\n return new Promise<ExecAsyncResult>((resolve) => {\n const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;\n const out = makeSink(maxBuffer);\n const err = makeSink(maxBuffer);\n\n // 调用前已被中断:不必启动进程\n if (opts.signal?.aborted) {\n resolve({\n status: null,\n stdout: Buffer.alloc(0),\n stderr: Buffer.alloc(0),\n timedOut: false,\n truncated: false,\n aborted: true,\n });\n return;\n }\n\n let child: ReturnType<typeof spawn>;\n try {\n child = spawn(file, argv, {\n ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),\n ...(opts.env !== undefined ? { env: opts.env } : {}),\n // 自成进程组(POSIX):超时/中断时用 killTree 一次收掉整棵进程树\n detached: process.platform !== \"win32\",\n stdio: [opts.input === undefined ? \"ignore\" : \"pipe\", \"pipe\", \"pipe\"],\n windowsHide: true,\n });\n } catch (e) {\n resolve({\n status: null,\n stdout: Buffer.alloc(0),\n stderr: Buffer.alloc(0),\n error: e as NodeJS.ErrnoException,\n timedOut: false,\n truncated: false,\n });\n return;\n }\n\n trackChild(child.pid);\n\n let settled = false;\n let timedOut = false;\n let aborted = false;\n let spawnError: NodeJS.ErrnoException | undefined;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let graceTimer: ReturnType<typeof setTimeout> | null = null;\n\n const onAbort = (): void => {\n aborted = true;\n killAndSettle();\n };\n\n const finish = (status: number | null): void => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n if (graceTimer) clearTimeout(graceTimer);\n if (child.pid !== undefined) liveChildren.delete(child.pid);\n opts.signal?.removeEventListener(\"abort\", onAbort);\n const result: ExecAsyncResult = {\n status,\n stdout: out.done(),\n stderr: err.done(),\n timedOut,\n truncated: out.over() || err.over(),\n };\n if (aborted) result.aborted = true;\n if (spawnError) result.error = spawnError;\n resolve(result);\n };\n\n /**\n * 终止子进程树,并在宽限期内仍未结算时强制结算。\n *\n * 只调 killTree 是不够的:结算依赖 `close`,而后代存活会让它永不触发\n * (原因见 KILL_GRACE_MS 注释)——本次会话卡死的直接成因。\n */\n const killAndSettle = (): void => {\n killTree(child.pid);\n if (settled || graceTimer) return;\n graceTimer = setTimeout(() => {\n // 输出已随 kill 前的 data 事件收完;这里放下可能仍被后代持有的管道,\n // 结算语义已由 timedOut / aborted 确定,不再继续等待\n child.stdout?.destroy();\n child.stderr?.destroy();\n finish(null);\n }, KILL_GRACE_MS);\n };\n\n if (opts.timeoutMs !== undefined && opts.timeoutMs > 0) {\n timer = setTimeout(() => {\n timedOut = true;\n killAndSettle();\n }, opts.timeoutMs);\n }\n\n opts.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n child.stdout?.on(\"data\", (c: Buffer) => out.push(c));\n child.stderr?.on(\"data\", (c: Buffer) => err.push(c));\n\n child.on(\"error\", (e) => {\n spawnError = e as NodeJS.ErrnoException;\n // 进程根本没起来时不会有 close 事件,立即结算避免调用方永久挂起\n if (child.pid === undefined) finish(null);\n });\n\n if (opts.input !== undefined && child.stdin) {\n // 子进程可能已退出(EPIPE),忽略写入错误\n child.stdin.on(\"error\", () => {});\n child.stdin.end(opts.input);\n }\n\n child.on(\"close\", (code) => finish(code));\n });\n}\n","/**\n * 终止整棵进程树\n *\n * 前台命令(execAsync 的超时/中断)与后台任务(ShellTaskManager.kill)共用:\n * 只有以 detached 启动的子进程才是进程组组长,负 pid 才能覆盖其派生的孙进程。\n */\nimport { spawnSync } from \"node:child_process\";\n\nexport function killTree(pid: number | undefined): void {\n if (!pid) return;\n if (process.platform === \"win32\") {\n try {\n spawnSync(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { windowsHide: true });\n } catch {\n /* 尽力而为 */\n }\n return;\n }\n // POSIX:detached 启动使子进程成为进程组组长,负 pid 即整组\n try {\n process.kill(-pid, \"SIGKILL\");\n } catch {\n try {\n process.kill(pid, \"SIGKILL\");\n } catch {\n /* 已退出 */\n }\n }\n}\n","/**\n * shell 运行时探测\n *\n * 决定 run_shell 用哪个 shell 执行命令,解决三件事:\n * - POSIX:沿用 Node 默认 /bin/sh,不改变既有语义;\n * - Windows:cmd.exe 没有 ls/grep/rm/cat,优先挑一个 POSIX 风格的 shell\n * (BusyBox-w32 / Git Bash / PowerShell),都没有时才回退到 %ComSpec%;\n * - 允许用 ZQAGENT_SHELL / ZQAGENT_BUSYBOX 显式覆盖,便于在装了非常规\n * shell(如内网自带的 busybox)的机器上接入。\n *\n * 本模块只做解析,不执行命令;platform / env / 文件探测均可注入,\n * 便于在 CI(Linux)上覆盖 Windows 分支。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/** shell 家族,决定命令行参数怎么拼 */\nexport type ShellKind = \"sh\" | \"busybox\" | \"powershell\" | \"cmd\" | \"default\";\n\nexport interface ShellSpec {\n kind: ShellKind;\n /** 人类可读标签,用于工具描述 */\n label: string;\n /** 可执行文件路径;null 表示交给 Node 默认(POSIX /bin/sh 或 %ComSpec%) */\n file: string | null;\n /** 拼出实际 argv(含命令本身) */\n buildArgs: (command: string) => string[];\n}\n\nexport interface ResolveShellOptions {\n platform?: NodeJS.Platform;\n env?: NodeJS.ProcessEnv;\n /** 判断文件是否存在,便于测试注入 */\n fileExists?: (p: string) => boolean;\n /** 用于定位「产物同目录」的 busybox,默认 process.execPath */\n execPath?: string;\n}\n\nexport interface ProbeContext {\n platform: NodeJS.Platform;\n env: NodeJS.ProcessEnv;\n fileExists: (p: string) => boolean;\n}\n\n/** Windows 可执行文件后缀(按优先级) */\nconst WINDOWS_SUFFIXES = [\".exe\", \".cmd\", \".bat\", \".com\", \"\"];\n\nfunction pathApi(platform: NodeJS.Platform) {\n return platform === \"win32\" ? path.win32 : path.posix;\n}\n\nfunction defaultFileExists(p: string): boolean {\n try {\n return fs.statSync(p).isFile();\n } catch {\n return false;\n }\n}\n\n/** 按目标平台(而非宿主)拼路径 */\nfunction joinPath(platform: NodeJS.Platform, ...parts: string[]): string {\n return pathApi(platform).join(...parts);\n}\n\n/**\n * 在 PATH 上查找可执行文件(同步,不 spawn 进程)。\n * name 含路径分隔符时按路径直接判定。Windows 会按 WINDOWS_SUFFIXES 补后缀。\n */\nexport function findExecutable(name: string, ctx: ProbeContext): string | null {\n if (name.includes(\"/\") || name.includes(\"\\\\\")) {\n return ctx.fileExists(name) ? name : null;\n }\n const suffixes = ctx.platform === \"win32\" ? WINDOWS_SUFFIXES : [\"\"];\n const delimiter = ctx.platform === \"win32\" ? \";\" : \":\";\n const dirs = (ctx.env.PATH ?? \"\").split(delimiter).filter((d) => d.length > 0);\n for (const dir of dirs) {\n for (const suffix of suffixes) {\n const candidate = joinPath(ctx.platform, dir, name + suffix);\n if (ctx.fileExists(candidate)) return candidate;\n }\n }\n return null;\n}\n\n/** 按文件名推断 shell 家族 */\nfunction kindFromBasename(base: string): ShellKind {\n const b = base.toLowerCase().replace(/\\.(exe|cmd|bat|com)$/, \"\");\n if (b === \"busybox\") return \"busybox\";\n if (b === \"powershell\" || b === \"pwsh\") return \"powershell\";\n if (b === \"cmd\") return \"cmd\";\n // sh/bash/dash/zsh/ash 以及不认识的名字(如 busybox 的 sh.exe 符号链接)都按 sh 处理\n return \"sh\";\n}\n\n/**\n * busybox 按 argv[0] 决定 applet:`busybox.exe` 需要显式给 applet 名\n * (`busybox.exe sh -c \"...\"`),而重命名/硬链接出来的 `sh.exe` 本身就是 ash,\n * 多传一个 \"sh\" 会被当成脚本文件名。\n */\nfunction busyboxArgs(file: string | null, command: string): string[] {\n const base = (file ? path.win32.basename(file) : \"busybox\")\n .toLowerCase()\n .replace(/\\.(exe|cmd|bat|com)$/, \"\");\n return base === \"busybox\" ? [\"sh\", \"-c\", command] : [\"-c\", command];\n}\n\nfunction argsFor(kind: ShellKind, command: string): string[] {\n switch (kind) {\n case \"busybox\":\n return [\"sh\", \"-c\", command];\n case \"powershell\":\n return [\"-NoProfile\", \"-NonInteractive\", \"-Command\", command];\n case \"cmd\":\n return [\"/d\", \"/s\", \"/c\", command];\n default:\n return [\"-c\", command];\n }\n}\n\nfunction makeSpec(kind: ShellKind, file: string | null, label: string): ShellSpec {\n return {\n kind,\n file,\n label,\n buildArgs: (command) =>\n kind === \"busybox\" ? busyboxArgs(file, command) : argsFor(kind, command),\n };\n}\n\nfunction defaultLabelFor(kind: ShellKind): string {\n switch (kind) {\n case \"busybox\":\n return \"busybox ash\";\n case \"powershell\":\n return \"PowerShell\";\n case \"cmd\":\n return \"cmd.exe\";\n case \"default\":\n return \"系统默认 shell\";\n default:\n return \"sh\";\n }\n}\n\n/**\n * 查找 BusyBox-w32。\n * 显式 ZQAGENT_BUSYBOX 优先;否则看 CLI 产物同目录及其 bin/ 子目录\n * (兼容 SEA 单文件、npm 安装与桌面 sidecar 三种布局,其中 sh.exe 是\n * busybox.exe 的重命名副本 —— busybox 按 argv[0] 决定 applet)。\n */\nfunction findBusybox(\n ctx: ProbeContext,\n execPath: string,\n): string | null {\n const explicit = (ctx.env.ZQAGENT_BUSYBOX ?? \"\").trim();\n if (explicit) return ctx.fileExists(explicit) ? explicit : null;\n\n const exeDir = pathApi(ctx.platform).dirname(execPath);\n const candidates = [\n joinPath(ctx.platform, exeDir, \"busybox.exe\"),\n joinPath(ctx.platform, exeDir, \"sh.exe\"),\n joinPath(ctx.platform, exeDir, \"bin\", \"busybox.exe\"),\n joinPath(ctx.platform, exeDir, \"bin\", \"sh.exe\"),\n ];\n for (const candidate of candidates) {\n if (ctx.fileExists(candidate)) return candidate;\n }\n return null;\n}\n\n/** 解析 ZQAGENT_SHELL 的显式取值(关键字或路径) */\nfunction resolveRequested(requested: string, ctx: ProbeContext, execPath: string): ShellSpec {\n const lower = requested.toLowerCase();\n\n if (lower === \"cmd\") {\n const file =\n findExecutable(\"cmd\", ctx) ?? ctx.env.ComSpec ?? ctx.env.COMSPEC ?? \"cmd.exe\";\n return makeSpec(\"cmd\", file, \"cmd.exe\");\n }\n if (lower === \"powershell\" || lower === \"pwsh\") {\n const file =\n findExecutable(lower, ctx) ?? findExecutable(\"powershell\", ctx) ?? `${lower}.exe`;\n return makeSpec(\"powershell\", file, \"PowerShell\");\n }\n if (lower === \"busybox\") {\n const found = findBusybox(ctx, execPath) ?? findExecutable(\"busybox\", ctx);\n return makeSpec(\"busybox\", found ?? requested, \"busybox ash\");\n }\n if ([\"sh\", \"bash\", \"dash\", \"zsh\", \"ash\"].includes(lower)) {\n return makeSpec(\"sh\", findExecutable(lower, ctx) ?? lower, lower);\n }\n\n // 视为路径(可能含空格,如 C:\\Program Files\\Git\\bin\\bash.exe)\n const base = pathApi(ctx.platform).basename(requested);\n const kind = kindFromBasename(base);\n return makeSpec(kind, requested, base || defaultLabelFor(kind));\n}\n\n/**\n * 解析当前应使用的 shell。\n * 返回值 file === null 时表示「不显式指定」,由 Node 走平台默认 shell\n * (POSIX /bin/sh / Windows %ComSpec%),即改造前的行为。\n */\nexport function resolveShellSpec(opts: ResolveShellOptions = {}): ShellSpec {\n const platform = opts.platform ?? process.platform;\n const env = opts.env ?? process.env;\n const execPath = opts.execPath ?? process.execPath;\n const ctx: ProbeContext = {\n platform,\n env,\n fileExists: opts.fileExists ?? defaultFileExists,\n };\n\n const requested = (env.ZQAGENT_SHELL ?? \"\").trim();\n if (requested && requested.toLowerCase() !== \"auto\") {\n return resolveRequested(requested, ctx, execPath);\n }\n\n // 非 Windows:保持 Node 默认(不改变既有行为)\n if (platform !== \"win32\") {\n return makeSpec(\"default\", null, \"/bin/sh\");\n }\n\n // Windows:优先 POSIX 风格 shell —— cmd.exe 缺 ls/grep/rm/cat,模型产出的\n // POSIX 命令在它下面几乎全废。\n const busybox = findBusybox(ctx, execPath);\n if (busybox) return makeSpec(\"busybox\", busybox, \"busybox ash (POSIX)\");\n\n const bash = findExecutable(\"bash\", ctx);\n if (bash) return makeSpec(\"sh\", bash, \"bash (Git for Windows)\");\n\n // PowerShell 排在 cmd 之前:它有 ls/cat/rm/cp/mv/pwd 别名,更接近模型习惯\n const shell = findExecutable(\"pwsh\", ctx) ?? findExecutable(\"powershell\", ctx);\n if (shell) return makeSpec(\"powershell\", shell, \"PowerShell\");\n\n return makeSpec(\"default\", null, env.ComSpec ?? env.COMSPEC ?? \"cmd.exe\");\n}\n\n/**\n * 为命令套上「编码设置 + 退出码传播」外壳,按 shell 家族区分。\n *\n * - PowerShell:先设置控制台输出编码为 UTF-8(同一次调用内即对后续命令生效,\n * 规避 `chcp 65001` 要等下一次调用才生效的问题),命令结束后若存在\n * `$LASTEXITCODE` 就以它作为进程退出码 —— 否则 PowerShell 对 `cmd /c ...`\n * 这类原生命令一律返回 0,工具拿到的 exit_code 不可信。\n * - cmd.exe(含 Windows 下未探测到其它 shell 时的 default):`chcp 65001` 切换\n * 代码页,避免中文被压成 `?`。\n * - POSIX sh / busybox:现代环境默认 UTF-8,不改动。\n */\nexport function wrapCommand(\n kind: ShellKind,\n command: string,\n platform: NodeJS.Platform = process.platform,\n): string {\n if (kind === \"powershell\") {\n return (\n \"$OutputEncoding = [Console]::OutputEncoding = [Text.Encoding]::UTF8; \" +\n command +\n \"\\nif ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE }\"\n );\n }\n if (kind === \"cmd\" || (kind === \"default\" && platform === \"win32\")) {\n return `chcp 65001>nul & ${command}`;\n }\n return command;\n}\n\n/** 生成给模型的运行环境提示,追加到 run_shell 描述里 */\nexport function describeShell(spec: ShellSpec, platform: NodeJS.Platform): string {\n if (spec.kind === \"sh\" || spec.kind === \"busybox\") {\n return `当前平台 ${platform},shell=${spec.label}:请使用 POSIX 风格命令。`;\n }\n if (spec.kind === \"powershell\") {\n return (\n `当前平台 ${platform},shell=PowerShell:ls/cat/rm/cp/mv/pwd 等别名可用,` +\n `也可用 cmdlet;路径分隔符为 \"\\\\\"。`\n );\n }\n if (spec.kind === \"default\" && platform !== \"win32\") {\n return `当前平台 ${platform},shell=${spec.label}:请使用 POSIX 风格命令。`;\n }\n return (\n `当前平台 ${platform},shell=${spec.label}:请使用 Windows 命令` +\n `(dir/type/del/copy/move/findstr),多条命令用 & 或 && 连接;` +\n `不要使用 ls/cat/rm/grep 等 POSIX 命令。`\n );\n}\n\n/**\n * 解析命令里的「切目录」意图,返回目录与剩余命令。\n *\n * 识别 POSIX 的 `cd dir`、PowerShell 的 `Set-Location`/`sl`、cmd 的 `cd /d dir`,\n * 以及 `pushd`/`Push-Location`(只跟踪目标目录;popd 的栈语义不在此处理)。\n * 带 `&&` 时(如 `cd /d D:\\x && dir`)只把切目录部分交给 cwd 跟踪,剩余命令\n * 在新目录下执行 —— 否则目录变更只影响那一个子 shell,无法跨调用保持。\n *\n * target 为 null 时调用方应原样执行命令(未识别到切目录意图)。\n */\nexport function parseCdChain(\n command: string,\n opts: { platform?: NodeJS.Platform } = {},\n): { target: string | null; rest: string | null } {\n const platform = opts.platform ?? process.platform;\n const trimmed = command.trim();\n\n const chain = /^([\\s\\S]*?)\\s*&&\\s*([\\s\\S]+)$/.exec(trimmed);\n const head = (chain ? chain[1] : trimmed).trim();\n const rest = chain ? chain[2].trim() : null;\n\n const m = /^(?:cd|set-location|sl|pushd|push-location)\\b\\s*([\\s\\S]*)$/i.exec(head);\n if (!m) return { target: null, rest: null };\n\n let target = m[1].trim();\n // PowerShell 显式参数与 cmd 的盘符切换\n target = target.replace(/^-(?:path|literalpath)\\s+/i, \"\");\n if (platform === \"win32\") target = target.replace(/^\\/d\\s+/i, \"\");\n // 去引号\n target = target.replace(/^\"(.*)\"$/, \"$1\").replace(/^'(.*)'$/, \"$1\").trim();\n\n // 裸 `cd` / `cd -` 不跟踪(无法确定目标目录)\n if (!target || target === \"-\") return { target: null, rest: null };\n return { target, rest };\n}\n","/**\n * 后台 shell 任务管理器\n *\n * 支撑 run_shell 的 `run_in_background` 能力(开发服务器 / watch 进程等长驻任务):\n * - start():detached 启动,stdout/stderr 进环形缓冲,立即返回 task_id;\n * - output():读取某任务的累计输出与状态;\n * - kill():终止整棵进程树(Windows 用 taskkill /T /F,POSIX 用进程组信号);\n * - disposeAll():进程退出时统一清理,避免残留子进程。\n *\n * 输出缓冲有上限(默认 64KB/流),超出丢弃最旧内容,防止长驻任务把内存吃满。\n */\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { killTree } from \"./killTree.js\";\n\n/** 每个输出流的缓冲上限(字符) */\nconst DEFAULT_BUFFER_LIMIT = 64 * 1024;\n\n/** 固定长度的环形字符缓冲:超出上限时从头部丢弃 */\nclass RingBuffer {\n private chunks: string[] = [];\n private size = 0;\n\n constructor(private readonly limit = DEFAULT_BUFFER_LIMIT) {}\n\n add(chunk: string): void {\n if (!chunk) return;\n this.chunks.push(chunk);\n this.size += chunk.length;\n while (this.size > this.limit && this.chunks.length > 1) {\n this.size -= this.chunks.shift()!.length;\n }\n // 单个 chunk 就可能超限:直接截取尾部\n if (this.size > this.limit) {\n const joined = this.chunks.join(\"\");\n this.chunks = [joined.slice(-this.limit)];\n this.size = this.chunks[0]!.length;\n }\n }\n\n get(): string {\n const joined = this.chunks.join(\"\");\n return joined.length > this.limit ? joined.slice(-this.limit) : joined;\n }\n}\n\ninterface BackgroundTask {\n id: string;\n command: string;\n /** 归属标识(server 传租户 id),用于按项目过滤任务列表 */\n scope: string | null;\n child: ChildProcess;\n stdout: RingBuffer;\n stderr: RingBuffer;\n status: \"running\" | \"exited\" | \"killed\";\n exitCode: number | null;\n signal: NodeJS.Signals | null;\n startedAt: number;\n endedAt: number | null;\n}\n\nexport interface BackgroundTaskSnapshot {\n task_id: string;\n command: string;\n status: \"running\" | \"exited\" | \"killed\";\n running: boolean;\n exit_code: number | null;\n signal: string | null;\n stdout: string;\n stderr: string;\n duration_ms: number;\n}\n\n/**\n * 推送用的精简任务快照。\n *\n * **刻意不含 stdout/stderr**:两路各 64KB,原样塞进通知会比轮询更费带宽与内存。\n * 需要输出时仍走 `output(taskId)` 按需读取。\n */\nexport interface ShellTaskLite {\n task_id: string;\n command: string;\n status: \"running\" | \"exited\" | \"killed\";\n running: boolean;\n exit_code: number | null;\n signal: string | null;\n duration_ms: number;\n /** 启动时刻(epoch ms):前端展开时据此本地累计耗时,不必再轮询 */\n started_at: number;\n}\n\n/** 任务变化回调:scope 为任务的归属(server 传租户 id),tasks 只含运行中的任务 */\nexport type ShellTasksListener = (\n scope: string | null,\n tasks: ShellTaskLite[],\n) => void;\n\nexport interface StartOptions {\n command: string;\n file: string;\n argv: string[];\n cwd: string;\n env: NodeJS.ProcessEnv;\n /** 归属标识(如租户 id),用于按项目过滤任务列表 */\n scope?: string | null;\n}\n\nexport class ShellTaskManager {\n private readonly tasks = new Map<string, BackgroundTask>();\n private seq = 0;\n private cleanupInstalled = false;\n /** 任务集合变化监听(server 用它推送,替代控制台的 5s 轮询) */\n private listener: ShellTasksListener | null = null;\n\n /** 注册变化监听(传 null 取消) */\n setListener(cb: ShellTasksListener | null): void {\n this.listener = cb;\n }\n\n /** 启动一个后台任务,立即返回快照(不等退出) */\n start(opts: StartOptions): BackgroundTaskSnapshot {\n this.installCleanup();\n const id = `shell-${++this.seq}-${Date.now().toString(36)}`;\n const child = spawn(opts.file, opts.argv, {\n cwd: opts.cwd,\n env: opts.env,\n detached: process.platform !== \"win32\",\n windowsHide: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n const task: BackgroundTask = {\n id,\n command: opts.command,\n scope: opts.scope ?? null,\n child,\n stdout: new RingBuffer(),\n stderr: new RingBuffer(),\n status: \"running\",\n exitCode: null,\n signal: null,\n startedAt: Date.now(),\n endedAt: null,\n };\n this.tasks.set(id, task);\n\n child.stdout?.on(\"data\", (d: Buffer) => task.stdout.add(d.toString(\"utf-8\")));\n child.stderr?.on(\"data\", (d: Buffer) => task.stderr.add(d.toString(\"utf-8\")));\n child.on(\"error\", (err) => {\n task.stderr.add(`[spawn error] ${err.message}\\n`);\n // spawn 失败(如 ENOENT)不会再触发 exit:不在这里收敛,界面上会永远显示「运行中」\n if (task.status === \"running\") {\n task.endedAt = Date.now();\n task.status = \"exited\";\n this._notify(task.scope);\n }\n });\n child.on(\"exit\", (code, signal) => {\n task.exitCode = code;\n task.signal = signal;\n task.endedAt = Date.now();\n // 已被 kill() 标记为 killed 的保持 killed,否则记为正常退出\n if (task.status !== \"killed\") task.status = \"exited\";\n this._notify(task.scope);\n });\n // 允许父进程退出后子进程继续(配合 disposeAll 显式清理)\n child.unref();\n\n const snap = this.snapshot(task);\n // 先返回快照再通知:订阅方拿到推送时任务已可查询\n this._notify(task.scope);\n return snap;\n }\n\n /** 读取任务输出与状态 */\n output(taskId: string): BackgroundTaskSnapshot | { error: string } {\n const task = this.tasks.get(taskId);\n if (!task) return { error: `未知的后台任务: ${taskId}` };\n return this.snapshot(task);\n }\n\n /** 终止任务(进程树)并返回快照;传 scope 时只允许终止该归属下的任务 */\n kill(taskId: string, scope?: string | null): BackgroundTaskSnapshot | { error: string } {\n const task = this.tasks.get(taskId);\n if (!task) return { error: `未知的后台任务: ${taskId}` };\n if (scope !== undefined && task.scope !== scope) {\n return { error: `未知的后台任务: ${taskId}` };\n }\n if (task.status === \"running\") {\n task.status = \"killed\";\n task.endedAt = Date.now();\n killTree(task.child.pid);\n this._notify(task.scope);\n }\n return this.snapshot(task);\n }\n\n /** 列出任务快照(调试/展示用);传 scope 时只返回该归属下的任务 */\n list(scope?: string | null): BackgroundTaskSnapshot[] {\n const all = [...this.tasks.values()];\n return (scope === undefined ? all : all.filter((t) => t.scope === scope)).map((t) =>\n this.snapshot(t),\n );\n }\n\n /** 终止全部任务(进程退出时调用) */\n disposeAll(): void {\n for (const task of this.tasks.values()) {\n if (task.status === \"running\") {\n task.status = \"killed\";\n killTree(task.child.pid);\n }\n }\n }\n\n /**\n * 通知订阅方「该归属下运行中的任务变了」。\n *\n * 只遍历内存任务表取字段,不调用 snapshot(后者会 join 两路 64KB 缓冲)。\n */\n private _notify(scope: string | null): void {\n const listener = this.listener;\n if (!listener) return;\n const tasks: ShellTaskLite[] = [];\n for (const task of this.tasks.values()) {\n if (task.scope !== scope || task.status !== \"running\") continue;\n tasks.push({\n task_id: task.id,\n command: task.command,\n status: task.status,\n running: true,\n exit_code: task.exitCode,\n signal: task.signal,\n duration_ms: Date.now() - task.startedAt,\n started_at: task.startedAt,\n });\n }\n try {\n listener(scope, tasks);\n } catch {\n // 推送失败不影响任务本身\n }\n }\n\n private snapshot(task: BackgroundTask): BackgroundTaskSnapshot {\n return {\n task_id: task.id,\n command: task.command,\n status: task.status,\n running: task.status === \"running\",\n exit_code: task.exitCode,\n signal: task.signal,\n stdout: task.stdout.get(),\n stderr: task.stderr.get(),\n duration_ms: (task.endedAt ?? Date.now()) - task.startedAt,\n };\n }\n\n private installCleanup(): void {\n if (this.cleanupInstalled) return;\n this.cleanupInstalled = true;\n process.once(\"exit\", () => this.disposeAll());\n }\n}\n","/**\n * Shell 沙箱(OS 级文件系统约束)\n *\n * 目标:让 `run_shell` 在工作区内可以免审批执行,而不是靠命令名白名单\n * 逐个放行——后者既挡不住 `python -c` 这类间接执行,也让日常开发寸步难行。\n *\n * 各平台取当前可用的最优方案:\n * - macOS:`/usr/bin/sandbox-exec`(SBPL profile)。系统自带,进程级,\n * 毫秒级开销;默认放行读与网络,只把 `file-write*` 收窄到工作区 + 临时目录。\n * - Linux:`bubblewrap`(bwrap)。用 `--ro-bind / /` 把整机只读挂载,\n * 再把工作区 `--bind` 成可写,二者叠加即为「只在工作区内可写」。\n * 需要发行版装了 bwrap 且允许非特权 user namespace(容器里通常需\n * `--security-opt seccomp=unconfined` 或 privileged)。\n * - Windows:没有进程级文件系统沙箱的等价物(AppContainer 需要额外的\n * 原生 helper 与 ACL 配置,不属于零依赖方案)。此处明确返回\n * `enforced: false`,由权限层退化为「工作区路径启发式」判定\n * (见 permissions/workspaceGuard.ts)——它是降低打扰的策略,不是安全边界。\n *\n * 探测结果做进程级缓存:CLI 在 /mode、/agent 等场景会重建引擎,\n * 但沙箱能力不会变,不必每次 spawn 探测进程。\n */\nimport { spawnSync } from \"node:child_process\";\nimport * as fs from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\n\n/** 沙箱后端 */\nexport type SandboxBackend = \"sandbox-exec\" | \"bwrap\" | \"none\";\n\n/** 配置取值:auto=探测到就用;off=禁用;required=探测不到时明确报错 */\nexport type SandboxMode = \"auto\" | \"off\" | \"required\";\n\nexport interface SandboxOptions {\n mode?: SandboxMode;\n /** 额外可写路径(绝对路径,支持 ~;相对路径按 workspace 解析) */\n extraWritable?: string[];\n /** 工作区根(解析相对路径的额外可写项) */\n workspace?: string;\n platform?: NodeJS.Platform;\n env?: NodeJS.ProcessEnv;\n fileExists?: (p: string) => boolean;\n /** 可用性探测(测试注入);提供时不走缓存 */\n probe?: (backend: Exclude<SandboxBackend, \"none\">, bin: string) => boolean;\n}\n\n/** 沙箱方案(可跨调用复用) */\nexport interface SandboxPlan {\n backend: SandboxBackend;\n /** OS 级文件系统约束是否生效 */\n enforced: boolean;\n /** 沙箱可执行文件;未生效时为 null */\n bin: string | null;\n /** 允许写入的路径(绝对路径,已去重) */\n writable: string[];\n mode: SandboxMode;\n /** 状态说明(日志 / 工具描述用) */\n reason: string;\n}\n\n/** 一次待执行的子进程调用(file + argv,不经 shell 解释) */\nexport interface SandboxInvocation {\n file: string;\n argv: string[];\n}\n\n/** 解析 ~ 与环境变量无关的常见写法 */\nfunction expandHome(p: string, env: NodeJS.ProcessEnv): string {\n if (p === \"~\") return env.HOME ?? p;\n if (p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) {\n return path.join(env.HOME ?? \"~\", p.slice(2));\n }\n return p;\n}\n\n/**\n * 工具链缓存目录:这些必须可写,否则 `pnpm install` / `npm ci` 会因\n * store / cache 落在工作区外而报 EPERM。只放行缓存与包仓库,\n * 不放行 `~/.ssh`、shell 配置等敏感位置。\n */\nfunction toolchainWritable(\n platform: NodeJS.Platform,\n env: NodeJS.ProcessEnv,\n): string[] {\n const home = env.HOME ?? os.homedir();\n const rel =\n platform === \"darwin\"\n ? [\"Library/pnpm\", \"Library/Caches\", \".npm\", \".cache\"]\n : [\".npm\", \".cache\", \".local/share/pnpm\", \".local/state/pnpm\", \".pnpm-store\"];\n return rel.map((r) => path.join(home, r));\n}\n\n/**\n * 加入工具链缓存目录。\n *\n * 只加**真实存在**的目录:bwrap 对不存在的 bind 源会直接报错并终止整个命令\n * (`Can't find source path ~/.pnpm-store`),一个没装过的缓存目录就能让沙箱\n * 里所有命令失败。缓存目录不存在本身也无所谓——第一次 `pnpm install` 会自己建。\n */\nfunction addToolchainWritable(\n platform: NodeJS.Platform,\n env: NodeJS.ProcessEnv,\n add: (p: string) => void,\n exists: (p: string) => boolean,\n): void {\n for (const dir of toolchainWritable(platform, env)) {\n if (exists(dir)) add(dir);\n }\n}\n\n/** 收集去重后的可写路径(工作区 + 临时目录 + 用户追加) */\nfunction collectWritable(opts: {\n workspace: string;\n extraWritable: string[];\n platform: NodeJS.Platform;\n env: NodeJS.ProcessEnv;\n /** 路径存在性判定(测试可注入) */\n exists: (p: string) => boolean;\n}): string[] {\n const out = new Set<string>();\n const add = (p: string): void => {\n if (!p) return;\n const abs = path.resolve(p);\n out.add(abs);\n // macOS 的 /tmp、/var 都是符号链接,SBPL 按真实路径匹配,两个都加上\n try {\n out.add(fs.realpathSync(abs));\n } catch {\n /* 路径尚不存在:只保留原始绝对路径 */\n }\n };\n\n add(opts.workspace);\n\n if (opts.platform === \"darwin\") {\n const tmp = os.tmpdir();\n add(tmp);\n add(\"/tmp\");\n add(\"/private/tmp\");\n add(\"/dev\");\n addToolchainWritable(opts.platform, opts.env, add, opts.exists);\n } else if (opts.platform !== \"win32\") {\n add(\"/tmp\");\n add(\"/dev\");\n addToolchainWritable(opts.platform, opts.env, add, opts.exists);\n }\n\n for (const raw of opts.extraWritable) {\n const expanded = expandHome(String(raw ?? \"\").trim(), opts.env);\n if (!expanded) continue;\n add(path.resolve(opts.workspace, expanded));\n }\n\n return [...out];\n}\n\n/** 默认探测:真正跑一次最小命令,确认后端在本机可用(含 user namespace 限制) */\nfunction defaultProbe(\n backend: Exclude<SandboxBackend, \"none\">,\n bin: string,\n): boolean {\n const args =\n backend === \"sandbox-exec\"\n ? [\"-p\", \"(version 1)(allow default)\", \"/bin/sh\", \"-c\", \"true\"]\n : [\"--ro-bind\", \"/\", \"/\", \"--dev-bind\", \"/dev\", \"/dev\", \"--\", \"/bin/true\"];\n try {\n const r = spawnSync(bin, args, { timeout: 3000, stdio: \"ignore\" });\n return r.status === 0;\n } catch {\n return false;\n }\n}\n\n/** 是否为正则化的模式取值 */\nfunction normalizeMode(raw: string | undefined): SandboxMode {\n const v = String(raw ?? \"\").trim().toLowerCase();\n if (v === \"off\" || v === \"none\" || v === \"false\") return \"off\";\n if (v === \"required\" || v === \"on\" || v === \"true\") return \"required\";\n return \"auto\";\n}\n\n/** 缓存键:同平台、同模式、同工作区、同追加路径只探测一次 */\nconst planCache = new Map<string, SandboxPlan>();\n\n/**\n * 解析当前平台可用的沙箱方案。\n *\n * `mode: \"off\"` 直接返回未生效;`required` 在探测失败时通过 `reason`\n * 说明原因(调用方可据此拒绝启动或提示用户安装 bwrap)。\n */\nexport function resolveSandbox(opts: SandboxOptions = {}): SandboxPlan {\n const platform = opts.platform ?? process.platform;\n const env = opts.env ?? process.env;\n const workspace = path.resolve(opts.workspace ?? process.cwd());\n const mode = normalizeMode(opts.mode ?? env.ZQAGENT_SANDBOX);\n const extraWritable = opts.extraWritable ?? [];\n // 目录(而非文件)的存在性:缓存目录不存在时不能写进 bind 列表,\n // 否则 bwrap 会因为「源路径不存在」让整条命令失败。\n // 复用 fileExists 注入点,测试可用它模拟「目录都存在」。\n const pathExists: (p: string) => boolean =\n opts.fileExists ??\n ((p: string) => {\n try {\n return fs.existsSync(p);\n } catch {\n return false;\n }\n });\n const writable = collectWritable({\n workspace,\n extraWritable,\n platform,\n env,\n exists: pathExists,\n });\n\n const cacheKey = [\n platform,\n mode,\n workspace,\n writable.join(\"\\u0000\"),\n opts.probe ? \"injected\" : \"default\",\n ].join(\"|\");\n const cached = planCache.get(cacheKey);\n if (cached && !opts.probe) return cached;\n\n const finish = (plan: SandboxPlan): SandboxPlan => {\n if (!opts.probe) planCache.set(cacheKey, plan);\n return plan;\n };\n\n if (mode === \"off\") {\n return finish({\n backend: \"none\",\n enforced: false,\n bin: null,\n writable,\n mode,\n reason: \"沙箱已按配置关闭\",\n });\n }\n\n const fileExists = opts.fileExists ?? ((p: string) => {\n try {\n return fs.statSync(p).isFile();\n } catch {\n return false;\n }\n });\n\n const candidate: { backend: Exclude<SandboxBackend, \"none\">; bin: string } | null =\n platform === \"darwin\"\n ? { backend: \"sandbox-exec\", bin: \"/usr/bin/sandbox-exec\" }\n : platform === \"linux\"\n ? { backend: \"bwrap\", bin: \"/usr/bin/bwrap\" }\n : null;\n\n if (!candidate) {\n return finish({\n backend: \"none\",\n enforced: false,\n bin: null,\n writable,\n mode,\n reason:\n platform === \"win32\"\n ? \"Windows 无零依赖的进程级文件系统沙箱,改用工作区路径判定\"\n : `平台 ${platform} 无受支持的沙箱后端`,\n });\n }\n\n if (!fileExists(candidate.bin)) {\n return finish({\n backend: \"none\",\n enforced: false,\n bin: null,\n writable,\n mode,\n reason: `未找到 ${candidate.bin}(${\n candidate.backend === \"bwrap\" ? \"安装 bubblewrap 后可启用\" : \"系统缺少 sandbox-exec\"\n }),改用工作区路径判定`,\n });\n }\n\n const probe = opts.probe ?? defaultProbe;\n if (!probe(candidate.backend, candidate.bin)) {\n return finish({\n backend: \"none\",\n enforced: false,\n bin: null,\n writable,\n mode,\n reason: `${candidate.bin} 探测失败(容器内可能禁用了 user namespace),改用工作区路径判定`,\n });\n }\n\n return finish({\n backend: candidate.backend,\n enforced: true,\n bin: candidate.bin,\n writable,\n mode,\n reason:\n candidate.backend === \"sandbox-exec\"\n ? \"macOS sandbox-exec:只允许写入工作区与临时目录\"\n : \"Linux bubblewrap:只允许写入工作区与临时目录\",\n });\n}\n\n/** SBPL 字符串字面量转义 */\nfunction sbplString(value: string): string {\n return `\"${value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/**\n * 生成 macOS sandbox profile。\n *\n * 采用「allow default + deny file-write* + 白名单放行」而不是\n * 「deny default」:后者要逐项放行进程、信号、sysctl、mach lookup 等,\n * 极易把 git / node / 包管理器弄挂,维护成本也高。这里只收窄写权限,\n * 读与网络保持默认——与 bwrap 方案的能力边界一致。\n */\nexport function buildMacProfile(writable: string[]): string {\n const rules = writable.map((p) => ` (subpath ${sbplString(p)})`).join(\"\\n\");\n return [\n \"(version 1)\",\n \"(allow default)\",\n \"(deny file-write*)\",\n \"(allow file-write*\",\n rules,\n \")\",\n ].join(\"\\n\");\n}\n\n/** 生成 bubblewrap 的参数前缀 */\nexport function buildBwrapArgs(writable: string[], cwd: string): string[] {\n const args: string[] = [\"--ro-bind\", \"/\", \"/\", \"--dev-bind\", \"/dev\", \"/dev\"];\n // /tmp 用独立 tmpfs:避免把宿主机 /tmp 暴露成可写(工作区内的 /tmp 除外)\n if (!writable.includes(\"/tmp\")) args.push(\"--tmpfs\", \"/tmp\");\n for (const dir of writable) {\n if (dir === \"/dev\" || dir === \"/proc\") continue;\n args.push(\"--bind\", dir, dir);\n }\n args.push(\"--chdir\", cwd, \"--die-with-parent\");\n return args;\n}\n\n/**\n * 把一次 shell 调用包进沙箱。\n *\n * 未生效时原样返回(调用方无需分支)。参数一律走 argv,不拼字符串,\n * 避免用户命令中的引号 / 空格影响外层命令。\n */\nexport function wrapWithSandbox(\n plan: SandboxPlan | null | undefined,\n invocation: SandboxInvocation,\n ctx: { cwd: string },\n): SandboxInvocation {\n if (!plan || !plan.enforced || !plan.bin) return invocation;\n\n if (plan.backend === \"sandbox-exec\") {\n return {\n file: plan.bin,\n argv: [\"-p\", buildMacProfile(plan.writable), invocation.file, ...invocation.argv],\n };\n }\n\n return {\n file: plan.bin,\n argv: [...buildBwrapArgs(plan.writable, ctx.cwd), \"--\", invocation.file, ...invocation.argv],\n };\n}\n\n/** 一行状态说明,用于日志与工具描述 */\nexport function describeSandbox(plan: SandboxPlan | null | undefined): string {\n if (!plan) return \"未配置\";\n return plan.enforced ? plan.reason : `未启用(${plan.reason})`;\n}\n\n/** 测试用:清空探测缓存 */\nexport function resetSandboxCache(): void {\n planCache.clear();\n}\n","/**\n * Shell 工具\n *\n * 提供 run_shell 工具(对齐 Python 版 LocalExecutor):\n * 在持久化 shell 会话中执行命令。cwd 在多次调用之间保持\n * (支持 cd / pushd 命令切换目录,状态按工作区存放,引擎重建也不丢),\n * 输出截断到末尾部分,超时与缓冲区大小均有上限。\n *\n * 用哪个 shell 由 shellRuntime 决定:POSIX 下沿用 Node 默认 /bin/sh\n * (行为不变),Windows 下优先 POSIX 风格 shell(busybox / Git Bash /\n * PowerShell),都没有才回退 cmd.exe。\n *\n * 执行统一走异步 execAsync:成功与失败都能拿到真实的 stdout / stderr / exit_code\n * (旧实现用 execSync,成功分支把 stderr 丢弃、exit_code 恒为 0);异步而非\n * spawnSync,避免命令执行期间阻塞事件循环、拖住其他会话与心跳。\n * 另外支持 run_in_background,配合 shell_task_output / shell_task_kill\n * 管理开发服务器等长驻进程。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { ToolRegistry } from \"./ToolRegistry.js\";\nimport { execAsync } from \"./execAsync.js\";\nimport {\n describeShell,\n parseCdChain,\n resolveShellSpec,\n wrapCommand,\n type ShellSpec,\n} from \"./shellRuntime.js\";\nimport { ShellTaskManager } from \"./shellBackground.js\";\nimport type { BackgroundTaskSnapshot, ShellTasksListener } from \"./shellBackground.js\";\nimport type { ToolExecContext } from \"./ToolRegistry.js\";\nimport {\n describeSandbox,\n wrapWithSandbox,\n type SandboxPlan,\n} from \"./sandbox.js\";\n\n/** 输出截断上限(字符) */\nconst MAX_STDOUT = 10000;\nconst MAX_STDERR = 5000;\n/** 单次命令输出缓冲上限(字节) */\nconst MAX_BUFFER = 1024 * 1024 * 5;\n\n/**\n * 持久 shell 会话状态:按「工作区 + 归属」存放 cwd。\n *\n * 之所以放在模块级而不是 makeRunShell 的闭包里:CLI 在 /mode、/agent、/model、\n * reset 时会重建引擎(重新调用 registerShellTool),闭包里的 cwd 会随之丢失。\n *\n * 键必须带上归属(owner):多个会话共享同一 workspaceRoot 时,如果只按工作区\n * 做键,A 会话 `cd src` 会让 B 会话的下一条命令也在 src 下执行——两个并行会话\n * 互相串目录。owner 由调用方给出会话级标识(server 传 tenant/session)。\n * 未给 owner 时(CLI 单会话)退化为按工作区,保持既有行为。\n */\nconst shellSessions = new Map<string, { cwd: string }>();\n\n/** 会话键:工作区绝对路径 + 可选的归属标识(用 NUL 分隔,不会与路径字符冲突) */\nfunction shellKey(workspace: string, owner?: string | null): string {\n // 用规范化后的绝对路径作键:避免尾斜杠 / 相对路径在不同注册点产生两个会话\n const root = path.resolve(workspace);\n const o = String(owner ?? \"\").trim();\n return o ? `${root}\\u0000${o}` : root;\n}\n\nfunction sessionFor(workspace: string, owner?: string | null): { cwd: string } {\n const key = shellKey(workspace, owner);\n let s = shellSessions.get(key);\n if (!s) {\n s = { cwd: path.resolve(workspace) };\n shellSessions.set(key, s);\n }\n return s;\n}\n\n/** 测试用:重置某个工作区的 shell 会话状态 */\nexport function resetShellSession(workspace: string, owner?: string | null): void {\n shellSessions.delete(shellKey(workspace, owner));\n}\n\n/** 当前 shell 会话的 cwd(供权限引擎判定相对路径是否越界) */\nexport function getShellCwd(workspace: string, owner?: string | null): string {\n return sessionFor(workspace, owner).cwd;\n}\n\n/** 解析实际执行的 shell 可执行文件与 argv 构造器(default 时按平台兜底) */\nfunction resolveInvocation(\n spec: ShellSpec,\n prepared: string,\n): { file: string; argv: string[] } {\n if (spec.file !== null) {\n return { file: spec.file, argv: spec.buildArgs(prepared) };\n }\n if (process.platform === \"win32\") {\n const comspec = process.env.ComSpec ?? process.env.COMSPEC ?? \"cmd.exe\";\n return { file: comspec, argv: [\"/d\", \"/s\", \"/c\", prepared] };\n }\n return { file: \"/bin/sh\", argv: [\"-c\", prepared] };\n}\n\n/** 供 run_shell 与后台任务共用的环境变量 */\nfunction shellEnv(): NodeJS.ProcessEnv {\n return {\n ...process.env,\n GIT_TERMINAL_PROMPT: \"0\",\n PYTHONUNBUFFERED: \"1\",\n PYTHONUTF8: \"1\",\n PYTHONIOENCODING: \"utf-8\",\n };\n}\n\n/**\n * 解码子进程输出。\n *\n * 正常情况下命令前已注入 UTF-8 编码设置,输出就是 UTF-8。但 PowerShell 的\n * **语法错误(ParserError)** 会在脚本执行前就抛出,此时编码设置那行还没跑,\n * stderr 用的是控制台 OEM 代码页(中文 Windows 为 936/GBK)——直接按 UTF-8\n * 读会得到乱码。这里先严格按 UTF-8 解,失败再回退到 GBK 等常见编码。\n */\nexport function decodeOutput(buf: Buffer | null | undefined): string {\n if (!buf || buf.length === 0) return \"\";\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(buf);\n } catch {\n for (const enc of [\"gbk\", \"big5\", \"shift_jis\", \"latin1\"]) {\n try {\n return new TextDecoder(enc).decode(buf);\n } catch {\n /* 当前 Node 构建不支持该编码,继续尝试 */\n }\n }\n return buf.toString(\"latin1\");\n }\n}\n\n/**\n * run_shell — 持久化 shell (对齐 Python 版 LocalExecutor)\n *\n * 内部维护 cwd 状态:识别 cd / pushd 命令并更新,使后续命令在新目录下执行。\n * 注意:不要用 require()(ESM 下未定义),必须用 import 的 fs/path。\n */\nfunction makeRunShell(\n workspace: string,\n spec: ShellSpec,\n tasks: ShellTaskManager,\n scope: string | null,\n sandbox: SandboxPlan | null,\n sessionKey: string | null,\n) {\n const session = sessionFor(workspace, sessionKey);\n\n return async function run_shell(args: Record<string, unknown>, ctx?: ToolExecContext) {\n const command = String(args.command ?? \"\");\n const description = String(args.description ?? \"\").slice(0, 100);\n const background = args.run_in_background === true;\n // 超时上限 600s,换算为毫秒;非法值回退默认 120s\n const rawTimeout = Number(args.timeout_seconds ?? 120);\n const timeoutSec = Number.isFinite(rawTimeout) ? rawTimeout : 120;\n const timeout = Math.min(Math.max(timeoutSec, 1), 600) * 1000;\n\n if (!command.trim()) {\n return { error: \"命令不能为空\" };\n }\n\n // 切目录:在执行前解析,这样 `cd dir && 其他命令` 的剩余部分能在新目录下跑\n // (否则目录变更只作用于那个子 shell,无法跨调用保持)。\n const { target, rest } = parseCdChain(command);\n let toRun = command;\n if (target) {\n try {\n const resolved = path.resolve(session.cwd, target);\n if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n session.cwd = resolved;\n }\n } catch {\n /* ignore */\n }\n if (rest) toRun = rest;\n }\n\n const prepared = wrapCommand(spec.kind, toRun);\n // 沙箱在这里套:命令仍是完整的 shell 调用,只是外层多一层\n // sandbox-exec / bwrap,用来把可写范围收窄到工作区\n const { file, argv } = wrapWithSandbox(\n sandbox,\n resolveInvocation(spec, prepared),\n { cwd: session.cwd },\n );\n const env = shellEnv();\n\n // ---- 后台执行 ----\n if (background) {\n const snap = tasks.start({ command, file, argv, cwd: session.cwd, env, scope });\n return {\n task_id: snap.task_id,\n status: snap.status,\n cwd: session.cwd,\n note: \"已在后台启动。用 shell_task_output 查看输出,shell_task_kill 终止。\",\n };\n }\n\n // ---- 前台执行 ----\n // 不用 encoding: \"utf-8\",先拿原始字节再自适应解码(见 decodeOutput)。\n const result = await execAsync(file, argv, {\n cwd: session.cwd,\n timeoutMs: timeout,\n maxBuffer: MAX_BUFFER,\n env,\n signal: ctx?.signal,\n });\n\n const stdout = decodeOutput(result.stdout).slice(-MAX_STDOUT);\n const stderr = decodeOutput(result.stderr).slice(-MAX_STDERR);\n\n // 被用户中断:报告为中断而非普通失败,引擎随即按中断收尾\n if (result.aborted) {\n return {\n stdout,\n stderr,\n exit_code: result.status ?? 1,\n cwd: session.cwd,\n interrupted: true,\n error: \"命令已被用户中断\",\n };\n }\n\n // 非超时的启动错误(如 shell 不存在)单独报告\n if (result.error && !result.timedOut) {\n const message = result.error.message;\n return {\n stdout,\n stderr: stderr || message,\n exit_code: 1,\n cwd: session.cwd,\n error: message.slice(0, 200),\n };\n }\n\n return {\n stdout,\n stderr,\n exit_code: result.status ?? 1,\n cwd: session.cwd,\n ...(result.timedOut\n ? { timed_out: true, error: `命令超时(${timeoutSec}s)后已终止` }\n : {}),\n };\n };\n}\n\n/**\n * 后台任务表:与 shellSessions 同理,放模块级单例。\n * 若每次注册都新建,引擎重建后旧 task_id 会查不到、后台进程也失去跟踪。\n */\nconst shellTasks = new ShellTaskManager();\n\n/** 列出后台任务快照;scope 为 undefined 时返回全部(控制台按项目过滤用) */\nexport function listShellTasks(scope?: string | null): BackgroundTaskSnapshot[] {\n return shellTasks.list(scope);\n}\n\n/** 终止后台任务(连同进程树);scope 用于限定归属,防止跨项目误杀 */\nexport function killShellTask(\n taskId: string,\n scope?: string | null,\n): BackgroundTaskSnapshot | { error: string } {\n return shellTasks.kill(taskId, scope);\n}\n\n/**\n * 订阅后台任务集合变化(启动 / 退出 / 被终止)。\n *\n * 传 null 取消订阅。server 用它把变化推给控制台,替代对 `/shell/tasks` 的轮询。\n */\nexport function setShellTasksListener(cb: ShellTasksListener | null): void {\n shellTasks.setListener(cb);\n}\n\n/**\n * 注册 Shell 工具(高风险:requiresApproval=true,由权限引擎门控)\n *\n * @param scope 后台任务归属(server 传租户 id);用于按项目过滤任务列表与终止\n * @param sandbox OS 级沙箱方案(未启用时传 null 或省略)\n * @param sessionKey shell 会话归属(server 传 tenant/session)。多会话共享同一\n * 工作区时必须传,否则它们的 cwd 会互相覆盖;CLI 单会话可省略\n */\nexport function registerShellTool(\n registry: ToolRegistry,\n workspace: string,\n scope?: string | null,\n sandbox?: SandboxPlan | null,\n sessionKey?: string | null,\n): void {\n const spec = resolveShellSpec();\n const tasks = shellTasks;\n const shell = makeRunShell(\n workspace,\n spec,\n tasks,\n scope ?? null,\n sandbox ?? null,\n sessionKey ?? null,\n );\n\n registry.register(\n (args, ctx) => shell(args, ctx),\n { category: \"shell\", riskLevel: \"high\", capabilities: [\"run_shell\"], requiresApproval: true },\n {\n type: \"function\",\n function: {\n name: \"run_shell\",\n description:\n \"在持久化 shell 会话中运行命令 (cwd 和环境变量在多次调用之间保持)。\" +\n \"输出截断到最后部分,stdout 与 stderr 都会返回。\" +\n \"传递 description 说明命令用途 (显示在审批提示中),\" +\n \"传递 timeout_seconds 控制超时。\" +\n \"对于开发服务器 / watch 等长驻进程,设置 run_in_background=true,\" +\n \"随后用 shell_task_output 轮询输出、shell_task_kill 终止。\" +\n `运行环境:${describeShell(spec, process.platform)}` +\n (sandbox?.enforced\n ? `沙箱:${describeSandbox(sandbox)};工作区外的写入会被系统拒绝。`\n : \"\"),\n parameters: {\n type: \"object\",\n properties: {\n command: { type: \"string\", description: \"要执行的 shell 命令\" },\n description: { type: \"string\", description: \"命令用途的简短描述 (显示在审批中)\" },\n timeout_seconds: { type: \"integer\", description: \"超时秒数 (默认 120,最大 600)\" },\n run_in_background: {\n type: \"boolean\",\n description: \"是否后台运行 (默认 false)。适合开发服务器等长驻进程。\",\n },\n },\n required: [\"command\"],\n },\n },\n },\n );\n\n // ---- shell_task_output ----\n registry.register(\n (args) => tasks.output(String(args.task_id ?? \"\")),\n { category: \"shell\", riskLevel: \"low\", capabilities: [\"run_shell\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"shell_task_output\",\n description:\n \"查看 run_shell(run_in_background=true) 启动的后台任务的累计输出与状态。\" +\n \"返回 status (running/exited/killed)、exit_code、stdout、stderr。\",\n parameters: {\n type: \"object\",\n properties: {\n task_id: { type: \"string\", description: \"run_shell 返回的 task_id\" },\n },\n required: [\"task_id\"],\n },\n },\n },\n );\n\n // ---- shell_task_kill ----\n registry.register(\n (args) => tasks.kill(String(args.task_id ?? \"\"), scope ?? null),\n { category: \"shell\", riskLevel: \"medium\", capabilities: [\"run_shell\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"shell_task_kill\",\n description: \"终止后台任务(连同其子进程树)。返回任务的最终状态。\",\n parameters: {\n type: \"object\",\n properties: {\n task_id: { type: \"string\", description: \"run_shell 返回的 task_id\" },\n },\n required: [\"task_id\"],\n },\n },\n },\n );\n}\n","/**\n * 纯 Node 实现的 grep 回退\n *\n * 当 ripgrep 不可用时(未捆绑 / 未安装),用 fs 递归遍历 + JS RegExp 匹配,\n * 保证 grep 工具在任意环境都能工作,且支持完整的 JS 正则语法\n * (\\d / \\s / + / | 等,findstr 的 POSIX 子集远不能及)。\n *\n * 取舍:比 ripgrep 慢,因此做了忽略目录(.git/node_modules)、文件大小上限、\n * 二进制探测与最大匹配数限制;作为最后兜底足够用。\n */\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nexport interface GrepMatch {\n file: string;\n line: number;\n text: string;\n}\n\nexport interface NodeGrepOptions {\n pattern: string;\n /** 搜索目标:文件或目录(可为绝对/相对路径) */\n target: string;\n /** 工作区根:用于把命中路径格式化为相对路径 */\n workspace: string;\n /** 单文件读取上限(字节),超过则跳过 */\n maxFileBytes?: number;\n /** 最大匹配数 */\n maxMatches?: number;\n}\n\n/** 遍历时跳过的目录名 */\nconst SKIP_DIRS = new Set([\n \".git\",\n \"node_modules\",\n \".svn\",\n \".hg\",\n \"dist\",\n \"build\",\n \".next\",\n \".cache\",\n]);\n\nfunction looksBinary(buf: Buffer): boolean {\n const n = Math.min(buf.length, 8192);\n for (let i = 0; i < n; i++) if (buf[i] === 0) return true;\n return false;\n}\n\n/**\n * 在目标文件 / 目录中搜索。命中返回 {file, line, text} 列表。\n * 目标不存在时抛错(区别于「无匹配」)。\n */\nexport function nodeGrep(opts: NodeGrepOptions): GrepMatch[] {\n const maxMatches = opts.maxMatches ?? 50;\n const maxFileBytes = opts.maxFileBytes ?? 1024 * 1024;\n\n let regex: RegExp;\n try {\n regex = new RegExp(opts.pattern);\n } catch (err) {\n throw new Error(`无效的正则表达式: ${opts.pattern}(${String(err)})`);\n }\n\n const target = path.resolve(opts.workspace, opts.target);\n if (!fs.existsSync(target)) {\n throw new Error(`搜索路径不存在: ${opts.target}`);\n }\n\n const matches: GrepMatch[] = [];\n\n const scanFile = (file: string): boolean => {\n let stat: fs.Stats;\n try {\n stat = fs.statSync(file);\n } catch {\n return false;\n }\n if (!stat.isFile() || stat.size > maxFileBytes) return false;\n\n let buf: Buffer;\n try {\n buf = fs.readFileSync(file);\n } catch {\n return false;\n }\n if (looksBinary(buf)) return false;\n\n const rel = path.relative(opts.workspace, file) || path.basename(file);\n const lines = buf.toString(\"utf-8\").split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const raw = lines[i]!.replace(/\\r$/, \"\");\n if (regex.test(raw)) {\n matches.push({ file: rel, line: i + 1, text: raw.trim().slice(0, 200) });\n if (matches.length >= maxMatches) return true; // 达到上限,通知停止\n }\n }\n return false;\n };\n\n const stat = fs.statSync(target);\n if (stat.isFile()) {\n scanFile(target);\n return matches;\n }\n\n // 迭代式深度优先遍历(避免深目录递归爆栈)\n const stack: string[] = [target];\n while (stack.length > 0) {\n const dir = stack.pop()!;\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (!SKIP_DIRS.has(entry.name)) stack.push(full);\n } else if (entry.isFile()) {\n if (scanFile(full)) return matches;\n }\n }\n }\n\n return matches;\n}\n","/**\n * ripgrep (rg) 定位\n *\n * 优先级:\n * 1. 环境变量 ZQAGENT_RG 显式指定的路径;\n * 2. SEA 单文件内嵌的 rg 资源(构建时由 build_cli_binary.mjs 作为 asset 打包,\n * 运行时用 node:sea 的 getAsset 释放到临时目录);\n * 3. PATH 上的 rg。\n * 都拿不到时返回 null,由调用方回退到纯 Node 扫描器(nodeGrep.ts)。\n */\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { createRequire } from \"node:module\";\nimport { findExecutable } from \"./shellRuntime.js\";\n\n/** 已解析结果缓存(null 表示确认没有,勿重复探测) */\nlet cached: string | null | undefined;\n\n/** 把 SEA 内嵌的 rg 资源释放到临时目录,返回可执行文件路径 */\nfunction extractBundledRipgrep(): string | null {\n try {\n // CJS(SEA 单文件)下 require 可用;ESM 构建下用 createRequire 兜底。\n // 以 cwd 为基准构造 createRequire,避免依赖 import.meta(CJS 产物里不稳定)。\n const req =\n typeof require === \"function\"\n ? require\n : createRequire(path.join(process.cwd(), \"__zqagent__.js\"));\n const sea = req(\"node:sea\") as\n | { isSea?: () => boolean; getAsset?: (key: string) => ArrayBuffer | undefined }\n | undefined;\n if (!sea?.isSea?.() || typeof sea.getAsset !== \"function\") return null;\n const asset = sea.getAsset(\"rg\");\n if (!asset) return null;\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), \"zqagent-rg-\"));\n const file = path.join(dir, process.platform === \"win32\" ? \"rg.exe\" : \"rg\");\n fs.writeFileSync(file, Buffer.from(asset));\n fs.chmodSync(file, 0o755);\n return file;\n } catch {\n // 未内嵌 rg(如 npm 安装、普通 node 运行)或 API 不可用\n return null;\n }\n}\n\n/** 解析可用的 rg 路径;无则返回 null(结果带缓存) */\nexport function resolveRipgrep(): string | null {\n if (cached !== undefined) return cached;\n\n const explicit = (process.env.ZQAGENT_RG ?? \"\").trim();\n if (explicit && fs.existsSync(explicit)) {\n cached = explicit;\n return cached;\n }\n\n const bundled = extractBundledRipgrep();\n if (bundled) {\n cached = bundled;\n return cached;\n }\n\n cached = findExecutable(\"rg\", {\n platform: process.platform,\n env: process.env,\n fileExists: (p) => {\n try {\n return fs.statSync(p).isFile();\n } catch {\n return false;\n }\n },\n });\n return cached;\n}\n\n/** 测试用:清除解析缓存 */\nexport function resetRipgrepCache(): void {\n cached = undefined;\n}\n","/**\n * 搜索工具\n *\n * 提供 grep 工具。执行策略:\n * 1. ripgrep (rg) —— 优先使用内嵌/已安装的 rg,性能与正则支持最好;\n * 2. 纯 Node 扫描器(nodeGrep.ts)—— rg 不可用时的跨平台兜底,\n * 支持完整 JS 正则,不依赖系统 findstr/grep。\n *\n * 返回匹配的文件名、行号和内容,最多 50 条。\n */\nimport { ToolRegistry } from \"./ToolRegistry.js\";\nimport { execAsync } from \"./execAsync.js\";\nimport { decodeOutput } from \"./shell.js\";\nimport { nodeGrep, type GrepMatch } from \"./nodeGrep.js\";\nimport { resolveRipgrep } from \"./ripgrep.js\";\n\n/** 解析 rg 的 `文件:行号:内容` 输出 */\nfunction parseRgOutput(result: string): GrepMatch[] {\n return result\n .split(\"\\n\")\n .map((l) => l.replace(/\\r$/, \"\"))\n .filter((l) => l.length > 0)\n .slice(0, 50)\n .map((line) => {\n const m = /^(.+?):(\\d+):(.*)$/.exec(line);\n if (!m) return { file: line.trim(), line: 0, text: \"\" };\n return { file: m[1]!, line: Number(m[2]), text: m[3]!.trim().slice(0, 200) };\n });\n}\n\n/**\n * grep — 使用 ripgrep,缺失时回退到纯 Node 扫描\n *\n * 安全说明:使用异步 execAsync(参数数组)而非 shell 字符串拼接,\n * pattern/path 作为独立参数传递,不经 shell 解释,杜绝命令注入。\n */\nfunction makeGrep(workspace: string) {\n return async function grep(args: Record<string, unknown>) {\n const pattern = String(args.pattern ?? \"\");\n const filePath = String(args.path ?? \".\");\n\n if (!pattern.trim()) {\n return { error: \"搜索模式不能为空\" };\n }\n\n const rg = resolveRipgrep();\n if (rg) {\n const res = await execAsync(\n rg,\n [\n // --with-filename:单文件模式下 rg 默认不输出文件名前缀,会导致\n // \"行号:内容\" 被误当成 \"文件:行号:内容\" 解析而错位;显式要求带文件名。\n \"--with-filename\",\n \"--line-number\",\n \"--no-heading\",\n \"--max-count\",\n \"50\",\n pattern,\n filePath,\n ],\n {\n cwd: workspace,\n timeoutMs: 10000,\n maxBuffer: 1024 * 1024 * 2,\n },\n );\n // rg 退出码:0=有匹配,1=无匹配,其余=错误\n if (res.status === 0) {\n const matches = parseRgOutput(decodeOutput(res.stdout));\n return { query: pattern, engine: \"ripgrep\", matches, count: matches.length };\n }\n if (res.status === 1) {\n return { query: pattern, engine: \"ripgrep\", matches: [], count: 0 };\n }\n // 其它情况(含 rg 崩溃、路径不存在被 rg 报错)→ 交给纯 Node 扫描,\n // 后者能给出「路径不存在」这类明确错误。\n }\n\n try {\n const matches = nodeGrep({ pattern, target: filePath, workspace });\n return { query: pattern, engine: \"node\", matches, count: matches.length };\n } catch (exc) {\n return { error: `搜索失败: ${String(exc instanceof Error ? exc.message : exc)}` };\n }\n };\n}\n\n/** 注册 grep 工具(低风险,无需审批) */\nexport function registerSearchTool(registry: ToolRegistry, workspace: string): void {\n const grepFunc = makeGrep(workspace);\n\n registry.register(\n (args) => grepFunc(args),\n { category: \"search\", riskLevel: \"low\", capabilities: [\"search\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"grep\",\n description:\n \"在文件中搜索文本模式(支持完整 JS 正则语法)。返回匹配的文件名、行号和内容,最多 50 条。\" +\n \"优先使用 ripgrep (rg) 以获得更好的性能;rg 不可用时自动回退到内置扫描器。\",\n parameters: {\n type: \"object\",\n properties: {\n pattern: { type: \"string\", description: \"搜索模式 (文本或正则表达式)\" },\n path: { type: \"string\", description: \"搜索路径 (文件或目录,默认 '.')\" },\n },\n required: [\"pattern\"],\n },\n },\n },\n );\n}\n"],"mappings":";;;;;;;;AAoBA,SAAS,aAAa;;;ACdtB,SAAS,iBAAiB;AAEnB,SAAS,SAAS,KAA+B;AACtD,MAAI,CAAC,IAAK;AACV,MAAI,QAAQ,aAAa,SAAS;AAChC,QAAI;AACF,gBAAU,YAAY,CAAC,QAAQ,OAAO,GAAG,GAAG,MAAM,IAAI,GAAG,EAAE,aAAa,KAAK,CAAC;AAAA,IAChF,QAAQ;AAAA,IAER;AACA;AAAA,EACF;AAEA,MAAI;AACF,YAAQ,KAAK,CAAC,KAAK,SAAS;AAAA,EAC9B,QAAQ;AACN,QAAI;AACF,cAAQ,KAAK,KAAK,SAAS;AAAA,IAC7B,QAAQ;AAAA,IAER;AAAA,EACF;AACF;;;AD4BA,IAAM,eAAe,oBAAI,IAAY;AACrC,IAAI,uBAAuB;AAE3B,SAAS,WAAW,KAA+B;AACjD,MAAI,CAAC,IAAK;AACV,eAAa,IAAI,GAAG;AACpB,MAAI,qBAAsB;AAC1B,yBAAuB;AACvB,UAAQ,KAAK,QAAQ,MAAM;AACzB,eAAW,KAAK,aAAc,UAAS,CAAC;AAAA,EAC1C,CAAC;AACH;AAEA,IAAM,qBAAqB,OAAO,OAAO;AAWzC,IAAM,gBAAgB;AAGtB,SAAS,SAAS,WAIhB;AACA,QAAM,SAAmB,CAAC;AAC1B,MAAI,MAAM;AACV,MAAI,OAAO;AACX,SAAO;AAAA,IACL,KAAK,OAAe;AAClB,UAAI,OAAO,WAAW;AACpB,eAAO;AACP;AAAA,MACF;AACA,YAAM,OAAO,YAAY;AACzB,UAAI,MAAM,SAAS,MAAM;AACvB,eAAO,KAAK,MAAM,SAAS,GAAG,IAAI,CAAC;AACnC,cAAM;AACN,eAAO;AACP;AAAA,MACF;AACA,aAAO,KAAK,KAAK;AACjB,aAAO,MAAM;AAAA,IACf;AAAA,IACA,MAAM,MAAM,OAAO,OAAO,MAAM;AAAA,IAChC,MAAM,MAAM;AAAA,EACd;AACF;AAEO,SAAS,UACd,MACA,MACA,OAAyB,CAAC,GACA;AAC1B,SAAO,IAAI,QAAyB,CAACA,aAAY;AAC/C,UAAM,YAAY,KAAK,aAAa;AACpC,UAAM,MAAM,SAAS,SAAS;AAC9B,UAAM,MAAM,SAAS,SAAS;AAG9B,QAAI,KAAK,QAAQ,SAAS;AACxB,MAAAA,SAAQ;AAAA,QACN,QAAQ;AAAA,QACR,QAAQ,OAAO,MAAM,CAAC;AAAA,QACtB,QAAQ,OAAO,MAAM,CAAC;AAAA,QACtB,UAAU;AAAA,QACV,WAAW;AAAA,QACX,SAAS;AAAA,MACX,CAAC;AACD;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,MAAM,MAAM;AAAA,QACxB,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA,QAClD,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA;AAAA,QAElD,UAAU,QAAQ,aAAa;AAAA,QAC/B,OAAO,CAAC,KAAK,UAAU,SAAY,WAAW,QAAQ,QAAQ,MAAM;AAAA,QACpE,aAAa;AAAA,MACf,CAAC;AAAA,IACH,SAAS,GAAG;AACV,MAAAA,SAAQ;AAAA,QACN,QAAQ;AAAA,QACR,QAAQ,OAAO,MAAM,CAAC;AAAA,QACtB,QAAQ,OAAO,MAAM,CAAC;AAAA,QACtB,OAAO;AAAA,QACP,UAAU;AAAA,QACV,WAAW;AAAA,MACb,CAAC;AACD;AAAA,IACF;AAEA,eAAW,MAAM,GAAG;AAEpB,QAAI,UAAU;AACd,QAAI,WAAW;AACf,QAAI,UAAU;AACd,QAAI;AACJ,QAAI,QAA8C;AAClD,QAAI,aAAmD;AAEvD,UAAM,UAAU,MAAY;AAC1B,gBAAU;AACV,oBAAc;AAAA,IAChB;AAEA,UAAM,SAAS,CAAC,WAAgC;AAC9C,UAAI,QAAS;AACb,gBAAU;AACV,UAAI,MAAO,cAAa,KAAK;AAC7B,UAAI,WAAY,cAAa,UAAU;AACvC,UAAI,MAAM,QAAQ,OAAW,cAAa,OAAO,MAAM,GAAG;AAC1D,WAAK,QAAQ,oBAAoB,SAAS,OAAO;AACjD,YAAM,SAA0B;AAAA,QAC9B;AAAA,QACA,QAAQ,IAAI,KAAK;AAAA,QACjB,QAAQ,IAAI,KAAK;AAAA,QACjB;AAAA,QACA,WAAW,IAAI,KAAK,KAAK,IAAI,KAAK;AAAA,MACpC;AACA,UAAI,QAAS,QAAO,UAAU;AAC9B,UAAI,WAAY,QAAO,QAAQ;AAC/B,MAAAA,SAAQ,MAAM;AAAA,IAChB;AAQA,UAAM,gBAAgB,MAAY;AAChC,eAAS,MAAM,GAAG;AAClB,UAAI,WAAW,WAAY;AAC3B,mBAAa,WAAW,MAAM;AAG5B,cAAM,QAAQ,QAAQ;AACtB,cAAM,QAAQ,QAAQ;AACtB,eAAO,IAAI;AAAA,MACb,GAAG,aAAa;AAAA,IAClB;AAEA,QAAI,KAAK,cAAc,UAAa,KAAK,YAAY,GAAG;AACtD,cAAQ,WAAW,MAAM;AACvB,mBAAW;AACX,sBAAc;AAAA,MAChB,GAAG,KAAK,SAAS;AAAA,IACnB;AAEA,SAAK,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAE9D,UAAM,QAAQ,GAAG,QAAQ,CAAC,MAAc,IAAI,KAAK,CAAC,CAAC;AACnD,UAAM,QAAQ,GAAG,QAAQ,CAAC,MAAc,IAAI,KAAK,CAAC,CAAC;AAEnD,UAAM,GAAG,SAAS,CAAC,MAAM;AACvB,mBAAa;AAEb,UAAI,MAAM,QAAQ,OAAW,QAAO,IAAI;AAAA,IAC1C,CAAC;AAED,QAAI,KAAK,UAAU,UAAa,MAAM,OAAO;AAE3C,YAAM,MAAM,GAAG,SAAS,MAAM;AAAA,MAAC,CAAC;AAChC,YAAM,MAAM,IAAI,KAAK,KAAK;AAAA,IAC5B;AAEA,UAAM,GAAG,SAAS,CAAC,SAAS,OAAO,IAAI,CAAC;AAAA,EAC1C,CAAC;AACH;;;AE5NA,YAAY,UAAU;AA+BtB,IAAM,mBAAmB,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,EAAE;AAE5D,SAAS,QAAQ,UAA2B;AAC1C,SAAO,aAAa,UAAe,aAAa;AAClD;AAWA,SAAS,SAAS,aAA8B,OAAyB;AACvE,SAAO,QAAQ,QAAQ,EAAE,KAAK,GAAG,KAAK;AACxC;AAMO,SAAS,eAAe,MAAc,KAAkC;AAC7E,MAAI,KAAK,SAAS,GAAG,KAAK,KAAK,SAAS,IAAI,GAAG;AAC7C,WAAO,IAAI,WAAW,IAAI,IAAI,OAAO;AAAA,EACvC;AACA,QAAM,WAAW,IAAI,aAAa,UAAU,mBAAmB,CAAC,EAAE;AAClE,QAAM,YAAY,IAAI,aAAa,UAAU,MAAM;AACnD,QAAM,QAAQ,IAAI,IAAI,QAAQ,IAAI,MAAM,SAAS,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC7E,aAAW,OAAO,MAAM;AACtB,eAAW,UAAU,UAAU;AAC7B,YAAM,YAAY,SAAS,IAAI,UAAU,KAAK,OAAO,MAAM;AAC3D,UAAI,IAAI,WAAW,SAAS,EAAG,QAAO;AAAA,IACxC;AAAA,EACF;AACA,SAAO;AACT;;;ACvEA,SAAS,SAAAC,cAAgC;AAIzC,IAAM,uBAAuB,KAAK;AAGlC,IAAM,aAAN,MAAiB;AAAA,EAIf,YAA6B,QAAQ,sBAAsB;AAA9B;AAAA,EAA+B;AAAA,EAA/B;AAAA,EAHrB,SAAmB,CAAC;AAAA,EACpB,OAAO;AAAA,EAIf,IAAI,OAAqB;AACvB,QAAI,CAAC,MAAO;AACZ,SAAK,OAAO,KAAK,KAAK;AACtB,SAAK,QAAQ,MAAM;AACnB,WAAO,KAAK,OAAO,KAAK,SAAS,KAAK,OAAO,SAAS,GAAG;AACvD,WAAK,QAAQ,KAAK,OAAO,MAAM,EAAG;AAAA,IACpC;AAEA,QAAI,KAAK,OAAO,KAAK,OAAO;AAC1B,YAAM,SAAS,KAAK,OAAO,KAAK,EAAE;AAClC,WAAK,SAAS,CAAC,OAAO,MAAM,CAAC,KAAK,KAAK,CAAC;AACxC,WAAK,OAAO,KAAK,OAAO,CAAC,EAAG;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,MAAc;AACZ,UAAM,SAAS,KAAK,OAAO,KAAK,EAAE;AAClC,WAAO,OAAO,SAAS,KAAK,QAAQ,OAAO,MAAM,CAAC,KAAK,KAAK,IAAI;AAAA,EAClE;AACF;AA+DO,IAAM,mBAAN,MAAuB;AAAA,EACX,QAAQ,oBAAI,IAA4B;AAAA,EACjD,MAAM;AAAA,EACN,mBAAmB;AAAA;AAAA,EAEnB,WAAsC;AAAA;AAAA,EAG9C,YAAY,IAAqC;AAC/C,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA,EAGA,MAAM,MAA4C;AAChD,SAAK,eAAe;AACpB,UAAM,KAAK,SAAS,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC;AACzD,UAAM,QAAQC,OAAM,KAAK,MAAM,KAAK,MAAM;AAAA,MACxC,KAAK,KAAK;AAAA,MACV,KAAK,KAAK;AAAA,MACV,UAAU,QAAQ,aAAa;AAAA,MAC/B,aAAa;AAAA,MACb,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,IAClC,CAAC;AAED,UAAM,OAAuB;AAAA,MAC3B;AAAA,MACA,SAAS,KAAK;AAAA,MACd,OAAO,KAAK,SAAS;AAAA,MACrB;AAAA,MACA,QAAQ,IAAI,WAAW;AAAA,MACvB,QAAQ,IAAI,WAAW;AAAA,MACvB,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI;AAAA,MACpB,SAAS;AAAA,IACX;AACA,SAAK,MAAM,IAAI,IAAI,IAAI;AAEvB,UAAM,QAAQ,GAAG,QAAQ,CAAC,MAAc,KAAK,OAAO,IAAI,EAAE,SAAS,OAAO,CAAC,CAAC;AAC5E,UAAM,QAAQ,GAAG,QAAQ,CAAC,MAAc,KAAK,OAAO,IAAI,EAAE,SAAS,OAAO,CAAC,CAAC;AAC5E,UAAM,GAAG,SAAS,CAAC,QAAQ;AACzB,WAAK,OAAO,IAAI,iBAAiB,IAAI,OAAO;AAAA,CAAI;AAEhD,UAAI,KAAK,WAAW,WAAW;AAC7B,aAAK,UAAU,KAAK,IAAI;AACxB,aAAK,SAAS;AACd,aAAK,QAAQ,KAAK,KAAK;AAAA,MACzB;AAAA,IACF,CAAC;AACD,UAAM,GAAG,QAAQ,CAAC,MAAM,WAAW;AACjC,WAAK,WAAW;AAChB,WAAK,SAAS;AACd,WAAK,UAAU,KAAK,IAAI;AAExB,UAAI,KAAK,WAAW,SAAU,MAAK,SAAS;AAC5C,WAAK,QAAQ,KAAK,KAAK;AAAA,IACzB,CAAC;AAED,UAAM,MAAM;AAEZ,UAAM,OAAO,KAAK,SAAS,IAAI;AAE/B,SAAK,QAAQ,KAAK,KAAK;AACvB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,QAA4D;AACjE,UAAM,OAAO,KAAK,MAAM,IAAI,MAAM;AAClC,QAAI,CAAC,KAAM,QAAO,EAAE,OAAO,+CAAY,MAAM,GAAG;AAChD,WAAO,KAAK,SAAS,IAAI;AAAA,EAC3B;AAAA;AAAA,EAGA,KAAK,QAAgB,OAAmE;AACtF,UAAM,OAAO,KAAK,MAAM,IAAI,MAAM;AAClC,QAAI,CAAC,KAAM,QAAO,EAAE,OAAO,+CAAY,MAAM,GAAG;AAChD,QAAI,UAAU,UAAa,KAAK,UAAU,OAAO;AAC/C,aAAO,EAAE,OAAO,+CAAY,MAAM,GAAG;AAAA,IACvC;AACA,QAAI,KAAK,WAAW,WAAW;AAC7B,WAAK,SAAS;AACd,WAAK,UAAU,KAAK,IAAI;AACxB,eAAS,KAAK,MAAM,GAAG;AACvB,WAAK,QAAQ,KAAK,KAAK;AAAA,IACzB;AACA,WAAO,KAAK,SAAS,IAAI;AAAA,EAC3B;AAAA;AAAA,EAGA,KAAK,OAAiD;AACpD,UAAM,MAAM,CAAC,GAAG,KAAK,MAAM,OAAO,CAAC;AACnC,YAAQ,UAAU,SAAY,MAAM,IAAI,OAAO,CAAC,MAAM,EAAE,UAAU,KAAK,GAAG;AAAA,MAAI,CAAC,MAC7E,KAAK,SAAS,CAAC;AAAA,IACjB;AAAA,EACF;AAAA;AAAA,EAGA,aAAmB;AACjB,eAAW,QAAQ,KAAK,MAAM,OAAO,GAAG;AACtC,UAAI,KAAK,WAAW,WAAW;AAC7B,aAAK,SAAS;AACd,iBAAS,KAAK,MAAM,GAAG;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,QAAQ,OAA4B;AAC1C,UAAM,WAAW,KAAK;AACtB,QAAI,CAAC,SAAU;AACf,UAAM,QAAyB,CAAC;AAChC,eAAW,QAAQ,KAAK,MAAM,OAAO,GAAG;AACtC,UAAI,KAAK,UAAU,SAAS,KAAK,WAAW,UAAW;AACvD,YAAM,KAAK;AAAA,QACT,SAAS,KAAK;AAAA,QACd,SAAS,KAAK;AAAA,QACd,QAAQ,KAAK;AAAA,QACb,SAAS;AAAA,QACT,WAAW,KAAK;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,aAAa,KAAK,IAAI,IAAI,KAAK;AAAA,QAC/B,YAAY,KAAK;AAAA,MACnB,CAAC;AAAA,IACH;AACA,QAAI;AACF,eAAS,OAAO,KAAK;AAAA,IACvB,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,SAAS,MAA8C;AAC7D,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,SAAS,KAAK;AAAA,MACd,QAAQ,KAAK;AAAA,MACb,SAAS,KAAK,WAAW;AAAA,MACzB,WAAW,KAAK;AAAA,MAChB,QAAQ,KAAK;AAAA,MACb,QAAQ,KAAK,OAAO,IAAI;AAAA,MACxB,QAAQ,KAAK,OAAO,IAAI;AAAA,MACxB,cAAc,KAAK,WAAW,KAAK,IAAI,KAAK,KAAK;AAAA,IACnD;AAAA,EACF;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,iBAAkB;AAC3B,SAAK,mBAAmB;AACxB,YAAQ,KAAK,QAAQ,MAAM,KAAK,WAAW,CAAC;AAAA,EAC9C;AACF;;;ACjPA,SAAS,aAAAC,kBAAiB;;;ACqB1B,IAAM,aAAa,OAAO,OAAO;AA6E1B,SAAS,aAAa,KAAwC;AACnE,MAAI,CAAC,OAAO,IAAI,WAAW,EAAG,QAAO;AACrC,MAAI;AACF,WAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,OAAO,GAAG;AAAA,EAC7D,QAAQ;AACN,eAAW,OAAO,CAAC,OAAO,QAAQ,aAAa,QAAQ,GAAG;AACxD,UAAI;AACF,eAAO,IAAI,YAAY,GAAG,EAAE,OAAO,GAAG;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,IAAI,SAAS,QAAQ;AAAA,EAC9B;AACF;AAyHA,IAAM,aAAa,IAAI,iBAAiB;;;ACpPxC,YAAY,QAAQ;AACpB,YAAYC,WAAU;AAqBtB,IAAM,YAAY,oBAAI,IAAI;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,YAAY,KAAsB;AACzC,QAAM,IAAI,KAAK,IAAI,IAAI,QAAQ,IAAI;AACnC,WAAS,IAAI,GAAG,IAAI,GAAG,IAAK,KAAI,IAAI,CAAC,MAAM,EAAG,QAAO;AACrD,SAAO;AACT;AAMO,SAAS,SAAS,MAAoC;AAC3D,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,eAAe,KAAK,gBAAgB,OAAO;AAEjD,MAAI;AACJ,MAAI;AACF,YAAQ,IAAI,OAAO,KAAK,OAAO;AAAA,EACjC,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,qDAAa,KAAK,OAAO,SAAI,OAAO,GAAG,CAAC,QAAG;AAAA,EAC7D;AAEA,QAAM,SAAc,cAAQ,KAAK,WAAW,KAAK,MAAM;AACvD,MAAI,CAAI,cAAW,MAAM,GAAG;AAC1B,UAAM,IAAI,MAAM,+CAAY,KAAK,MAAM,EAAE;AAAA,EAC3C;AAEA,QAAM,UAAuB,CAAC;AAE9B,QAAM,WAAW,CAAC,SAA0B;AAC1C,QAAIC;AACJ,QAAI;AACF,MAAAA,QAAU,YAAS,IAAI;AAAA,IACzB,QAAQ;AACN,aAAO;AAAA,IACT;AACA,QAAI,CAACA,MAAK,OAAO,KAAKA,MAAK,OAAO,aAAc,QAAO;AAEvD,QAAI;AACJ,QAAI;AACF,YAAS,gBAAa,IAAI;AAAA,IAC5B,QAAQ;AACN,aAAO;AAAA,IACT;AACA,QAAI,YAAY,GAAG,EAAG,QAAO;AAE7B,UAAM,MAAW,eAAS,KAAK,WAAW,IAAI,KAAU,eAAS,IAAI;AACrE,UAAM,QAAQ,IAAI,SAAS,OAAO,EAAE,MAAM,IAAI;AAC9C,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAM,MAAM,MAAM,CAAC,EAAG,QAAQ,OAAO,EAAE;AACvC,UAAI,MAAM,KAAK,GAAG,GAAG;AACnB,gBAAQ,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,GAAG,MAAM,IAAI,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,CAAC;AACvE,YAAI,QAAQ,UAAU,WAAY,QAAO;AAAA,MAC3C;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,OAAU,YAAS,MAAM;AAC/B,MAAI,KAAK,OAAO,GAAG;AACjB,aAAS,MAAM;AACf,WAAO;AAAA,EACT;AAGA,QAAM,QAAkB,CAAC,MAAM;AAC/B,SAAO,MAAM,SAAS,GAAG;AACvB,UAAM,MAAM,MAAM,IAAI;AACtB,QAAI;AACJ,QAAI;AACF,gBAAa,eAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAAA,IACvD,QAAQ;AACN;AAAA,IACF;AACA,eAAW,SAAS,SAAS;AAC3B,YAAM,OAAY,WAAK,KAAK,MAAM,IAAI;AACtC,UAAI,MAAM,YAAY,GAAG;AACvB,YAAI,CAAC,UAAU,IAAI,MAAM,IAAI,EAAG,OAAM,KAAK,IAAI;AAAA,MACjD,WAAW,MAAM,OAAO,GAAG;AACzB,YAAI,SAAS,IAAI,EAAG,QAAO;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;;;ACrHA,YAAYC,SAAQ;AACpB,YAAY,QAAQ;AACpB,YAAYC,WAAU;AACtB,SAAS,qBAAqB;AAI9B,IAAI;AAGJ,SAAS,wBAAuC;AAC9C,MAAI;AAGF,UAAM,MACJ,OAAO,cAAY,aACf,YACA,cAAmB,WAAK,QAAQ,IAAI,GAAG,gBAAgB,CAAC;AAC9D,UAAM,MAAM,IAAI,UAAU;AAG1B,QAAI,CAAC,KAAK,QAAQ,KAAK,OAAO,IAAI,aAAa,WAAY,QAAO;AAClE,UAAM,QAAQ,IAAI,SAAS,IAAI;AAC/B,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,MAAS,gBAAiB,WAAQ,UAAO,GAAG,aAAa,CAAC;AAChE,UAAM,OAAY,WAAK,KAAK,QAAQ,aAAa,UAAU,WAAW,IAAI;AAC1E,IAAG,kBAAc,MAAM,OAAO,KAAK,KAAK,CAAC;AACzC,IAAG,cAAU,MAAM,GAAK;AACxB,WAAO;AAAA,EACT,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,iBAAgC;AAC9C,MAAI,WAAW,OAAW,QAAO;AAEjC,QAAM,YAAY,QAAQ,IAAI,cAAc,IAAI,KAAK;AACrD,MAAI,YAAe,eAAW,QAAQ,GAAG;AACvC,aAAS;AACT,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,sBAAsB;AACtC,MAAI,SAAS;AACX,aAAS;AACT,WAAO;AAAA,EACT;AAEA,WAAS,eAAe,MAAM;AAAA,IAC5B,UAAU,QAAQ;AAAA,IAClB,KAAK,QAAQ;AAAA,IACb,YAAY,CAAC,MAAM;AACjB,UAAI;AACF,eAAU,aAAS,CAAC,EAAE,OAAO;AAAA,MAC/B,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF,CAAC;AACD,SAAO;AACT;;;ACxDA,SAAS,cAAc,QAA6B;AAClD,SAAO,OACJ,MAAM,IAAI,EACV,IAAI,CAAC,MAAM,EAAE,QAAQ,OAAO,EAAE,CAAC,EAC/B,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAC1B,MAAM,GAAG,EAAE,EACX,IAAI,CAAC,SAAS;AACb,UAAM,IAAI,qBAAqB,KAAK,IAAI;AACxC,QAAI,CAAC,EAAG,QAAO,EAAE,MAAM,KAAK,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG;AACtD,WAAO,EAAE,MAAM,EAAE,CAAC,GAAI,MAAM,OAAO,EAAE,CAAC,CAAC,GAAG,MAAM,EAAE,CAAC,EAAG,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;AAAA,EAC7E,CAAC;AACL;AAQA,SAAS,SAAS,WAAmB;AACnC,SAAO,eAAe,KAAK,MAA+B;AACxD,UAAM,UAAU,OAAO,KAAK,WAAW,EAAE;AACzC,UAAM,WAAW,OAAO,KAAK,QAAQ,GAAG;AAExC,QAAI,CAAC,QAAQ,KAAK,GAAG;AACnB,aAAO,EAAE,OAAO,mDAAW;AAAA,IAC7B;AAEA,UAAM,KAAK,eAAe;AAC1B,QAAI,IAAI;AACN,YAAM,MAAM,MAAM;AAAA,QAChB;AAAA,QACA;AAAA;AAAA;AAAA,UAGE;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA;AAAA,UACE,KAAK;AAAA,UACL,WAAW;AAAA,UACX,WAAW,OAAO,OAAO;AAAA,QAC3B;AAAA,MACF;AAEA,UAAI,IAAI,WAAW,GAAG;AACpB,cAAM,UAAU,cAAc,aAAa,IAAI,MAAM,CAAC;AACtD,eAAO,EAAE,OAAO,SAAS,QAAQ,WAAW,SAAS,OAAO,QAAQ,OAAO;AAAA,MAC7E;AACA,UAAI,IAAI,WAAW,GAAG;AACpB,eAAO,EAAE,OAAO,SAAS,QAAQ,WAAW,SAAS,CAAC,GAAG,OAAO,EAAE;AAAA,MACpE;AAAA,IAGF;AAEA,QAAI;AACF,YAAM,UAAU,SAAS,EAAE,SAAS,QAAQ,UAAU,UAAU,CAAC;AACjE,aAAO,EAAE,OAAO,SAAS,QAAQ,QAAQ,SAAS,OAAO,QAAQ,OAAO;AAAA,IAC1E,SAAS,KAAK;AACZ,aAAO,EAAE,OAAO,6BAAS,OAAO,eAAe,QAAQ,IAAI,UAAU,GAAG,CAAC,GAAG;AAAA,IAC9E;AAAA,EACF;AACF;AAGO,SAAS,mBAAmB,UAAwB,WAAyB;AAClF,QAAM,WAAW,SAAS,SAAS;AAEnC,WAAS;AAAA,IACP,CAAC,SAAS,SAAS,IAAI;AAAA,IACvB,EAAE,UAAU,UAAU,WAAW,OAAO,cAAc,CAAC,QAAQ,GAAG,kBAAkB,MAAM;AAAA,IAC1F;AAAA,MACE,MAAM;AAAA,MACN,UAAU;AAAA,QACR,MAAM;AAAA,QACN,aACE;AAAA,QAEF,YAAY;AAAA,UACV,MAAM;AAAA,UACN,YAAY;AAAA,YACV,SAAS,EAAE,MAAM,UAAU,aAAa,8EAAkB;AAAA,YAC1D,MAAM,EAAE,MAAM,UAAU,aAAa,kFAAsB;AAAA,UAC7D;AAAA,UACA,UAAU,CAAC,SAAS;AAAA,QACtB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":["resolve","spawn","spawn","spawnSync","path","stat","fs","path"]}
|
|
1
|
+
{"version":3,"sources":["../../src/tools/execAsync.ts","../../src/tools/killTree.ts","../../src/tools/shellRuntime.ts","../../src/tools/shellBackground.ts","../../src/tools/sandbox.ts","../../src/tools/shell.ts","../../src/tools/nodeGrep.ts","../../src/tools/ripgrep.ts","../../src/tools/search.ts"],"sourcesContent":["/**\n * 异步子进程执行(spawnSync 的异步替身)\n *\n * 为什么需要:spawnSync 期间 Node 事件循环被整体阻塞,单进程节点上多个会话\n * 并发跑命令时会互相拖死——一条长命令(run_shell / ffmpeg 可达数百秒)就让\n * server 停止响应,SSE 心跳、HTTP 请求、其他会话的工具调用全部排队。\n * 换成异步 spawn 后,等待子进程的时间可以正常处理事件循环。\n *\n * 与 spawnSync 的差异(异步 spawn 不自带的语义在这里补齐):\n * - 超时:自行计时 + SIGKILL,结果里给出 `timedOut` 布尔\n * (spawnSync 是 ETIMEDOUT 错误码);\n * - 中断:监听 opts.signal,abort 时终止整棵进程树,结果里给出 `aborted` 布尔;\n * - 结算兜底:kill 后若仍有后代持有管道(`close` 不触发),宽限期到点强制结算,\n * 保证 Promise 一定有界,绝不永久挂起;\n * - 输出上限:自行累积 + 截断,结果里给出 `truncated` 布尔\n * (spawnSync 是 ENOBUFS 错误);\n * - stdout/stderr 保留原始 Buffer,交调用方的 decodeOutput 做编码回退。\n *\n * 参数一律用数组传递,不经 shell 解释,保持与原先一致的注入防护。\n */\nimport { spawn } from \"node:child_process\";\nimport { killTree } from \"./killTree.js\";\n\nexport interface ExecAsyncOptions {\n cwd?: string;\n /** 超时毫秒数;到点 SIGKILL 子进程并置 timedOut。不传则不限时 */\n timeoutMs?: number;\n /** stdout / stderr 各自的累积上限(字节);超出部分丢弃并置 truncated */\n maxBuffer?: number;\n env?: NodeJS.ProcessEnv;\n /** 需要向 stdin 写入的内容(如 git apply 的补丁);不传则 stdin 关闭 */\n input?: string;\n /**\n * 中断信号:abort 时终止整棵进程树并置 aborted。\n * 用于把 TurnEngine 的用户中断传导到正在执行的命令。\n */\n signal?: AbortSignal;\n}\n\nexport interface ExecAsyncResult {\n /** 退出码;被信号杀死或启动失败时为 null */\n status: number | null;\n stdout: Buffer;\n stderr: Buffer;\n /** 仅启动失败(命令不存在 ENOENT、无执行权限等)时存在 */\n error?: NodeJS.ErrnoException;\n timedOut: boolean;\n truncated: boolean;\n /** 是否因 signal abort 被终止 */\n aborted?: boolean;\n}\n\n/**\n * 存活的子进程(pid)。子进程以 detached 启动(自成进程组,便于整组终止),\n * 因此终端 Ctrl+C 不再波及它们;进程退出时统一收掉,避免遗留孤儿。\n */\nconst liveChildren = new Set<number>();\nlet exitCleanupInstalled = false;\n\nfunction trackChild(pid: number | undefined): void {\n if (!pid) return;\n liveChildren.add(pid);\n if (exitCleanupInstalled) return;\n exitCleanupInstalled = true;\n process.once(\"exit\", () => {\n for (const p of liveChildren) killTree(p);\n });\n}\n\nconst DEFAULT_MAX_BUFFER = 1024 * 1024 * 5;\n\n/**\n * 终止进程树后等待 stdio 关闭的宽限期(毫秒)。\n *\n * 结算挂在 `close` 上,而 `close` 要求子进程的 stdout/stderr 全部关闭。子进程虽以\n * detached 启动(自成进程组,killTree 的 `-pid` 覆盖整组),但仍有后代存活的可能:\n * detached 未生效、`process.kill(-pid)` 抛错走了只杀直接 pid 的兜底分支、或后代\n * 自行 setsid / 被 reparent 脱离进程组。此时管道写端不关,`close` 永不触发,\n * Promise 永久挂起(会话卡在 running,interrupt 也因等不到安全点而无效)。\n */\nconst KILL_GRACE_MS = 1000;\n\n/** 累积器:到上限后丢弃后续数据,但仍消费流避免子进程写阻塞 */\nfunction makeSink(maxBuffer: number): {\n push: (chunk: Buffer) => void;\n done: () => Buffer;\n over: () => boolean;\n} {\n const chunks: Buffer[] = [];\n let len = 0;\n let over = false;\n return {\n push(chunk: Buffer) {\n if (len >= maxBuffer) {\n over = true;\n return;\n }\n const room = maxBuffer - len;\n if (chunk.length > room) {\n chunks.push(chunk.subarray(0, room));\n len = maxBuffer;\n over = true;\n return;\n }\n chunks.push(chunk);\n len += chunk.length;\n },\n done: () => Buffer.concat(chunks),\n over: () => over,\n };\n}\n\nexport function execAsync(\n file: string,\n argv: string[],\n opts: ExecAsyncOptions = {},\n): Promise<ExecAsyncResult> {\n return new Promise<ExecAsyncResult>((resolve) => {\n const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;\n const out = makeSink(maxBuffer);\n const err = makeSink(maxBuffer);\n\n // 调用前已被中断:不必启动进程\n if (opts.signal?.aborted) {\n resolve({\n status: null,\n stdout: Buffer.alloc(0),\n stderr: Buffer.alloc(0),\n timedOut: false,\n truncated: false,\n aborted: true,\n });\n return;\n }\n\n let child: ReturnType<typeof spawn>;\n try {\n child = spawn(file, argv, {\n ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),\n ...(opts.env !== undefined ? { env: opts.env } : {}),\n // 自成进程组(POSIX):超时/中断时用 killTree 一次收掉整棵进程树\n detached: process.platform !== \"win32\",\n stdio: [opts.input === undefined ? \"ignore\" : \"pipe\", \"pipe\", \"pipe\"],\n windowsHide: true,\n });\n } catch (e) {\n resolve({\n status: null,\n stdout: Buffer.alloc(0),\n stderr: Buffer.alloc(0),\n error: e as NodeJS.ErrnoException,\n timedOut: false,\n truncated: false,\n });\n return;\n }\n\n trackChild(child.pid);\n\n let settled = false;\n let timedOut = false;\n let aborted = false;\n let spawnError: NodeJS.ErrnoException | undefined;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let graceTimer: ReturnType<typeof setTimeout> | null = null;\n\n const onAbort = (): void => {\n aborted = true;\n killAndSettle();\n };\n\n const finish = (status: number | null): void => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n if (graceTimer) clearTimeout(graceTimer);\n if (child.pid !== undefined) liveChildren.delete(child.pid);\n opts.signal?.removeEventListener(\"abort\", onAbort);\n const result: ExecAsyncResult = {\n status,\n stdout: out.done(),\n stderr: err.done(),\n timedOut,\n truncated: out.over() || err.over(),\n };\n if (aborted) result.aborted = true;\n if (spawnError) result.error = spawnError;\n resolve(result);\n };\n\n /**\n * 终止子进程树,并在宽限期内仍未结算时强制结算。\n *\n * 只调 killTree 是不够的:结算依赖 `close`,而后代存活会让它永不触发\n * (原因见 KILL_GRACE_MS 注释)——本次会话卡死的直接成因。\n */\n const killAndSettle = (): void => {\n killTree(child.pid);\n if (settled || graceTimer) return;\n graceTimer = setTimeout(() => {\n // 输出已随 kill 前的 data 事件收完;这里放下可能仍被后代持有的管道,\n // 结算语义已由 timedOut / aborted 确定,不再继续等待\n child.stdout?.destroy();\n child.stderr?.destroy();\n finish(null);\n }, KILL_GRACE_MS);\n };\n\n if (opts.timeoutMs !== undefined && opts.timeoutMs > 0) {\n timer = setTimeout(() => {\n timedOut = true;\n killAndSettle();\n }, opts.timeoutMs);\n }\n\n opts.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n child.stdout?.on(\"data\", (c: Buffer) => out.push(c));\n child.stderr?.on(\"data\", (c: Buffer) => err.push(c));\n\n child.on(\"error\", (e) => {\n spawnError = e as NodeJS.ErrnoException;\n // 进程根本没起来时不会有 close 事件,立即结算避免调用方永久挂起\n if (child.pid === undefined) finish(null);\n });\n\n if (opts.input !== undefined && child.stdin) {\n // 子进程可能已退出(EPIPE),忽略写入错误\n child.stdin.on(\"error\", () => {});\n child.stdin.end(opts.input);\n }\n\n child.on(\"close\", (code) => finish(code));\n });\n}\n","/**\n * 终止整棵进程树\n *\n * 前台命令(execAsync 的超时/中断)与后台任务(ShellTaskManager.kill)共用:\n * 只有以 detached 启动的子进程才是进程组组长,负 pid 才能覆盖其派生的孙进程。\n */\nimport { spawnSync } from \"node:child_process\";\n\nexport function killTree(pid: number | undefined): void {\n if (!pid) return;\n if (process.platform === \"win32\") {\n try {\n spawnSync(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { windowsHide: true });\n } catch {\n /* 尽力而为 */\n }\n return;\n }\n // POSIX:detached 启动使子进程成为进程组组长,负 pid 即整组\n try {\n process.kill(-pid, \"SIGKILL\");\n } catch {\n try {\n process.kill(pid, \"SIGKILL\");\n } catch {\n /* 已退出 */\n }\n }\n}\n","/**\n * shell 运行时探测\n *\n * 决定 run_shell 用哪个 shell 执行命令,解决三件事:\n * - POSIX:沿用 Node 默认 /bin/sh,不改变既有语义;\n * - Windows:cmd.exe 没有 ls/grep/rm/cat,优先挑一个 POSIX 风格的 shell\n * (BusyBox-w32 / Git Bash / PowerShell),都没有时才回退到 %ComSpec%;\n * - 允许用 ZQAGENT_SHELL / ZQAGENT_BUSYBOX 显式覆盖,便于在装了非常规\n * shell(如内网自带的 busybox)的机器上接入。\n *\n * 本模块只做解析,不执行命令;platform / env / 文件探测均可注入,\n * 便于在 CI(Linux)上覆盖 Windows 分支。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/** shell 家族,决定命令行参数怎么拼 */\nexport type ShellKind = \"sh\" | \"busybox\" | \"powershell\" | \"cmd\" | \"default\";\n\nexport interface ShellSpec {\n kind: ShellKind;\n /** 人类可读标签,用于工具描述 */\n label: string;\n /** 可执行文件路径;null 表示交给 Node 默认(POSIX /bin/sh 或 %ComSpec%) */\n file: string | null;\n /** 拼出实际 argv(含命令本身) */\n buildArgs: (command: string) => string[];\n}\n\nexport interface ResolveShellOptions {\n platform?: NodeJS.Platform;\n env?: NodeJS.ProcessEnv;\n /** 判断文件是否存在,便于测试注入 */\n fileExists?: (p: string) => boolean;\n /** 用于定位「产物同目录」的 busybox,默认 process.execPath */\n execPath?: string;\n}\n\nexport interface ProbeContext {\n platform: NodeJS.Platform;\n env: NodeJS.ProcessEnv;\n fileExists: (p: string) => boolean;\n}\n\n/** Windows 可执行文件后缀(按优先级) */\nconst WINDOWS_SUFFIXES = [\".exe\", \".cmd\", \".bat\", \".com\", \"\"];\n\nfunction pathApi(platform: NodeJS.Platform) {\n return platform === \"win32\" ? path.win32 : path.posix;\n}\n\nfunction defaultFileExists(p: string): boolean {\n try {\n return fs.statSync(p).isFile();\n } catch {\n return false;\n }\n}\n\n/** 按目标平台(而非宿主)拼路径 */\nfunction joinPath(platform: NodeJS.Platform, ...parts: string[]): string {\n return pathApi(platform).join(...parts);\n}\n\n/**\n * 在 PATH 上查找可执行文件(同步,不 spawn 进程)。\n * name 含路径分隔符时按路径直接判定。Windows 会按 WINDOWS_SUFFIXES 补后缀。\n */\nexport function findExecutable(name: string, ctx: ProbeContext): string | null {\n if (name.includes(\"/\") || name.includes(\"\\\\\")) {\n return ctx.fileExists(name) ? name : null;\n }\n const suffixes = ctx.platform === \"win32\" ? WINDOWS_SUFFIXES : [\"\"];\n const delimiter = ctx.platform === \"win32\" ? \";\" : \":\";\n const dirs = (ctx.env.PATH ?? \"\").split(delimiter).filter((d) => d.length > 0);\n for (const dir of dirs) {\n for (const suffix of suffixes) {\n const candidate = joinPath(ctx.platform, dir, name + suffix);\n if (ctx.fileExists(candidate)) return candidate;\n }\n }\n return null;\n}\n\n/** 按文件名推断 shell 家族 */\nfunction kindFromBasename(base: string): ShellKind {\n const b = base.toLowerCase().replace(/\\.(exe|cmd|bat|com)$/, \"\");\n if (b === \"busybox\") return \"busybox\";\n if (b === \"powershell\" || b === \"pwsh\") return \"powershell\";\n if (b === \"cmd\") return \"cmd\";\n // sh/bash/dash/zsh/ash 以及不认识的名字(如 busybox 的 sh.exe 符号链接)都按 sh 处理\n return \"sh\";\n}\n\n/**\n * busybox 按 argv[0] 决定 applet:`busybox.exe` 需要显式给 applet 名\n * (`busybox.exe sh -c \"...\"`),而重命名/硬链接出来的 `sh.exe` 本身就是 ash,\n * 多传一个 \"sh\" 会被当成脚本文件名。\n */\nfunction busyboxArgs(file: string | null, command: string): string[] {\n const base = (file ? path.win32.basename(file) : \"busybox\")\n .toLowerCase()\n .replace(/\\.(exe|cmd|bat|com)$/, \"\");\n return base === \"busybox\" ? [\"sh\", \"-c\", command] : [\"-c\", command];\n}\n\nfunction argsFor(kind: ShellKind, command: string): string[] {\n switch (kind) {\n case \"busybox\":\n return [\"sh\", \"-c\", command];\n case \"powershell\":\n return [\"-NoProfile\", \"-NonInteractive\", \"-Command\", command];\n case \"cmd\":\n return [\"/d\", \"/s\", \"/c\", command];\n default:\n return [\"-c\", command];\n }\n}\n\nfunction makeSpec(kind: ShellKind, file: string | null, label: string): ShellSpec {\n return {\n kind,\n file,\n label,\n buildArgs: (command) =>\n kind === \"busybox\" ? busyboxArgs(file, command) : argsFor(kind, command),\n };\n}\n\nfunction defaultLabelFor(kind: ShellKind): string {\n switch (kind) {\n case \"busybox\":\n return \"busybox ash\";\n case \"powershell\":\n return \"PowerShell\";\n case \"cmd\":\n return \"cmd.exe\";\n case \"default\":\n return \"系统默认 shell\";\n default:\n return \"sh\";\n }\n}\n\n/**\n * 查找 BusyBox-w32。\n * 显式 ZQAGENT_BUSYBOX 优先;否则看 CLI 产物同目录及其 bin/ 子目录\n * (兼容 SEA 单文件、npm 安装与桌面 sidecar 三种布局,其中 sh.exe 是\n * busybox.exe 的重命名副本 —— busybox 按 argv[0] 决定 applet)。\n */\nfunction findBusybox(\n ctx: ProbeContext,\n execPath: string,\n): string | null {\n const explicit = (ctx.env.ZQAGENT_BUSYBOX ?? \"\").trim();\n if (explicit) return ctx.fileExists(explicit) ? explicit : null;\n\n const exeDir = pathApi(ctx.platform).dirname(execPath);\n const candidates = [\n joinPath(ctx.platform, exeDir, \"busybox.exe\"),\n joinPath(ctx.platform, exeDir, \"sh.exe\"),\n joinPath(ctx.platform, exeDir, \"bin\", \"busybox.exe\"),\n joinPath(ctx.platform, exeDir, \"bin\", \"sh.exe\"),\n ];\n for (const candidate of candidates) {\n if (ctx.fileExists(candidate)) return candidate;\n }\n return null;\n}\n\n/** 解析 ZQAGENT_SHELL 的显式取值(关键字或路径) */\nfunction resolveRequested(requested: string, ctx: ProbeContext, execPath: string): ShellSpec {\n const lower = requested.toLowerCase();\n\n if (lower === \"cmd\") {\n const file =\n findExecutable(\"cmd\", ctx) ?? ctx.env.ComSpec ?? ctx.env.COMSPEC ?? \"cmd.exe\";\n return makeSpec(\"cmd\", file, \"cmd.exe\");\n }\n if (lower === \"powershell\" || lower === \"pwsh\") {\n const file =\n findExecutable(lower, ctx) ?? findExecutable(\"powershell\", ctx) ?? `${lower}.exe`;\n return makeSpec(\"powershell\", file, \"PowerShell\");\n }\n if (lower === \"busybox\") {\n const found = findBusybox(ctx, execPath) ?? findExecutable(\"busybox\", ctx);\n return makeSpec(\"busybox\", found ?? requested, \"busybox ash\");\n }\n if ([\"sh\", \"bash\", \"dash\", \"zsh\", \"ash\"].includes(lower)) {\n return makeSpec(\"sh\", findExecutable(lower, ctx) ?? lower, lower);\n }\n\n // 视为路径(可能含空格,如 C:\\Program Files\\Git\\bin\\bash.exe)\n const base = pathApi(ctx.platform).basename(requested);\n const kind = kindFromBasename(base);\n return makeSpec(kind, requested, base || defaultLabelFor(kind));\n}\n\n/**\n * 解析当前应使用的 shell。\n * 返回值 file === null 时表示「不显式指定」,由 Node 走平台默认 shell\n * (POSIX /bin/sh / Windows %ComSpec%),即改造前的行为。\n */\nexport function resolveShellSpec(opts: ResolveShellOptions = {}): ShellSpec {\n const platform = opts.platform ?? process.platform;\n const env = opts.env ?? process.env;\n const execPath = opts.execPath ?? process.execPath;\n const ctx: ProbeContext = {\n platform,\n env,\n fileExists: opts.fileExists ?? defaultFileExists,\n };\n\n const requested = (env.ZQAGENT_SHELL ?? \"\").trim();\n if (requested && requested.toLowerCase() !== \"auto\") {\n return resolveRequested(requested, ctx, execPath);\n }\n\n // 非 Windows:保持 Node 默认(不改变既有行为)\n if (platform !== \"win32\") {\n return makeSpec(\"default\", null, \"/bin/sh\");\n }\n\n // Windows:优先 POSIX 风格 shell —— cmd.exe 缺 ls/grep/rm/cat,模型产出的\n // POSIX 命令在它下面几乎全废。\n const busybox = findBusybox(ctx, execPath);\n if (busybox) return makeSpec(\"busybox\", busybox, \"busybox ash (POSIX)\");\n\n const bash = findExecutable(\"bash\", ctx);\n if (bash) return makeSpec(\"sh\", bash, \"bash (Git for Windows)\");\n\n // PowerShell 排在 cmd 之前:它有 ls/cat/rm/cp/mv/pwd 别名,更接近模型习惯\n const shell = findExecutable(\"pwsh\", ctx) ?? findExecutable(\"powershell\", ctx);\n if (shell) return makeSpec(\"powershell\", shell, \"PowerShell\");\n\n return makeSpec(\"default\", null, env.ComSpec ?? env.COMSPEC ?? \"cmd.exe\");\n}\n\n/**\n * 为命令套上「编码设置 + 退出码传播」外壳,按 shell 家族区分。\n *\n * - PowerShell:先设置控制台输出编码为 UTF-8(同一次调用内即对后续命令生效,\n * 规避 `chcp 65001` 要等下一次调用才生效的问题),命令结束后若存在\n * `$LASTEXITCODE` 就以它作为进程退出码 —— 否则 PowerShell 对 `cmd /c ...`\n * 这类原生命令一律返回 0,工具拿到的 exit_code 不可信。\n * - cmd.exe(含 Windows 下未探测到其它 shell 时的 default):`chcp 65001` 切换\n * 代码页,避免中文被压成 `?`。\n * - POSIX sh / busybox:现代环境默认 UTF-8,不改动。\n */\nexport function wrapCommand(\n kind: ShellKind,\n command: string,\n platform: NodeJS.Platform = process.platform,\n): string {\n if (kind === \"powershell\") {\n return (\n \"$OutputEncoding = [Console]::OutputEncoding = [Text.Encoding]::UTF8; \" +\n command +\n \"\\nif ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE }\"\n );\n }\n if (kind === \"cmd\" || (kind === \"default\" && platform === \"win32\")) {\n return `chcp 65001>nul & ${command}`;\n }\n return command;\n}\n\n/** 生成给模型的运行环境提示,追加到 run_shell 描述里 */\nexport function describeShell(spec: ShellSpec, platform: NodeJS.Platform): string {\n if (spec.kind === \"sh\" || spec.kind === \"busybox\") {\n return `当前平台 ${platform},shell=${spec.label}:请使用 POSIX 风格命令。`;\n }\n if (spec.kind === \"powershell\") {\n return (\n `当前平台 ${platform},shell=PowerShell:ls/cat/rm/cp/mv/pwd 等别名可用,` +\n `也可用 cmdlet;路径分隔符为 \"\\\\\"。`\n );\n }\n if (spec.kind === \"default\" && platform !== \"win32\") {\n return `当前平台 ${platform},shell=${spec.label}:请使用 POSIX 风格命令。`;\n }\n return (\n `当前平台 ${platform},shell=${spec.label}:请使用 Windows 命令` +\n `(dir/type/del/copy/move/findstr),多条命令用 & 或 && 连接;` +\n `不要使用 ls/cat/rm/grep 等 POSIX 命令。`\n );\n}\n\n/**\n * 解析命令里的「切目录」意图,返回目录与剩余命令。\n *\n * 识别 POSIX 的 `cd dir`、PowerShell 的 `Set-Location`/`sl`、cmd 的 `cd /d dir`,\n * 以及 `pushd`/`Push-Location`(只跟踪目标目录;popd 的栈语义不在此处理)。\n * 带 `&&` 时(如 `cd /d D:\\x && dir`)只把切目录部分交给 cwd 跟踪,剩余命令\n * 在新目录下执行 —— 否则目录变更只影响那一个子 shell,无法跨调用保持。\n *\n * target 为 null 时调用方应原样执行命令(未识别到切目录意图)。\n */\nexport function parseCdChain(\n command: string,\n opts: { platform?: NodeJS.Platform } = {},\n): { target: string | null; rest: string | null } {\n const platform = opts.platform ?? process.platform;\n const trimmed = command.trim();\n\n const chain = /^([\\s\\S]*?)\\s*&&\\s*([\\s\\S]+)$/.exec(trimmed);\n const head = (chain ? chain[1] : trimmed).trim();\n const rest = chain ? chain[2].trim() : null;\n\n const m = /^(?:cd|set-location|sl|pushd|push-location)\\b\\s*([\\s\\S]*)$/i.exec(head);\n if (!m) return { target: null, rest: null };\n\n let target = m[1].trim();\n // PowerShell 显式参数与 cmd 的盘符切换\n target = target.replace(/^-(?:path|literalpath)\\s+/i, \"\");\n if (platform === \"win32\") target = target.replace(/^\\/d\\s+/i, \"\");\n // 去引号\n target = target.replace(/^\"(.*)\"$/, \"$1\").replace(/^'(.*)'$/, \"$1\").trim();\n\n // 裸 `cd` / `cd -` 不跟踪(无法确定目标目录)\n if (!target || target === \"-\") return { target: null, rest: null };\n return { target, rest };\n}\n","/**\n * 后台 shell 任务管理器\n *\n * 支撑 run_shell 的 `run_in_background` 能力(开发服务器 / watch 进程等长驻任务):\n * - start():detached 启动,stdout/stderr 进环形缓冲,立即返回 task_id;\n * - output():读取某任务的累计输出与状态;\n * - kill():终止整棵进程树(Windows 用 taskkill /T /F,POSIX 用进程组信号);\n * - disposeAll():进程退出时统一清理,避免残留子进程。\n *\n * 输出缓冲有上限(默认 64KB/流),超出丢弃最旧内容,防止长驻任务把内存吃满。\n */\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { killTree } from \"./killTree.js\";\n\n/** 每个输出流的缓冲上限(字符) */\nconst DEFAULT_BUFFER_LIMIT = 64 * 1024;\n\n/** 固定长度的环形字符缓冲:超出上限时从头部丢弃 */\nclass RingBuffer {\n private chunks: string[] = [];\n private size = 0;\n\n constructor(private readonly limit = DEFAULT_BUFFER_LIMIT) {}\n\n add(chunk: string): void {\n if (!chunk) return;\n this.chunks.push(chunk);\n this.size += chunk.length;\n while (this.size > this.limit && this.chunks.length > 1) {\n this.size -= this.chunks.shift()!.length;\n }\n // 单个 chunk 就可能超限:直接截取尾部\n if (this.size > this.limit) {\n const joined = this.chunks.join(\"\");\n this.chunks = [joined.slice(-this.limit)];\n this.size = this.chunks[0]!.length;\n }\n }\n\n get(): string {\n const joined = this.chunks.join(\"\");\n return joined.length > this.limit ? joined.slice(-this.limit) : joined;\n }\n}\n\ninterface BackgroundTask {\n id: string;\n command: string;\n /** 归属标识(server 传租户 id),用于按项目过滤任务列表 */\n scope: string | null;\n child: ChildProcess;\n stdout: RingBuffer;\n stderr: RingBuffer;\n status: \"running\" | \"exited\" | \"killed\";\n exitCode: number | null;\n signal: NodeJS.Signals | null;\n startedAt: number;\n endedAt: number | null;\n}\n\nexport interface BackgroundTaskSnapshot {\n task_id: string;\n command: string;\n status: \"running\" | \"exited\" | \"killed\";\n running: boolean;\n exit_code: number | null;\n signal: string | null;\n stdout: string;\n stderr: string;\n duration_ms: number;\n}\n\n/**\n * 推送用的精简任务快照。\n *\n * **刻意不含 stdout/stderr**:两路各 64KB,原样塞进通知会比轮询更费带宽与内存。\n * 需要输出时仍走 `output(taskId)` 按需读取。\n */\nexport interface ShellTaskLite {\n task_id: string;\n command: string;\n status: \"running\" | \"exited\" | \"killed\";\n running: boolean;\n exit_code: number | null;\n signal: string | null;\n duration_ms: number;\n /** 启动时刻(epoch ms):前端展开时据此本地累计耗时,不必再轮询 */\n started_at: number;\n}\n\n/** 任务变化回调:scope 为任务的归属(server 传租户 id),tasks 只含运行中的任务 */\nexport type ShellTasksListener = (\n scope: string | null,\n tasks: ShellTaskLite[],\n) => void;\n\nexport interface StartOptions {\n command: string;\n file: string;\n argv: string[];\n cwd: string;\n env: NodeJS.ProcessEnv;\n /** 归属标识(如租户 id),用于按项目过滤任务列表 */\n scope?: string | null;\n}\n\nexport class ShellTaskManager {\n private readonly tasks = new Map<string, BackgroundTask>();\n private seq = 0;\n private cleanupInstalled = false;\n /** 任务集合变化监听(server 用它推送,替代控制台的 5s 轮询) */\n private listener: ShellTasksListener | null = null;\n\n /** 注册变化监听(传 null 取消) */\n setListener(cb: ShellTasksListener | null): void {\n this.listener = cb;\n }\n\n /** 启动一个后台任务,立即返回快照(不等退出) */\n start(opts: StartOptions): BackgroundTaskSnapshot {\n this.installCleanup();\n const id = `shell-${++this.seq}-${Date.now().toString(36)}`;\n const child = spawn(opts.file, opts.argv, {\n cwd: opts.cwd,\n env: opts.env,\n detached: process.platform !== \"win32\",\n windowsHide: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n const task: BackgroundTask = {\n id,\n command: opts.command,\n scope: opts.scope ?? null,\n child,\n stdout: new RingBuffer(),\n stderr: new RingBuffer(),\n status: \"running\",\n exitCode: null,\n signal: null,\n startedAt: Date.now(),\n endedAt: null,\n };\n this.tasks.set(id, task);\n\n child.stdout?.on(\"data\", (d: Buffer) => task.stdout.add(d.toString(\"utf-8\")));\n child.stderr?.on(\"data\", (d: Buffer) => task.stderr.add(d.toString(\"utf-8\")));\n child.on(\"error\", (err) => {\n task.stderr.add(`[spawn error] ${err.message}\\n`);\n // spawn 失败(如 ENOENT)不会再触发 exit:不在这里收敛,界面上会永远显示「运行中」\n if (task.status === \"running\") {\n task.endedAt = Date.now();\n task.status = \"exited\";\n this._notify(task.scope);\n }\n });\n child.on(\"exit\", (code, signal) => {\n task.exitCode = code;\n task.signal = signal;\n task.endedAt = Date.now();\n // 已被 kill() 标记为 killed 的保持 killed,否则记为正常退出\n if (task.status !== \"killed\") task.status = \"exited\";\n this._notify(task.scope);\n });\n // 允许父进程退出后子进程继续(配合 disposeAll 显式清理)\n child.unref();\n\n const snap = this.snapshot(task);\n // 先返回快照再通知:订阅方拿到推送时任务已可查询\n this._notify(task.scope);\n return snap;\n }\n\n /** 读取任务输出与状态 */\n output(taskId: string): BackgroundTaskSnapshot | { error: string } {\n const task = this.tasks.get(taskId);\n if (!task) return { error: `未知的后台任务: ${taskId}` };\n return this.snapshot(task);\n }\n\n /** 终止任务(进程树)并返回快照;传 scope 时只允许终止该归属下的任务 */\n kill(taskId: string, scope?: string | null): BackgroundTaskSnapshot | { error: string } {\n const task = this.tasks.get(taskId);\n if (!task) return { error: `未知的后台任务: ${taskId}` };\n if (scope !== undefined && task.scope !== scope) {\n return { error: `未知的后台任务: ${taskId}` };\n }\n if (task.status === \"running\") {\n task.status = \"killed\";\n task.endedAt = Date.now();\n killTree(task.child.pid);\n this._notify(task.scope);\n }\n return this.snapshot(task);\n }\n\n /** 列出任务快照(调试/展示用);传 scope 时只返回该归属下的任务 */\n list(scope?: string | null): BackgroundTaskSnapshot[] {\n const all = [...this.tasks.values()];\n return (scope === undefined ? all : all.filter((t) => t.scope === scope)).map((t) =>\n this.snapshot(t),\n );\n }\n\n /** 终止全部任务(进程退出时调用) */\n disposeAll(): void {\n for (const task of this.tasks.values()) {\n if (task.status === \"running\") {\n task.status = \"killed\";\n killTree(task.child.pid);\n }\n }\n }\n\n /**\n * 通知订阅方「该归属下运行中的任务变了」。\n *\n * 只遍历内存任务表取字段,不调用 snapshot(后者会 join 两路 64KB 缓冲)。\n */\n private _notify(scope: string | null): void {\n const listener = this.listener;\n if (!listener) return;\n const tasks: ShellTaskLite[] = [];\n for (const task of this.tasks.values()) {\n if (task.scope !== scope || task.status !== \"running\") continue;\n tasks.push({\n task_id: task.id,\n command: task.command,\n status: task.status,\n running: true,\n exit_code: task.exitCode,\n signal: task.signal,\n duration_ms: Date.now() - task.startedAt,\n started_at: task.startedAt,\n });\n }\n try {\n listener(scope, tasks);\n } catch {\n // 推送失败不影响任务本身\n }\n }\n\n private snapshot(task: BackgroundTask): BackgroundTaskSnapshot {\n return {\n task_id: task.id,\n command: task.command,\n status: task.status,\n running: task.status === \"running\",\n exit_code: task.exitCode,\n signal: task.signal,\n stdout: task.stdout.get(),\n stderr: task.stderr.get(),\n duration_ms: (task.endedAt ?? Date.now()) - task.startedAt,\n };\n }\n\n private installCleanup(): void {\n if (this.cleanupInstalled) return;\n this.cleanupInstalled = true;\n process.once(\"exit\", () => this.disposeAll());\n }\n}\n","/**\n * Shell 沙箱(OS 级文件系统约束)\n *\n * 目标:让 `run_shell` 在工作区内可以免审批执行,而不是靠命令名白名单\n * 逐个放行——后者既挡不住 `python -c` 这类间接执行,也让日常开发寸步难行。\n *\n * 各平台取当前可用的最优方案:\n * - macOS:`/usr/bin/sandbox-exec`(SBPL profile)。系统自带,进程级,\n * 毫秒级开销;默认放行读与网络,只把 `file-write*` 收窄到工作区 + 临时目录。\n * - Linux:`bubblewrap`(bwrap)。用 `--ro-bind / /` 把整机只读挂载,\n * 再把工作区 `--bind` 成可写,二者叠加即为「只在工作区内可写」。\n * 需要发行版装了 bwrap 且允许非特权 user namespace(容器里通常需\n * `--security-opt seccomp=unconfined` 或 privileged)。\n * - Windows:没有进程级文件系统沙箱的等价物(AppContainer 需要额外的\n * 原生 helper 与 ACL 配置,不属于零依赖方案)。此处明确返回\n * `enforced: false`,由权限层退化为「工作区路径启发式」判定\n * (见 permissions/workspaceGuard.ts)——它是降低打扰的策略,不是安全边界。\n *\n * 探测结果做进程级缓存:CLI 在 /mode、/agent 等场景会重建引擎,\n * 但沙箱能力不会变,不必每次 spawn 探测进程。\n */\nimport { spawnSync } from \"node:child_process\";\nimport * as fs from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\n\n/** 沙箱后端 */\nexport type SandboxBackend = \"sandbox-exec\" | \"bwrap\" | \"none\";\n\n/** 配置取值:auto=探测到就用;off=禁用;required=探测不到时明确报错 */\nexport type SandboxMode = \"auto\" | \"off\" | \"required\";\n\nexport interface SandboxOptions {\n mode?: SandboxMode;\n /** 额外可写路径(绝对路径,支持 ~;相对路径按 workspace 解析) */\n extraWritable?: string[];\n /** 工作区根(解析相对路径的额外可写项) */\n workspace?: string;\n platform?: NodeJS.Platform;\n env?: NodeJS.ProcessEnv;\n fileExists?: (p: string) => boolean;\n /** 可用性探测(测试注入);提供时不走缓存 */\n probe?: (backend: Exclude<SandboxBackend, \"none\">, bin: string) => boolean;\n}\n\n/** 沙箱方案(可跨调用复用) */\nexport interface SandboxPlan {\n backend: SandboxBackend;\n /** OS 级文件系统约束是否生效 */\n enforced: boolean;\n /** 沙箱可执行文件;未生效时为 null */\n bin: string | null;\n /** 允许写入的路径(绝对路径,已去重) */\n writable: string[];\n mode: SandboxMode;\n /** 状态说明(日志 / 工具描述用) */\n reason: string;\n}\n\n/** 一次待执行的子进程调用(file + argv,不经 shell 解释) */\nexport interface SandboxInvocation {\n file: string;\n argv: string[];\n}\n\n/** 解析 ~ 与环境变量无关的常见写法 */\nfunction expandHome(p: string, env: NodeJS.ProcessEnv): string {\n if (p === \"~\") return env.HOME ?? p;\n if (p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) {\n return path.join(env.HOME ?? \"~\", p.slice(2));\n }\n return p;\n}\n\n/**\n * 工具链缓存目录:这些必须可写,否则 `pnpm install` / `npm ci` 会因\n * store / cache 落在工作区外而报 EPERM。只放行缓存与包仓库,\n * 不放行 `~/.ssh`、shell 配置等敏感位置。\n */\nfunction toolchainWritable(\n platform: NodeJS.Platform,\n env: NodeJS.ProcessEnv,\n): string[] {\n const home = env.HOME ?? os.homedir();\n const rel =\n platform === \"darwin\"\n ? [\"Library/pnpm\", \"Library/Caches\", \".npm\", \".cache\"]\n : [\".npm\", \".cache\", \".local/share/pnpm\", \".local/state/pnpm\", \".pnpm-store\"];\n return rel.map((r) => path.join(home, r));\n}\n\n/**\n * 加入工具链缓存目录。\n *\n * 只加**真实存在**的目录:bwrap 对不存在的 bind 源会直接报错并终止整个命令\n * (`Can't find source path ~/.pnpm-store`),一个没装过的缓存目录就能让沙箱\n * 里所有命令失败。缓存目录不存在本身也无所谓——第一次 `pnpm install` 会自己建。\n */\nfunction addToolchainWritable(\n platform: NodeJS.Platform,\n env: NodeJS.ProcessEnv,\n add: (p: string) => void,\n exists: (p: string) => boolean,\n): void {\n for (const dir of toolchainWritable(platform, env)) {\n if (exists(dir)) add(dir);\n }\n}\n\n/** 收集去重后的可写路径(工作区 + 临时目录 + 用户追加) */\nfunction collectWritable(opts: {\n workspace: string;\n extraWritable: string[];\n platform: NodeJS.Platform;\n env: NodeJS.ProcessEnv;\n /** 路径存在性判定(测试可注入) */\n exists: (p: string) => boolean;\n}): string[] {\n const out = new Set<string>();\n const add = (p: string): void => {\n if (!p) return;\n const abs = path.resolve(p);\n out.add(abs);\n // macOS 的 /tmp、/var 都是符号链接,SBPL 按真实路径匹配,两个都加上\n try {\n out.add(fs.realpathSync(abs));\n } catch {\n /* 路径尚不存在:只保留原始绝对路径 */\n }\n };\n\n add(opts.workspace);\n\n if (opts.platform === \"darwin\") {\n const tmp = os.tmpdir();\n add(tmp);\n add(\"/tmp\");\n add(\"/private/tmp\");\n add(\"/dev\");\n addToolchainWritable(opts.platform, opts.env, add, opts.exists);\n } else if (opts.platform !== \"win32\") {\n add(\"/tmp\");\n add(\"/dev\");\n addToolchainWritable(opts.platform, opts.env, add, opts.exists);\n }\n\n for (const raw of opts.extraWritable) {\n const expanded = expandHome(String(raw ?? \"\").trim(), opts.env);\n if (!expanded) continue;\n add(path.resolve(opts.workspace, expanded));\n }\n\n return [...out];\n}\n\n/** 默认探测:真正跑一次最小命令,确认后端在本机可用(含 user namespace 限制) */\nfunction defaultProbe(\n backend: Exclude<SandboxBackend, \"none\">,\n bin: string,\n): boolean {\n const args =\n backend === \"sandbox-exec\"\n ? [\"-p\", \"(version 1)(allow default)\", \"/bin/sh\", \"-c\", \"true\"]\n : [\"--ro-bind\", \"/\", \"/\", \"--dev-bind\", \"/dev\", \"/dev\", \"--\", \"/bin/true\"];\n try {\n const r = spawnSync(bin, args, { timeout: 3000, stdio: \"ignore\" });\n return r.status === 0;\n } catch {\n return false;\n }\n}\n\n/** 是否为正则化的模式取值 */\nfunction normalizeMode(raw: string | undefined): SandboxMode {\n const v = String(raw ?? \"\").trim().toLowerCase();\n if (v === \"off\" || v === \"none\" || v === \"false\") return \"off\";\n if (v === \"required\" || v === \"on\" || v === \"true\") return \"required\";\n return \"auto\";\n}\n\n/** 缓存键:同平台、同模式、同工作区、同追加路径只探测一次 */\nconst planCache = new Map<string, SandboxPlan>();\n\n/**\n * 解析当前平台可用的沙箱方案。\n *\n * `mode: \"off\"` 直接返回未生效;`required` 在探测失败时通过 `reason`\n * 说明原因(调用方可据此拒绝启动或提示用户安装 bwrap)。\n */\nexport function resolveSandbox(opts: SandboxOptions = {}): SandboxPlan {\n const platform = opts.platform ?? process.platform;\n const env = opts.env ?? process.env;\n const workspace = path.resolve(opts.workspace ?? process.cwd());\n const mode = normalizeMode(opts.mode ?? env.ZQAGENT_SANDBOX);\n const extraWritable = opts.extraWritable ?? [];\n // 目录(而非文件)的存在性:缓存目录不存在时不能写进 bind 列表,\n // 否则 bwrap 会因为「源路径不存在」让整条命令失败。\n // 复用 fileExists 注入点,测试可用它模拟「目录都存在」。\n const pathExists: (p: string) => boolean =\n opts.fileExists ??\n ((p: string) => {\n try {\n return fs.existsSync(p);\n } catch {\n return false;\n }\n });\n const writable = collectWritable({\n workspace,\n extraWritable,\n platform,\n env,\n exists: pathExists,\n });\n\n const cacheKey = [\n platform,\n mode,\n workspace,\n writable.join(\"\\u0000\"),\n opts.probe ? \"injected\" : \"default\",\n ].join(\"|\");\n const cached = planCache.get(cacheKey);\n if (cached && !opts.probe) return cached;\n\n const finish = (plan: SandboxPlan): SandboxPlan => {\n if (!opts.probe) planCache.set(cacheKey, plan);\n return plan;\n };\n\n if (mode === \"off\") {\n return finish({\n backend: \"none\",\n enforced: false,\n bin: null,\n writable,\n mode,\n reason: \"沙箱已按配置关闭\",\n });\n }\n\n const fileExists = opts.fileExists ?? ((p: string) => {\n try {\n return fs.statSync(p).isFile();\n } catch {\n return false;\n }\n });\n\n const candidate: { backend: Exclude<SandboxBackend, \"none\">; bin: string } | null =\n platform === \"darwin\"\n ? { backend: \"sandbox-exec\", bin: \"/usr/bin/sandbox-exec\" }\n : platform === \"linux\"\n ? { backend: \"bwrap\", bin: \"/usr/bin/bwrap\" }\n : null;\n\n if (!candidate) {\n return finish({\n backend: \"none\",\n enforced: false,\n bin: null,\n writable,\n mode,\n reason:\n platform === \"win32\"\n ? \"Windows 无零依赖的进程级文件系统沙箱,改用工作区路径判定\"\n : `平台 ${platform} 无受支持的沙箱后端`,\n });\n }\n\n if (!fileExists(candidate.bin)) {\n return finish({\n backend: \"none\",\n enforced: false,\n bin: null,\n writable,\n mode,\n reason: `未找到 ${candidate.bin}(${\n candidate.backend === \"bwrap\" ? \"安装 bubblewrap 后可启用\" : \"系统缺少 sandbox-exec\"\n }),改用工作区路径判定`,\n });\n }\n\n const probe = opts.probe ?? defaultProbe;\n if (!probe(candidate.backend, candidate.bin)) {\n return finish({\n backend: \"none\",\n enforced: false,\n bin: null,\n writable,\n mode,\n reason: `${candidate.bin} 探测失败(容器内可能禁用了 user namespace),改用工作区路径判定`,\n });\n }\n\n return finish({\n backend: candidate.backend,\n enforced: true,\n bin: candidate.bin,\n writable,\n mode,\n reason:\n candidate.backend === \"sandbox-exec\"\n ? \"macOS sandbox-exec:只允许写入工作区与临时目录\"\n : \"Linux bubblewrap:只允许写入工作区与临时目录\",\n });\n}\n\n/** SBPL 字符串字面量转义 */\nfunction sbplString(value: string): string {\n return `\"${value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/**\n * 生成 macOS sandbox profile。\n *\n * 采用「allow default + deny file-write* + 白名单放行」而不是\n * 「deny default」:后者要逐项放行进程、信号、sysctl、mach lookup 等,\n * 极易把 git / node / 包管理器弄挂,维护成本也高。这里只收窄写权限,\n * 读与网络保持默认——与 bwrap 方案的能力边界一致。\n */\nexport function buildMacProfile(writable: string[]): string {\n const rules = writable.map((p) => ` (subpath ${sbplString(p)})`).join(\"\\n\");\n return [\n \"(version 1)\",\n \"(allow default)\",\n \"(deny file-write*)\",\n \"(allow file-write*\",\n rules,\n \")\",\n ].join(\"\\n\");\n}\n\n/** 生成 bubblewrap 的参数前缀 */\nexport function buildBwrapArgs(writable: string[], cwd: string): string[] {\n const args: string[] = [\"--ro-bind\", \"/\", \"/\", \"--dev-bind\", \"/dev\", \"/dev\"];\n // /tmp 用独立 tmpfs:避免把宿主机 /tmp 暴露成可写(工作区内的 /tmp 除外)\n if (!writable.includes(\"/tmp\")) args.push(\"--tmpfs\", \"/tmp\");\n for (const dir of writable) {\n if (dir === \"/dev\" || dir === \"/proc\") continue;\n args.push(\"--bind\", dir, dir);\n }\n args.push(\"--chdir\", cwd, \"--die-with-parent\");\n return args;\n}\n\n/**\n * 把一次 shell 调用包进沙箱。\n *\n * 未生效时原样返回(调用方无需分支)。参数一律走 argv,不拼字符串,\n * 避免用户命令中的引号 / 空格影响外层命令。\n */\nexport function wrapWithSandbox(\n plan: SandboxPlan | null | undefined,\n invocation: SandboxInvocation,\n ctx: { cwd: string },\n): SandboxInvocation {\n if (!plan || !plan.enforced || !plan.bin) return invocation;\n\n if (plan.backend === \"sandbox-exec\") {\n return {\n file: plan.bin,\n argv: [\"-p\", buildMacProfile(plan.writable), invocation.file, ...invocation.argv],\n };\n }\n\n return {\n file: plan.bin,\n argv: [...buildBwrapArgs(plan.writable, ctx.cwd), \"--\", invocation.file, ...invocation.argv],\n };\n}\n\n/** 一行状态说明,用于日志与工具描述 */\nexport function describeSandbox(plan: SandboxPlan | null | undefined): string {\n if (!plan) return \"未配置\";\n return plan.enforced ? plan.reason : `未启用(${plan.reason})`;\n}\n\n/** 测试用:清空探测缓存 */\nexport function resetSandboxCache(): void {\n planCache.clear();\n}\n","/**\n * Shell 工具\n *\n * 提供 run_shell 工具(对齐 Python 版 LocalExecutor):\n * 在持久化 shell 会话中执行命令。cwd 在多次调用之间保持\n * (支持 cd / pushd 命令切换目录,状态按工作区存放,引擎重建也不丢),\n * 输出截断到末尾部分,超时与缓冲区大小均有上限。\n *\n * 用哪个 shell 由 shellRuntime 决定:POSIX 下沿用 Node 默认 /bin/sh\n * (行为不变),Windows 下优先 POSIX 风格 shell(busybox / Git Bash /\n * PowerShell),都没有才回退 cmd.exe。\n *\n * 执行统一走异步 execAsync:成功与失败都能拿到真实的 stdout / stderr / exit_code\n * (旧实现用 execSync,成功分支把 stderr 丢弃、exit_code 恒为 0);异步而非\n * spawnSync,避免命令执行期间阻塞事件循环、拖住其他会话与心跳。\n * 另外支持 run_in_background,配合 shell_task_output / shell_task_kill\n * 管理开发服务器等长驻进程。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { ToolRegistry } from \"./ToolRegistry.js\";\nimport { execAsync } from \"./execAsync.js\";\nimport {\n describeShell,\n parseCdChain,\n resolveShellSpec,\n wrapCommand,\n type ShellSpec,\n} from \"./shellRuntime.js\";\nimport { ShellTaskManager } from \"./shellBackground.js\";\nimport type { BackgroundTaskSnapshot, ShellTasksListener } from \"./shellBackground.js\";\nimport type { ToolExecContext } from \"./ToolRegistry.js\";\nimport {\n describeSandbox,\n wrapWithSandbox,\n type SandboxPlan,\n} from \"./sandbox.js\";\n\n/** 输出截断上限(字符) */\nconst MAX_STDOUT = 10000;\nconst MAX_STDERR = 5000;\n/** 单次命令输出缓冲上限(字节) */\nconst MAX_BUFFER = 1024 * 1024 * 5;\n\n/**\n * 持久 shell 会话状态:按「工作区 + 归属」存放 cwd。\n *\n * 之所以放在模块级而不是 makeRunShell 的闭包里:CLI 在 /mode、/agent、/model、\n * reset 时会重建引擎(重新调用 registerShellTool),闭包里的 cwd 会随之丢失。\n *\n * 键必须带上归属(owner):多个会话共享同一 workspaceRoot 时,如果只按工作区\n * 做键,A 会话 `cd src` 会让 B 会话的下一条命令也在 src 下执行——两个并行会话\n * 互相串目录。owner 由调用方给出会话级标识(server 传 tenant/session)。\n * 未给 owner 时(CLI 单会话)退化为按工作区,保持既有行为。\n */\nconst shellSessions = new Map<string, { cwd: string }>();\n\n/** 会话键:工作区绝对路径 + 可选的归属标识(用 NUL 分隔,不会与路径字符冲突) */\nfunction shellKey(workspace: string, owner?: string | null): string {\n // 用规范化后的绝对路径作键:避免尾斜杠 / 相对路径在不同注册点产生两个会话\n const root = path.resolve(workspace);\n const o = String(owner ?? \"\").trim();\n return o ? `${root}\\u0000${o}` : root;\n}\n\nfunction sessionFor(workspace: string, owner?: string | null): { cwd: string } {\n const key = shellKey(workspace, owner);\n let s = shellSessions.get(key);\n if (!s) {\n s = { cwd: path.resolve(workspace) };\n shellSessions.set(key, s);\n }\n return s;\n}\n\n/** 测试用:重置某个工作区的 shell 会话状态 */\nexport function resetShellSession(workspace: string, owner?: string | null): void {\n shellSessions.delete(shellKey(workspace, owner));\n}\n\n/** 当前 shell 会话的 cwd(供权限引擎判定相对路径是否越界) */\nexport function getShellCwd(workspace: string, owner?: string | null): string {\n return sessionFor(workspace, owner).cwd;\n}\n\n/** 解析实际执行的 shell 可执行文件与 argv 构造器(default 时按平台兜底) */\nfunction resolveInvocation(\n spec: ShellSpec,\n prepared: string,\n): { file: string; argv: string[] } {\n if (spec.file !== null) {\n return { file: spec.file, argv: spec.buildArgs(prepared) };\n }\n if (process.platform === \"win32\") {\n const comspec = process.env.ComSpec ?? process.env.COMSPEC ?? \"cmd.exe\";\n return { file: comspec, argv: [\"/d\", \"/s\", \"/c\", prepared] };\n }\n return { file: \"/bin/sh\", argv: [\"-c\", prepared] };\n}\n\n/** 供 run_shell 与后台任务共用的环境变量 */\nfunction shellEnv(): NodeJS.ProcessEnv {\n return {\n ...process.env,\n GIT_TERMINAL_PROMPT: \"0\",\n PYTHONUNBUFFERED: \"1\",\n PYTHONUTF8: \"1\",\n PYTHONIOENCODING: \"utf-8\",\n };\n}\n\n/**\n * 解码子进程输出。\n *\n * 正常情况下命令前已注入 UTF-8 编码设置,输出就是 UTF-8。但 PowerShell 的\n * **语法错误(ParserError)** 会在脚本执行前就抛出,此时编码设置那行还没跑,\n * stderr 用的是控制台 OEM 代码页(中文 Windows 为 936/GBK)——直接按 UTF-8\n * 读会得到乱码。这里先严格按 UTF-8 解,失败再回退到 GBK 等常见编码。\n */\nexport function decodeOutput(buf: Buffer | null | undefined): string {\n if (!buf || buf.length === 0) return \"\";\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(buf);\n } catch {\n for (const enc of [\"gbk\", \"big5\", \"shift_jis\", \"latin1\"]) {\n try {\n return new TextDecoder(enc).decode(buf);\n } catch {\n /* 当前 Node 构建不支持该编码,继续尝试 */\n }\n }\n return buf.toString(\"latin1\");\n }\n}\n\n/**\n * run_shell — 持久化 shell (对齐 Python 版 LocalExecutor)\n *\n * 内部维护 cwd 状态:识别 cd / pushd 命令并更新,使后续命令在新目录下执行。\n * 注意:不要用 require()(ESM 下未定义),必须用 import 的 fs/path。\n */\nfunction makeRunShell(\n workspace: string,\n spec: ShellSpec,\n tasks: ShellTaskManager,\n scope: string | null,\n sandbox: SandboxPlan | null,\n sessionKey: string | null,\n) {\n const session = sessionFor(workspace, sessionKey);\n\n return async function run_shell(args: Record<string, unknown>, ctx?: ToolExecContext) {\n const command = String(args.command ?? \"\");\n const description = String(args.description ?? \"\").slice(0, 100);\n const background = args.run_in_background === true;\n // 超时上限 600s,换算为毫秒;非法值回退默认 120s\n const rawTimeout = Number(args.timeout_seconds ?? 120);\n const timeoutSec = Number.isFinite(rawTimeout) ? rawTimeout : 120;\n const timeout = Math.min(Math.max(timeoutSec, 1), 600) * 1000;\n\n if (!command.trim()) {\n return { error: \"命令不能为空\" };\n }\n\n // 切目录:在执行前解析,这样 `cd dir && 其他命令` 的剩余部分能在新目录下跑\n // (否则目录变更只作用于那个子 shell,无法跨调用保持)。\n const { target, rest } = parseCdChain(command);\n let toRun = command;\n if (target) {\n try {\n const resolved = path.resolve(session.cwd, target);\n if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n session.cwd = resolved;\n }\n } catch {\n /* ignore */\n }\n if (rest) toRun = rest;\n }\n\n const prepared = wrapCommand(spec.kind, toRun);\n // 沙箱在这里套:命令仍是完整的 shell 调用,只是外层多一层\n // sandbox-exec / bwrap,用来把可写范围收窄到工作区\n const { file, argv } = wrapWithSandbox(\n sandbox,\n resolveInvocation(spec, prepared),\n { cwd: session.cwd },\n );\n const env = shellEnv();\n\n // ---- 后台执行 ----\n if (background) {\n const snap = tasks.start({ command, file, argv, cwd: session.cwd, env, scope });\n return {\n task_id: snap.task_id,\n status: snap.status,\n cwd: session.cwd,\n note: \"已在后台启动。用 shell_task_output 查看输出,shell_task_kill 终止。\",\n };\n }\n\n // ---- 前台执行 ----\n // 不用 encoding: \"utf-8\",先拿原始字节再自适应解码(见 decodeOutput)。\n const result = await execAsync(file, argv, {\n cwd: session.cwd,\n timeoutMs: timeout,\n maxBuffer: MAX_BUFFER,\n env,\n signal: ctx?.signal,\n });\n\n const stdout = decodeOutput(result.stdout).slice(-MAX_STDOUT);\n const stderr = decodeOutput(result.stderr).slice(-MAX_STDERR);\n\n // 被用户中断:报告为中断而非普通失败,引擎随即按中断收尾\n if (result.aborted) {\n return {\n stdout,\n stderr,\n exit_code: result.status ?? 1,\n cwd: session.cwd,\n interrupted: true,\n error: \"命令已被用户中断\",\n };\n }\n\n // 非超时的启动错误(如 shell 不存在)单独报告\n if (result.error && !result.timedOut) {\n const message = result.error.message;\n return {\n stdout,\n stderr: stderr || message,\n exit_code: 1,\n cwd: session.cwd,\n error: message.slice(0, 200),\n };\n }\n\n return {\n stdout,\n stderr,\n exit_code: result.status ?? 1,\n cwd: session.cwd,\n ...(result.timedOut\n ? { timed_out: true, error: `命令超时(${timeoutSec}s)后已终止` }\n : {}),\n };\n };\n}\n\n/**\n * 后台任务表:与 shellSessions 同理,放模块级单例。\n * 若每次注册都新建,引擎重建后旧 task_id 会查不到、后台进程也失去跟踪。\n */\nconst shellTasks = new ShellTaskManager();\n\n/** 列出后台任务快照;scope 为 undefined 时返回全部(控制台按项目过滤用) */\nexport function listShellTasks(scope?: string | null): BackgroundTaskSnapshot[] {\n return shellTasks.list(scope);\n}\n\n/** 终止后台任务(连同进程树);scope 用于限定归属,防止跨项目误杀 */\nexport function killShellTask(\n taskId: string,\n scope?: string | null,\n): BackgroundTaskSnapshot | { error: string } {\n return shellTasks.kill(taskId, scope);\n}\n\n/**\n * 订阅后台任务集合变化(启动 / 退出 / 被终止)。\n *\n * 传 null 取消订阅。server 用它把变化推给控制台,替代对 `/shell/tasks` 的轮询。\n */\nexport function setShellTasksListener(cb: ShellTasksListener | null): void {\n shellTasks.setListener(cb);\n}\n\n/**\n * 注册 Shell 工具(高风险:requiresApproval=true,由权限引擎门控)\n *\n * @param scope 后台任务归属(server 传租户 id);用于按项目过滤任务列表与终止\n * @param sandbox OS 级沙箱方案(未启用时传 null 或省略)\n * @param sessionKey shell 会话归属(server 传 tenant/session)。多会话共享同一\n * 工作区时必须传,否则它们的 cwd 会互相覆盖;CLI 单会话可省略\n */\nexport function registerShellTool(\n registry: ToolRegistry,\n workspace: string,\n scope?: string | null,\n sandbox?: SandboxPlan | null,\n sessionKey?: string | null,\n): void {\n const spec = resolveShellSpec();\n const tasks = shellTasks;\n const shell = makeRunShell(\n workspace,\n spec,\n tasks,\n scope ?? null,\n sandbox ?? null,\n sessionKey ?? null,\n );\n\n registry.register(\n (args, ctx) => shell(args, ctx),\n { category: \"shell\", riskLevel: \"high\", capabilities: [\"run_shell\"], requiresApproval: true },\n {\n type: \"function\",\n function: {\n name: \"run_shell\",\n description:\n \"在持久化 shell 会话中运行命令 (cwd 和环境变量在多次调用之间保持)。\" +\n \"输出截断到最后部分,stdout 与 stderr 都会返回。\" +\n \"传递 description 说明命令用途 (显示在审批提示中),\" +\n \"传递 timeout_seconds 控制超时。\" +\n \"对于开发服务器 / watch 等长驻进程,设置 run_in_background=true,\" +\n \"随后用 shell_task_output 轮询输出、shell_task_kill 终止。\" +\n `运行环境:${describeShell(spec, process.platform)}` +\n (sandbox?.enforced\n ? `沙箱:${describeSandbox(sandbox)};工作区外的写入会被系统拒绝。`\n : \"\"),\n parameters: {\n type: \"object\",\n properties: {\n command: { type: \"string\", description: \"要执行的 shell 命令\" },\n description: { type: \"string\", description: \"命令用途的简短描述 (显示在审批中)\" },\n timeout_seconds: { type: \"integer\", description: \"超时秒数 (默认 120,最大 600)\" },\n run_in_background: {\n type: \"boolean\",\n description: \"是否后台运行 (默认 false)。适合开发服务器等长驻进程。\",\n },\n },\n required: [\"command\"],\n },\n },\n },\n );\n\n // ---- shell_task_output ----\n registry.register(\n (args) => tasks.output(String(args.task_id ?? \"\")),\n { category: \"shell\", riskLevel: \"low\", capabilities: [\"run_shell\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"shell_task_output\",\n description:\n \"查看 run_shell(run_in_background=true) 启动的后台任务的累计输出与状态。\" +\n \"返回 status (running/exited/killed)、exit_code、stdout、stderr。\",\n parameters: {\n type: \"object\",\n properties: {\n task_id: { type: \"string\", description: \"run_shell 返回的 task_id\" },\n },\n required: [\"task_id\"],\n },\n },\n },\n );\n\n // ---- shell_task_kill ----\n registry.register(\n (args) => tasks.kill(String(args.task_id ?? \"\"), scope ?? null),\n { category: \"shell\", riskLevel: \"medium\", capabilities: [\"run_shell\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"shell_task_kill\",\n description: \"终止后台任务(连同其子进程树)。返回任务的最终状态。\",\n parameters: {\n type: \"object\",\n properties: {\n task_id: { type: \"string\", description: \"run_shell 返回的 task_id\" },\n },\n required: [\"task_id\"],\n },\n },\n },\n );\n}\n","/**\n * 纯 Node 实现的 grep 回退\n *\n * 当 ripgrep 不可用时(未捆绑 / 未安装),用 fs 递归遍历 + JS RegExp 匹配,\n * 保证 grep 工具在任意环境都能工作,且支持完整的 JS 正则语法\n * (\\d / \\s / + / | 等,findstr 的 POSIX 子集远不能及)。\n *\n * 取舍:比 ripgrep 慢,因此做了忽略目录(.git/node_modules)、文件大小上限、\n * 二进制探测与最大匹配数限制;作为最后兜底足够用。\n */\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nexport interface GrepMatch {\n file: string;\n line: number;\n text: string;\n}\n\nexport interface NodeGrepOptions {\n pattern: string;\n /** 搜索目标:文件或目录(可为绝对/相对路径) */\n target: string;\n /** 工作区根:用于把命中路径格式化为相对路径 */\n workspace: string;\n /** 单文件读取上限(字节),超过则跳过 */\n maxFileBytes?: number;\n /** 最大匹配数 */\n maxMatches?: number;\n}\n\n/** 遍历时跳过的目录名 */\nconst SKIP_DIRS = new Set([\n \".git\",\n \"node_modules\",\n \".svn\",\n \".hg\",\n \"dist\",\n \"build\",\n \".next\",\n \".cache\",\n]);\n\nfunction looksBinary(buf: Buffer): boolean {\n const n = Math.min(buf.length, 8192);\n for (let i = 0; i < n; i++) if (buf[i] === 0) return true;\n return false;\n}\n\n/**\n * 在目标文件 / 目录中搜索。命中返回 {file, line, text} 列表。\n * 目标不存在时抛错(区别于「无匹配」)。\n */\nexport function nodeGrep(opts: NodeGrepOptions): GrepMatch[] {\n const maxMatches = opts.maxMatches ?? 50;\n const maxFileBytes = opts.maxFileBytes ?? 1024 * 1024;\n\n let regex: RegExp;\n try {\n regex = new RegExp(opts.pattern);\n } catch (err) {\n throw new Error(`无效的正则表达式: ${opts.pattern}(${String(err)})`);\n }\n\n const target = path.resolve(opts.workspace, opts.target);\n if (!fs.existsSync(target)) {\n throw new Error(`搜索路径不存在: ${opts.target}`);\n }\n\n const matches: GrepMatch[] = [];\n\n const scanFile = (file: string): boolean => {\n let stat: fs.Stats;\n try {\n stat = fs.statSync(file);\n } catch {\n return false;\n }\n if (!stat.isFile() || stat.size > maxFileBytes) return false;\n\n let buf: Buffer;\n try {\n buf = fs.readFileSync(file);\n } catch {\n return false;\n }\n if (looksBinary(buf)) return false;\n\n const rel = path.relative(opts.workspace, file) || path.basename(file);\n const lines = buf.toString(\"utf-8\").split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const raw = lines[i]!.replace(/\\r$/, \"\");\n if (regex.test(raw)) {\n matches.push({ file: rel, line: i + 1, text: raw.trim().slice(0, 200) });\n if (matches.length >= maxMatches) return true; // 达到上限,通知停止\n }\n }\n return false;\n };\n\n const stat = fs.statSync(target);\n if (stat.isFile()) {\n scanFile(target);\n return matches;\n }\n\n // 迭代式深度优先遍历(避免深目录递归爆栈)\n const stack: string[] = [target];\n while (stack.length > 0) {\n const dir = stack.pop()!;\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (!SKIP_DIRS.has(entry.name)) stack.push(full);\n } else if (entry.isFile()) {\n if (scanFile(full)) return matches;\n }\n }\n }\n\n return matches;\n}\n","/**\n * ripgrep (rg) 定位\n *\n * 优先级:\n * 1. 环境变量 ZQAGENT_RG 显式指定的路径;\n * 2. SEA 单文件内嵌的 rg 资源(构建时由 build_cli_binary.mjs 作为 asset 打包,\n * 运行时用 node:sea 的 getAsset 释放到临时目录);\n * 3. PATH 上的 rg。\n * 都拿不到时返回 null,由调用方回退到纯 Node 扫描器(nodeGrep.ts)。\n */\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { createRequire } from \"node:module\";\nimport { findExecutable } from \"./shellRuntime.js\";\n\n/** 已解析结果缓存(null 表示确认没有,勿重复探测) */\nlet cached: string | null | undefined;\n\n/** 把 SEA 内嵌的 rg 资源释放到临时目录,返回可执行文件路径 */\nfunction extractBundledRipgrep(): string | null {\n try {\n // CJS(SEA 单文件)下 require 可用;ESM 构建下用 createRequire 兜底。\n // 以 cwd 为基准构造 createRequire,避免依赖 import.meta(CJS 产物里不稳定)。\n const req =\n typeof require === \"function\"\n ? require\n : createRequire(path.join(process.cwd(), \"__zqagent__.js\"));\n const sea = req(\"node:sea\") as\n | { isSea?: () => boolean; getAsset?: (key: string) => ArrayBuffer | undefined }\n | undefined;\n if (!sea?.isSea?.() || typeof sea.getAsset !== \"function\") return null;\n const asset = sea.getAsset(\"rg\");\n if (!asset) return null;\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), \"zqagent-rg-\"));\n const file = path.join(dir, process.platform === \"win32\" ? \"rg.exe\" : \"rg\");\n fs.writeFileSync(file, Buffer.from(asset));\n fs.chmodSync(file, 0o755);\n return file;\n } catch {\n // 未内嵌 rg(如 npm 安装、普通 node 运行)或 API 不可用\n return null;\n }\n}\n\n/** 解析可用的 rg 路径;无则返回 null(结果带缓存) */\nexport function resolveRipgrep(): string | null {\n if (cached !== undefined) return cached;\n\n const explicit = (process.env.ZQAGENT_RG ?? \"\").trim();\n if (explicit && fs.existsSync(explicit)) {\n cached = explicit;\n return cached;\n }\n\n const bundled = extractBundledRipgrep();\n if (bundled) {\n cached = bundled;\n return cached;\n }\n\n cached = findExecutable(\"rg\", {\n platform: process.platform,\n env: process.env,\n fileExists: (p) => {\n try {\n return fs.statSync(p).isFile();\n } catch {\n return false;\n }\n },\n });\n return cached;\n}\n\n/** 测试用:清除解析缓存 */\nexport function resetRipgrepCache(): void {\n cached = undefined;\n}\n","/**\n * 搜索工具\n *\n * 提供 grep 工具。执行策略:\n * 1. ripgrep (rg) —— 优先使用内嵌/已安装的 rg,性能与正则支持最好;\n * 2. 纯 Node 扫描器(nodeGrep.ts)—— **仅在 rg 二进制起不来时**兜底,\n * 支持完整 JS 正则,不依赖系统 findstr/grep。\n *\n * ## 为什么兜底条件收得这么窄\n *\n * 早先只要 rg 退出码不是 0/1(包含 10s 超时、rg 自身报错)就落到 nodeGrep。\n * 而 nodeGrep 是**同步**全盘递归扫描、不读 .gitignore、无超时:大仓库里\n * 「rg 慢 → 超时 → 同步扫全部文件」会把整个 Node 事件循环冻住,多租户\n * server 上所有会话一起卡死。现在只有 spawn 失败(rg 根本没跑起来)才兜底,\n * 超时与 rg 报错都如实返回给模型,让它缩小范围重试。\n */\nimport { ToolRegistry } from \"./ToolRegistry.js\";\nimport { execAsync } from \"./execAsync.js\";\nimport { decodeOutput } from \"./shell.js\";\nimport { nodeGrep, type GrepMatch } from \"./nodeGrep.js\";\nimport { resolveRipgrep } from \"./ripgrep.js\";\n\n/** 默认返回条数上限 */\nconst DEFAULT_MAX_RESULTS = 50;\n/** 硬上限:再多模型也读不过来,只会挤占上下文 */\nconst MAX_RESULTS_CAP = 200;\n/** 每条命中内容的字符上限 */\nconst MAX_TEXT_CHARS = 200;\n/** rg 单次搜索超时 */\nconst RG_TIMEOUT_MS = 10_000;\n\n/**\n * 解析 rg 的 `文件:行号:内容` 输出。\n *\n * 带 `-C` 时上下文行用 `-` 分隔(`文件-行号-内容`),因此分隔符要同时接受\n * `:` 与 `-`。用非贪婪 + 行号锚定:文件名里的 `-` / `:` 不会误切。\n */\nfunction parseRgOutput(result: string, limit: number): GrepMatch[] {\n const out: GrepMatch[] = [];\n for (const raw of result.split(\"\\n\")) {\n if (out.length >= limit) break;\n const line = raw.replace(/\\r$/, \"\");\n if (!line) continue;\n const m = /^(.+?)[:-](\\d+)[:-](.*)$/.exec(line);\n if (!m) continue;\n out.push({\n file: m[1]!,\n line: Number(m[2]),\n text: m[3]!.trim().slice(0, MAX_TEXT_CHARS),\n });\n }\n return out;\n}\n\n/** 解析 `-l` 输出:每行一个文件路径,去重保序 */\nfunction parseRgFiles(result: string, limit: number): string[] {\n const files: string[] = [];\n const seen = new Set<string>();\n for (const raw of result.split(\"\\n\")) {\n if (files.length >= limit) break;\n const f = raw.replace(/\\r$/, \"\").trim();\n if (!f || seen.has(f)) continue;\n seen.add(f);\n files.push(f);\n }\n return files;\n}\n\n/**\n * 组装 rg 参数。\n *\n * pattern 放在 `--` 之后:以 `-` 开头的搜索词(如 `-foo`)否则会被 rg 当成选项。\n */\nfunction rgArgs(opts: {\n pattern: string;\n path: string;\n filesOnly: boolean;\n ignoreCase: boolean;\n glob?: string;\n contextLines: number;\n maxResults: number;\n}): string[] {\n const args: string[] = [];\n if (opts.filesOnly) {\n // -l 只回文件名:一条命中 ≈ 几十字节,比「行号 + 内容」省一个数量级,\n // 适合先定位再精准读取\n args.push(\"--files-with-matches\");\n } else {\n // --with-filename:单文件模式下 rg 默认不输出文件名前缀,会导致\n // \"行号:内容\" 被误当成 \"文件:行号:内容\" 解析而错位;显式要求带文件名。\n args.push(\"--with-filename\", \"--line-number\", \"--no-heading\");\n // --max-count 在 rg 里是**每文件**上限,全局上限由调用方事后截断\n args.push(\"--max-count\", String(Math.max(opts.maxResults, 1)));\n }\n if (opts.ignoreCase) args.push(\"--ignore-case\");\n if (opts.glob) args.push(\"--glob\", opts.glob);\n if (opts.contextLines > 0 && !opts.filesOnly) {\n args.push(\"--context\", String(opts.contextLines));\n }\n args.push(\"--\", opts.pattern, opts.path);\n return args;\n}\n\n/** 把超时 / rg 报错翻译成可执行的提示(非\"无匹配\",绝不能当成空结果) */\nfunction rgFailureHint(res: {\n timedOut: boolean;\n status: number | null;\n stderr: Buffer;\n aborted?: boolean;\n}): string {\n if (res.timedOut) {\n return (\n `搜索超时(${RG_TIMEOUT_MS / 1000}s)。大仓库全量搜索很容易超时:` +\n `请缩小 path 到具体子目录,或用 glob 参数限制文件类型(如 \"*.ts\")后重试`\n );\n }\n if (res.aborted) return \"搜索已中断\";\n const stderr = decodeOutput(res.stderr).trim().slice(0, 300);\n return `搜索失败(rg 退出码 ${String(res.status)})${stderr ? `:${stderr}` : \"\"}`;\n}\n\n/**\n * grep — 使用 ripgrep,rg 二进制不可用时回退到纯 Node 扫描\n *\n * 安全说明:使用异步 execAsync(参数数组)而非 shell 字符串拼接,\n * pattern/path 作为独立参数传递,不经 shell 解释,杜绝命令注入。\n */\nfunction makeGrep(workspace: string) {\n return async function grep(args: Record<string, unknown>) {\n const pattern = String(args.pattern ?? \"\");\n const filePath = String(args.path ?? \".\");\n\n if (!pattern.trim()) {\n return { error: \"搜索模式不能为空\" };\n }\n\n const filesOnly = args.files_only === true;\n const ignoreCase = args.ignore_case === true;\n const glob = typeof args.glob === \"string\" && args.glob.trim() ? args.glob.trim() : undefined;\n const contextLines = Math.max(\n 0,\n Math.min(Number(args.context_lines ?? 0) || 0, 5),\n );\n const maxResults = Math.max(\n 1,\n Math.min(Number(args.max_results ?? DEFAULT_MAX_RESULTS) || DEFAULT_MAX_RESULTS, MAX_RESULTS_CAP),\n );\n\n const rg = resolveRipgrep();\n if (rg) {\n const res = await execAsync(\n rg,\n rgArgs({ pattern, path: filePath, filesOnly, ignoreCase, glob, contextLines, maxResults }),\n {\n cwd: workspace,\n timeoutMs: RG_TIMEOUT_MS,\n maxBuffer: 1024 * 1024 * 2,\n },\n );\n // rg 退出码:0=有匹配,1=无匹配,其余=错误\n if (res.status === 0) {\n const out = decodeOutput(res.stdout);\n return filesOnly\n ? { query: pattern, engine: \"ripgrep\", files: parseRgFiles(out, maxResults) }\n : (() => {\n const matches = parseRgOutput(out, maxResults);\n return { query: pattern, engine: \"ripgrep\", matches, count: matches.length };\n })();\n }\n if (res.status === 1) {\n return filesOnly\n ? { query: pattern, engine: \"ripgrep\", files: [], count: 0 }\n : { query: pattern, engine: \"ripgrep\", matches: [], count: 0 };\n }\n // 真正的「rg 没跑起来」(ENOENT / 无执行权限)才兜底。\n // 超时与 rg 自身报错一律如实返回 —— 见文件头注释。\n if (!res.error) {\n return { query: pattern, error: rgFailureHint(res) };\n }\n }\n\n // rg 不可用:纯 Node 扫描(同步、慢、不读 .gitignore,仅作最后兜底)\n if (filesOnly || glob || ignoreCase || contextLines > 0) {\n // nodeGrep 不支持这些参数,返回结果会与请求语义不符,不如直接说明\n return {\n query: pattern,\n error:\n \"当前节点未安装 ripgrep,files_only / glob / ignore_case / context_lines \" +\n \"需要 ripgrep 才能生效;请去掉这些参数后重试,或先安装 ripgrep\",\n };\n }\n try {\n const matches = nodeGrep({ pattern, target: filePath, workspace, maxMatches: maxResults });\n return { query: pattern, engine: \"node\", matches, count: matches.length };\n } catch (exc) {\n return { error: `搜索失败: ${String(exc instanceof Error ? exc.message : exc)}` };\n }\n };\n}\n\n/** 注册 grep 工具(低风险,无需审批) */\nexport function registerSearchTool(registry: ToolRegistry, workspace: string): void {\n const grepFunc = makeGrep(workspace);\n\n registry.register(\n (args) => grepFunc(args),\n { category: \"search\", riskLevel: \"low\", capabilities: [\"search\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"grep\",\n description:\n \"在文件中搜索文本模式(支持完整 JS 正则语法)。默认返回匹配的文件名、行号和内容(最多 50 条)。\" +\n \"定位代码位置时优先用 files_only=true 只取文件列表(体积小一个数量级),再用 read_file 精准读取;\" +\n \"大仓库请配合 path 缩小范围或 glob 限制文件类型,避免全量扫描超时。\",\n parameters: {\n type: \"object\",\n properties: {\n pattern: { type: \"string\", description: \"搜索模式 (文本或正则表达式)\" },\n path: { type: \"string\", description: \"搜索路径 (文件或目录,默认 '.';大仓库建议先缩小到子目录)\" },\n files_only: {\n type: \"boolean\",\n description: \"只返回匹配的文件路径,不返回行内容(推荐用于定位,显著节省上下文)\",\n },\n glob: {\n type: \"string\",\n description: '文件过滤,如 \"*.ts\" 或 \"!*.min.js\"(可多次排除需自行组合为 {a,b} 形式)',\n },\n ignore_case: { type: \"boolean\", description: \"忽略大小写\" },\n context_lines: {\n type: \"integer\",\n description: \"每条命中附带的前后上下文行数(0-5,默认 0)\",\n },\n max_results: {\n type: \"integer\",\n description: `最多返回条数(默认 ${DEFAULT_MAX_RESULTS},上限 ${MAX_RESULTS_CAP})`,\n },\n },\n required: [\"pattern\"],\n },\n },\n },\n );\n}\n"],"mappings":";;;;;;;;AAoBA,SAAS,aAAa;;;ACdtB,SAAS,iBAAiB;AAEnB,SAAS,SAAS,KAA+B;AACtD,MAAI,CAAC,IAAK;AACV,MAAI,QAAQ,aAAa,SAAS;AAChC,QAAI;AACF,gBAAU,YAAY,CAAC,QAAQ,OAAO,GAAG,GAAG,MAAM,IAAI,GAAG,EAAE,aAAa,KAAK,CAAC;AAAA,IAChF,QAAQ;AAAA,IAER;AACA;AAAA,EACF;AAEA,MAAI;AACF,YAAQ,KAAK,CAAC,KAAK,SAAS;AAAA,EAC9B,QAAQ;AACN,QAAI;AACF,cAAQ,KAAK,KAAK,SAAS;AAAA,IAC7B,QAAQ;AAAA,IAER;AAAA,EACF;AACF;;;AD4BA,IAAM,eAAe,oBAAI,IAAY;AACrC,IAAI,uBAAuB;AAE3B,SAAS,WAAW,KAA+B;AACjD,MAAI,CAAC,IAAK;AACV,eAAa,IAAI,GAAG;AACpB,MAAI,qBAAsB;AAC1B,yBAAuB;AACvB,UAAQ,KAAK,QAAQ,MAAM;AACzB,eAAW,KAAK,aAAc,UAAS,CAAC;AAAA,EAC1C,CAAC;AACH;AAEA,IAAM,qBAAqB,OAAO,OAAO;AAWzC,IAAM,gBAAgB;AAGtB,SAAS,SAAS,WAIhB;AACA,QAAM,SAAmB,CAAC;AAC1B,MAAI,MAAM;AACV,MAAI,OAAO;AACX,SAAO;AAAA,IACL,KAAK,OAAe;AAClB,UAAI,OAAO,WAAW;AACpB,eAAO;AACP;AAAA,MACF;AACA,YAAM,OAAO,YAAY;AACzB,UAAI,MAAM,SAAS,MAAM;AACvB,eAAO,KAAK,MAAM,SAAS,GAAG,IAAI,CAAC;AACnC,cAAM;AACN,eAAO;AACP;AAAA,MACF;AACA,aAAO,KAAK,KAAK;AACjB,aAAO,MAAM;AAAA,IACf;AAAA,IACA,MAAM,MAAM,OAAO,OAAO,MAAM;AAAA,IAChC,MAAM,MAAM;AAAA,EACd;AACF;AAEO,SAAS,UACd,MACA,MACA,OAAyB,CAAC,GACA;AAC1B,SAAO,IAAI,QAAyB,CAACA,aAAY;AAC/C,UAAM,YAAY,KAAK,aAAa;AACpC,UAAM,MAAM,SAAS,SAAS;AAC9B,UAAM,MAAM,SAAS,SAAS;AAG9B,QAAI,KAAK,QAAQ,SAAS;AACxB,MAAAA,SAAQ;AAAA,QACN,QAAQ;AAAA,QACR,QAAQ,OAAO,MAAM,CAAC;AAAA,QACtB,QAAQ,OAAO,MAAM,CAAC;AAAA,QACtB,UAAU;AAAA,QACV,WAAW;AAAA,QACX,SAAS;AAAA,MACX,CAAC;AACD;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,MAAM,MAAM;AAAA,QACxB,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA,QAClD,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA;AAAA,QAElD,UAAU,QAAQ,aAAa;AAAA,QAC/B,OAAO,CAAC,KAAK,UAAU,SAAY,WAAW,QAAQ,QAAQ,MAAM;AAAA,QACpE,aAAa;AAAA,MACf,CAAC;AAAA,IACH,SAAS,GAAG;AACV,MAAAA,SAAQ;AAAA,QACN,QAAQ;AAAA,QACR,QAAQ,OAAO,MAAM,CAAC;AAAA,QACtB,QAAQ,OAAO,MAAM,CAAC;AAAA,QACtB,OAAO;AAAA,QACP,UAAU;AAAA,QACV,WAAW;AAAA,MACb,CAAC;AACD;AAAA,IACF;AAEA,eAAW,MAAM,GAAG;AAEpB,QAAI,UAAU;AACd,QAAI,WAAW;AACf,QAAI,UAAU;AACd,QAAI;AACJ,QAAI,QAA8C;AAClD,QAAI,aAAmD;AAEvD,UAAM,UAAU,MAAY;AAC1B,gBAAU;AACV,oBAAc;AAAA,IAChB;AAEA,UAAM,SAAS,CAAC,WAAgC;AAC9C,UAAI,QAAS;AACb,gBAAU;AACV,UAAI,MAAO,cAAa,KAAK;AAC7B,UAAI,WAAY,cAAa,UAAU;AACvC,UAAI,MAAM,QAAQ,OAAW,cAAa,OAAO,MAAM,GAAG;AAC1D,WAAK,QAAQ,oBAAoB,SAAS,OAAO;AACjD,YAAM,SAA0B;AAAA,QAC9B;AAAA,QACA,QAAQ,IAAI,KAAK;AAAA,QACjB,QAAQ,IAAI,KAAK;AAAA,QACjB;AAAA,QACA,WAAW,IAAI,KAAK,KAAK,IAAI,KAAK;AAAA,MACpC;AACA,UAAI,QAAS,QAAO,UAAU;AAC9B,UAAI,WAAY,QAAO,QAAQ;AAC/B,MAAAA,SAAQ,MAAM;AAAA,IAChB;AAQA,UAAM,gBAAgB,MAAY;AAChC,eAAS,MAAM,GAAG;AAClB,UAAI,WAAW,WAAY;AAC3B,mBAAa,WAAW,MAAM;AAG5B,cAAM,QAAQ,QAAQ;AACtB,cAAM,QAAQ,QAAQ;AACtB,eAAO,IAAI;AAAA,MACb,GAAG,aAAa;AAAA,IAClB;AAEA,QAAI,KAAK,cAAc,UAAa,KAAK,YAAY,GAAG;AACtD,cAAQ,WAAW,MAAM;AACvB,mBAAW;AACX,sBAAc;AAAA,MAChB,GAAG,KAAK,SAAS;AAAA,IACnB;AAEA,SAAK,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAE9D,UAAM,QAAQ,GAAG,QAAQ,CAAC,MAAc,IAAI,KAAK,CAAC,CAAC;AACnD,UAAM,QAAQ,GAAG,QAAQ,CAAC,MAAc,IAAI,KAAK,CAAC,CAAC;AAEnD,UAAM,GAAG,SAAS,CAAC,MAAM;AACvB,mBAAa;AAEb,UAAI,MAAM,QAAQ,OAAW,QAAO,IAAI;AAAA,IAC1C,CAAC;AAED,QAAI,KAAK,UAAU,UAAa,MAAM,OAAO;AAE3C,YAAM,MAAM,GAAG,SAAS,MAAM;AAAA,MAAC,CAAC;AAChC,YAAM,MAAM,IAAI,KAAK,KAAK;AAAA,IAC5B;AAEA,UAAM,GAAG,SAAS,CAAC,SAAS,OAAO,IAAI,CAAC;AAAA,EAC1C,CAAC;AACH;;;AE5NA,YAAY,UAAU;AA+BtB,IAAM,mBAAmB,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,EAAE;AAE5D,SAAS,QAAQ,UAA2B;AAC1C,SAAO,aAAa,UAAe,aAAa;AAClD;AAWA,SAAS,SAAS,aAA8B,OAAyB;AACvE,SAAO,QAAQ,QAAQ,EAAE,KAAK,GAAG,KAAK;AACxC;AAMO,SAAS,eAAe,MAAc,KAAkC;AAC7E,MAAI,KAAK,SAAS,GAAG,KAAK,KAAK,SAAS,IAAI,GAAG;AAC7C,WAAO,IAAI,WAAW,IAAI,IAAI,OAAO;AAAA,EACvC;AACA,QAAM,WAAW,IAAI,aAAa,UAAU,mBAAmB,CAAC,EAAE;AAClE,QAAM,YAAY,IAAI,aAAa,UAAU,MAAM;AACnD,QAAM,QAAQ,IAAI,IAAI,QAAQ,IAAI,MAAM,SAAS,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC7E,aAAW,OAAO,MAAM;AACtB,eAAW,UAAU,UAAU;AAC7B,YAAM,YAAY,SAAS,IAAI,UAAU,KAAK,OAAO,MAAM;AAC3D,UAAI,IAAI,WAAW,SAAS,EAAG,QAAO;AAAA,IACxC;AAAA,EACF;AACA,SAAO;AACT;;;ACvEA,SAAS,SAAAC,cAAgC;AAIzC,IAAM,uBAAuB,KAAK;AAGlC,IAAM,aAAN,MAAiB;AAAA,EAIf,YAA6B,QAAQ,sBAAsB;AAA9B;AAAA,EAA+B;AAAA,EAA/B;AAAA,EAHrB,SAAmB,CAAC;AAAA,EACpB,OAAO;AAAA,EAIf,IAAI,OAAqB;AACvB,QAAI,CAAC,MAAO;AACZ,SAAK,OAAO,KAAK,KAAK;AACtB,SAAK,QAAQ,MAAM;AACnB,WAAO,KAAK,OAAO,KAAK,SAAS,KAAK,OAAO,SAAS,GAAG;AACvD,WAAK,QAAQ,KAAK,OAAO,MAAM,EAAG;AAAA,IACpC;AAEA,QAAI,KAAK,OAAO,KAAK,OAAO;AAC1B,YAAM,SAAS,KAAK,OAAO,KAAK,EAAE;AAClC,WAAK,SAAS,CAAC,OAAO,MAAM,CAAC,KAAK,KAAK,CAAC;AACxC,WAAK,OAAO,KAAK,OAAO,CAAC,EAAG;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,MAAc;AACZ,UAAM,SAAS,KAAK,OAAO,KAAK,EAAE;AAClC,WAAO,OAAO,SAAS,KAAK,QAAQ,OAAO,MAAM,CAAC,KAAK,KAAK,IAAI;AAAA,EAClE;AACF;AA+DO,IAAM,mBAAN,MAAuB;AAAA,EACX,QAAQ,oBAAI,IAA4B;AAAA,EACjD,MAAM;AAAA,EACN,mBAAmB;AAAA;AAAA,EAEnB,WAAsC;AAAA;AAAA,EAG9C,YAAY,IAAqC;AAC/C,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA,EAGA,MAAM,MAA4C;AAChD,SAAK,eAAe;AACpB,UAAM,KAAK,SAAS,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC;AACzD,UAAM,QAAQC,OAAM,KAAK,MAAM,KAAK,MAAM;AAAA,MACxC,KAAK,KAAK;AAAA,MACV,KAAK,KAAK;AAAA,MACV,UAAU,QAAQ,aAAa;AAAA,MAC/B,aAAa;AAAA,MACb,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,IAClC,CAAC;AAED,UAAM,OAAuB;AAAA,MAC3B;AAAA,MACA,SAAS,KAAK;AAAA,MACd,OAAO,KAAK,SAAS;AAAA,MACrB;AAAA,MACA,QAAQ,IAAI,WAAW;AAAA,MACvB,QAAQ,IAAI,WAAW;AAAA,MACvB,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI;AAAA,MACpB,SAAS;AAAA,IACX;AACA,SAAK,MAAM,IAAI,IAAI,IAAI;AAEvB,UAAM,QAAQ,GAAG,QAAQ,CAAC,MAAc,KAAK,OAAO,IAAI,EAAE,SAAS,OAAO,CAAC,CAAC;AAC5E,UAAM,QAAQ,GAAG,QAAQ,CAAC,MAAc,KAAK,OAAO,IAAI,EAAE,SAAS,OAAO,CAAC,CAAC;AAC5E,UAAM,GAAG,SAAS,CAAC,QAAQ;AACzB,WAAK,OAAO,IAAI,iBAAiB,IAAI,OAAO;AAAA,CAAI;AAEhD,UAAI,KAAK,WAAW,WAAW;AAC7B,aAAK,UAAU,KAAK,IAAI;AACxB,aAAK,SAAS;AACd,aAAK,QAAQ,KAAK,KAAK;AAAA,MACzB;AAAA,IACF,CAAC;AACD,UAAM,GAAG,QAAQ,CAAC,MAAM,WAAW;AACjC,WAAK,WAAW;AAChB,WAAK,SAAS;AACd,WAAK,UAAU,KAAK,IAAI;AAExB,UAAI,KAAK,WAAW,SAAU,MAAK,SAAS;AAC5C,WAAK,QAAQ,KAAK,KAAK;AAAA,IACzB,CAAC;AAED,UAAM,MAAM;AAEZ,UAAM,OAAO,KAAK,SAAS,IAAI;AAE/B,SAAK,QAAQ,KAAK,KAAK;AACvB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,QAA4D;AACjE,UAAM,OAAO,KAAK,MAAM,IAAI,MAAM;AAClC,QAAI,CAAC,KAAM,QAAO,EAAE,OAAO,+CAAY,MAAM,GAAG;AAChD,WAAO,KAAK,SAAS,IAAI;AAAA,EAC3B;AAAA;AAAA,EAGA,KAAK,QAAgB,OAAmE;AACtF,UAAM,OAAO,KAAK,MAAM,IAAI,MAAM;AAClC,QAAI,CAAC,KAAM,QAAO,EAAE,OAAO,+CAAY,MAAM,GAAG;AAChD,QAAI,UAAU,UAAa,KAAK,UAAU,OAAO;AAC/C,aAAO,EAAE,OAAO,+CAAY,MAAM,GAAG;AAAA,IACvC;AACA,QAAI,KAAK,WAAW,WAAW;AAC7B,WAAK,SAAS;AACd,WAAK,UAAU,KAAK,IAAI;AACxB,eAAS,KAAK,MAAM,GAAG;AACvB,WAAK,QAAQ,KAAK,KAAK;AAAA,IACzB;AACA,WAAO,KAAK,SAAS,IAAI;AAAA,EAC3B;AAAA;AAAA,EAGA,KAAK,OAAiD;AACpD,UAAM,MAAM,CAAC,GAAG,KAAK,MAAM,OAAO,CAAC;AACnC,YAAQ,UAAU,SAAY,MAAM,IAAI,OAAO,CAAC,MAAM,EAAE,UAAU,KAAK,GAAG;AAAA,MAAI,CAAC,MAC7E,KAAK,SAAS,CAAC;AAAA,IACjB;AAAA,EACF;AAAA;AAAA,EAGA,aAAmB;AACjB,eAAW,QAAQ,KAAK,MAAM,OAAO,GAAG;AACtC,UAAI,KAAK,WAAW,WAAW;AAC7B,aAAK,SAAS;AACd,iBAAS,KAAK,MAAM,GAAG;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,QAAQ,OAA4B;AAC1C,UAAM,WAAW,KAAK;AACtB,QAAI,CAAC,SAAU;AACf,UAAM,QAAyB,CAAC;AAChC,eAAW,QAAQ,KAAK,MAAM,OAAO,GAAG;AACtC,UAAI,KAAK,UAAU,SAAS,KAAK,WAAW,UAAW;AACvD,YAAM,KAAK;AAAA,QACT,SAAS,KAAK;AAAA,QACd,SAAS,KAAK;AAAA,QACd,QAAQ,KAAK;AAAA,QACb,SAAS;AAAA,QACT,WAAW,KAAK;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,aAAa,KAAK,IAAI,IAAI,KAAK;AAAA,QAC/B,YAAY,KAAK;AAAA,MACnB,CAAC;AAAA,IACH;AACA,QAAI;AACF,eAAS,OAAO,KAAK;AAAA,IACvB,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,SAAS,MAA8C;AAC7D,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,SAAS,KAAK;AAAA,MACd,QAAQ,KAAK;AAAA,MACb,SAAS,KAAK,WAAW;AAAA,MACzB,WAAW,KAAK;AAAA,MAChB,QAAQ,KAAK;AAAA,MACb,QAAQ,KAAK,OAAO,IAAI;AAAA,MACxB,QAAQ,KAAK,OAAO,IAAI;AAAA,MACxB,cAAc,KAAK,WAAW,KAAK,IAAI,KAAK,KAAK;AAAA,IACnD;AAAA,EACF;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,iBAAkB;AAC3B,SAAK,mBAAmB;AACxB,YAAQ,KAAK,QAAQ,MAAM,KAAK,WAAW,CAAC;AAAA,EAC9C;AACF;;;ACjPA,SAAS,aAAAC,kBAAiB;;;ACqB1B,IAAM,aAAa,OAAO,OAAO;AA6E1B,SAAS,aAAa,KAAwC;AACnE,MAAI,CAAC,OAAO,IAAI,WAAW,EAAG,QAAO;AACrC,MAAI;AACF,WAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,OAAO,GAAG;AAAA,EAC7D,QAAQ;AACN,eAAW,OAAO,CAAC,OAAO,QAAQ,aAAa,QAAQ,GAAG;AACxD,UAAI;AACF,eAAO,IAAI,YAAY,GAAG,EAAE,OAAO,GAAG;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,IAAI,SAAS,QAAQ;AAAA,EAC9B;AACF;AAyHA,IAAM,aAAa,IAAI,iBAAiB;;;ACpPxC,YAAY,QAAQ;AACpB,YAAYC,WAAU;AAqBtB,IAAM,YAAY,oBAAI,IAAI;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,YAAY,KAAsB;AACzC,QAAM,IAAI,KAAK,IAAI,IAAI,QAAQ,IAAI;AACnC,WAAS,IAAI,GAAG,IAAI,GAAG,IAAK,KAAI,IAAI,CAAC,MAAM,EAAG,QAAO;AACrD,SAAO;AACT;AAMO,SAAS,SAAS,MAAoC;AAC3D,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,eAAe,KAAK,gBAAgB,OAAO;AAEjD,MAAI;AACJ,MAAI;AACF,YAAQ,IAAI,OAAO,KAAK,OAAO;AAAA,EACjC,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,qDAAa,KAAK,OAAO,SAAI,OAAO,GAAG,CAAC,QAAG;AAAA,EAC7D;AAEA,QAAM,SAAc,cAAQ,KAAK,WAAW,KAAK,MAAM;AACvD,MAAI,CAAI,cAAW,MAAM,GAAG;AAC1B,UAAM,IAAI,MAAM,+CAAY,KAAK,MAAM,EAAE;AAAA,EAC3C;AAEA,QAAM,UAAuB,CAAC;AAE9B,QAAM,WAAW,CAAC,SAA0B;AAC1C,QAAIC;AACJ,QAAI;AACF,MAAAA,QAAU,YAAS,IAAI;AAAA,IACzB,QAAQ;AACN,aAAO;AAAA,IACT;AACA,QAAI,CAACA,MAAK,OAAO,KAAKA,MAAK,OAAO,aAAc,QAAO;AAEvD,QAAI;AACJ,QAAI;AACF,YAAS,gBAAa,IAAI;AAAA,IAC5B,QAAQ;AACN,aAAO;AAAA,IACT;AACA,QAAI,YAAY,GAAG,EAAG,QAAO;AAE7B,UAAM,MAAW,eAAS,KAAK,WAAW,IAAI,KAAU,eAAS,IAAI;AACrE,UAAM,QAAQ,IAAI,SAAS,OAAO,EAAE,MAAM,IAAI;AAC9C,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAM,MAAM,MAAM,CAAC,EAAG,QAAQ,OAAO,EAAE;AACvC,UAAI,MAAM,KAAK,GAAG,GAAG;AACnB,gBAAQ,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,GAAG,MAAM,IAAI,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,CAAC;AACvE,YAAI,QAAQ,UAAU,WAAY,QAAO;AAAA,MAC3C;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,OAAU,YAAS,MAAM;AAC/B,MAAI,KAAK,OAAO,GAAG;AACjB,aAAS,MAAM;AACf,WAAO;AAAA,EACT;AAGA,QAAM,QAAkB,CAAC,MAAM;AAC/B,SAAO,MAAM,SAAS,GAAG;AACvB,UAAM,MAAM,MAAM,IAAI;AACtB,QAAI;AACJ,QAAI;AACF,gBAAa,eAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAAA,IACvD,QAAQ;AACN;AAAA,IACF;AACA,eAAW,SAAS,SAAS;AAC3B,YAAM,OAAY,WAAK,KAAK,MAAM,IAAI;AACtC,UAAI,MAAM,YAAY,GAAG;AACvB,YAAI,CAAC,UAAU,IAAI,MAAM,IAAI,EAAG,OAAM,KAAK,IAAI;AAAA,MACjD,WAAW,MAAM,OAAO,GAAG;AACzB,YAAI,SAAS,IAAI,EAAG,QAAO;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;;;ACrHA,YAAYC,SAAQ;AACpB,YAAY,QAAQ;AACpB,YAAYC,WAAU;AACtB,SAAS,qBAAqB;AAI9B,IAAI;AAGJ,SAAS,wBAAuC;AAC9C,MAAI;AAGF,UAAM,MACJ,OAAO,cAAY,aACf,YACA,cAAmB,WAAK,QAAQ,IAAI,GAAG,gBAAgB,CAAC;AAC9D,UAAM,MAAM,IAAI,UAAU;AAG1B,QAAI,CAAC,KAAK,QAAQ,KAAK,OAAO,IAAI,aAAa,WAAY,QAAO;AAClE,UAAM,QAAQ,IAAI,SAAS,IAAI;AAC/B,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,MAAS,gBAAiB,WAAQ,UAAO,GAAG,aAAa,CAAC;AAChE,UAAM,OAAY,WAAK,KAAK,QAAQ,aAAa,UAAU,WAAW,IAAI;AAC1E,IAAG,kBAAc,MAAM,OAAO,KAAK,KAAK,CAAC;AACzC,IAAG,cAAU,MAAM,GAAK;AACxB,WAAO;AAAA,EACT,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,iBAAgC;AAC9C,MAAI,WAAW,OAAW,QAAO;AAEjC,QAAM,YAAY,QAAQ,IAAI,cAAc,IAAI,KAAK;AACrD,MAAI,YAAe,eAAW,QAAQ,GAAG;AACvC,aAAS;AACT,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,sBAAsB;AACtC,MAAI,SAAS;AACX,aAAS;AACT,WAAO;AAAA,EACT;AAEA,WAAS,eAAe,MAAM;AAAA,IAC5B,UAAU,QAAQ;AAAA,IAClB,KAAK,QAAQ;AAAA,IACb,YAAY,CAAC,MAAM;AACjB,UAAI;AACF,eAAU,aAAS,CAAC,EAAE,OAAO;AAAA,MAC/B,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF,CAAC;AACD,SAAO;AACT;;;AClDA,IAAM,sBAAsB;AAE5B,IAAM,kBAAkB;AAExB,IAAM,iBAAiB;AAEvB,IAAM,gBAAgB;AAQtB,SAAS,cAAc,QAAgB,OAA4B;AACjE,QAAM,MAAmB,CAAC;AAC1B,aAAW,OAAO,OAAO,MAAM,IAAI,GAAG;AACpC,QAAI,IAAI,UAAU,MAAO;AACzB,UAAM,OAAO,IAAI,QAAQ,OAAO,EAAE;AAClC,QAAI,CAAC,KAAM;AACX,UAAM,IAAI,2BAA2B,KAAK,IAAI;AAC9C,QAAI,CAAC,EAAG;AACR,QAAI,KAAK;AAAA,MACP,MAAM,EAAE,CAAC;AAAA,MACT,MAAM,OAAO,EAAE,CAAC,CAAC;AAAA,MACjB,MAAM,EAAE,CAAC,EAAG,KAAK,EAAE,MAAM,GAAG,cAAc;AAAA,IAC5C,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAGA,SAAS,aAAa,QAAgB,OAAyB;AAC7D,QAAM,QAAkB,CAAC;AACzB,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,OAAO,OAAO,MAAM,IAAI,GAAG;AACpC,QAAI,MAAM,UAAU,MAAO;AAC3B,UAAM,IAAI,IAAI,QAAQ,OAAO,EAAE,EAAE,KAAK;AACtC,QAAI,CAAC,KAAK,KAAK,IAAI,CAAC,EAAG;AACvB,SAAK,IAAI,CAAC;AACV,UAAM,KAAK,CAAC;AAAA,EACd;AACA,SAAO;AACT;AAOA,SAAS,OAAO,MAQH;AACX,QAAM,OAAiB,CAAC;AACxB,MAAI,KAAK,WAAW;AAGlB,SAAK,KAAK,sBAAsB;AAAA,EAClC,OAAO;AAGL,SAAK,KAAK,mBAAmB,iBAAiB,cAAc;AAE5D,SAAK,KAAK,eAAe,OAAO,KAAK,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC;AAAA,EAC/D;AACA,MAAI,KAAK,WAAY,MAAK,KAAK,eAAe;AAC9C,MAAI,KAAK,KAAM,MAAK,KAAK,UAAU,KAAK,IAAI;AAC5C,MAAI,KAAK,eAAe,KAAK,CAAC,KAAK,WAAW;AAC5C,SAAK,KAAK,aAAa,OAAO,KAAK,YAAY,CAAC;AAAA,EAClD;AACA,OAAK,KAAK,MAAM,KAAK,SAAS,KAAK,IAAI;AACvC,SAAO;AACT;AAGA,SAAS,cAAc,KAKZ;AACT,MAAI,IAAI,UAAU;AAChB,WACE,iCAAQ,gBAAgB,GAAI;AAAA,EAGhC;AACA,MAAI,IAAI,QAAS,QAAO;AACxB,QAAM,SAAS,aAAa,IAAI,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG;AAC3D,SAAO,uDAAe,OAAO,IAAI,MAAM,CAAC,SAAI,SAAS,SAAI,MAAM,KAAK,EAAE;AACxE;AAQA,SAAS,SAAS,WAAmB;AACnC,SAAO,eAAe,KAAK,MAA+B;AACxD,UAAM,UAAU,OAAO,KAAK,WAAW,EAAE;AACzC,UAAM,WAAW,OAAO,KAAK,QAAQ,GAAG;AAExC,QAAI,CAAC,QAAQ,KAAK,GAAG;AACnB,aAAO,EAAE,OAAO,mDAAW;AAAA,IAC7B;AAEA,UAAM,YAAY,KAAK,eAAe;AACtC,UAAM,aAAa,KAAK,gBAAgB;AACxC,UAAM,OAAO,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI;AACpF,UAAM,eAAe,KAAK;AAAA,MACxB;AAAA,MACA,KAAK,IAAI,OAAO,KAAK,iBAAiB,CAAC,KAAK,GAAG,CAAC;AAAA,IAClD;AACA,UAAM,aAAa,KAAK;AAAA,MACtB;AAAA,MACA,KAAK,IAAI,OAAO,KAAK,eAAe,mBAAmB,KAAK,qBAAqB,eAAe;AAAA,IAClG;AAEA,UAAM,KAAK,eAAe;AAC1B,QAAI,IAAI;AACN,YAAM,MAAM,MAAM;AAAA,QAChB;AAAA,QACA,OAAO,EAAE,SAAS,MAAM,UAAU,WAAW,YAAY,MAAM,cAAc,WAAW,CAAC;AAAA,QACzF;AAAA,UACE,KAAK;AAAA,UACL,WAAW;AAAA,UACX,WAAW,OAAO,OAAO;AAAA,QAC3B;AAAA,MACF;AAEA,UAAI,IAAI,WAAW,GAAG;AACpB,cAAM,MAAM,aAAa,IAAI,MAAM;AACnC,eAAO,YACH,EAAE,OAAO,SAAS,QAAQ,WAAW,OAAO,aAAa,KAAK,UAAU,EAAE,KACzE,MAAM;AACL,gBAAM,UAAU,cAAc,KAAK,UAAU;AAC7C,iBAAO,EAAE,OAAO,SAAS,QAAQ,WAAW,SAAS,OAAO,QAAQ,OAAO;AAAA,QAC7E,GAAG;AAAA,MACT;AACA,UAAI,IAAI,WAAW,GAAG;AACpB,eAAO,YACH,EAAE,OAAO,SAAS,QAAQ,WAAW,OAAO,CAAC,GAAG,OAAO,EAAE,IACzD,EAAE,OAAO,SAAS,QAAQ,WAAW,SAAS,CAAC,GAAG,OAAO,EAAE;AAAA,MACjE;AAGA,UAAI,CAAC,IAAI,OAAO;AACd,eAAO,EAAE,OAAO,SAAS,OAAO,cAAc,GAAG,EAAE;AAAA,MACrD;AAAA,IACF;AAGA,QAAI,aAAa,QAAQ,cAAc,eAAe,GAAG;AAEvD,aAAO;AAAA,QACL,OAAO;AAAA,QACP,OACE;AAAA,MAEJ;AAAA,IACF;AACA,QAAI;AACF,YAAM,UAAU,SAAS,EAAE,SAAS,QAAQ,UAAU,WAAW,YAAY,WAAW,CAAC;AACzF,aAAO,EAAE,OAAO,SAAS,QAAQ,QAAQ,SAAS,OAAO,QAAQ,OAAO;AAAA,IAC1E,SAAS,KAAK;AACZ,aAAO,EAAE,OAAO,6BAAS,OAAO,eAAe,QAAQ,IAAI,UAAU,GAAG,CAAC,GAAG;AAAA,IAC9E;AAAA,EACF;AACF;AAGO,SAAS,mBAAmB,UAAwB,WAAyB;AAClF,QAAM,WAAW,SAAS,SAAS;AAEnC,WAAS;AAAA,IACP,CAAC,SAAS,SAAS,IAAI;AAAA,IACvB,EAAE,UAAU,UAAU,WAAW,OAAO,cAAc,CAAC,QAAQ,GAAG,kBAAkB,MAAM;AAAA,IAC1F;AAAA,MACE,MAAM;AAAA,MACN,UAAU;AAAA,QACR,MAAM;AAAA,QACN,aACE;AAAA,QAGF,YAAY;AAAA,UACV,MAAM;AAAA,UACN,YAAY;AAAA,YACV,SAAS,EAAE,MAAM,UAAU,aAAa,8EAAkB;AAAA,YAC1D,MAAM,EAAE,MAAM,UAAU,aAAa,gKAAmC;AAAA,YACxE,YAAY;AAAA,cACV,MAAM;AAAA,cACN,aAAa;AAAA,YACf;AAAA,YACA,MAAM;AAAA,cACJ,MAAM;AAAA,cACN,aAAa;AAAA,YACf;AAAA,YACA,aAAa,EAAE,MAAM,WAAW,aAAa,iCAAQ;AAAA,YACrD,eAAe;AAAA,cACb,MAAM;AAAA,cACN,aAAa;AAAA,YACf;AAAA,YACA,aAAa;AAAA,cACX,MAAM;AAAA,cACN,aAAa,0DAAa,mBAAmB,sBAAO,eAAe;AAAA,YACrE;AAAA,UACF;AAAA,UACA,UAAU,CAAC,SAAS;AAAA,QACtB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":["resolve","spawn","spawn","spawnSync","path","stat","fs","path"]}
|