@zqagent/core 0.1.30 → 0.2.4
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/CONTRIBUTING.md +65 -0
- package/LICENSE +190 -0
- package/README.md +42 -165
- package/README.oss.en.md +161 -0
- package/README.oss.md +165 -0
- package/README.oss.zh-TW.md +165 -0
- package/SECURITY.md +51 -0
- package/dist/{ToolRegistry-BmvPxgMr.d.cts → ToolRegistry-C3pTFiaJ.d.cts} +13 -4
- package/dist/{ToolRegistry-BmvPxgMr.d.ts → ToolRegistry-C3pTFiaJ.d.ts} +13 -4
- package/dist/cli/index.cjs +17799 -2062
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +16729 -985
- package/dist/cli/index.js.map +1 -1
- package/dist/{explore-OI6htHN1.d.cts → explore-D4lCKXbA.d.ts} +160 -19
- package/dist/{explore-Pmhyk4gv.d.ts → explore-Dj1j_GyN.d.cts} +160 -19
- package/dist/index.cjs +3626 -453
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +337 -27
- package/dist/index.d.ts +337 -27
- package/dist/index.js +3617 -452
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +14029 -1482
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.js +14036 -1482
- package/dist/server/index.js.map +1 -1
- package/dist/tools/ask.cjs +14 -0
- package/dist/tools/ask.cjs.map +1 -1
- package/dist/tools/ask.d.cts +8 -2
- package/dist/tools/ask.d.ts +8 -2
- package/dist/tools/ask.js +13 -0
- package/dist/tools/ask.js.map +1 -1
- package/dist/tools/diff.cjs +59 -30
- package/dist/tools/diff.cjs.map +1 -1
- package/dist/tools/diff.d.cts +1 -1
- package/dist/tools/diff.d.ts +1 -1
- package/dist/tools/diff.js +59 -30
- package/dist/tools/diff.js.map +1 -1
- package/dist/tools/directories.d.cts +1 -1
- package/dist/tools/directories.d.ts +1 -1
- package/dist/tools/explore.cjs +1050 -309
- package/dist/tools/explore.cjs.map +1 -1
- package/dist/tools/explore.d.cts +2 -2
- package/dist/tools/explore.d.ts +2 -2
- package/dist/tools/explore.js +1057 -309
- package/dist/tools/explore.js.map +1 -1
- package/dist/tools/files.cjs +58 -43
- package/dist/tools/files.cjs.map +1 -1
- package/dist/tools/files.d.cts +10 -4
- package/dist/tools/files.d.ts +10 -4
- package/dist/tools/files.js +56 -42
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/git.cjs +816 -63
- package/dist/tools/git.cjs.map +1 -1
- package/dist/tools/git.d.cts +200 -2
- package/dist/tools/git.d.ts +200 -2
- package/dist/tools/git.js +788 -62
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/media.cjs +1191 -0
- package/dist/tools/media.cjs.map +1 -0
- package/dist/tools/media.d.cts +31 -0
- package/dist/tools/media.d.ts +31 -0
- package/dist/tools/media.js +1159 -0
- package/dist/tools/media.js.map +1 -0
- package/dist/tools/plan.d.cts +1 -1
- package/dist/tools/plan.d.ts +1 -1
- package/dist/tools/search.cjs +513 -33
- package/dist/tools/search.cjs.map +1 -1
- package/dist/tools/search.d.cts +12 -1
- package/dist/tools/search.d.ts +12 -1
- package/dist/tools/search.js +508 -33
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/shell.cjs +631 -41
- package/dist/tools/shell.cjs.map +1 -1
- package/dist/tools/shell.d.cts +37 -4
- package/dist/tools/shell.d.ts +37 -4
- package/dist/tools/shell.js +626 -40
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/todo.d.cts +1 -1
- package/dist/tools/todo.d.ts +1 -1
- package/dist/tools/web.cjs +51 -18
- package/dist/tools/web.cjs.map +1 -1
- package/dist/tools/web.d.cts +1 -1
- package/dist/tools/web.d.ts +1 -1
- package/dist/tools/web.js +51 -18
- package/dist/tools/web.js.map +1 -1
- package/package.json +43 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tools/media.ts","../../src/tools/execAsync.ts","../../src/tools/killTree.ts","../../src/tools/shellRuntime.ts","../../src/tools/shellBackground.ts","../../src/tools/shell.ts","../../src/tools/pathSafety.ts","../../src/tools/ffmpegRuntime.ts"],"sourcesContent":["/**\n * 媒体处理工具(ffmpeg / ffprobe 封装)\n *\n * 一个工具覆盖常见音视频操作:探元数据、裁剪、转码、抽音轨、抽帧、缩略图、压缩。\n * 外部二进制一律通过异步 execAsync + 参数数组调用(不经 shell),路径写入前用\n * isWithinRoot 校验,避免 symlink 逃逸。\n *\n * ffmpeg 缺失时按「四级降级」返回结构化指引,而不是直接报错:\n * 1. PATH / 环境变量 / .agent/bin / SEA 内嵌(见 ffmpegRuntime.resolveMedia);\n * 2. op=\"setup\" 由本工具下载官方静态构建到 <workspace>/.agent/bin;\n * 3. 引导模型改用 run_shell 安装(需用户审批);\n * 4. 返回各平台精确的安装命令 / 下载页。\n *\n * 之所以把 2 做成显式的 op 而不是静默自动下载:一次几十 MB 的下载写入工作区,\n * 必须是用户明确发起、经过审批的动作(权限引擎对 needsUser 的工具每次弹审批)。\n */\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { ToolRegistry } from \"./ToolRegistry.js\";\nimport { execAsync } from \"./execAsync.js\";\nimport { decodeOutput } from \"./shell.js\";\nimport { resolveWithinRoot } from \"./pathSafety.js\";\nimport {\n canAutoDownload,\n downloadFfmpeg,\n installHints,\n resolveMedia,\n type ResolvedBin,\n} from \"./ffmpegRuntime.js\";\n\n/** 输出截断上限(字符) */\nconst MAX_OUTPUT = 4000;\n/** 单次转码超时(毫秒)——与 shell 工具的上限保持一致 */\nconst TIMEOUT_MS = 600 * 1000;\n/** 输出缓冲上限(字节) */\nconst MAX_BUFFER = 1024 * 1024 * 8;\n\n/** 需要输入文件的操作 */\nconst NEEDS_INPUT = new Set([\n \"probe\",\n \"trim\",\n \"convert\",\n \"extract_audio\",\n \"extract_frame\",\n \"thumbnail\",\n \"compress\",\n]);\n\n/** 会产生输出文件的操作 */\nconst NEEDS_OUTPUT = new Set([\n \"trim\",\n \"convert\",\n \"extract_audio\",\n \"extract_frame\",\n \"thumbnail\",\n \"compress\",\n]);\n\nconst OPS = [\n \"probe\",\n \"trim\",\n \"convert\",\n \"extract_audio\",\n \"extract_frame\",\n \"thumbnail\",\n \"compress\",\n \"setup\",\n] as const;\n\n/** 允许的 x264 preset(挡掉拼错的值,避免模型拿到晦涩的 ffmpeg 报错) */\nconst PRESETS = new Set([\n \"ultrafast\",\n \"superfast\",\n \"veryfast\",\n \"faster\",\n \"fast\",\n \"medium\",\n \"slow\",\n \"slower\",\n \"veryslow\",\n]);\n\n// =====================================================================\n// 参数解析辅助\n// =====================================================================\n\nfunction str(v: unknown): string | undefined {\n if (typeof v === \"string\" && v.trim().length > 0) return v.trim();\n return undefined;\n}\n\nfunction finiteNum(v: unknown, min: number, max: number): number | null {\n const n = typeof v === \"string\" ? Number(v.trim()) : v;\n if (typeof n !== \"number\" || !Number.isFinite(n) || n < min || n > max) return null;\n return n;\n}\n\n/** 解析时间码:秒数、`MM:SS`、`HH:MM:SS(.mmm)` 都接受;非法返回 null */\nexport function parseTimecode(v: unknown): number | null {\n if (typeof v === \"number\") return Number.isFinite(v) && v >= 0 ? v : null;\n if (typeof v !== \"string\") return null;\n const t = v.trim();\n if (t === \"\") return null;\n if (/^\\d+(\\.\\d+)?$/.test(t)) return Number(t);\n const parts = t.split(\":\");\n if (parts.length < 2 || parts.length > 3) return null;\n let sec = 0;\n for (const p of parts) {\n if (!/^\\d+(\\.\\d+)?$/.test(p)) return null;\n sec = sec * 60 + Number(p);\n }\n return sec;\n}\n\n/** 截断到末尾 max 个字符(对齐 shell 工具:报错信息通常在末尾) */\nfunction truncateTail(s: string, max = MAX_OUTPUT): string {\n const t = s.trim();\n if (t.length <= max) return t;\n return `…(已截断前 ${t.length - max} 字符)\\n` + t.slice(-max);\n}\n\n// =====================================================================\n// ffmpeg / ffprobe 参数构建(纯函数,便于单测)\n// =====================================================================\n\nexport interface BuildPaths {\n /** 已解析为绝对路径的输入 */\n input?: string;\n /** 已解析为绝对路径的输出 */\n output?: string;\n}\n\n/** 缩放滤镜:宽或高留空表示按比例自适应 */\nfunction scaleFilter(width?: number | null, height?: number | null): string | null {\n if (width && height) return `scale=${width}:${height}`;\n if (width) return `scale=${width}:-2`;\n if (height) return `scale=-2:${height}`;\n return null;\n}\n\n/**\n * 构建 ffmpeg 参数数组。\n *\n * @returns 参数数组;参数不合法时返回 { error }\n */\nexport function buildFfmpegArgs(\n op: string,\n args: Record<string, unknown>,\n paths: BuildPaths,\n): string[] | { error: string } {\n const input = paths.input ?? \"\";\n // -y 覆盖输出、-hide_banner 精简日志、-nostdin 防止 ffmpeg 抢 stdin(spawnSync 无 tty)\n const argv: string[] = [\"-y\", \"-hide_banner\", \"-nostdin\"];\n\n const ss = args[\"ss\"] === undefined ? null : parseTimecode(args[\"ss\"]);\n if (args[\"ss\"] !== undefined && ss === null) {\n return { error: \"参数 ss 不是合法时间码(示例:12.5 / 00:12.5 / 00:00:12.5)\" };\n }\n const to = args[\"to\"] === undefined ? null : parseTimecode(args[\"to\"]);\n if (args[\"to\"] !== undefined && to === null) {\n return { error: \"参数 to 不是合法时间码(示例:30 / 00:30 / 00:00:30)\" };\n }\n const duration = args[\"duration\"] === undefined ? null : finiteNum(args[\"duration\"], 0, 24 * 3600);\n if (args[\"duration\"] !== undefined && duration === null) {\n return { error: \"参数 duration 必须是 0~86400 之间的秒数\" };\n }\n if (to !== null && ss !== null && to <= ss) {\n return { error: `参数 to (${to}s) 必须大于 ss (${ss}s)` };\n }\n\n const output = paths.output ?? \"\";\n const format = str(args[\"format\"]);\n\n switch (op) {\n case \"trim\": {\n // 流拷贝裁剪:快且无损,但切点会对齐到关键帧\n if (ss !== null) argv.push(\"-ss\", String(ss));\n argv.push(\"-i\", input);\n // -t 相对已定位的起点;to 与 ss 同时给出时换算成时长,语义更确定\n if (duration !== null) {\n argv.push(\"-t\", String(duration));\n } else if (to !== null) {\n argv.push(\"-t\", String(to - (ss ?? 0)));\n }\n argv.push(\"-c\", \"copy\");\n if (format) argv.push(\"-f\", format);\n argv.push(output);\n return argv;\n }\n\n case \"convert\": {\n argv.push(\"-i\", input);\n const vf = scaleFilter(\n args[\"width\"] === undefined ? null : finiteNum(args[\"width\"], 1, 16384),\n args[\"height\"] === undefined ? null : finiteNum(args[\"height\"], 1, 16384),\n );\n if (vf) argv.push(\"-vf\", vf);\n const fps = args[\"fps\"] === undefined ? null : finiteNum(args[\"fps\"], 0.01, 240);\n if (fps !== null) argv.push(\"-r\", String(fps));\n const codec = str(args[\"codec\"]);\n if (codec) argv.push(\"-c:v\", codec);\n const bitrate = str(args[\"bitrate\"]);\n if (bitrate) argv.push(\"-b:v\", bitrate);\n if (format) argv.push(\"-f\", format);\n argv.push(output);\n return argv;\n }\n\n case \"extract_audio\": {\n argv.push(\"-i\", input, \"-vn\");\n const codec = str(args[\"codec\"]);\n if (codec) argv.push(\"-c:a\", codec);\n const bitrate = str(args[\"bitrate\"]);\n if (bitrate) argv.push(\"-b:a\", bitrate);\n if (format) argv.push(\"-f\", format);\n argv.push(output);\n return argv;\n }\n\n case \"extract_frame\":\n case \"thumbnail\": {\n const at = ss ?? 0;\n argv.push(\"-ss\", String(at), \"-i\", input, \"-frames:v\", \"1\");\n const width = args[\"width\"] === undefined ? null : finiteNum(args[\"width\"], 1, 16384);\n const height = args[\"height\"] === undefined ? null : finiteNum(args[\"height\"], 1, 16384);\n // 缩略图默认压到 320 宽;抽帧默认保持原尺寸\n const vf = scaleFilter(op === \"thumbnail\" ? (width ?? 320) : width, height);\n if (vf) argv.push(\"-vf\", vf);\n if (format) argv.push(\"-f\", format);\n argv.push(output);\n return argv;\n }\n\n case \"compress\": {\n const quality = args[\"quality\"] === undefined ? 28 : finiteNum(args[\"quality\"], 0, 51);\n if (quality === null) return { error: \"参数 quality 必须是 0~51 之间的数字(越大越糊、越小越清晰)\" };\n const preset = str(args[\"preset\"]) ?? \"medium\";\n if (!PRESETS.has(preset)) {\n return { error: `参数 preset 不合法,可选:${[...PRESETS].join(\" / \")}` };\n }\n argv.push(\"-i\", input);\n const vf = scaleFilter(\n args[\"width\"] === undefined ? null : finiteNum(args[\"width\"], 1, 16384),\n args[\"height\"] === undefined ? null : finiteNum(args[\"height\"], 1, 16384),\n );\n if (vf) argv.push(\"-vf\", vf);\n argv.push(\"-c:v\", \"libx264\", \"-crf\", String(quality), \"-preset\", preset, \"-c:a\", \"aac\", \"-b:a\", \"128k\");\n if (format) argv.push(\"-f\", format);\n argv.push(output);\n return argv;\n }\n\n default:\n return { error: `未知操作: ${op}` };\n }\n}\n\n/** ffprobe 参数:只输出需要的字段,避免超大 JSON */\nexport function buildFfprobeArgs(input: string): string[] {\n return [\"-v\", \"error\", \"-print_format\", \"json\", \"-show_format\", \"-show_streams\", input];\n}\n\n/** 把 ffprobe 的原始 JSON 压成模型易读的结构(体积可控) */\nexport function summarizeProbe(raw: unknown): Record<string, unknown> {\n const o = (raw ?? {}) as Record<string, any>;\n const fmt = (o[\"format\"] ?? {}) as Record<string, any>;\n const num = (v: unknown): number | undefined => {\n const n = Number(v);\n return Number.isFinite(n) ? n : undefined;\n };\n const streams = Array.isArray(o[\"streams\"]) ? o[\"streams\"] : [];\n return {\n format: {\n format_name: fmt[\"format_name\"],\n duration_sec: num(fmt[\"duration\"]),\n size_bytes: num(fmt[\"size\"]),\n bit_rate: num(fmt[\"bit_rate\"]),\n },\n streams: streams.map((s: Record<string, any>) => ({\n index: s[\"index\"],\n type: s[\"codec_type\"],\n codec: s[\"codec_name\"],\n profile: s[\"profile\"],\n width: s[\"width\"],\n height: s[\"height\"],\n fps: s[\"avg_frame_rate\"] ?? s[\"r_frame_rate\"],\n sample_rate: num(s[\"sample_rate\"]),\n channels: s[\"channels\"],\n })),\n };\n}\n\n// =====================================================================\n// 缺失二进制的降级指引\n// =====================================================================\n\n/**\n * 组装「找不到 ffmpeg」时的结构化错误。\n *\n * 第 1 级已经失败(调用方才走到这里),因此给出 2/3/4 级的具体做法。\n */\nfunction missingGuidance(\n missing: string,\n op: string,\n platform: NodeJS.Platform,\n arch: string,\n): Record<string, unknown> {\n const hints = installHints(platform);\n const howToFix: string[] = [];\n\n if (canAutoDownload(platform, arch)) {\n howToFix.push(\n `本工具自动下载:改用 op=\"setup\"(下载官方静态构建到 .agent/bin,需要出网;一次即可,之后一直可用)。`,\n );\n } else {\n howToFix.push(`当前平台 (${platform}/${arch}) 没有自动下载源,请用下面两种方式之一。`);\n }\n howToFix.push(\n `改用 run_shell 安装(需用户审批):先执行 ${JSON.stringify(hints[0])};这只在本机交互式使用时可行,服务端/Docker 无法提权。`,\n );\n howToFix.push(\n `用户手工安装后,把可执行文件路径写入环境变量 ZQAGENT_FFMPEG / ZQAGENT_FFPROBE,或放进 <workspace>/.agent/bin。`,\n );\n\n return {\n error: `未找到 ${missing},操作 ${op} 无法执行(ffmpeg 未安装或不在所有已知位置)。`,\n searched: [\n `环境变量 ZQAGENT_${missing.toUpperCase()}`,\n `<workspace>/.agent/bin/${missing}`,\n \"SEA 单文件内嵌资源\",\n \"PATH\",\n ],\n how_to_fix: howToFix,\n install_commands: hints,\n notes: [\n \"run_shell 的每次调用不保留环境变量(导出 PATH 只在当次有效),装完必须让用户重启 agent 进程才能被发现。\",\n \"ffmpeg 静态构建多为 GPL/LGPL 许可,随产品分发前请确认源码提供义务。\",\n ],\n };\n}\n\n// =====================================================================\n// 工具注册\n// =====================================================================\n\n/** 注册选项:可注入平台/环境/文件探测,便于在测试中演练降级路径 */\nexport interface MediaToolOptions {\n platform?: NodeJS.Platform;\n env?: NodeJS.ProcessEnv;\n fileExists?: (p: string) => boolean;\n}\n\ninterface RunOutcome {\n status: number | null;\n stdout: string;\n stderr: string;\n error?: Error;\n /** 超时被终止(异步 execAsync 下不再走 error.code === \"ETIMEDOUT\") */\n timedOut: boolean;\n}\n\nasync function runBinary(bin: string, argv: string[], cwd: string): Promise<RunOutcome> {\n const res = await execAsync(bin, argv, {\n cwd,\n timeoutMs: TIMEOUT_MS,\n maxBuffer: MAX_BUFFER,\n });\n return {\n status: res.status,\n stdout: decodeOutput(res.stdout),\n stderr: decodeOutput(res.stderr),\n error: res.error ?? undefined,\n timedOut: res.timedOut,\n };\n}\n\n/** 把工作区相对路径解析为绝对真实路径,并校验不逃出工作区 */\nfunction resolveTarget(\n workspace: string,\n value: string,\n): { abs: string; root: string } | { error: string } {\n const resolved = resolveWithinRoot(workspace, value);\n if (!resolved) {\n return { error: `路径不在工作区内(已拒绝): ${value}` };\n }\n return { abs: resolved.real, root: resolved.root };\n}\n\nfunction relOut(workspace: string, abs: string): string {\n return path.relative(workspace, abs) || abs;\n}\n\nexport function registerMediaTool(\n registry: ToolRegistry,\n workspace: string,\n opts: MediaToolOptions = {},\n): void {\n const platform = opts.platform ?? process.platform;\n const arch = process.arch;\n const media = async (args: Record<string, unknown>): Promise<unknown> => {\n const root = path.resolve(workspace);\n const op = str(args[\"op\"]);\n\n if (!op) {\n return { error: `缺少参数 op。可选值:${OPS.join(\" / \")}` };\n }\n if (!(OPS as readonly string[]).includes(op)) {\n return { error: `未知操作 ${JSON.stringify(op)}。可选值:${OPS.join(\" / \")}` };\n }\n\n const bins = resolveMedia(root, opts);\n // probe 只需 ffprobe,其余操作需要 ffmpeg\n const needed: \"ffmpeg\" | \"ffprobe\" = op === \"probe\" ? \"ffprobe\" : \"ffmpeg\";\n\n // ---- 第 2 级:显式下载安装 ----\n if (op === \"setup\") {\n const already: ResolvedBin | null = bins[needed];\n if (already) {\n return {\n ok: true,\n already_available: true,\n path: already.path,\n source: already.source,\n message: \"ffmpeg 已可用,无需下载。\",\n };\n }\n const res = await downloadFfmpeg(root, {});\n if (!res.ok) {\n return {\n ...missingGuidance(\"ffmpeg\", \"setup\", platform, arch),\n error: `自动下载失败:${res.error ?? \"未知原因\"}`,\n };\n }\n return {\n ok: true,\n installed: res.installed,\n notes: res.notes,\n message: \"已安装到 .agent/bin,后续 media 调用会直接使用它。\",\n };\n }\n\n // ---- 第 1 级失败 → 第 3/4 级指引 ----\n if (!bins[needed]) {\n return missingGuidance(needed, op, platform, arch);\n }\n\n // ---- 输入 / 输出路径 ----\n let inputAbs: string | undefined;\n // 展示用相对路径的基准:与解析出的真实路径配套,避免根目录含符号链接时算出错误相对路径\n let relRoot = root;\n if (NEEDS_INPUT.has(op)) {\n const input = str(args[\"input\"]);\n if (!input) return { error: `操作 ${op} 需要参数 input(工作区内相对路径)` };\n const r = resolveTarget(root, input);\n if (\"error\" in r) return { error: r.error };\n if (!fs.existsSync(r.abs) || !fs.statSync(r.abs).isFile()) {\n return { error: `输入文件不存在: ${input}` };\n }\n inputAbs = r.abs;\n relRoot = r.root;\n }\n\n let outputAbs: string | undefined;\n if (NEEDS_OUTPUT.has(op)) {\n const output = str(args[\"output\"]);\n if (!output) return { error: `操作 ${op} 需要参数 output(工作区内相对路径)` };\n const r = resolveTarget(root, output);\n if (\"error\" in r) return { error: r.error };\n // 输出目录通常由模型拼路径时漏建,这里补上(仍在校验过的工作区内)\n fs.mkdirSync(path.dirname(r.abs), { recursive: true });\n outputAbs = r.abs;\n relRoot = r.root;\n }\n\n // ---- probe:走 ffprobe,返回结构化元数据 ----\n if (op === \"probe\") {\n const out = await runBinary(bins.ffprobe!.path, buildFfprobeArgs(inputAbs!), root);\n if (out.error) return { error: `执行 ffprobe 失败: ${out.error.message}` };\n if (out.timedOut) {\n return { error: `ffprobe 超时(${TIMEOUT_MS / 1000}s)后已终止` };\n }\n if (out.status !== 0 && !out.stdout.trim()) {\n return { error: `ffprobe 退出码 ${out.status}`, stderr: truncateTail(out.stderr) };\n }\n try {\n return {\n ok: true,\n input: relOut(relRoot, inputAbs!),\n ...summarizeProbe(JSON.parse(out.stdout)),\n };\n } catch {\n return { error: \"ffprobe 输出不是合法 JSON\", stdout: truncateTail(out.stdout) };\n }\n }\n\n // ---- 其余操作:构建参数并执行 ffmpeg ----\n const built = buildFfmpegArgs(op, args, { input: inputAbs, output: outputAbs });\n if (!Array.isArray(built)) return built;\n\n const out = await runBinary(bins.ffmpeg!.path, built, root);\n if (out.error) {\n return {\n error: `执行 ffmpeg 失败: ${out.error.message}`,\n };\n }\n // 超时不带 status,单独说清楚,避免模型反复重试同一个大文件\n if (out.timedOut) {\n return {\n error: `ffmpeg 超过 ${TIMEOUT_MS / 1000} 秒未完成,已终止`,\n hint: \"可考虑用更短的片段或 gpu 编码\",\n };\n }\n if (out.status !== 0) {\n return {\n error: `ffmpeg 退出码 ${out.status}`,\n stderr: truncateTail(out.stderr),\n };\n }\n\n const result: Record<string, unknown> = {\n ok: true,\n op,\n output: outputAbs ? relOut(relRoot, outputAbs) : undefined,\n };\n if (outputAbs) {\n try {\n result[\"size_bytes\"] = fs.statSync(outputAbs).size;\n } catch {\n // 极少数操作(如只写日志)可能没有输出文件,忽略\n }\n }\n if (out.stderr.trim()) result[\"ffmpeg_log\"] = truncateTail(out.stderr);\n return result;\n };\n\n registry.register(\n media,\n {\n category: \"media\",\n riskLevel: \"medium\",\n capabilities: [\"ffmpeg 音视频处理:探元数据、裁剪、转码、抽音轨、抽帧、缩略图、压缩\"],\n // 会调用外部二进制并写文件,且 op=\"setup\" 还会下载安装;一律弹审批\n requiresApproval: true,\n },\n {\n type: \"function\",\n function: {\n name: \"media\",\n description:\n \"音视频处理(基于 ffmpeg)。op 取值:\" +\n \"probe 探元数据;trim 裁剪(流拷贝,无损但切点对齐关键帧);convert 转码;\" +\n \"extract_audio 抽音轨;extract_frame 在指定时间抽一帧;thumbnail 生成缩略图;\" +\n \"compress 压缩(默认 H.264 CRF 28)。\" +\n \"setup 用于在缺少 ffmpeg 时自动下载官方静态构建到 .agent/bin。\" +\n \"时间参数 ss/to/duration 接受秒数或 HH:MM:SS。输入输出均为工作区内相对路径。\",\n parameters: {\n type: \"object\",\n properties: {\n op: {\n type: \"string\",\n enum: [...OPS],\n description: \"要执行的操作\",\n },\n input: { type: \"string\", description: \"输入文件路径(工作区内相对路径)\" },\n output: { type: \"string\", description: \"输出文件路径(工作区内相对路径)\" },\n ss: { type: \"string\", description: \"起始时间,如 12.5 / 00:12.5 / 00:00:12.5\" },\n to: { type: \"string\", description: \"结束时间(与 ss 配合,trim 用)\" },\n duration: { type: \"string\", description: \"时长(秒,trim 用;与 to 二选一)\" },\n format: { type: \"string\", description: \"输出容器格式,如 mp4 / webm / mp3 / png\" },\n fps: { type: \"number\", description: \"输出帧率(convert)\" },\n width: { type: \"number\", description: \"输出宽度(像素)\" },\n height: { type: \"number\", description: \"输出高度(像素)\" },\n bitrate: { type: \"string\", description: \"视频码率,如 2M / 1500k\" },\n codec: { type: \"string\", description: \"视频编码器,如 libx264 / libvpx-vp9\" },\n quality: { type: \"number\", description: \"compress 的 CRF 值 0~51,默认 28\" },\n preset: { type: \"string\", description: \"compress 的 x264 preset,默认 medium\" },\n },\n required: [\"op\"],\n },\n },\n },\n );\n}\n","/**\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\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\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) => task.stderr.add(`[spawn error] ${err.message}\\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 });\n // 允许父进程退出后子进程继续(配合 disposeAll 显式清理)\n child.unref();\n\n return this.snapshot(task);\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 }\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 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 工具\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 } from \"./shellBackground.js\";\nimport type { ToolExecContext } from \"./ToolRegistry.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 */\nconst shellSessions = new Map<string, { cwd: string }>();\n\nfunction sessionFor(workspace: string): { cwd: string } {\n // 用规范化后的绝对路径作键:避免尾斜杠 / 相对路径在不同注册点产生两个会话\n const key = path.resolve(workspace);\n let s = shellSessions.get(key);\n if (!s) {\n s = { cwd: key };\n shellSessions.set(key, s);\n }\n return s;\n}\n\n/** 测试用:重置某个工作区的 shell 会话状态 */\nexport function resetShellSession(workspace: string): void {\n shellSessions.delete(path.resolve(workspace));\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(workspace: string, spec: ShellSpec, tasks: ShellTaskManager, scope: string | null) {\n const session = sessionFor(workspace);\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 const { file, argv } = resolveInvocation(spec, prepared);\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 * 注册 Shell 工具(高风险:requiresApproval=true,由权限引擎门控)\n *\n * @param scope 后台任务归属(server 传租户 id);用于按项目过滤任务列表与终止\n */\nexport function registerShellTool(\n registry: ToolRegistry,\n workspace: string,\n scope?: string | null,\n): void {\n const spec = resolveShellSpec();\n const tasks = shellTasks;\n const shell = makeRunShell(workspace, spec, tasks, scope ?? null);\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 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 * 路径安全辅助函数\n *\n * 判断目标路径是否真正位于工作区根目录之内。\n *\n * 仅做字符串比较(path.relative)无法防御符号链接逃逸:\n * 工作区内若存在指向外部的 symlink,`workspace/link/secret.txt`\n * 的字符串前缀合法,但实际读写发生在链接目标(工作区外)。\n *\n * 因此这里用 fs.realpathSync 解析符号链接后再与根目录的 realpath\n * 比较。目标尚不存在(如 write_file 新建文件)时,向上回溯到最近\n * 已存在的祖先目录解析,再拼回剩余路径段。\n *\n * 校验通过时把解析后的真实路径一并返回:调用方必须用它(而非原始\n * 相对路径)做实际 fs 操作,否则「校验后、操作前」路径组件被替换为\n * 指向外部的 symlink 仍会逃逸(TOCTOU)。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/**\n * 判断 `path.relative(root, target)` 的结果是否落在 root 之内。\n *\n * 逃逸必然恰好是 `..` 或以 `..` + 分隔符开头,不能用 `startsWith(\"..\")`:\n * 工作区里名为 `..foo` 的合法条目会被误判为逃逸而拒绝。\n * 跨盘符时(Windows)path.relative 返回绝对路径,一并拒绝。\n */\nfunction isInsideRelative(rel: string): boolean {\n return (\n !path.isAbsolute(rel) && rel !== \"..\" && !rel.startsWith(`..${path.sep}`)\n );\n}\n\n/** 是否为文件系统根(\"/\"、\"C:\\\\\") */\nfunction isFilesystemRoot(p: string): boolean {\n return path.parse(p).root === p;\n}\n\n/** 校验通过的路径:root 为工作区根的真实路径,real 为目标的真实路径 */\nexport interface ResolvedPath {\n root: string;\n real: string;\n}\n\n/**\n * 校验 target(相对 workspace 的路径)是否位于 workspace 内,并返回\n * 解析符号链接后的真实路径;不在工作区内时返回 null。\n *\n * - 解析符号链接:路径中任何 symlink 组件都会先 realpath 再校验;\n * broken symlink(存在但无法解析)一律拒绝。\n * - 目标不存在时(写新文件):回溯到最近存在的祖先做 realpath,\n * 再拼接剩余段,仍能捕获祖先链中的 symlink 逃逸。\n * - 工作区根若就是文件系统根(`WORKSPACE=/`):此时任何绝对路径都在\n * 其内,等同没有沙箱,直接判为越界。\n */\nexport function resolveWithinRoot(\n workspace: string,\n target: string,\n): ResolvedPath | null {\n const abs = path.resolve(workspace, target);\n\n let rootReal: string;\n try {\n rootReal = fs.realpathSync(workspace);\n } catch {\n // 根目录不存在(如权限引擎尚未初始化工作区):\n // 没有实际目录结构即无 symlink 逃逸面,退化为纯字符串路径比较。\n const rootAbs = path.resolve(workspace);\n if (!isInsideRelative(path.relative(rootAbs, abs))) return null;\n return { root: rootAbs, real: abs };\n }\n if (isFilesystemRoot(rootReal)) return null;\n\n let real: string;\n try {\n real = fs.realpathSync(abs);\n } catch {\n // abs 不存在:向上回溯找最近存在的祖先\n let probe = abs;\n const tail: string[] = [];\n for (;;) {\n // 组件存在(含 broken symlink)但 realpath 失败 → 无法解析,拒绝\n const exists = fs.existsSync(probe) || fs.lstatSync(probe, { throwIfNoEntry: false }) !== undefined;\n if (exists) {\n try {\n real = path.join(fs.realpathSync(probe), ...tail);\n break;\n } catch {\n return null;\n }\n }\n tail.unshift(path.basename(probe));\n const parent = path.dirname(probe);\n if (parent === probe) return null; // 已到文件系统根\n probe = parent;\n }\n }\n\n if (!isInsideRelative(path.relative(rootReal, real))) return null;\n return { root: rootReal, real };\n}\n\n/**\n * 判断 target 是否安全地位于 workspace 内(不需要真实路径时用这个)。\n *\n * 注意:删除类操作不应据此改用 realpath 去 unlink —— 那会删掉符号链接\n * 指向的目标而非链接本身;这类操作请用 resolveEntryWithinRoot。\n */\nexport function isWithinRoot(workspace: string, target: string): boolean {\n return resolveWithinRoot(workspace, target) !== null;\n}\n\n/**\n * 解析「目录项本身」的路径:父目录取 realpath,最后一段保持原样。\n *\n * 供 unlink / rename 等作用于条目自身的操作用:父目录不可被 symlink\n * 替换,而最后一段若是符号链接,被操作的是链接本身,不会被跟随。\n */\nexport function resolveEntryWithinRoot(\n workspace: string,\n target: string,\n): ResolvedPath | null {\n const abs = path.resolve(workspace, target);\n const parent = resolveWithinRoot(workspace, path.dirname(abs));\n if (!parent) return null;\n return { root: parent.root, real: path.join(parent.real, path.basename(abs)) };\n}\n","/**\n * ffmpeg / ffprobe 运行时定位与获取\n *\n * media 工具的「四级降级」把前两级放在这里实现:\n *\n * 1. 定位(resolveMedia):环境变量 ZQAGENT_FFMPEG / ZQAGENT_FFPROBE\n * → 工作区 .agent/bin/(本工具下载的静态构建)→ SEA 内嵌资源\n * → PATH。优先级对齐 ripgrep.ts 的既有范式。\n * 2. 获取(downloadFfmpeg):从各平台官方/社区编译的静态构建下载、\n * 校验、解包、安装到 .agent/bin/。\n *\n * 第 3/4 级(引导用户用 run_shell 安装 / 手工安装)由 media.ts 负责组织文案,\n * 本模块只提供 installHints() 的精确命令。\n *\n * 安全约定:\n * - 所有外部进程调用都走异步 execAsync + 参数数组,不拼 shell 字符串;\n * - 下载到临时目录 → 校验校验和 → 校验通过才原子重命名进 .agent/bin/,\n * 半成品不会污染安装目录。\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 { createHash } from \"node:crypto\";\nimport { pipeline } from \"node:stream/promises\";\nimport { SocksProxyAgent } from \"socks-proxy-agent\";\nimport nodeFetch from \"node-fetch\";\nimport { execAsync } from \"./execAsync.js\";\nimport { decodeOutput } from \"./shell.js\";\nimport { findExecutable } from \"./shellRuntime.js\";\n\n// =====================================================================\n// 常量\n// =====================================================================\n\n/** 覆盖 ffmpeg / ffprobe 路径的环境变量 */\nconst ENV_FFMPEG = \"ZQAGENT_FFMPEG\";\nconst ENV_FFPROBE = \"ZQAGENT_FFPROBE\";\n\n/**\n * SOCKS5 代理地址(环境变量 SOCKS5_PROXY)。\n * 与 web.ts / cli/connection.ts 保持同一约定:配置了就走代理,否则直连。\n * 下载体积大(数十 MB),国内环境经常需要代理,因此这里同样支持。\n */\nconst PROXY_ENV = process.env.SOCKS5_PROXY;\n\nlet socksAgent: SocksProxyAgent | null = null;\nif (PROXY_ENV) {\n try {\n socksAgent = new SocksProxyAgent(PROXY_ENV);\n } catch {\n try {\n // 旧版实现只认 socks5://,把 socks5h:// 降级重试(与 web.ts 一致)\n socksAgent = new SocksProxyAgent(PROXY_ENV.replace(/^socks5h:\\/\\//, \"socks5://\"));\n } catch {\n socksAgent = null;\n }\n }\n}\n\n/** 单次下载超时(毫秒)——静态构建可达上百 MB,给足时间 */\nconst DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;\n\n// =====================================================================\n// 定位\n// =====================================================================\n\n/** 二进制的来源(用于错误信息与文档,也便于诊断「为什么用的是那个」) */\nexport type BinSource = \"env\" | \"agent-bin\" | \"bundled\" | \"path\";\n\nexport interface ResolvedBin {\n path: string;\n source: BinSource;\n}\n\nexport interface ResolvedMedia {\n ffmpeg: ResolvedBin | null;\n ffprobe: ResolvedBin | null;\n}\n\n/** 本工具下载的静态构建的安装目录(随工作区,不进版本库:.agent/* 已 ignore) */\nexport function agentBinDir(workspace: string): string {\n return path.join(path.resolve(workspace), \".agent\", \"bin\");\n}\n\nfunction exeName(name: string, platform: NodeJS.Platform): string {\n return platform === \"win32\" ? `${name}.exe` : name;\n}\n\nfunction defaultFileExists(p: string): boolean {\n try {\n return fs.statSync(p).isFile();\n } catch {\n return false;\n }\n}\n\n/** 把 SEA 内嵌的二进制释放到临时目录,返回可执行文件路径 */\nfunction extractBundled(name: string, platform: NodeJS.Platform): string | null {\n try {\n // CJS(SEA 单文件)下 require 可用;ESM 构建下用 createRequire 兜底。\n // 与 ripgrep.ts 相同的取法,避免依赖 import.meta。\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(name);\n if (!asset) return null;\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), `zqagent-${name}-`));\n const file = path.join(dir, exeName(name, platform));\n fs.writeFileSync(file, Buffer.from(asset));\n fs.chmodSync(file, 0o755);\n return file;\n } catch {\n // 未内嵌(npm 安装、普通 node 运行)或 API 不可用\n return null;\n }\n}\n\nexport interface ResolveMediaOptions {\n platform?: NodeJS.Platform;\n env?: NodeJS.ProcessEnv;\n /** 判断文件存在,便于测试注入 */\n fileExists?: (p: string) => boolean;\n}\n\nfunction resolveOne(\n name: string,\n workspace: string,\n o: Required<Pick<ResolveMediaOptions, \"platform\" | \"env\" | \"fileExists\">>,\n): ResolvedBin | null {\n // 1. 环境变量显式指定\n const envKey = name === \"ffmpeg\" ? ENV_FFMPEG : ENV_FFPROBE;\n const explicit = (o.env[envKey] ?? \"\").trim();\n if (explicit && o.fileExists(explicit)) return { path: explicit, source: \"env\" };\n\n // 2. 工作区内 .agent/bin(本工具下载的静态构建)\n const local = path.join(agentBinDir(workspace), exeName(name, o.platform));\n if (o.fileExists(local)) return { path: local, source: \"agent-bin\" };\n\n // 3. SEA 内嵌资源\n const bundled = extractBundled(name, o.platform);\n if (bundled) return { path: bundled, source: \"bundled\" };\n\n // 4. PATH\n const onPath = findExecutable(name, {\n platform: o.platform,\n env: o.env,\n fileExists: o.fileExists,\n });\n return onPath ? { path: onPath, source: \"path\" } : null;\n}\n\n/** 解析结果缓存(按 平台 + 工作区),避免每次调用都探测文件系统 */\nconst cache = new Map<string, ResolvedMedia>();\n\n/** 解析可用的 ffmpeg / ffprobe(结果带缓存) */\nexport function resolveMedia(\n workspace: string,\n opts: ResolveMediaOptions = {},\n): ResolvedMedia {\n const platform = opts.platform ?? process.platform;\n const key = `${platform}:${path.resolve(workspace)}`;\n const hit = cache.get(key);\n if (hit) return hit;\n\n const ctx = {\n platform,\n env: opts.env ?? process.env,\n fileExists: opts.fileExists ?? defaultFileExists,\n };\n const resolved: ResolvedMedia = {\n ffmpeg: resolveOne(\"ffmpeg\", workspace, ctx),\n ffprobe: resolveOne(\"ffprobe\", workspace, ctx),\n };\n cache.set(key, resolved);\n return resolved;\n}\n\n/** 测试用 / 安装完成后:清除解析缓存 */\nexport function resetFfmpegCache(): void {\n cache.clear();\n}\n\n// =====================================================================\n// 安装指引(第 3/4 级的素材)\n// =====================================================================\n\n/**\n * 返回目标平台的精确安装命令(按推荐顺序)。\n *\n * 这些命令同时用于第 3 级(模型转用 run_shell 安装)与第 4 级(用户手工安装),\n * 因此必须是可以直接复制执行的、非交互的形式。\n */\nexport function installHints(platform: NodeJS.Platform = process.platform): string[] {\n switch (platform) {\n case \"darwin\":\n return [\n \"brew install ffmpeg\",\n \"下载页: https://evermeet.cx/ffmpeg/ 与 https://evermeet.cx/ffprobe/(解压后把 ffmpeg / ffprobe 放进 PATH)\",\n ];\n case \"win32\":\n return [\n 'winget install \"FFmpeg (Essentials Build)\"',\n \"choco install ffmpeg\",\n \"scoop install ffmpeg-essentials\",\n \"下载页: https://www.gyan.dev/ffmpeg/builds/(解压后把 bin 目录加入 PATH)\",\n ];\n default:\n return [\n \"apt-get install -y ffmpeg # Debian / Ubuntu\",\n \"dnf install -y ffmpeg # Fedora / RHEL(需启用 RPM Fusion)\",\n \"apk add ffmpeg # Alpine\",\n \"pacman -S ffmpeg # Arch\",\n \"下载页: https://johnvansickle.com/ffmpeg/(静态构建,解压后把 ffmpeg / ffprobe 放进 PATH 或 .agent/bin)\",\n ];\n }\n}\n\n// =====================================================================\n// 下载(第 2 级)\n// =====================================================================\n\nexport type ArchiveKind = \"tar.xz\" | \"zip\";\nexport type ChecksumAlgo = \"sha256\" | \"md5\";\n\n/** 下载源里的单个二进制 */\nexport interface DownloadComponent {\n /** 期望的二进制名(不含 .exe) */\n name: string;\n /** 直接下载地址(与 infoUrl 二选一) */\n url?: string;\n /** 通过 JSON 元数据接口动态取当前版本地址(evermeet 用) */\n infoUrl?: string;\n archive: ArchiveKind;\n /** 校验和边车文件地址(内容为 `<hash> <文件名>`,或纯 hash) */\n checksumUrl?: string;\n checksumAlgo?: ChecksumAlgo;\n}\n\nexport interface DistSpec {\n components: DownloadComponent[];\n}\n\nconst JVS = \"https://johnvansickle.com/ffmpeg/releases\";\nconst EVERMEET_INFO = \"https://evermeet.cx/ffmpeg/info\";\nconst GYAN = \"https://www.gyan.dev/ffmpeg/builds\";\nconst BTBN = \"https://github.com/BtbN/FFmpeg-Builds/releases/download/latest\";\n\nfunction distEntry(name: string, url: string, checksumUrl?: string, algo?: ChecksumAlgo): DownloadComponent {\n return { name, url, archive: url.endsWith(\".zip\") ? \"zip\" : \"tar.xz\", checksumUrl, checksumAlgo: algo };\n}\n\n/** 解析目标平台的下载源;不支持的平台返回 null(由调用方降级到第 3/4 级) */\nexport function distSpecFor(platform: NodeJS.Platform, arch: string): DistSpec | null {\n if (platform === \"linux\") {\n // johnvansickle 静态构建体积最小(x64 约 40MB),tar.xz 内含 ffmpeg + ffprobe\n const suffix =\n arch === \"x64\"\n ? \"amd64\"\n : arch === \"arm64\"\n ? \"arm64\"\n : arch === \"arm\"\n ? \"armhf\"\n : arch === \"ia32\"\n ? \"i686\"\n : null;\n if (!suffix) return null;\n return {\n components: [\n distEntry(\"ffmpeg\", `${JVS}/ffmpeg-release-${suffix}-static.tar.xz`, `${JVS}/ffmpeg-release-${suffix}-static.tar.xz.md5`, \"md5\"),\n distEntry(\"ffprobe\", `${JVS}/ffmpeg-release-${suffix}-static.tar.xz`),\n ],\n };\n }\n\n if (platform === \"darwin\") {\n // evermeet 分别发布 ffmpeg / ffprobe;用 JSON 元数据接口取当前版本的 zip 地址,\n // 避免把版本号写死在代码里(每次发版都要改代码)。\n return {\n components: [\n {\n name: \"ffmpeg\",\n infoUrl: `${EVERMEET_INFO}/ffmpeg/release`,\n archive: \"zip\",\n },\n {\n name: \"ffprobe\",\n infoUrl: `${EVERMEET_INFO}/ffprobe/release`,\n archive: \"zip\",\n },\n ],\n };\n }\n\n if (platform === \"win32\") {\n if (arch === \"x64\") {\n // gyan.dev essentials 包:含 ffmpeg + ffprobe + ffplay,站方提供 sha256\n const url = `${GYAN}/ffmpeg-release-essentials.zip`;\n return {\n components: [\n distEntry(\"ffmpeg\", url, `${url}.sha256`, \"sha256\"),\n distEntry(\"ffprobe\", url),\n ],\n };\n }\n if (arch === \"arm64\") {\n // BtbN 的 winarm64 构建;校验和在同仓库的 checksums.sha256 里按文件名查\n const url = `${BTBN}/ffmpeg-master-latest-winarm64-gpl.zip`;\n return {\n components: [\n distEntry(\"ffmpeg\", url, `${BTBN}/checksums.sha256`, \"sha256\"),\n distEntry(\"ffprobe\", url),\n ],\n };\n }\n return null;\n }\n\n return null;\n}\n\n/** 校验和文件解析:支持 `<hash> <文件名>`、纯 hash、以及多行按文件名匹配 */\nexport function parseChecksumFile(text: string, filename: string): string | null {\n const lines = text.split(/\\r?\\n/).filter((l) => l.trim().length > 0);\n for (const line of lines) {\n const m = /^([0-9a-f]{32,128})\\s+\\*?(.*)$/i.exec(line.trim());\n if (!m) continue;\n const hash = m[1]!.toLowerCase();\n const name = m[2]?.trim() ?? \"\";\n // 多行文件(如 BtbN checksums.sha256)按文件名挑选;单行文件无文件名也接受\n if (!name || name === filename || path.basename(name) === filename) return hash;\n }\n // 纯 hash 无文件名的情况\n const single = lines[0]?.trim();\n if (lines.length === 1 && single && /^[0-9a-f]{32,128}$/i.test(single)) {\n return single.toLowerCase();\n }\n return null;\n}\n\n/** 计算文件校验和(流式,避免把上百 MB 一次性读进内存) */\nexport async function computeChecksum(file: string, algo: ChecksumAlgo): Promise<string> {\n const hash = createHash(algo);\n await pipeline(fs.createReadStream(file), hash);\n return hash.digest(\"hex\");\n}\n\nfunction fetchInit(): Record<string, unknown> {\n return socksAgent ? { agent: socksAgent } : {};\n}\n\nasync function fetchText(url: string): Promise<string> {\n const resp = await nodeFetch(url, fetchInit() as never);\n if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);\n return resp.text();\n}\n\nasync function downloadToFile(url: string, dest: string): Promise<void> {\n const resp = await nodeFetch(url, fetchInit() as never);\n if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);\n if (!resp.body) throw new Error(\"响应没有内容\");\n await pipeline(resp.body, fs.createWriteStream(dest));\n}\n\n/** 从 JSON 元数据接口取当前版本的 zip 地址(evermeet) */\nasync function resolveComponentUrl(c: DownloadComponent): Promise<string> {\n if (c.url) return c.url;\n if (!c.infoUrl) throw new Error(`下载源缺少 url / infoUrl: ${c.name}`);\n const text = await fetchText(c.infoUrl);\n const json = JSON.parse(text) as { download?: { zip?: { url?: string } } };\n const url = json.download?.zip?.url;\n if (!url) throw new Error(`元数据接口未给出 zip 地址: ${c.infoUrl}`);\n return url;\n}\n\n/** 递归查找解包目录下指定文件名的文件(各家归档的目录层级不一致,按名字找最稳) */\nexport function findBinaryInTree(dir: string, names: string[]): string | null {\n const stack = [dir];\n while (stack.length > 0) {\n const cur = stack.pop()!;\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(cur, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const e of entries) {\n const full = path.join(cur, e.name);\n if (e.isDirectory()) {\n stack.push(full);\n } else if (names.includes(e.name)) {\n return full;\n }\n }\n }\n return null;\n}\n\n/** 解包归档(tar.xz 用 tar;zip 用 unzip,Windows 用 PowerShell Expand-Archive) */\nasync function extractArchive(\n archive: string,\n dest: string,\n kind: ArchiveKind,\n platform: NodeJS.Platform,\n): Promise<string | null> {\n fs.mkdirSync(dest, { recursive: true });\n let res;\n if (kind === \"tar.xz\") {\n // -J 走 xz 解压;多数发行版自带,缺失时会报错并降级到第 3/4 级\n res = await execAsync(\"tar\", [\"-xJf\", archive, \"-C\", dest]);\n } else if (platform === \"win32\") {\n res = await execAsync(\n \"powershell\",\n [\n \"-NoProfile\",\n \"-NonInteractive\",\n \"-Command\",\n `Expand-Archive -LiteralPath '${archive.replace(/'/g, \"''\")}' -DestinationPath '${dest.replace(/'/g, \"''\")}' -Force`,\n ],\n );\n } else {\n res = await execAsync(\"unzip\", [\"-o\", \"-q\", archive, \"-d\", dest]);\n }\n if (res.error || res.status !== 0) {\n const detail = res.error?.message ?? decodeOutput(res.stderr).trim();\n return `解包失败 (${kind}): ${detail}`;\n }\n return null;\n}\n\nexport interface DownloadResult {\n ok: boolean;\n /** 成功时:安装到 .agent/bin 的绝对路径 */\n installed?: Record<string, string>;\n error?: string;\n /** 完成但值得提示的信息(缺校验和、许可证义务等) */\n notes?: string[];\n}\n\nexport interface DownloadOptions {\n platform?: NodeJS.Platform;\n arch?: string;\n /** 覆盖下载源(测试注入) */\n spec?: DistSpec | null;\n log?: (msg: string) => void;\n}\n\n/**\n * 下载并安装静态构建到 `<workspace>/.agent/bin`。\n *\n * 供 media 工具的 `op=\"setup\"` 调用——不静默触发:下载数十 MB 并写入工作区,\n * 必须是用户明确发起的动作(由权限引擎弹审批)。\n */\nexport async function downloadFfmpeg(\n workspace: string,\n opts: DownloadOptions = {},\n): Promise<DownloadResult> {\n const platform = opts.platform ?? process.platform;\n const arch = opts.arch ?? process.arch;\n const log = opts.log ?? (() => {});\n const notes: string[] = [];\n\n const spec = opts.spec === undefined ? distSpecFor(platform, arch) : opts.spec;\n if (!spec || spec.components.length === 0) {\n return {\n ok: false,\n error: `暂不支持自动下载的平台: ${platform}/${arch}`,\n };\n }\n\n const tmp = fs.mkdtempSync(path.join(os.tmpdir(), \"zqagent-ffmpeg-\"));\n const binDir = agentBinDir(workspace);\n const installed: Record<string, string> = {};\n\n try {\n // 同一归档承载多个二进制时只下一次(如 linux 的 tar.xz、gyan 的 zip)\n const fetched = new Map<string, { file: string; kind: ArchiveKind }>();\n\n for (const c of spec.components) {\n let url: string;\n try {\n url = await resolveComponentUrl(c);\n } catch (e) {\n return { ok: false, error: `获取下载地址失败: ${String(e instanceof Error ? e.message : e)}` };\n }\n\n let archive = fetched.get(url);\n if (!archive) {\n const archivePath = path.join(tmp, path.basename(new URL(url).pathname) || `${c.name}.${c.archive}`);\n log(`下载 ${url}`);\n try {\n await downloadToFile(url, archivePath);\n } catch (e) {\n return { ok: false, error: `下载失败 (${url}): ${String(e instanceof Error ? e.message : e)}` };\n }\n\n // 校验:拿不到边车文件时只告警不阻断(例如 macOS 的 evermeet 只有 GPG 签名)\n if (c.checksumUrl) {\n const algo = c.checksumAlgo ?? \"sha256\";\n try {\n const expected = parseChecksumFile(await fetchText(c.checksumUrl), path.basename(archivePath));\n if (!expected) {\n notes.push(`未能从 ${c.checksumUrl} 解析出校验和,已跳过校验`);\n } else {\n const actual = await computeChecksum(archivePath, algo);\n if (actual !== expected) {\n return {\n ok: false,\n error: `校验和不匹配 (${algo}): 期望 ${expected},实际 ${actual}。已中止安装。`,\n };\n }\n log(`${algo} 校验通过`);\n }\n } catch (e) {\n notes.push(`校验和获取失败 (${c.checksumUrl}): ${String(e instanceof Error ? e.message : e)}`);\n }\n } else {\n notes.push(`${path.basename(archivePath)} 未提供校验和(仅 HTTPS 传输保护)`);\n }\n\n archive = { file: archivePath, kind: c.archive };\n fetched.set(url, archive);\n }\n\n const extractDir = fs.mkdtempSync(path.join(tmp, \"x-\"));\n const err = await extractArchive(archive.file, extractDir, archive.kind, platform);\n if (err) return { ok: false, error: err };\n\n const found = findBinaryInTree(extractDir, [exeName(c.name, platform), c.name]);\n if (!found) {\n return { ok: false, error: `归档内未找到 ${c.name}: ${path.basename(archive.file)}` };\n }\n\n // 装进 .agent/bin:先复制到临时名再 rename,避免半成品被后续调用当成可用二进制\n fs.mkdirSync(binDir, { recursive: true });\n const target = path.join(binDir, exeName(c.name, platform));\n const staging = `${target}.tmp-${process.pid}`;\n fs.copyFileSync(found, staging);\n fs.chmodSync(staging, 0o755);\n fs.renameSync(staging, target);\n installed[c.name] = target;\n }\n\n resetFfmpegCache();\n notes.push(\"ffmpeg 静态构建多为 GPL/LGPL 许可,随产品分发前请确认相应的源码提供义务\");\n return { ok: true, installed, notes };\n } catch (e) {\n return { ok: false, error: `安装失败: ${String(e instanceof Error ? e.message : e)}` };\n } finally {\n fs.rmSync(tmp, { recursive: true, force: true });\n }\n}\n\n/** 供上层展示:本模块是否具备自动下载能力(不发起网络请求) */\nexport function canAutoDownload(platform: NodeJS.Platform, arch: string): boolean {\n return distSpecFor(platform, arch) !== null;\n}\n"],"mappings":"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","names":["fs","path","import_node_child_process","resolve","import_node_child_process","path","fs","path","nodeFetch","MAX_BUFFER","out"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { T as ToolRegistry } from '../ToolRegistry-C3pTFiaJ.cjs';
|
|
2
|
+
|
|
3
|
+
/** 解析时间码:秒数、`MM:SS`、`HH:MM:SS(.mmm)` 都接受;非法返回 null */
|
|
4
|
+
declare function parseTimecode(v: unknown): number | null;
|
|
5
|
+
interface BuildPaths {
|
|
6
|
+
/** 已解析为绝对路径的输入 */
|
|
7
|
+
input?: string;
|
|
8
|
+
/** 已解析为绝对路径的输出 */
|
|
9
|
+
output?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 构建 ffmpeg 参数数组。
|
|
13
|
+
*
|
|
14
|
+
* @returns 参数数组;参数不合法时返回 { error }
|
|
15
|
+
*/
|
|
16
|
+
declare function buildFfmpegArgs(op: string, args: Record<string, unknown>, paths: BuildPaths): string[] | {
|
|
17
|
+
error: string;
|
|
18
|
+
};
|
|
19
|
+
/** ffprobe 参数:只输出需要的字段,避免超大 JSON */
|
|
20
|
+
declare function buildFfprobeArgs(input: string): string[];
|
|
21
|
+
/** 把 ffprobe 的原始 JSON 压成模型易读的结构(体积可控) */
|
|
22
|
+
declare function summarizeProbe(raw: unknown): Record<string, unknown>;
|
|
23
|
+
/** 注册选项:可注入平台/环境/文件探测,便于在测试中演练降级路径 */
|
|
24
|
+
interface MediaToolOptions {
|
|
25
|
+
platform?: NodeJS.Platform;
|
|
26
|
+
env?: NodeJS.ProcessEnv;
|
|
27
|
+
fileExists?: (p: string) => boolean;
|
|
28
|
+
}
|
|
29
|
+
declare function registerMediaTool(registry: ToolRegistry, workspace: string, opts?: MediaToolOptions): void;
|
|
30
|
+
|
|
31
|
+
export { type BuildPaths, type MediaToolOptions, buildFfmpegArgs, buildFfprobeArgs, parseTimecode, registerMediaTool, summarizeProbe };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { T as ToolRegistry } from '../ToolRegistry-C3pTFiaJ.js';
|
|
2
|
+
|
|
3
|
+
/** 解析时间码:秒数、`MM:SS`、`HH:MM:SS(.mmm)` 都接受;非法返回 null */
|
|
4
|
+
declare function parseTimecode(v: unknown): number | null;
|
|
5
|
+
interface BuildPaths {
|
|
6
|
+
/** 已解析为绝对路径的输入 */
|
|
7
|
+
input?: string;
|
|
8
|
+
/** 已解析为绝对路径的输出 */
|
|
9
|
+
output?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 构建 ffmpeg 参数数组。
|
|
13
|
+
*
|
|
14
|
+
* @returns 参数数组;参数不合法时返回 { error }
|
|
15
|
+
*/
|
|
16
|
+
declare function buildFfmpegArgs(op: string, args: Record<string, unknown>, paths: BuildPaths): string[] | {
|
|
17
|
+
error: string;
|
|
18
|
+
};
|
|
19
|
+
/** ffprobe 参数:只输出需要的字段,避免超大 JSON */
|
|
20
|
+
declare function buildFfprobeArgs(input: string): string[];
|
|
21
|
+
/** 把 ffprobe 的原始 JSON 压成模型易读的结构(体积可控) */
|
|
22
|
+
declare function summarizeProbe(raw: unknown): Record<string, unknown>;
|
|
23
|
+
/** 注册选项:可注入平台/环境/文件探测,便于在测试中演练降级路径 */
|
|
24
|
+
interface MediaToolOptions {
|
|
25
|
+
platform?: NodeJS.Platform;
|
|
26
|
+
env?: NodeJS.ProcessEnv;
|
|
27
|
+
fileExists?: (p: string) => boolean;
|
|
28
|
+
}
|
|
29
|
+
declare function registerMediaTool(registry: ToolRegistry, workspace: string, opts?: MediaToolOptions): void;
|
|
30
|
+
|
|
31
|
+
export { type BuildPaths, type MediaToolOptions, buildFfmpegArgs, buildFfprobeArgs, parseTimecode, registerMediaTool, summarizeProbe };
|