@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,1159 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/tools/media.ts
|
|
9
|
+
import * as fs3 from "fs";
|
|
10
|
+
import * as path4 from "path";
|
|
11
|
+
|
|
12
|
+
// src/tools/execAsync.ts
|
|
13
|
+
import { spawn } from "child_process";
|
|
14
|
+
|
|
15
|
+
// src/tools/killTree.ts
|
|
16
|
+
import { spawnSync } from "child_process";
|
|
17
|
+
function killTree(pid) {
|
|
18
|
+
if (!pid) return;
|
|
19
|
+
if (process.platform === "win32") {
|
|
20
|
+
try {
|
|
21
|
+
spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
process.kill(-pid, "SIGKILL");
|
|
28
|
+
} catch {
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, "SIGKILL");
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/tools/execAsync.ts
|
|
37
|
+
var liveChildren = /* @__PURE__ */ new Set();
|
|
38
|
+
var exitCleanupInstalled = false;
|
|
39
|
+
function trackChild(pid) {
|
|
40
|
+
if (!pid) return;
|
|
41
|
+
liveChildren.add(pid);
|
|
42
|
+
if (exitCleanupInstalled) return;
|
|
43
|
+
exitCleanupInstalled = true;
|
|
44
|
+
process.once("exit", () => {
|
|
45
|
+
for (const p of liveChildren) killTree(p);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
var DEFAULT_MAX_BUFFER = 1024 * 1024 * 5;
|
|
49
|
+
var KILL_GRACE_MS = 1e3;
|
|
50
|
+
function makeSink(maxBuffer) {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
let len = 0;
|
|
53
|
+
let over = false;
|
|
54
|
+
return {
|
|
55
|
+
push(chunk) {
|
|
56
|
+
if (len >= maxBuffer) {
|
|
57
|
+
over = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const room = maxBuffer - len;
|
|
61
|
+
if (chunk.length > room) {
|
|
62
|
+
chunks.push(chunk.subarray(0, room));
|
|
63
|
+
len = maxBuffer;
|
|
64
|
+
over = true;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
chunks.push(chunk);
|
|
68
|
+
len += chunk.length;
|
|
69
|
+
},
|
|
70
|
+
done: () => Buffer.concat(chunks),
|
|
71
|
+
over: () => over
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function execAsync(file, argv, opts = {}) {
|
|
75
|
+
return new Promise((resolve4) => {
|
|
76
|
+
const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
77
|
+
const out = makeSink(maxBuffer);
|
|
78
|
+
const err = makeSink(maxBuffer);
|
|
79
|
+
if (opts.signal?.aborted) {
|
|
80
|
+
resolve4({
|
|
81
|
+
status: null,
|
|
82
|
+
stdout: Buffer.alloc(0),
|
|
83
|
+
stderr: Buffer.alloc(0),
|
|
84
|
+
timedOut: false,
|
|
85
|
+
truncated: false,
|
|
86
|
+
aborted: true
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let child;
|
|
91
|
+
try {
|
|
92
|
+
child = spawn(file, argv, {
|
|
93
|
+
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
94
|
+
...opts.env !== void 0 ? { env: opts.env } : {},
|
|
95
|
+
// 自成进程组(POSIX):超时/中断时用 killTree 一次收掉整棵进程树
|
|
96
|
+
detached: process.platform !== "win32",
|
|
97
|
+
stdio: [opts.input === void 0 ? "ignore" : "pipe", "pipe", "pipe"],
|
|
98
|
+
windowsHide: true
|
|
99
|
+
});
|
|
100
|
+
} catch (e) {
|
|
101
|
+
resolve4({
|
|
102
|
+
status: null,
|
|
103
|
+
stdout: Buffer.alloc(0),
|
|
104
|
+
stderr: Buffer.alloc(0),
|
|
105
|
+
error: e,
|
|
106
|
+
timedOut: false,
|
|
107
|
+
truncated: false
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
trackChild(child.pid);
|
|
112
|
+
let settled = false;
|
|
113
|
+
let timedOut = false;
|
|
114
|
+
let aborted = false;
|
|
115
|
+
let spawnError;
|
|
116
|
+
let timer = null;
|
|
117
|
+
let graceTimer = null;
|
|
118
|
+
const onAbort = () => {
|
|
119
|
+
aborted = true;
|
|
120
|
+
killAndSettle();
|
|
121
|
+
};
|
|
122
|
+
const finish = (status) => {
|
|
123
|
+
if (settled) return;
|
|
124
|
+
settled = true;
|
|
125
|
+
if (timer) clearTimeout(timer);
|
|
126
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
127
|
+
if (child.pid !== void 0) liveChildren.delete(child.pid);
|
|
128
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
129
|
+
const result = {
|
|
130
|
+
status,
|
|
131
|
+
stdout: out.done(),
|
|
132
|
+
stderr: err.done(),
|
|
133
|
+
timedOut,
|
|
134
|
+
truncated: out.over() || err.over()
|
|
135
|
+
};
|
|
136
|
+
if (aborted) result.aborted = true;
|
|
137
|
+
if (spawnError) result.error = spawnError;
|
|
138
|
+
resolve4(result);
|
|
139
|
+
};
|
|
140
|
+
const killAndSettle = () => {
|
|
141
|
+
killTree(child.pid);
|
|
142
|
+
if (settled || graceTimer) return;
|
|
143
|
+
graceTimer = setTimeout(() => {
|
|
144
|
+
child.stdout?.destroy();
|
|
145
|
+
child.stderr?.destroy();
|
|
146
|
+
finish(null);
|
|
147
|
+
}, KILL_GRACE_MS);
|
|
148
|
+
};
|
|
149
|
+
if (opts.timeoutMs !== void 0 && opts.timeoutMs > 0) {
|
|
150
|
+
timer = setTimeout(() => {
|
|
151
|
+
timedOut = true;
|
|
152
|
+
killAndSettle();
|
|
153
|
+
}, opts.timeoutMs);
|
|
154
|
+
}
|
|
155
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
156
|
+
child.stdout?.on("data", (c) => out.push(c));
|
|
157
|
+
child.stderr?.on("data", (c) => err.push(c));
|
|
158
|
+
child.on("error", (e) => {
|
|
159
|
+
spawnError = e;
|
|
160
|
+
if (child.pid === void 0) finish(null);
|
|
161
|
+
});
|
|
162
|
+
if (opts.input !== void 0 && child.stdin) {
|
|
163
|
+
child.stdin.on("error", () => {
|
|
164
|
+
});
|
|
165
|
+
child.stdin.end(opts.input);
|
|
166
|
+
}
|
|
167
|
+
child.on("close", (code) => finish(code));
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/tools/shellRuntime.ts
|
|
172
|
+
import * as path from "path";
|
|
173
|
+
var WINDOWS_SUFFIXES = [".exe", ".cmd", ".bat", ".com", ""];
|
|
174
|
+
function pathApi(platform) {
|
|
175
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
176
|
+
}
|
|
177
|
+
function joinPath(platform, ...parts) {
|
|
178
|
+
return pathApi(platform).join(...parts);
|
|
179
|
+
}
|
|
180
|
+
function findExecutable(name, ctx) {
|
|
181
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
182
|
+
return ctx.fileExists(name) ? name : null;
|
|
183
|
+
}
|
|
184
|
+
const suffixes = ctx.platform === "win32" ? WINDOWS_SUFFIXES : [""];
|
|
185
|
+
const delimiter = ctx.platform === "win32" ? ";" : ":";
|
|
186
|
+
const dirs = (ctx.env.PATH ?? "").split(delimiter).filter((d) => d.length > 0);
|
|
187
|
+
for (const dir of dirs) {
|
|
188
|
+
for (const suffix of suffixes) {
|
|
189
|
+
const candidate = joinPath(ctx.platform, dir, name + suffix);
|
|
190
|
+
if (ctx.fileExists(candidate)) return candidate;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/tools/shellBackground.ts
|
|
197
|
+
import { spawn as spawn2 } from "child_process";
|
|
198
|
+
var DEFAULT_BUFFER_LIMIT = 64 * 1024;
|
|
199
|
+
var RingBuffer = class {
|
|
200
|
+
constructor(limit = DEFAULT_BUFFER_LIMIT) {
|
|
201
|
+
this.limit = limit;
|
|
202
|
+
}
|
|
203
|
+
limit;
|
|
204
|
+
chunks = [];
|
|
205
|
+
size = 0;
|
|
206
|
+
add(chunk) {
|
|
207
|
+
if (!chunk) return;
|
|
208
|
+
this.chunks.push(chunk);
|
|
209
|
+
this.size += chunk.length;
|
|
210
|
+
while (this.size > this.limit && this.chunks.length > 1) {
|
|
211
|
+
this.size -= this.chunks.shift().length;
|
|
212
|
+
}
|
|
213
|
+
if (this.size > this.limit) {
|
|
214
|
+
const joined = this.chunks.join("");
|
|
215
|
+
this.chunks = [joined.slice(-this.limit)];
|
|
216
|
+
this.size = this.chunks[0].length;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
get() {
|
|
220
|
+
const joined = this.chunks.join("");
|
|
221
|
+
return joined.length > this.limit ? joined.slice(-this.limit) : joined;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var ShellTaskManager = class {
|
|
225
|
+
tasks = /* @__PURE__ */ new Map();
|
|
226
|
+
seq = 0;
|
|
227
|
+
cleanupInstalled = false;
|
|
228
|
+
/** 启动一个后台任务,立即返回快照(不等退出) */
|
|
229
|
+
start(opts) {
|
|
230
|
+
this.installCleanup();
|
|
231
|
+
const id = `shell-${++this.seq}-${Date.now().toString(36)}`;
|
|
232
|
+
const child = spawn2(opts.file, opts.argv, {
|
|
233
|
+
cwd: opts.cwd,
|
|
234
|
+
env: opts.env,
|
|
235
|
+
detached: process.platform !== "win32",
|
|
236
|
+
windowsHide: true,
|
|
237
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
238
|
+
});
|
|
239
|
+
const task = {
|
|
240
|
+
id,
|
|
241
|
+
command: opts.command,
|
|
242
|
+
scope: opts.scope ?? null,
|
|
243
|
+
child,
|
|
244
|
+
stdout: new RingBuffer(),
|
|
245
|
+
stderr: new RingBuffer(),
|
|
246
|
+
status: "running",
|
|
247
|
+
exitCode: null,
|
|
248
|
+
signal: null,
|
|
249
|
+
startedAt: Date.now(),
|
|
250
|
+
endedAt: null
|
|
251
|
+
};
|
|
252
|
+
this.tasks.set(id, task);
|
|
253
|
+
child.stdout?.on("data", (d) => task.stdout.add(d.toString("utf-8")));
|
|
254
|
+
child.stderr?.on("data", (d) => task.stderr.add(d.toString("utf-8")));
|
|
255
|
+
child.on("error", (err) => task.stderr.add(`[spawn error] ${err.message}
|
|
256
|
+
`));
|
|
257
|
+
child.on("exit", (code, signal) => {
|
|
258
|
+
task.exitCode = code;
|
|
259
|
+
task.signal = signal;
|
|
260
|
+
task.endedAt = Date.now();
|
|
261
|
+
if (task.status !== "killed") task.status = "exited";
|
|
262
|
+
});
|
|
263
|
+
child.unref();
|
|
264
|
+
return this.snapshot(task);
|
|
265
|
+
}
|
|
266
|
+
/** 读取任务输出与状态 */
|
|
267
|
+
output(taskId) {
|
|
268
|
+
const task = this.tasks.get(taskId);
|
|
269
|
+
if (!task) return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
|
|
270
|
+
return this.snapshot(task);
|
|
271
|
+
}
|
|
272
|
+
/** 终止任务(进程树)并返回快照;传 scope 时只允许终止该归属下的任务 */
|
|
273
|
+
kill(taskId, scope) {
|
|
274
|
+
const task = this.tasks.get(taskId);
|
|
275
|
+
if (!task) return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
|
|
276
|
+
if (scope !== void 0 && task.scope !== scope) {
|
|
277
|
+
return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
|
|
278
|
+
}
|
|
279
|
+
if (task.status === "running") {
|
|
280
|
+
task.status = "killed";
|
|
281
|
+
task.endedAt = Date.now();
|
|
282
|
+
killTree(task.child.pid);
|
|
283
|
+
}
|
|
284
|
+
return this.snapshot(task);
|
|
285
|
+
}
|
|
286
|
+
/** 列出任务快照(调试/展示用);传 scope 时只返回该归属下的任务 */
|
|
287
|
+
list(scope) {
|
|
288
|
+
const all = [...this.tasks.values()];
|
|
289
|
+
return (scope === void 0 ? all : all.filter((t) => t.scope === scope)).map(
|
|
290
|
+
(t) => this.snapshot(t)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
/** 终止全部任务(进程退出时调用) */
|
|
294
|
+
disposeAll() {
|
|
295
|
+
for (const task of this.tasks.values()) {
|
|
296
|
+
if (task.status === "running") {
|
|
297
|
+
task.status = "killed";
|
|
298
|
+
killTree(task.child.pid);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
snapshot(task) {
|
|
303
|
+
return {
|
|
304
|
+
task_id: task.id,
|
|
305
|
+
command: task.command,
|
|
306
|
+
status: task.status,
|
|
307
|
+
running: task.status === "running",
|
|
308
|
+
exit_code: task.exitCode,
|
|
309
|
+
signal: task.signal,
|
|
310
|
+
stdout: task.stdout.get(),
|
|
311
|
+
stderr: task.stderr.get(),
|
|
312
|
+
duration_ms: (task.endedAt ?? Date.now()) - task.startedAt
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
installCleanup() {
|
|
316
|
+
if (this.cleanupInstalled) return;
|
|
317
|
+
this.cleanupInstalled = true;
|
|
318
|
+
process.once("exit", () => this.disposeAll());
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// src/tools/shell.ts
|
|
323
|
+
var MAX_BUFFER = 1024 * 1024 * 5;
|
|
324
|
+
function decodeOutput(buf) {
|
|
325
|
+
if (!buf || buf.length === 0) return "";
|
|
326
|
+
try {
|
|
327
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
328
|
+
} catch {
|
|
329
|
+
for (const enc of ["gbk", "big5", "shift_jis", "latin1"]) {
|
|
330
|
+
try {
|
|
331
|
+
return new TextDecoder(enc).decode(buf);
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return buf.toString("latin1");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
var shellTasks = new ShellTaskManager();
|
|
339
|
+
|
|
340
|
+
// src/tools/pathSafety.ts
|
|
341
|
+
import * as fs from "fs";
|
|
342
|
+
import * as path2 from "path";
|
|
343
|
+
function isInsideRelative(rel) {
|
|
344
|
+
return !path2.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path2.sep}`);
|
|
345
|
+
}
|
|
346
|
+
function isFilesystemRoot(p) {
|
|
347
|
+
return path2.parse(p).root === p;
|
|
348
|
+
}
|
|
349
|
+
function resolveWithinRoot(workspace, target) {
|
|
350
|
+
const abs = path2.resolve(workspace, target);
|
|
351
|
+
let rootReal;
|
|
352
|
+
try {
|
|
353
|
+
rootReal = fs.realpathSync(workspace);
|
|
354
|
+
} catch {
|
|
355
|
+
const rootAbs = path2.resolve(workspace);
|
|
356
|
+
if (!isInsideRelative(path2.relative(rootAbs, abs))) return null;
|
|
357
|
+
return { root: rootAbs, real: abs };
|
|
358
|
+
}
|
|
359
|
+
if (isFilesystemRoot(rootReal)) return null;
|
|
360
|
+
let real;
|
|
361
|
+
try {
|
|
362
|
+
real = fs.realpathSync(abs);
|
|
363
|
+
} catch {
|
|
364
|
+
let probe = abs;
|
|
365
|
+
const tail = [];
|
|
366
|
+
for (; ; ) {
|
|
367
|
+
const exists = fs.existsSync(probe) || fs.lstatSync(probe, { throwIfNoEntry: false }) !== void 0;
|
|
368
|
+
if (exists) {
|
|
369
|
+
try {
|
|
370
|
+
real = path2.join(fs.realpathSync(probe), ...tail);
|
|
371
|
+
break;
|
|
372
|
+
} catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
tail.unshift(path2.basename(probe));
|
|
377
|
+
const parent = path2.dirname(probe);
|
|
378
|
+
if (parent === probe) return null;
|
|
379
|
+
probe = parent;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (!isInsideRelative(path2.relative(rootReal, real))) return null;
|
|
383
|
+
return { root: rootReal, real };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/tools/ffmpegRuntime.ts
|
|
387
|
+
import * as fs2 from "fs";
|
|
388
|
+
import * as os from "os";
|
|
389
|
+
import * as path3 from "path";
|
|
390
|
+
import { createRequire } from "module";
|
|
391
|
+
import { createHash } from "crypto";
|
|
392
|
+
import { pipeline } from "stream/promises";
|
|
393
|
+
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
394
|
+
import nodeFetch from "node-fetch";
|
|
395
|
+
var ENV_FFMPEG = "ZQAGENT_FFMPEG";
|
|
396
|
+
var ENV_FFPROBE = "ZQAGENT_FFPROBE";
|
|
397
|
+
var PROXY_ENV = process.env.SOCKS5_PROXY;
|
|
398
|
+
var socksAgent = null;
|
|
399
|
+
if (PROXY_ENV) {
|
|
400
|
+
try {
|
|
401
|
+
socksAgent = new SocksProxyAgent(PROXY_ENV);
|
|
402
|
+
} catch {
|
|
403
|
+
try {
|
|
404
|
+
socksAgent = new SocksProxyAgent(PROXY_ENV.replace(/^socks5h:\/\//, "socks5://"));
|
|
405
|
+
} catch {
|
|
406
|
+
socksAgent = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
var DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
411
|
+
function agentBinDir(workspace) {
|
|
412
|
+
return path3.join(path3.resolve(workspace), ".agent", "bin");
|
|
413
|
+
}
|
|
414
|
+
function exeName(name, platform) {
|
|
415
|
+
return platform === "win32" ? `${name}.exe` : name;
|
|
416
|
+
}
|
|
417
|
+
function defaultFileExists(p) {
|
|
418
|
+
try {
|
|
419
|
+
return fs2.statSync(p).isFile();
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function extractBundled(name, platform) {
|
|
425
|
+
try {
|
|
426
|
+
const req = typeof __require === "function" ? __require : createRequire(path3.join(process.cwd(), "__zqagent__.js"));
|
|
427
|
+
const sea = req("node:sea");
|
|
428
|
+
if (!sea?.isSea?.() || typeof sea.getAsset !== "function") return null;
|
|
429
|
+
const asset = sea.getAsset(name);
|
|
430
|
+
if (!asset) return null;
|
|
431
|
+
const dir = fs2.mkdtempSync(path3.join(os.tmpdir(), `zqagent-${name}-`));
|
|
432
|
+
const file = path3.join(dir, exeName(name, platform));
|
|
433
|
+
fs2.writeFileSync(file, Buffer.from(asset));
|
|
434
|
+
fs2.chmodSync(file, 493);
|
|
435
|
+
return file;
|
|
436
|
+
} catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function resolveOne(name, workspace, o) {
|
|
441
|
+
const envKey = name === "ffmpeg" ? ENV_FFMPEG : ENV_FFPROBE;
|
|
442
|
+
const explicit = (o.env[envKey] ?? "").trim();
|
|
443
|
+
if (explicit && o.fileExists(explicit)) return { path: explicit, source: "env" };
|
|
444
|
+
const local = path3.join(agentBinDir(workspace), exeName(name, o.platform));
|
|
445
|
+
if (o.fileExists(local)) return { path: local, source: "agent-bin" };
|
|
446
|
+
const bundled = extractBundled(name, o.platform);
|
|
447
|
+
if (bundled) return { path: bundled, source: "bundled" };
|
|
448
|
+
const onPath = findExecutable(name, {
|
|
449
|
+
platform: o.platform,
|
|
450
|
+
env: o.env,
|
|
451
|
+
fileExists: o.fileExists
|
|
452
|
+
});
|
|
453
|
+
return onPath ? { path: onPath, source: "path" } : null;
|
|
454
|
+
}
|
|
455
|
+
var cache = /* @__PURE__ */ new Map();
|
|
456
|
+
function resolveMedia(workspace, opts = {}) {
|
|
457
|
+
const platform = opts.platform ?? process.platform;
|
|
458
|
+
const key = `${platform}:${path3.resolve(workspace)}`;
|
|
459
|
+
const hit = cache.get(key);
|
|
460
|
+
if (hit) return hit;
|
|
461
|
+
const ctx = {
|
|
462
|
+
platform,
|
|
463
|
+
env: opts.env ?? process.env,
|
|
464
|
+
fileExists: opts.fileExists ?? defaultFileExists
|
|
465
|
+
};
|
|
466
|
+
const resolved = {
|
|
467
|
+
ffmpeg: resolveOne("ffmpeg", workspace, ctx),
|
|
468
|
+
ffprobe: resolveOne("ffprobe", workspace, ctx)
|
|
469
|
+
};
|
|
470
|
+
cache.set(key, resolved);
|
|
471
|
+
return resolved;
|
|
472
|
+
}
|
|
473
|
+
function resetFfmpegCache() {
|
|
474
|
+
cache.clear();
|
|
475
|
+
}
|
|
476
|
+
function installHints(platform = process.platform) {
|
|
477
|
+
switch (platform) {
|
|
478
|
+
case "darwin":
|
|
479
|
+
return [
|
|
480
|
+
"brew install ffmpeg",
|
|
481
|
+
"\u4E0B\u8F7D\u9875: https://evermeet.cx/ffmpeg/ \u4E0E https://evermeet.cx/ffprobe/\uFF08\u89E3\u538B\u540E\u628A ffmpeg / ffprobe \u653E\u8FDB PATH\uFF09"
|
|
482
|
+
];
|
|
483
|
+
case "win32":
|
|
484
|
+
return [
|
|
485
|
+
'winget install "FFmpeg (Essentials Build)"',
|
|
486
|
+
"choco install ffmpeg",
|
|
487
|
+
"scoop install ffmpeg-essentials",
|
|
488
|
+
"\u4E0B\u8F7D\u9875: https://www.gyan.dev/ffmpeg/builds/\uFF08\u89E3\u538B\u540E\u628A bin \u76EE\u5F55\u52A0\u5165 PATH\uFF09"
|
|
489
|
+
];
|
|
490
|
+
default:
|
|
491
|
+
return [
|
|
492
|
+
"apt-get install -y ffmpeg # Debian / Ubuntu",
|
|
493
|
+
"dnf install -y ffmpeg # Fedora / RHEL\uFF08\u9700\u542F\u7528 RPM Fusion\uFF09",
|
|
494
|
+
"apk add ffmpeg # Alpine",
|
|
495
|
+
"pacman -S ffmpeg # Arch",
|
|
496
|
+
"\u4E0B\u8F7D\u9875: https://johnvansickle.com/ffmpeg/\uFF08\u9759\u6001\u6784\u5EFA\uFF0C\u89E3\u538B\u540E\u628A ffmpeg / ffprobe \u653E\u8FDB PATH \u6216 .agent/bin\uFF09"
|
|
497
|
+
];
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
var JVS = "https://johnvansickle.com/ffmpeg/releases";
|
|
501
|
+
var EVERMEET_INFO = "https://evermeet.cx/ffmpeg/info";
|
|
502
|
+
var GYAN = "https://www.gyan.dev/ffmpeg/builds";
|
|
503
|
+
var BTBN = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest";
|
|
504
|
+
function distEntry(name, url, checksumUrl, algo) {
|
|
505
|
+
return { name, url, archive: url.endsWith(".zip") ? "zip" : "tar.xz", checksumUrl, checksumAlgo: algo };
|
|
506
|
+
}
|
|
507
|
+
function distSpecFor(platform, arch) {
|
|
508
|
+
if (platform === "linux") {
|
|
509
|
+
const suffix = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : arch === "arm" ? "armhf" : arch === "ia32" ? "i686" : null;
|
|
510
|
+
if (!suffix) return null;
|
|
511
|
+
return {
|
|
512
|
+
components: [
|
|
513
|
+
distEntry("ffmpeg", `${JVS}/ffmpeg-release-${suffix}-static.tar.xz`, `${JVS}/ffmpeg-release-${suffix}-static.tar.xz.md5`, "md5"),
|
|
514
|
+
distEntry("ffprobe", `${JVS}/ffmpeg-release-${suffix}-static.tar.xz`)
|
|
515
|
+
]
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
if (platform === "darwin") {
|
|
519
|
+
return {
|
|
520
|
+
components: [
|
|
521
|
+
{
|
|
522
|
+
name: "ffmpeg",
|
|
523
|
+
infoUrl: `${EVERMEET_INFO}/ffmpeg/release`,
|
|
524
|
+
archive: "zip"
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "ffprobe",
|
|
528
|
+
infoUrl: `${EVERMEET_INFO}/ffprobe/release`,
|
|
529
|
+
archive: "zip"
|
|
530
|
+
}
|
|
531
|
+
]
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (platform === "win32") {
|
|
535
|
+
if (arch === "x64") {
|
|
536
|
+
const url = `${GYAN}/ffmpeg-release-essentials.zip`;
|
|
537
|
+
return {
|
|
538
|
+
components: [
|
|
539
|
+
distEntry("ffmpeg", url, `${url}.sha256`, "sha256"),
|
|
540
|
+
distEntry("ffprobe", url)
|
|
541
|
+
]
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (arch === "arm64") {
|
|
545
|
+
const url = `${BTBN}/ffmpeg-master-latest-winarm64-gpl.zip`;
|
|
546
|
+
return {
|
|
547
|
+
components: [
|
|
548
|
+
distEntry("ffmpeg", url, `${BTBN}/checksums.sha256`, "sha256"),
|
|
549
|
+
distEntry("ffprobe", url)
|
|
550
|
+
]
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
function parseChecksumFile(text, filename) {
|
|
558
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
559
|
+
for (const line of lines) {
|
|
560
|
+
const m = /^([0-9a-f]{32,128})\s+\*?(.*)$/i.exec(line.trim());
|
|
561
|
+
if (!m) continue;
|
|
562
|
+
const hash = m[1].toLowerCase();
|
|
563
|
+
const name = m[2]?.trim() ?? "";
|
|
564
|
+
if (!name || name === filename || path3.basename(name) === filename) return hash;
|
|
565
|
+
}
|
|
566
|
+
const single = lines[0]?.trim();
|
|
567
|
+
if (lines.length === 1 && single && /^[0-9a-f]{32,128}$/i.test(single)) {
|
|
568
|
+
return single.toLowerCase();
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
async function computeChecksum(file, algo) {
|
|
573
|
+
const hash = createHash(algo);
|
|
574
|
+
await pipeline(fs2.createReadStream(file), hash);
|
|
575
|
+
return hash.digest("hex");
|
|
576
|
+
}
|
|
577
|
+
function fetchInit() {
|
|
578
|
+
return socksAgent ? { agent: socksAgent } : {};
|
|
579
|
+
}
|
|
580
|
+
async function fetchText(url) {
|
|
581
|
+
const resp = await nodeFetch(url, fetchInit());
|
|
582
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
|
|
583
|
+
return resp.text();
|
|
584
|
+
}
|
|
585
|
+
async function downloadToFile(url, dest) {
|
|
586
|
+
const resp = await nodeFetch(url, fetchInit());
|
|
587
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
|
|
588
|
+
if (!resp.body) throw new Error("\u54CD\u5E94\u6CA1\u6709\u5185\u5BB9");
|
|
589
|
+
await pipeline(resp.body, fs2.createWriteStream(dest));
|
|
590
|
+
}
|
|
591
|
+
async function resolveComponentUrl(c) {
|
|
592
|
+
if (c.url) return c.url;
|
|
593
|
+
if (!c.infoUrl) throw new Error(`\u4E0B\u8F7D\u6E90\u7F3A\u5C11 url / infoUrl: ${c.name}`);
|
|
594
|
+
const text = await fetchText(c.infoUrl);
|
|
595
|
+
const json = JSON.parse(text);
|
|
596
|
+
const url = json.download?.zip?.url;
|
|
597
|
+
if (!url) throw new Error(`\u5143\u6570\u636E\u63A5\u53E3\u672A\u7ED9\u51FA zip \u5730\u5740: ${c.infoUrl}`);
|
|
598
|
+
return url;
|
|
599
|
+
}
|
|
600
|
+
function findBinaryInTree(dir, names) {
|
|
601
|
+
const stack = [dir];
|
|
602
|
+
while (stack.length > 0) {
|
|
603
|
+
const cur = stack.pop();
|
|
604
|
+
let entries;
|
|
605
|
+
try {
|
|
606
|
+
entries = fs2.readdirSync(cur, { withFileTypes: true });
|
|
607
|
+
} catch {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
for (const e of entries) {
|
|
611
|
+
const full = path3.join(cur, e.name);
|
|
612
|
+
if (e.isDirectory()) {
|
|
613
|
+
stack.push(full);
|
|
614
|
+
} else if (names.includes(e.name)) {
|
|
615
|
+
return full;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
async function extractArchive(archive, dest, kind, platform) {
|
|
622
|
+
fs2.mkdirSync(dest, { recursive: true });
|
|
623
|
+
let res;
|
|
624
|
+
if (kind === "tar.xz") {
|
|
625
|
+
res = await execAsync("tar", ["-xJf", archive, "-C", dest]);
|
|
626
|
+
} else if (platform === "win32") {
|
|
627
|
+
res = await execAsync(
|
|
628
|
+
"powershell",
|
|
629
|
+
[
|
|
630
|
+
"-NoProfile",
|
|
631
|
+
"-NonInteractive",
|
|
632
|
+
"-Command",
|
|
633
|
+
`Expand-Archive -LiteralPath '${archive.replace(/'/g, "''")}' -DestinationPath '${dest.replace(/'/g, "''")}' -Force`
|
|
634
|
+
]
|
|
635
|
+
);
|
|
636
|
+
} else {
|
|
637
|
+
res = await execAsync("unzip", ["-o", "-q", archive, "-d", dest]);
|
|
638
|
+
}
|
|
639
|
+
if (res.error || res.status !== 0) {
|
|
640
|
+
const detail = res.error?.message ?? decodeOutput(res.stderr).trim();
|
|
641
|
+
return `\u89E3\u5305\u5931\u8D25 (${kind}): ${detail}`;
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
async function downloadFfmpeg(workspace, opts = {}) {
|
|
646
|
+
const platform = opts.platform ?? process.platform;
|
|
647
|
+
const arch = opts.arch ?? process.arch;
|
|
648
|
+
const log = opts.log ?? (() => {
|
|
649
|
+
});
|
|
650
|
+
const notes = [];
|
|
651
|
+
const spec = opts.spec === void 0 ? distSpecFor(platform, arch) : opts.spec;
|
|
652
|
+
if (!spec || spec.components.length === 0) {
|
|
653
|
+
return {
|
|
654
|
+
ok: false,
|
|
655
|
+
error: `\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4E0B\u8F7D\u7684\u5E73\u53F0: ${platform}/${arch}`
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
const tmp = fs2.mkdtempSync(path3.join(os.tmpdir(), "zqagent-ffmpeg-"));
|
|
659
|
+
const binDir = agentBinDir(workspace);
|
|
660
|
+
const installed = {};
|
|
661
|
+
try {
|
|
662
|
+
const fetched = /* @__PURE__ */ new Map();
|
|
663
|
+
for (const c of spec.components) {
|
|
664
|
+
let url;
|
|
665
|
+
try {
|
|
666
|
+
url = await resolveComponentUrl(c);
|
|
667
|
+
} catch (e) {
|
|
668
|
+
return { ok: false, error: `\u83B7\u53D6\u4E0B\u8F7D\u5730\u5740\u5931\u8D25: ${String(e instanceof Error ? e.message : e)}` };
|
|
669
|
+
}
|
|
670
|
+
let archive = fetched.get(url);
|
|
671
|
+
if (!archive) {
|
|
672
|
+
const archivePath = path3.join(tmp, path3.basename(new URL(url).pathname) || `${c.name}.${c.archive}`);
|
|
673
|
+
log(`\u4E0B\u8F7D ${url}`);
|
|
674
|
+
try {
|
|
675
|
+
await downloadToFile(url, archivePath);
|
|
676
|
+
} catch (e) {
|
|
677
|
+
return { ok: false, error: `\u4E0B\u8F7D\u5931\u8D25 (${url}): ${String(e instanceof Error ? e.message : e)}` };
|
|
678
|
+
}
|
|
679
|
+
if (c.checksumUrl) {
|
|
680
|
+
const algo = c.checksumAlgo ?? "sha256";
|
|
681
|
+
try {
|
|
682
|
+
const expected = parseChecksumFile(await fetchText(c.checksumUrl), path3.basename(archivePath));
|
|
683
|
+
if (!expected) {
|
|
684
|
+
notes.push(`\u672A\u80FD\u4ECE ${c.checksumUrl} \u89E3\u6790\u51FA\u6821\u9A8C\u548C\uFF0C\u5DF2\u8DF3\u8FC7\u6821\u9A8C`);
|
|
685
|
+
} else {
|
|
686
|
+
const actual = await computeChecksum(archivePath, algo);
|
|
687
|
+
if (actual !== expected) {
|
|
688
|
+
return {
|
|
689
|
+
ok: false,
|
|
690
|
+
error: `\u6821\u9A8C\u548C\u4E0D\u5339\u914D (${algo}): \u671F\u671B ${expected}\uFF0C\u5B9E\u9645 ${actual}\u3002\u5DF2\u4E2D\u6B62\u5B89\u88C5\u3002`
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
log(`${algo} \u6821\u9A8C\u901A\u8FC7`);
|
|
694
|
+
}
|
|
695
|
+
} catch (e) {
|
|
696
|
+
notes.push(`\u6821\u9A8C\u548C\u83B7\u53D6\u5931\u8D25 (${c.checksumUrl}): ${String(e instanceof Error ? e.message : e)}`);
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
notes.push(`${path3.basename(archivePath)} \u672A\u63D0\u4F9B\u6821\u9A8C\u548C\uFF08\u4EC5 HTTPS \u4F20\u8F93\u4FDD\u62A4\uFF09`);
|
|
700
|
+
}
|
|
701
|
+
archive = { file: archivePath, kind: c.archive };
|
|
702
|
+
fetched.set(url, archive);
|
|
703
|
+
}
|
|
704
|
+
const extractDir = fs2.mkdtempSync(path3.join(tmp, "x-"));
|
|
705
|
+
const err = await extractArchive(archive.file, extractDir, archive.kind, platform);
|
|
706
|
+
if (err) return { ok: false, error: err };
|
|
707
|
+
const found = findBinaryInTree(extractDir, [exeName(c.name, platform), c.name]);
|
|
708
|
+
if (!found) {
|
|
709
|
+
return { ok: false, error: `\u5F52\u6863\u5185\u672A\u627E\u5230 ${c.name}: ${path3.basename(archive.file)}` };
|
|
710
|
+
}
|
|
711
|
+
fs2.mkdirSync(binDir, { recursive: true });
|
|
712
|
+
const target = path3.join(binDir, exeName(c.name, platform));
|
|
713
|
+
const staging = `${target}.tmp-${process.pid}`;
|
|
714
|
+
fs2.copyFileSync(found, staging);
|
|
715
|
+
fs2.chmodSync(staging, 493);
|
|
716
|
+
fs2.renameSync(staging, target);
|
|
717
|
+
installed[c.name] = target;
|
|
718
|
+
}
|
|
719
|
+
resetFfmpegCache();
|
|
720
|
+
notes.push("ffmpeg \u9759\u6001\u6784\u5EFA\u591A\u4E3A GPL/LGPL \u8BB8\u53EF\uFF0C\u968F\u4EA7\u54C1\u5206\u53D1\u524D\u8BF7\u786E\u8BA4\u76F8\u5E94\u7684\u6E90\u7801\u63D0\u4F9B\u4E49\u52A1");
|
|
721
|
+
return { ok: true, installed, notes };
|
|
722
|
+
} catch (e) {
|
|
723
|
+
return { ok: false, error: `\u5B89\u88C5\u5931\u8D25: ${String(e instanceof Error ? e.message : e)}` };
|
|
724
|
+
} finally {
|
|
725
|
+
fs2.rmSync(tmp, { recursive: true, force: true });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function canAutoDownload(platform, arch) {
|
|
729
|
+
return distSpecFor(platform, arch) !== null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/tools/media.ts
|
|
733
|
+
var MAX_OUTPUT = 4e3;
|
|
734
|
+
var TIMEOUT_MS = 600 * 1e3;
|
|
735
|
+
var MAX_BUFFER2 = 1024 * 1024 * 8;
|
|
736
|
+
var NEEDS_INPUT = /* @__PURE__ */ new Set([
|
|
737
|
+
"probe",
|
|
738
|
+
"trim",
|
|
739
|
+
"convert",
|
|
740
|
+
"extract_audio",
|
|
741
|
+
"extract_frame",
|
|
742
|
+
"thumbnail",
|
|
743
|
+
"compress"
|
|
744
|
+
]);
|
|
745
|
+
var NEEDS_OUTPUT = /* @__PURE__ */ new Set([
|
|
746
|
+
"trim",
|
|
747
|
+
"convert",
|
|
748
|
+
"extract_audio",
|
|
749
|
+
"extract_frame",
|
|
750
|
+
"thumbnail",
|
|
751
|
+
"compress"
|
|
752
|
+
]);
|
|
753
|
+
var OPS = [
|
|
754
|
+
"probe",
|
|
755
|
+
"trim",
|
|
756
|
+
"convert",
|
|
757
|
+
"extract_audio",
|
|
758
|
+
"extract_frame",
|
|
759
|
+
"thumbnail",
|
|
760
|
+
"compress",
|
|
761
|
+
"setup"
|
|
762
|
+
];
|
|
763
|
+
var PRESETS = /* @__PURE__ */ new Set([
|
|
764
|
+
"ultrafast",
|
|
765
|
+
"superfast",
|
|
766
|
+
"veryfast",
|
|
767
|
+
"faster",
|
|
768
|
+
"fast",
|
|
769
|
+
"medium",
|
|
770
|
+
"slow",
|
|
771
|
+
"slower",
|
|
772
|
+
"veryslow"
|
|
773
|
+
]);
|
|
774
|
+
function str(v) {
|
|
775
|
+
if (typeof v === "string" && v.trim().length > 0) return v.trim();
|
|
776
|
+
return void 0;
|
|
777
|
+
}
|
|
778
|
+
function finiteNum(v, min, max) {
|
|
779
|
+
const n = typeof v === "string" ? Number(v.trim()) : v;
|
|
780
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n < min || n > max) return null;
|
|
781
|
+
return n;
|
|
782
|
+
}
|
|
783
|
+
function parseTimecode(v) {
|
|
784
|
+
if (typeof v === "number") return Number.isFinite(v) && v >= 0 ? v : null;
|
|
785
|
+
if (typeof v !== "string") return null;
|
|
786
|
+
const t = v.trim();
|
|
787
|
+
if (t === "") return null;
|
|
788
|
+
if (/^\d+(\.\d+)?$/.test(t)) return Number(t);
|
|
789
|
+
const parts = t.split(":");
|
|
790
|
+
if (parts.length < 2 || parts.length > 3) return null;
|
|
791
|
+
let sec = 0;
|
|
792
|
+
for (const p of parts) {
|
|
793
|
+
if (!/^\d+(\.\d+)?$/.test(p)) return null;
|
|
794
|
+
sec = sec * 60 + Number(p);
|
|
795
|
+
}
|
|
796
|
+
return sec;
|
|
797
|
+
}
|
|
798
|
+
function truncateTail(s, max = MAX_OUTPUT) {
|
|
799
|
+
const t = s.trim();
|
|
800
|
+
if (t.length <= max) return t;
|
|
801
|
+
return `\u2026\uFF08\u5DF2\u622A\u65AD\u524D ${t.length - max} \u5B57\u7B26\uFF09
|
|
802
|
+
` + t.slice(-max);
|
|
803
|
+
}
|
|
804
|
+
function scaleFilter(width, height) {
|
|
805
|
+
if (width && height) return `scale=${width}:${height}`;
|
|
806
|
+
if (width) return `scale=${width}:-2`;
|
|
807
|
+
if (height) return `scale=-2:${height}`;
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
function buildFfmpegArgs(op, args, paths) {
|
|
811
|
+
const input = paths.input ?? "";
|
|
812
|
+
const argv = ["-y", "-hide_banner", "-nostdin"];
|
|
813
|
+
const ss = args["ss"] === void 0 ? null : parseTimecode(args["ss"]);
|
|
814
|
+
if (args["ss"] !== void 0 && ss === null) {
|
|
815
|
+
return { error: "\u53C2\u6570 ss \u4E0D\u662F\u5408\u6CD5\u65F6\u95F4\u7801\uFF08\u793A\u4F8B\uFF1A12.5 / 00:12.5 / 00:00:12.5\uFF09" };
|
|
816
|
+
}
|
|
817
|
+
const to = args["to"] === void 0 ? null : parseTimecode(args["to"]);
|
|
818
|
+
if (args["to"] !== void 0 && to === null) {
|
|
819
|
+
return { error: "\u53C2\u6570 to \u4E0D\u662F\u5408\u6CD5\u65F6\u95F4\u7801\uFF08\u793A\u4F8B\uFF1A30 / 00:30 / 00:00:30\uFF09" };
|
|
820
|
+
}
|
|
821
|
+
const duration = args["duration"] === void 0 ? null : finiteNum(args["duration"], 0, 24 * 3600);
|
|
822
|
+
if (args["duration"] !== void 0 && duration === null) {
|
|
823
|
+
return { error: "\u53C2\u6570 duration \u5FC5\u987B\u662F 0\uFF5E86400 \u4E4B\u95F4\u7684\u79D2\u6570" };
|
|
824
|
+
}
|
|
825
|
+
if (to !== null && ss !== null && to <= ss) {
|
|
826
|
+
return { error: `\u53C2\u6570 to (${to}s) \u5FC5\u987B\u5927\u4E8E ss (${ss}s)` };
|
|
827
|
+
}
|
|
828
|
+
const output = paths.output ?? "";
|
|
829
|
+
const format = str(args["format"]);
|
|
830
|
+
switch (op) {
|
|
831
|
+
case "trim": {
|
|
832
|
+
if (ss !== null) argv.push("-ss", String(ss));
|
|
833
|
+
argv.push("-i", input);
|
|
834
|
+
if (duration !== null) {
|
|
835
|
+
argv.push("-t", String(duration));
|
|
836
|
+
} else if (to !== null) {
|
|
837
|
+
argv.push("-t", String(to - (ss ?? 0)));
|
|
838
|
+
}
|
|
839
|
+
argv.push("-c", "copy");
|
|
840
|
+
if (format) argv.push("-f", format);
|
|
841
|
+
argv.push(output);
|
|
842
|
+
return argv;
|
|
843
|
+
}
|
|
844
|
+
case "convert": {
|
|
845
|
+
argv.push("-i", input);
|
|
846
|
+
const vf = scaleFilter(
|
|
847
|
+
args["width"] === void 0 ? null : finiteNum(args["width"], 1, 16384),
|
|
848
|
+
args["height"] === void 0 ? null : finiteNum(args["height"], 1, 16384)
|
|
849
|
+
);
|
|
850
|
+
if (vf) argv.push("-vf", vf);
|
|
851
|
+
const fps = args["fps"] === void 0 ? null : finiteNum(args["fps"], 0.01, 240);
|
|
852
|
+
if (fps !== null) argv.push("-r", String(fps));
|
|
853
|
+
const codec = str(args["codec"]);
|
|
854
|
+
if (codec) argv.push("-c:v", codec);
|
|
855
|
+
const bitrate = str(args["bitrate"]);
|
|
856
|
+
if (bitrate) argv.push("-b:v", bitrate);
|
|
857
|
+
if (format) argv.push("-f", format);
|
|
858
|
+
argv.push(output);
|
|
859
|
+
return argv;
|
|
860
|
+
}
|
|
861
|
+
case "extract_audio": {
|
|
862
|
+
argv.push("-i", input, "-vn");
|
|
863
|
+
const codec = str(args["codec"]);
|
|
864
|
+
if (codec) argv.push("-c:a", codec);
|
|
865
|
+
const bitrate = str(args["bitrate"]);
|
|
866
|
+
if (bitrate) argv.push("-b:a", bitrate);
|
|
867
|
+
if (format) argv.push("-f", format);
|
|
868
|
+
argv.push(output);
|
|
869
|
+
return argv;
|
|
870
|
+
}
|
|
871
|
+
case "extract_frame":
|
|
872
|
+
case "thumbnail": {
|
|
873
|
+
const at = ss ?? 0;
|
|
874
|
+
argv.push("-ss", String(at), "-i", input, "-frames:v", "1");
|
|
875
|
+
const width = args["width"] === void 0 ? null : finiteNum(args["width"], 1, 16384);
|
|
876
|
+
const height = args["height"] === void 0 ? null : finiteNum(args["height"], 1, 16384);
|
|
877
|
+
const vf = scaleFilter(op === "thumbnail" ? width ?? 320 : width, height);
|
|
878
|
+
if (vf) argv.push("-vf", vf);
|
|
879
|
+
if (format) argv.push("-f", format);
|
|
880
|
+
argv.push(output);
|
|
881
|
+
return argv;
|
|
882
|
+
}
|
|
883
|
+
case "compress": {
|
|
884
|
+
const quality = args["quality"] === void 0 ? 28 : finiteNum(args["quality"], 0, 51);
|
|
885
|
+
if (quality === null) return { error: "\u53C2\u6570 quality \u5FC5\u987B\u662F 0\uFF5E51 \u4E4B\u95F4\u7684\u6570\u5B57\uFF08\u8D8A\u5927\u8D8A\u7CCA\u3001\u8D8A\u5C0F\u8D8A\u6E05\u6670\uFF09" };
|
|
886
|
+
const preset = str(args["preset"]) ?? "medium";
|
|
887
|
+
if (!PRESETS.has(preset)) {
|
|
888
|
+
return { error: `\u53C2\u6570 preset \u4E0D\u5408\u6CD5\uFF0C\u53EF\u9009\uFF1A${[...PRESETS].join(" / ")}` };
|
|
889
|
+
}
|
|
890
|
+
argv.push("-i", input);
|
|
891
|
+
const vf = scaleFilter(
|
|
892
|
+
args["width"] === void 0 ? null : finiteNum(args["width"], 1, 16384),
|
|
893
|
+
args["height"] === void 0 ? null : finiteNum(args["height"], 1, 16384)
|
|
894
|
+
);
|
|
895
|
+
if (vf) argv.push("-vf", vf);
|
|
896
|
+
argv.push("-c:v", "libx264", "-crf", String(quality), "-preset", preset, "-c:a", "aac", "-b:a", "128k");
|
|
897
|
+
if (format) argv.push("-f", format);
|
|
898
|
+
argv.push(output);
|
|
899
|
+
return argv;
|
|
900
|
+
}
|
|
901
|
+
default:
|
|
902
|
+
return { error: `\u672A\u77E5\u64CD\u4F5C: ${op}` };
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function buildFfprobeArgs(input) {
|
|
906
|
+
return ["-v", "error", "-print_format", "json", "-show_format", "-show_streams", input];
|
|
907
|
+
}
|
|
908
|
+
function summarizeProbe(raw) {
|
|
909
|
+
const o = raw ?? {};
|
|
910
|
+
const fmt = o["format"] ?? {};
|
|
911
|
+
const num = (v) => {
|
|
912
|
+
const n = Number(v);
|
|
913
|
+
return Number.isFinite(n) ? n : void 0;
|
|
914
|
+
};
|
|
915
|
+
const streams = Array.isArray(o["streams"]) ? o["streams"] : [];
|
|
916
|
+
return {
|
|
917
|
+
format: {
|
|
918
|
+
format_name: fmt["format_name"],
|
|
919
|
+
duration_sec: num(fmt["duration"]),
|
|
920
|
+
size_bytes: num(fmt["size"]),
|
|
921
|
+
bit_rate: num(fmt["bit_rate"])
|
|
922
|
+
},
|
|
923
|
+
streams: streams.map((s) => ({
|
|
924
|
+
index: s["index"],
|
|
925
|
+
type: s["codec_type"],
|
|
926
|
+
codec: s["codec_name"],
|
|
927
|
+
profile: s["profile"],
|
|
928
|
+
width: s["width"],
|
|
929
|
+
height: s["height"],
|
|
930
|
+
fps: s["avg_frame_rate"] ?? s["r_frame_rate"],
|
|
931
|
+
sample_rate: num(s["sample_rate"]),
|
|
932
|
+
channels: s["channels"]
|
|
933
|
+
}))
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function missingGuidance(missing, op, platform, arch) {
|
|
937
|
+
const hints = installHints(platform);
|
|
938
|
+
const howToFix = [];
|
|
939
|
+
if (canAutoDownload(platform, arch)) {
|
|
940
|
+
howToFix.push(
|
|
941
|
+
`\u672C\u5DE5\u5177\u81EA\u52A8\u4E0B\u8F7D\uFF1A\u6539\u7528 op="setup"\uFF08\u4E0B\u8F7D\u5B98\u65B9\u9759\u6001\u6784\u5EFA\u5230 .agent/bin\uFF0C\u9700\u8981\u51FA\u7F51\uFF1B\u4E00\u6B21\u5373\u53EF\uFF0C\u4E4B\u540E\u4E00\u76F4\u53EF\u7528\uFF09\u3002`
|
|
942
|
+
);
|
|
943
|
+
} else {
|
|
944
|
+
howToFix.push(`\u5F53\u524D\u5E73\u53F0 (${platform}/${arch}) \u6CA1\u6709\u81EA\u52A8\u4E0B\u8F7D\u6E90\uFF0C\u8BF7\u7528\u4E0B\u9762\u4E24\u79CD\u65B9\u5F0F\u4E4B\u4E00\u3002`);
|
|
945
|
+
}
|
|
946
|
+
howToFix.push(
|
|
947
|
+
`\u6539\u7528 run_shell \u5B89\u88C5\uFF08\u9700\u7528\u6237\u5BA1\u6279\uFF09\uFF1A\u5148\u6267\u884C ${JSON.stringify(hints[0])}\uFF1B\u8FD9\u53EA\u5728\u672C\u673A\u4EA4\u4E92\u5F0F\u4F7F\u7528\u65F6\u53EF\u884C\uFF0C\u670D\u52A1\u7AEF/Docker \u65E0\u6CD5\u63D0\u6743\u3002`
|
|
948
|
+
);
|
|
949
|
+
howToFix.push(
|
|
950
|
+
`\u7528\u6237\u624B\u5DE5\u5B89\u88C5\u540E\uFF0C\u628A\u53EF\u6267\u884C\u6587\u4EF6\u8DEF\u5F84\u5199\u5165\u73AF\u5883\u53D8\u91CF ZQAGENT_FFMPEG / ZQAGENT_FFPROBE\uFF0C\u6216\u653E\u8FDB <workspace>/.agent/bin\u3002`
|
|
951
|
+
);
|
|
952
|
+
return {
|
|
953
|
+
error: `\u672A\u627E\u5230 ${missing}\uFF0C\u64CD\u4F5C ${op} \u65E0\u6CD5\u6267\u884C\uFF08ffmpeg \u672A\u5B89\u88C5\u6216\u4E0D\u5728\u6240\u6709\u5DF2\u77E5\u4F4D\u7F6E\uFF09\u3002`,
|
|
954
|
+
searched: [
|
|
955
|
+
`\u73AF\u5883\u53D8\u91CF ZQAGENT_${missing.toUpperCase()}`,
|
|
956
|
+
`<workspace>/.agent/bin/${missing}`,
|
|
957
|
+
"SEA \u5355\u6587\u4EF6\u5185\u5D4C\u8D44\u6E90",
|
|
958
|
+
"PATH"
|
|
959
|
+
],
|
|
960
|
+
how_to_fix: howToFix,
|
|
961
|
+
install_commands: hints,
|
|
962
|
+
notes: [
|
|
963
|
+
"run_shell \u7684\u6BCF\u6B21\u8C03\u7528\u4E0D\u4FDD\u7559\u73AF\u5883\u53D8\u91CF\uFF08\u5BFC\u51FA PATH \u53EA\u5728\u5F53\u6B21\u6709\u6548\uFF09\uFF0C\u88C5\u5B8C\u5FC5\u987B\u8BA9\u7528\u6237\u91CD\u542F agent \u8FDB\u7A0B\u624D\u80FD\u88AB\u53D1\u73B0\u3002",
|
|
964
|
+
"ffmpeg \u9759\u6001\u6784\u5EFA\u591A\u4E3A GPL/LGPL \u8BB8\u53EF\uFF0C\u968F\u4EA7\u54C1\u5206\u53D1\u524D\u8BF7\u786E\u8BA4\u6E90\u7801\u63D0\u4F9B\u4E49\u52A1\u3002"
|
|
965
|
+
]
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
async function runBinary(bin, argv, cwd) {
|
|
969
|
+
const res = await execAsync(bin, argv, {
|
|
970
|
+
cwd,
|
|
971
|
+
timeoutMs: TIMEOUT_MS,
|
|
972
|
+
maxBuffer: MAX_BUFFER2
|
|
973
|
+
});
|
|
974
|
+
return {
|
|
975
|
+
status: res.status,
|
|
976
|
+
stdout: decodeOutput(res.stdout),
|
|
977
|
+
stderr: decodeOutput(res.stderr),
|
|
978
|
+
error: res.error ?? void 0,
|
|
979
|
+
timedOut: res.timedOut
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function resolveTarget(workspace, value) {
|
|
983
|
+
const resolved = resolveWithinRoot(workspace, value);
|
|
984
|
+
if (!resolved) {
|
|
985
|
+
return { error: `\u8DEF\u5F84\u4E0D\u5728\u5DE5\u4F5C\u533A\u5185\uFF08\u5DF2\u62D2\u7EDD\uFF09: ${value}` };
|
|
986
|
+
}
|
|
987
|
+
return { abs: resolved.real, root: resolved.root };
|
|
988
|
+
}
|
|
989
|
+
function relOut(workspace, abs) {
|
|
990
|
+
return path4.relative(workspace, abs) || abs;
|
|
991
|
+
}
|
|
992
|
+
function registerMediaTool(registry, workspace, opts = {}) {
|
|
993
|
+
const platform = opts.platform ?? process.platform;
|
|
994
|
+
const arch = process.arch;
|
|
995
|
+
const media = async (args) => {
|
|
996
|
+
const root = path4.resolve(workspace);
|
|
997
|
+
const op = str(args["op"]);
|
|
998
|
+
if (!op) {
|
|
999
|
+
return { error: `\u7F3A\u5C11\u53C2\u6570 op\u3002\u53EF\u9009\u503C\uFF1A${OPS.join(" / ")}` };
|
|
1000
|
+
}
|
|
1001
|
+
if (!OPS.includes(op)) {
|
|
1002
|
+
return { error: `\u672A\u77E5\u64CD\u4F5C ${JSON.stringify(op)}\u3002\u53EF\u9009\u503C\uFF1A${OPS.join(" / ")}` };
|
|
1003
|
+
}
|
|
1004
|
+
const bins = resolveMedia(root, opts);
|
|
1005
|
+
const needed = op === "probe" ? "ffprobe" : "ffmpeg";
|
|
1006
|
+
if (op === "setup") {
|
|
1007
|
+
const already = bins[needed];
|
|
1008
|
+
if (already) {
|
|
1009
|
+
return {
|
|
1010
|
+
ok: true,
|
|
1011
|
+
already_available: true,
|
|
1012
|
+
path: already.path,
|
|
1013
|
+
source: already.source,
|
|
1014
|
+
message: "ffmpeg \u5DF2\u53EF\u7528\uFF0C\u65E0\u9700\u4E0B\u8F7D\u3002"
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
const res = await downloadFfmpeg(root, {});
|
|
1018
|
+
if (!res.ok) {
|
|
1019
|
+
return {
|
|
1020
|
+
...missingGuidance("ffmpeg", "setup", platform, arch),
|
|
1021
|
+
error: `\u81EA\u52A8\u4E0B\u8F7D\u5931\u8D25\uFF1A${res.error ?? "\u672A\u77E5\u539F\u56E0"}`
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
ok: true,
|
|
1026
|
+
installed: res.installed,
|
|
1027
|
+
notes: res.notes,
|
|
1028
|
+
message: "\u5DF2\u5B89\u88C5\u5230 .agent/bin\uFF0C\u540E\u7EED media \u8C03\u7528\u4F1A\u76F4\u63A5\u4F7F\u7528\u5B83\u3002"
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
if (!bins[needed]) {
|
|
1032
|
+
return missingGuidance(needed, op, platform, arch);
|
|
1033
|
+
}
|
|
1034
|
+
let inputAbs;
|
|
1035
|
+
let relRoot = root;
|
|
1036
|
+
if (NEEDS_INPUT.has(op)) {
|
|
1037
|
+
const input = str(args["input"]);
|
|
1038
|
+
if (!input) return { error: `\u64CD\u4F5C ${op} \u9700\u8981\u53C2\u6570 input\uFF08\u5DE5\u4F5C\u533A\u5185\u76F8\u5BF9\u8DEF\u5F84\uFF09` };
|
|
1039
|
+
const r = resolveTarget(root, input);
|
|
1040
|
+
if ("error" in r) return { error: r.error };
|
|
1041
|
+
if (!fs3.existsSync(r.abs) || !fs3.statSync(r.abs).isFile()) {
|
|
1042
|
+
return { error: `\u8F93\u5165\u6587\u4EF6\u4E0D\u5B58\u5728: ${input}` };
|
|
1043
|
+
}
|
|
1044
|
+
inputAbs = r.abs;
|
|
1045
|
+
relRoot = r.root;
|
|
1046
|
+
}
|
|
1047
|
+
let outputAbs;
|
|
1048
|
+
if (NEEDS_OUTPUT.has(op)) {
|
|
1049
|
+
const output = str(args["output"]);
|
|
1050
|
+
if (!output) return { error: `\u64CD\u4F5C ${op} \u9700\u8981\u53C2\u6570 output\uFF08\u5DE5\u4F5C\u533A\u5185\u76F8\u5BF9\u8DEF\u5F84\uFF09` };
|
|
1051
|
+
const r = resolveTarget(root, output);
|
|
1052
|
+
if ("error" in r) return { error: r.error };
|
|
1053
|
+
fs3.mkdirSync(path4.dirname(r.abs), { recursive: true });
|
|
1054
|
+
outputAbs = r.abs;
|
|
1055
|
+
relRoot = r.root;
|
|
1056
|
+
}
|
|
1057
|
+
if (op === "probe") {
|
|
1058
|
+
const out2 = await runBinary(bins.ffprobe.path, buildFfprobeArgs(inputAbs), root);
|
|
1059
|
+
if (out2.error) return { error: `\u6267\u884C ffprobe \u5931\u8D25: ${out2.error.message}` };
|
|
1060
|
+
if (out2.timedOut) {
|
|
1061
|
+
return { error: `ffprobe \u8D85\u65F6\uFF08${TIMEOUT_MS / 1e3}s\uFF09\u540E\u5DF2\u7EC8\u6B62` };
|
|
1062
|
+
}
|
|
1063
|
+
if (out2.status !== 0 && !out2.stdout.trim()) {
|
|
1064
|
+
return { error: `ffprobe \u9000\u51FA\u7801 ${out2.status}`, stderr: truncateTail(out2.stderr) };
|
|
1065
|
+
}
|
|
1066
|
+
try {
|
|
1067
|
+
return {
|
|
1068
|
+
ok: true,
|
|
1069
|
+
input: relOut(relRoot, inputAbs),
|
|
1070
|
+
...summarizeProbe(JSON.parse(out2.stdout))
|
|
1071
|
+
};
|
|
1072
|
+
} catch {
|
|
1073
|
+
return { error: "ffprobe \u8F93\u51FA\u4E0D\u662F\u5408\u6CD5 JSON", stdout: truncateTail(out2.stdout) };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const built = buildFfmpegArgs(op, args, { input: inputAbs, output: outputAbs });
|
|
1077
|
+
if (!Array.isArray(built)) return built;
|
|
1078
|
+
const out = await runBinary(bins.ffmpeg.path, built, root);
|
|
1079
|
+
if (out.error) {
|
|
1080
|
+
return {
|
|
1081
|
+
error: `\u6267\u884C ffmpeg \u5931\u8D25: ${out.error.message}`
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
if (out.timedOut) {
|
|
1085
|
+
return {
|
|
1086
|
+
error: `ffmpeg \u8D85\u8FC7 ${TIMEOUT_MS / 1e3} \u79D2\u672A\u5B8C\u6210\uFF0C\u5DF2\u7EC8\u6B62`,
|
|
1087
|
+
hint: "\u53EF\u8003\u8651\u7528\u66F4\u77ED\u7684\u7247\u6BB5\u6216 gpu \u7F16\u7801"
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
if (out.status !== 0) {
|
|
1091
|
+
return {
|
|
1092
|
+
error: `ffmpeg \u9000\u51FA\u7801 ${out.status}`,
|
|
1093
|
+
stderr: truncateTail(out.stderr)
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
const result = {
|
|
1097
|
+
ok: true,
|
|
1098
|
+
op,
|
|
1099
|
+
output: outputAbs ? relOut(relRoot, outputAbs) : void 0
|
|
1100
|
+
};
|
|
1101
|
+
if (outputAbs) {
|
|
1102
|
+
try {
|
|
1103
|
+
result["size_bytes"] = fs3.statSync(outputAbs).size;
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (out.stderr.trim()) result["ffmpeg_log"] = truncateTail(out.stderr);
|
|
1108
|
+
return result;
|
|
1109
|
+
};
|
|
1110
|
+
registry.register(
|
|
1111
|
+
media,
|
|
1112
|
+
{
|
|
1113
|
+
category: "media",
|
|
1114
|
+
riskLevel: "medium",
|
|
1115
|
+
capabilities: ["ffmpeg \u97F3\u89C6\u9891\u5904\u7406\uFF1A\u63A2\u5143\u6570\u636E\u3001\u88C1\u526A\u3001\u8F6C\u7801\u3001\u62BD\u97F3\u8F68\u3001\u62BD\u5E27\u3001\u7F29\u7565\u56FE\u3001\u538B\u7F29"],
|
|
1116
|
+
// 会调用外部二进制并写文件,且 op="setup" 还会下载安装;一律弹审批
|
|
1117
|
+
requiresApproval: true
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
type: "function",
|
|
1121
|
+
function: {
|
|
1122
|
+
name: "media",
|
|
1123
|
+
description: "\u97F3\u89C6\u9891\u5904\u7406\uFF08\u57FA\u4E8E ffmpeg\uFF09\u3002op \u53D6\u503C\uFF1Aprobe \u63A2\u5143\u6570\u636E\uFF1Btrim \u88C1\u526A\uFF08\u6D41\u62F7\u8D1D\uFF0C\u65E0\u635F\u4F46\u5207\u70B9\u5BF9\u9F50\u5173\u952E\u5E27\uFF09\uFF1Bconvert \u8F6C\u7801\uFF1Bextract_audio \u62BD\u97F3\u8F68\uFF1Bextract_frame \u5728\u6307\u5B9A\u65F6\u95F4\u62BD\u4E00\u5E27\uFF1Bthumbnail \u751F\u6210\u7F29\u7565\u56FE\uFF1Bcompress \u538B\u7F29\uFF08\u9ED8\u8BA4 H.264 CRF 28\uFF09\u3002setup \u7528\u4E8E\u5728\u7F3A\u5C11 ffmpeg \u65F6\u81EA\u52A8\u4E0B\u8F7D\u5B98\u65B9\u9759\u6001\u6784\u5EFA\u5230 .agent/bin\u3002\u65F6\u95F4\u53C2\u6570 ss/to/duration \u63A5\u53D7\u79D2\u6570\u6216 HH:MM:SS\u3002\u8F93\u5165\u8F93\u51FA\u5747\u4E3A\u5DE5\u4F5C\u533A\u5185\u76F8\u5BF9\u8DEF\u5F84\u3002",
|
|
1124
|
+
parameters: {
|
|
1125
|
+
type: "object",
|
|
1126
|
+
properties: {
|
|
1127
|
+
op: {
|
|
1128
|
+
type: "string",
|
|
1129
|
+
enum: [...OPS],
|
|
1130
|
+
description: "\u8981\u6267\u884C\u7684\u64CD\u4F5C"
|
|
1131
|
+
},
|
|
1132
|
+
input: { type: "string", description: "\u8F93\u5165\u6587\u4EF6\u8DEF\u5F84\uFF08\u5DE5\u4F5C\u533A\u5185\u76F8\u5BF9\u8DEF\u5F84\uFF09" },
|
|
1133
|
+
output: { type: "string", description: "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84\uFF08\u5DE5\u4F5C\u533A\u5185\u76F8\u5BF9\u8DEF\u5F84\uFF09" },
|
|
1134
|
+
ss: { type: "string", description: "\u8D77\u59CB\u65F6\u95F4\uFF0C\u5982 12.5 / 00:12.5 / 00:00:12.5" },
|
|
1135
|
+
to: { type: "string", description: "\u7ED3\u675F\u65F6\u95F4\uFF08\u4E0E ss \u914D\u5408\uFF0Ctrim \u7528\uFF09" },
|
|
1136
|
+
duration: { type: "string", description: "\u65F6\u957F\uFF08\u79D2\uFF0Ctrim \u7528\uFF1B\u4E0E to \u4E8C\u9009\u4E00\uFF09" },
|
|
1137
|
+
format: { type: "string", description: "\u8F93\u51FA\u5BB9\u5668\u683C\u5F0F\uFF0C\u5982 mp4 / webm / mp3 / png" },
|
|
1138
|
+
fps: { type: "number", description: "\u8F93\u51FA\u5E27\u7387\uFF08convert\uFF09" },
|
|
1139
|
+
width: { type: "number", description: "\u8F93\u51FA\u5BBD\u5EA6\uFF08\u50CF\u7D20\uFF09" },
|
|
1140
|
+
height: { type: "number", description: "\u8F93\u51FA\u9AD8\u5EA6\uFF08\u50CF\u7D20\uFF09" },
|
|
1141
|
+
bitrate: { type: "string", description: "\u89C6\u9891\u7801\u7387\uFF0C\u5982 2M / 1500k" },
|
|
1142
|
+
codec: { type: "string", description: "\u89C6\u9891\u7F16\u7801\u5668\uFF0C\u5982 libx264 / libvpx-vp9" },
|
|
1143
|
+
quality: { type: "number", description: "compress \u7684 CRF \u503C 0\uFF5E51\uFF0C\u9ED8\u8BA4 28" },
|
|
1144
|
+
preset: { type: "string", description: "compress \u7684 x264 preset\uFF0C\u9ED8\u8BA4 medium" }
|
|
1145
|
+
},
|
|
1146
|
+
required: ["op"]
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
export {
|
|
1153
|
+
buildFfmpegArgs,
|
|
1154
|
+
buildFfprobeArgs,
|
|
1155
|
+
parseTimecode,
|
|
1156
|
+
registerMediaTool,
|
|
1157
|
+
summarizeProbe
|
|
1158
|
+
};
|
|
1159
|
+
//# sourceMappingURL=media.js.map
|