@gitruck/cli 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1892,11 +1892,11 @@ var require_index_umd = __commonJS((exports, module) => {
1892
1892
  };
1893
1893
  function __awaiter(thisArg, _arguments, P, generator) {
1894
1894
  function adopt(value) {
1895
- return value instanceof P ? value : new P(function(resolve3) {
1896
- resolve3(value);
1895
+ return value instanceof P ? value : new P(function(resolve4) {
1896
+ resolve4(value);
1897
1897
  });
1898
1898
  }
1899
- return new (P || (P = Promise))(function(resolve3, reject) {
1899
+ return new (P || (P = Promise))(function(resolve4, reject) {
1900
1900
  function fulfilled(value) {
1901
1901
  try {
1902
1902
  step(generator.next(value));
@@ -1912,7 +1912,7 @@ var require_index_umd = __commonJS((exports, module) => {
1912
1912
  }
1913
1913
  }
1914
1914
  function step(result) {
1915
- result.done ? resolve3(result.value) : adopt(result.value).then(fulfilled, rejected);
1915
+ result.done ? resolve4(result.value) : adopt(result.value).then(fulfilled, rejected);
1916
1916
  }
1917
1917
  step((generator = generator.apply(thisArg, _arguments || [])).next());
1918
1918
  });
@@ -4197,7 +4197,7 @@ var {
4197
4197
 
4198
4198
  // src/index.ts
4199
4199
  import { readFileSync as readFileSync5 } from "node:fs";
4200
- import { join as join26 } from "node:path";
4200
+ import { join as join28 } from "node:path";
4201
4201
 
4202
4202
  // src/lib/paths.ts
4203
4203
  import { dirname, join } from "node:path";
@@ -4246,9 +4246,10 @@ function migrateLegacyHome() {
4246
4246
  }
4247
4247
 
4248
4248
  // src/commands/skills.ts
4249
- import { homedir as homedir2 } from "node:os";
4250
- import { join as join2 } from "node:path";
4249
+ import { spawnSync } from "node:child_process";
4251
4250
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, cpSync as cpSync2 } from "node:fs";
4251
+ import { homedir as homedir2 } from "node:os";
4252
+ import { dirname as dirname2, join as join2, resolve } from "node:path";
4252
4253
 
4253
4254
  // src/lib/log.ts
4254
4255
  var c = {
@@ -4284,40 +4285,248 @@ var SKILL_NAMES = [
4284
4285
  "gtrk-mg",
4285
4286
  "gtrk-ai-drama",
4286
4287
  "gtrk-style-maker",
4287
- "gtrk-tools"
4288
+ "gtrk-transcript",
4289
+ "gtrk-tools",
4290
+ "gtrk-music-visualizer"
4288
4291
  ];
4289
- function installSkill(opts = {}) {
4290
- const destRoot = opts.dir ?? join2(homedir2(), ".claude", "skills");
4292
+ var SUPPLEMENTAL_AGENTS = [
4293
+ {
4294
+ id: "workbuddy",
4295
+ displayName: "WorkBuddy",
4296
+ dataDir: ".workbuddy"
4297
+ },
4298
+ {
4299
+ id: "qoderwork",
4300
+ displayName: "QoderWork",
4301
+ dataDir: ".qoderwork"
4302
+ },
4303
+ {
4304
+ id: "comate",
4305
+ displayName: "Baidu Comate",
4306
+ dataDir: ".comate"
4307
+ }
4308
+ ];
4309
+ var SUPPLEMENTAL_AGENT_IDS = new Set(SUPPLEMENTAL_AGENTS.map((agent) => agent.id));
4310
+ var AGENT_ALIASES = {
4311
+ claude: "claude-code",
4312
+ "trae-global": "trae",
4313
+ "tencent-workbuddy": "workbuddy",
4314
+ "qoder-work": "qoderwork",
4315
+ "baidu-comate": "comate",
4316
+ "wenxin-comate": "comate",
4317
+ qwen: "qwen-code",
4318
+ kimi: "kimi-code-cli",
4319
+ "kimi-code": "kimi-code-cli",
4320
+ iflow: "iflow-cli",
4321
+ codearts: "codearts-agent",
4322
+ "tongyi-lingma": "lingma",
4323
+ "tencent-codebuddy": "codebuddy"
4324
+ };
4325
+ var SAFE_AGENT_ID = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/u;
4326
+ var ANSI_ESCAPE = /\x1B\[[0-?]*[ -/]*[@-~]/gu;
4327
+ var PROMPTSCRIPT_GLOBAL_ERROR = "PromptScript: PromptScript does not support global skill installation";
4328
+ function parseAgentIds(input) {
4329
+ const values = (input ?? "").split(/[\s,]+/u).map((value) => value.trim().toLowerCase()).filter(Boolean).map((value) => AGENT_ALIASES[value] ?? value);
4330
+ const invalid = values.filter((value) => !SAFE_AGENT_ID.test(value));
4331
+ if (invalid.length > 0) {
4332
+ throw new Error(`Agent ID 格式不合法:${invalid.join(", ")}`);
4333
+ }
4334
+ return [...new Set(values)];
4335
+ }
4336
+ function splitAgentSelection(input) {
4337
+ const requested = parseAgentIds(input);
4338
+ return {
4339
+ requested,
4340
+ upstream: requested.filter((id) => !SUPPLEMENTAL_AGENT_IDS.has(id)),
4341
+ supplemental: requested.filter((id) => SUPPLEMENTAL_AGENT_IDS.has(id))
4342
+ };
4343
+ }
4344
+ function resolveSupplementalAgentTargets(opts = {}, home = homedir2(), pathExists = existsSync2) {
4345
+ const selection = splitAgentSelection(opts.agents);
4346
+ const explicitIds = new Set(selection.supplemental);
4347
+ return SUPPLEMENTAL_AGENTS.filter((agent) => {
4348
+ if (opts.all)
4349
+ return true;
4350
+ if (selection.requested.length > 0)
4351
+ return explicitIds.has(agent.id);
4352
+ return pathExists(join2(home, agent.dataDir));
4353
+ }).map((agent) => ({
4354
+ id: agent.id,
4355
+ displayName: agent.displayName,
4356
+ destRoot: join2(home, agent.dataDir, "skills")
4357
+ }));
4358
+ }
4359
+ function plainTerminalLine(line2) {
4360
+ return line2.replace(ANSI_ESCAPE, "").trim();
4361
+ }
4362
+ function filterKnownAdapterOutput(output) {
4363
+ const newline = output.includes(`\r
4364
+ `) ? `\r
4365
+ ` : `
4366
+ `;
4367
+ const lines = output.split(/\r?\n/u);
4368
+ const failureStart = lines.findIndex((line2) => plainTerminalLine(line2).includes("Failed to install"));
4369
+ if (failureStart < 0)
4370
+ return { output, suppressedPromptScriptFailures: 0 };
4371
+ const doneIndex = lines.findIndex((line2, index) => index > failureStart && plainTerminalLine(line2).includes("Done!"));
4372
+ if (doneIndex < 0)
4373
+ return { output, suppressedPromptScriptFailures: 0 };
4374
+ const failureLines = lines.slice(failureStart, doneIndex).map(plainTerminalLine).filter((line2) => line2.includes("✗"));
4375
+ if (failureLines.length === 0 || failureLines.some((line2) => !line2.includes(PROMPTSCRIPT_GLOBAL_ERROR))) {
4376
+ return { output, suppressedPromptScriptFailures: 0 };
4377
+ }
4378
+ let removeStart = failureStart;
4379
+ while (removeStart > 0) {
4380
+ const previous = plainTerminalLine(lines[removeStart - 1] ?? "");
4381
+ if (previous !== "" && previous !== "│" && previous !== "|")
4382
+ break;
4383
+ removeStart -= 1;
4384
+ }
4385
+ let removeEnd = doneIndex + 1;
4386
+ while (removeEnd < lines.length && plainTerminalLine(lines[removeEnd] ?? "") === "") {
4387
+ removeEnd += 1;
4388
+ }
4389
+ return {
4390
+ output: [...lines.slice(0, removeStart), ...lines.slice(removeEnd)].join(newline),
4391
+ suppressedPromptScriptFailures: failureLines.length
4392
+ };
4393
+ }
4394
+ function buildSkillsAdapterArgs(source, opts = {}) {
4395
+ const args = ["-y", "skills", "add", source, "-g", "-y"];
4396
+ if (opts.all) {
4397
+ args.push("--all");
4398
+ } else {
4399
+ for (const agent of splitAgentSelection(opts.agents).upstream) {
4400
+ args.push("--agent", agent);
4401
+ }
4402
+ }
4403
+ if (opts.copy)
4404
+ args.push("--copy");
4405
+ return args;
4406
+ }
4407
+ function npxInvocation(args) {
4408
+ const npmExecPath = process.env.npm_execpath;
4409
+ const candidates = [
4410
+ npmExecPath && join2(dirname2(npmExecPath), "npx-cli.js"),
4411
+ join2(dirname2(process.execPath), "node_modules", "npm", "bin", "npx-cli.js"),
4412
+ resolve(dirname2(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npx-cli.js")
4413
+ ].filter((value) => Boolean(value));
4414
+ const npxCli = candidates.find((value) => existsSync2(value));
4415
+ if (npxCli)
4416
+ return { command: process.execPath, args: [npxCli, ...args], shell: false };
4417
+ return { command: "npx", args, shell: process.platform === "win32" };
4418
+ }
4419
+ function validateBundledSkills(source) {
4291
4420
  let allOk = true;
4292
4421
  for (const name of SKILL_NAMES) {
4293
- const src = join2(packageRoot(), "skills", name);
4294
- if (!existsSync2(join2(src, "SKILL.md"))) {
4295
- log.warn(`找不到打包的 skill 源:${join2(src, "SKILL.md")}(跳过 ${name},不影响命令行)`);
4422
+ const manifest = join2(source, name, "SKILL.md");
4423
+ if (!existsSync2(manifest)) {
4424
+ log.warn(`找不到打包的 skill 源:${manifest}(跳过 ${name},不影响命令行)`);
4296
4425
  allOk = false;
4297
- continue;
4298
4426
  }
4427
+ }
4428
+ return allOk;
4429
+ }
4430
+ function copySkillsToTarget(dir, source, targetLabel) {
4431
+ const destRoot = resolve(dir);
4432
+ let allOk = validateBundledSkills(source);
4433
+ for (const name of SKILL_NAMES) {
4434
+ const src = join2(source, name);
4435
+ if (!existsSync2(join2(src, "SKILL.md")))
4436
+ continue;
4299
4437
  const dest = join2(destRoot, name);
4300
4438
  try {
4301
4439
  mkdirSync2(dest, { recursive: true });
4302
4440
  cpSync2(src, dest, { recursive: true });
4303
- log.ok(`已安装 /${name} → ${join2(dest, "SKILL.md")}`);
4304
- } catch (e) {
4441
+ log.ok(`已安装 ${name}${targetLabel ? ` → ${targetLabel}` : ""}:${join2(dest, "SKILL.md")}`);
4442
+ } catch (error) {
4305
4443
  allOk = false;
4306
- log.warn(`skill 安装失败(${name},不影响命令行使用):${e instanceof Error ? e.message : String(e)}`);
4444
+ log.warn(`skill 安装失败(${name},不影响命令行使用):${error instanceof Error ? error.message : String(error)}`);
4307
4445
  }
4308
4446
  }
4309
- log.info("在 Claude Code 里打 /gtrk-oralcut、/gtrk-splitter 或 /gtrk-style-maker,也可直接说「帮我剪个口播 / 拆个分镜 / 造我栏目的风格 skill」触发(可能需重载会话)。");
4447
+ if (targetLabel) {
4448
+ log.info(`已写入 ${targetLabel};若当前会话未出现新 Skill,请重启或刷新该 Agent。`);
4449
+ } else {
4450
+ log.info("已写入自定义 skills 目录;具体调用入口以该 Agent 的界面为准。");
4451
+ }
4310
4452
  return allOk;
4311
4453
  }
4454
+ function copySkillsToDirectory(dir, source = join2(packageRoot(), "skills")) {
4455
+ return copySkillsToTarget(dir, source);
4456
+ }
4457
+ function installSkill(opts = {}) {
4458
+ const source = resolve(opts.source ?? join2(packageRoot(), "skills"));
4459
+ if (opts.dir)
4460
+ return copySkillsToDirectory(opts.dir, source);
4461
+ let selection;
4462
+ try {
4463
+ selection = splitAgentSelection(opts.agents);
4464
+ } catch (error) {
4465
+ log.err(error instanceof Error ? error.message : String(error));
4466
+ return false;
4467
+ }
4468
+ const sourcesOk = validateBundledSkills(source);
4469
+ const shouldRunAdapter = opts.all || selection.requested.length === 0 || selection.upstream.length > 0;
4470
+ let adapterOk = true;
4471
+ if (shouldRunAdapter) {
4472
+ const args = buildSkillsAdapterArgs(source, {
4473
+ ...opts,
4474
+ agents: selection.upstream.join(",")
4475
+ });
4476
+ log.info("使用通用 Agent Skills 适配器:自动探测宿主、统一存储,并链接到各 Agent。");
4477
+ const invocation = npxInvocation(args);
4478
+ const result = spawnSync(invocation.command, invocation.args, {
4479
+ stdio: ["inherit", "pipe", "pipe"],
4480
+ shell: invocation.shell,
4481
+ encoding: "utf8",
4482
+ maxBuffer: 16 * 1024 * 1024
4483
+ });
4484
+ const defaultAutoInstall = !opts.all && selection.requested.length === 0;
4485
+ const filtered = defaultAutoInstall ? filterKnownAdapterOutput(result.stdout ?? "") : { output: result.stdout ?? "", suppressedPromptScriptFailures: 0 };
4486
+ if (filtered.output) {
4487
+ process.stdout.write(filtered.output);
4488
+ if (!filtered.output.endsWith(`
4489
+ `))
4490
+ process.stdout.write(`
4491
+ `);
4492
+ }
4493
+ if (result.stderr)
4494
+ process.stderr.write(result.stderr);
4495
+ if (filtered.suppressedPromptScriptFailures > 0) {
4496
+ log.info("已跳过 PromptScript:它只支持项目级 Skill,不参与本次全局安装;其他 Agent 不受影响。");
4497
+ }
4498
+ if (result.error) {
4499
+ adapterOk = false;
4500
+ log.warn(`无法启动 skills 适配器:${result.error.message}`);
4501
+ log.info(`可手动重试:npx -y skills add "${source}" -g -y`);
4502
+ } else if (result.status !== 0) {
4503
+ adapterOk = false;
4504
+ log.warn(`skills 适配器安装失败(退出码 ${result.status ?? "未知"})。`);
4505
+ log.info(`可查看支持的 Agent ID:npx -y skills add "${source}" --list`);
4506
+ }
4507
+ }
4508
+ let supplementalOk = true;
4509
+ const supplementalTargets = resolveSupplementalAgentTargets(opts, opts.home ?? homedir2());
4510
+ for (const target of supplementalTargets) {
4511
+ log.info(`使用 gtrk 补充适配:${target.displayName} → ${target.destRoot}`);
4512
+ if (!copySkillsToTarget(target.destRoot, source, target.displayName))
4513
+ supplementalOk = false;
4514
+ }
4515
+ if (adapterOk && supplementalOk) {
4516
+ log.info("若当前会话没有立刻出现新 skill,请刷新窗口或新开一个会话。");
4517
+ }
4518
+ return sourcesOk && adapterOk && supplementalOk;
4519
+ }
4312
4520
  function registerSkills(program2) {
4313
- const skills = program2.command("skills").description("管理 agent skill(安装到 Claude Code)");
4314
- skills.command("install").description("把 gtrk 全家框架 skill 安装到 ~/.claude/skills(对标飞书 skills add)").option("--dir <dir>", "自定义 skills 目录(缺省 ~/.claude/skills)").action((opts) => {
4315
- installSkill({ dir: opts.dir });
4521
+ const skills = program2.command("skills").description("管理跨 Agent Skills(通用适配器 + gtrk 补充宿主)");
4522
+ skills.command("install").description("把 gtrk 全家框架 skill 安装到自动检测或指定的 Agent").option("--agents <list>", "指定 Agent ID,逗号分隔,如 codex,trae-cn,workbuddy,comate").option("--all", "安装到上游和 gtrk 已登记的全部 Agent").option("--copy", "每个 Agent 各复制一份(默认统一存储 + symlink/junction)").option("--dir <dir>", "兼容模式:直接复制到单个 skills 目录,绕过 Agent 适配器").action((opts) => {
4523
+ if (!installSkill(opts))
4524
+ process.exitCode = 1;
4316
4525
  });
4317
4526
  }
4318
4527
 
4319
4528
  // src/commands/init.ts
4320
- import { join as join9, resolve as resolve2 } from "node:path";
4529
+ import { join as join9, resolve as resolve3 } from "node:path";
4321
4530
  import { existsSync as existsSync8 } from "node:fs";
4322
4531
 
4323
4532
  // src/lib/user-config.ts
@@ -4345,7 +4554,7 @@ function writeUserConfig(patch) {
4345
4554
  }
4346
4555
 
4347
4556
  // src/lib/jianying.ts
4348
- import { join as join4, resolve } from "node:path";
4557
+ import { join as join4, resolve as resolve2 } from "node:path";
4349
4558
  import { existsSync as existsSync4 } from "node:fs";
4350
4559
  function probeJianyingDraftDir() {
4351
4560
  const local = process.env.LOCALAPPDATA;
@@ -4359,7 +4568,7 @@ function probeJianyingDraftDir() {
4359
4568
  }
4360
4569
  function resolveJianyingDraftDir(opt) {
4361
4570
  if (opt && opt !== "auto")
4362
- return resolve(opt);
4571
+ return resolve2(opt);
4363
4572
  const saved = readUserConfig().jianyingDraftDir;
4364
4573
  if (saved && existsSync4(saved))
4365
4574
  return saved;
@@ -4386,7 +4595,7 @@ function launch(cmd) {
4386
4595
 
4387
4596
  // src/lib/prompt.ts
4388
4597
  import { stdin, stdout } from "node:process";
4389
- import { spawnSync } from "node:child_process";
4598
+ import { spawnSync as spawnSync2 } from "node:child_process";
4390
4599
  var c2 = {
4391
4600
  dim: (s) => `\x1B[2m${s}\x1B[0m`,
4392
4601
  cyan: (s) => `\x1B[36m${s}\x1B[0m`
@@ -4394,24 +4603,24 @@ var c2 = {
4394
4603
  function readClipboard() {
4395
4604
  try {
4396
4605
  if (process.platform === "win32") {
4397
- const r = spawnSync("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], {
4606
+ const r = spawnSync2("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], {
4398
4607
  encoding: "utf8"
4399
4608
  });
4400
4609
  return (r.stdout ?? "").replace(/\r?\n$/, "");
4401
4610
  }
4402
4611
  if (process.platform === "darwin") {
4403
- return spawnSync("pbpaste", { encoding: "utf8" }).stdout ?? "";
4612
+ return spawnSync2("pbpaste", { encoding: "utf8" }).stdout ?? "";
4404
4613
  }
4405
- const x = spawnSync("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf8" });
4614
+ const x = spawnSync2("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf8" });
4406
4615
  if (x.status === 0)
4407
4616
  return x.stdout ?? "";
4408
- return spawnSync("wl-paste", ["-n"], { encoding: "utf8" }).stdout ?? "";
4617
+ return spawnSync2("wl-paste", ["-n"], { encoding: "utf8" }).stdout ?? "";
4409
4618
  } catch {
4410
4619
  return "";
4411
4620
  }
4412
4621
  }
4413
4622
  function ask(message, opts = {}) {
4414
- return new Promise((resolve2) => {
4623
+ return new Promise((resolve3) => {
4415
4624
  const hint = opts.defaultValue ? c2.dim(` (${opts.defaultValue})`) : "";
4416
4625
  stdout.write(`${c2.cyan("?")} ${message}${hint} `);
4417
4626
  let value = "";
@@ -4432,7 +4641,7 @@ function ask(message, opts = {}) {
4432
4641
  stdout.write(`
4433
4642
  `);
4434
4643
  cleanup();
4435
- resolve2(value || opts.defaultValue || "");
4644
+ resolve3(value || opts.defaultValue || "");
4436
4645
  return;
4437
4646
  }
4438
4647
  if (ch === "\x03") {
@@ -5139,7 +5348,7 @@ function effectiveVocab(config) {
5139
5348
  }
5140
5349
 
5141
5350
  // src/lib/ffmpeg.ts
5142
- import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
5351
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
5143
5352
  import { existsSync as existsSync6 } from "node:fs";
5144
5353
  import { join as join6 } from "node:path";
5145
5354
  var isWin = process.platform === "win32";
@@ -5148,7 +5357,7 @@ var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。请把二者放到 ${ffmpe
5148
5357
  var _cache = new Map;
5149
5358
  function onSystemPath(cmd) {
5150
5359
  try {
5151
- return spawnSync2(cmd, ["-version"], { stdio: "ignore" }).status === 0;
5360
+ return spawnSync3(cmd, ["-version"], { stdio: "ignore" }).status === 0;
5152
5361
  } catch {
5153
5362
  return false;
5154
5363
  }
@@ -5183,14 +5392,14 @@ function requireFfmpeg(ffmpegPath) {
5183
5392
  return r;
5184
5393
  }
5185
5394
  function ffprobeJson(ffprobePath, args) {
5186
- const r = spawnSync2(ffprobePath, args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
5395
+ const r = spawnSync3(ffprobePath, args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
5187
5396
  if (r.status !== 0) {
5188
5397
  throw new Error(`ffprobe 失败(code=${r.status}):${(r.stderr || "").slice(-300)}`);
5189
5398
  }
5190
5399
  return JSON.parse(r.stdout || "{}");
5191
5400
  }
5192
5401
  function runFfmpeg(ffmpegPath, args, onLine) {
5193
- return new Promise((resolve2, reject) => {
5402
+ return new Promise((resolve3, reject) => {
5194
5403
  const p = spawn2(ffmpegPath, args, { env: process.env });
5195
5404
  let tail = "";
5196
5405
  p.stderr.on("data", (buf) => {
@@ -5205,20 +5414,20 @@ function runFfmpeg(ffmpegPath, args, onLine) {
5205
5414
  p.on("error", (e) => reject(e));
5206
5415
  p.on("close", (code) => {
5207
5416
  if (code === 0)
5208
- resolve2();
5417
+ resolve3();
5209
5418
  else
5210
5419
  reject(new Error(`ffmpeg 退出码 ${code}:${tail.slice(-600)}`));
5211
5420
  });
5212
5421
  });
5213
5422
  }
5214
5423
  function probeCapabilities(res) {
5215
- const ver = spawnSync2(res.ffmpeg, ["-version"], { encoding: "utf8" });
5424
+ const ver = spawnSync3(res.ffmpeg, ["-version"], { encoding: "utf8" });
5216
5425
  const version = (ver.stdout || "").split(/\r?\n/)[0]?.trim() || "unknown";
5217
- const enc = spawnSync2(res.ffmpeg, ["-hide_banner", "-encoders"], {
5426
+ const enc = spawnSync3(res.ffmpeg, ["-hide_banner", "-encoders"], {
5218
5427
  encoding: "utf8",
5219
5428
  maxBuffer: 16 * 1024 * 1024
5220
5429
  });
5221
- const fil = spawnSync2(res.ffmpeg, ["-hide_banner", "-filters"], {
5430
+ const fil = spawnSync3(res.ffmpeg, ["-hide_banner", "-filters"], {
5222
5431
  encoding: "utf8",
5223
5432
  maxBuffer: 16 * 1024 * 1024
5224
5433
  });
@@ -5452,7 +5661,7 @@ async function runInit(opts) {
5452
5661
  const manual = (await promptText("剪映草稿根目录(…\\com.lveditor.draft),留空跳过:")).trim();
5453
5662
  if (manual) {
5454
5663
  if (existsSync8(manual))
5455
- jianyingDraftDir = resolve2(manual);
5664
+ jianyingDraftDir = resolve3(manual);
5456
5665
  else
5457
5666
  log.warn(`目录不存在,已跳过:${manual}`);
5458
5667
  }
@@ -5475,7 +5684,7 @@ async function afterConfigDoctor() {
5475
5684
  if (healthy) {
5476
5685
  log.step("装好了!两种用法任选:");
5477
5686
  log.info('① 命令行直接剪:gtrk oralcut "<毛片.mp4>" --script "<文字稿.txt>"(无稿就别加 --script)');
5478
- log.info("② 重启你常用的 AI agent(Claude / Codex / Trae / WorkBuddy 等),用 /gtrk-oralcut <你的口播剪辑需求>,一句话交给它,体验更智能的剪辑~");
5687
+ log.info("② 刷新你常用的 AI Agent,在它的 Skills 入口选择或点名 gtrk-oralcut;不同客户端也都可以直接描述剪辑需求触发。");
5479
5688
  log.info("想有自己栏目的风格体系?在 agent 里跑 /gtrk-style-maker 建一次「你的厨房」(skill 家族 + 栏目配置);不建就直接用默认,照常开剪。");
5480
5689
  } else {
5481
5690
  log.warn("上面体检有项没通,按提示处理好再开剪(多半是 API Key 或剪映目录)。");
@@ -5493,7 +5702,7 @@ async function runInitNonInteractive(opts) {
5493
5702
  let jianyingDraftDir;
5494
5703
  const dirOpt = opts.jianyingDraftDir;
5495
5704
  if (dirOpt && dirOpt !== "auto")
5496
- jianyingDraftDir = resolve2(dirOpt);
5705
+ jianyingDraftDir = resolve3(dirOpt);
5497
5706
  else if (dirOpt === "auto" || !existing.jianyingDraftDir)
5498
5707
  jianyingDraftDir = probeJianyingDraftDir();
5499
5708
  else
@@ -5509,16 +5718,23 @@ async function runInitNonInteractive(opts) {
5509
5718
 
5510
5719
  // src/commands/install.ts
5511
5720
  function registerInstall(program2) {
5512
- program2.command("install").description("一条命令装全:安装 /gtrk-oralcut skill + 配置(对标飞书 lark-cli install)").option("--api-key <key>", "非交互:直接指定 API Key").option("--api-base <url>", "非交互:指定 API 根地址").option("--jianying-draft-dir <dir>", "非交互:剪映草稿目录(传 auto 则自动探测)").option("--skills-dir <dir>", "自定义 skills 目录(缺省 ~/.claude/skills)").option("--reconfigure", "重走配置向导(默认:已配过则保留现有配置、只刷新 skill)").option("-y, --yes", "非交互:用传入值 + 自动探测,不弹任何提示").action(async (opts) => {
5721
+ program2.command("install").description("一条命令装全:安装 /gtrk-oralcut skill + 配置(对标飞书 lark-cli install)").option("--api-key <key>", "非交互:直接指定 API Key").option("--api-base <url>", "非交互:指定 API 根地址").option("--jianying-draft-dir <dir>", "非交互:剪映草稿目录(传 auto 则自动探测)").option("--skills-dir <dir>", "自定义单个 skills 目录(优先于 Agent 自动检测)").option("--skill-agents <list>", "skills CLI Agent ID,逗号分隔,如 codex,cursor,trae-cn").option("--all-agents", "把 skill 安装到上游和 gtrk 已登记的全部 Agent").option("--copy-skills", "每个 Agent 各复制一份(默认统一存储 + symlink/junction)").option("--reconfigure", "重走配置向导(默认:已配过则保留现有配置、只刷新 skill)").option("-y, --yes", "非交互:用传入值 + 自动探测,不弹任何提示").action(async (opts) => {
5513
5722
  log.step("① 安装 / 刷新 agent skill…");
5514
- installSkill({ dir: opts.skillsDir });
5723
+ const skillsOk = installSkill({
5724
+ dir: opts.skillsDir,
5725
+ agents: opts.skillAgents,
5726
+ all: opts.allAgents,
5727
+ copy: opts.copySkills
5728
+ });
5729
+ if (!skillsOk)
5730
+ process.exitCode = 1;
5515
5731
  log.step("② 配置 + 体检…");
5516
5732
  await runInit(opts);
5517
5733
  });
5518
5734
  }
5519
5735
 
5520
5736
  // src/commands/oralcut.ts
5521
- import { resolve as resolve3, join as join14, dirname as dirname2, basename as basename5, extname as extname2 } from "node:path";
5737
+ import { resolve as resolve4, join as join14, dirname as dirname3, basename as basename5, extname as extname2 } from "node:path";
5522
5738
  import { mkdir as mkdir4, writeFile as writeFile5, readFile as readFile3 } from "node:fs/promises";
5523
5739
  import { existsSync as existsSync12 } from "node:fs";
5524
5740
 
@@ -5548,6 +5764,10 @@ class CloudError extends Error {
5548
5764
  this.name = "CloudError";
5549
5765
  }
5550
5766
  }
5767
+ function cloudErrorCode(error) {
5768
+ const code = error && typeof error === "object" ? error.code : undefined;
5769
+ return typeof code === "number" ? code : undefined;
5770
+ }
5551
5771
  async function parseJson(res) {
5552
5772
  try {
5553
5773
  return await res.json();
@@ -5555,38 +5775,58 @@ async function parseJson(res) {
5555
5775
  throw new Error(`服务响应解析失败 (HTTP ${res.status})`);
5556
5776
  }
5557
5777
  }
5558
- async function uploadFile(cfg, path) {
5559
- const size = (await stat(path)).size;
5560
- const boundary = `----gtrkFormBoundary${randomBytes(16).toString("hex")}`;
5561
- const head = Buffer.from(`--${boundary}\r
5778
+ function globalBunFile() {
5779
+ const bun = globalThis.Bun;
5780
+ return bun ? bun.file.bind(bun) : undefined;
5781
+ }
5782
+ async function uploadFile(cfg, path, runtime = {}) {
5783
+ const fetchFn = runtime.fetchFn ?? fetch;
5784
+ const bunFile = runtime.bunFile ?? globalBunFile();
5785
+ const useBun = runtime.runtime ? runtime.runtime === "bun" : bunFile != null;
5786
+ let res;
5787
+ if (useBun) {
5788
+ if (!bunFile)
5789
+ throw new Error("Bun 上传运行时缺少 Bun.file");
5790
+ const form = new FormData;
5791
+ form.append("file", bunFile(path), basename(path));
5792
+ res = await fetchFn(`${cfg.base}/base/file/upload`, {
5793
+ method: "POST",
5794
+ headers: { Authorization: cfg.apiKey },
5795
+ body: form
5796
+ });
5797
+ } else {
5798
+ const size = (await stat(path)).size;
5799
+ const boundary = `----gtrkFormBoundary${randomBytes(16).toString("hex")}`;
5800
+ const head = Buffer.from(`--${boundary}\r
5562
5801
  ` + `Content-Disposition: form-data; name="file"; filename="${basename(path)}"\r
5563
5802
  ` + `Content-Type: application/octet-stream\r
5564
5803
  \r
5565
5804
  `, "utf8");
5566
- const tail = Buffer.from(`\r
5805
+ const tail = Buffer.from(`\r
5567
5806
  --${boundary}--\r
5568
5807
  `, "utf8");
5569
- async function* multipart() {
5570
- yield head;
5571
- for await (const chunk of createReadStream(path))
5572
- yield chunk;
5573
- yield tail;
5808
+ async function* multipart() {
5809
+ yield head;
5810
+ for await (const chunk of createReadStream(path))
5811
+ yield chunk;
5812
+ yield tail;
5813
+ }
5814
+ res = await fetchFn(`${cfg.base}/base/file/upload`, {
5815
+ method: "POST",
5816
+ headers: {
5817
+ Authorization: cfg.apiKey,
5818
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
5819
+ "Content-Length": String(head.length + size + tail.length)
5820
+ },
5821
+ body: Readable.toWeb(Readable.from(multipart())),
5822
+ duplex: "half"
5823
+ });
5574
5824
  }
5575
- const res = await fetch(`${cfg.base}/base/file/upload`, {
5576
- method: "POST",
5577
- headers: {
5578
- Authorization: cfg.apiKey,
5579
- "Content-Type": `multipart/form-data; boundary=${boundary}`,
5580
- "Content-Length": String(head.length + size + tail.length)
5581
- },
5582
- body: Readable.toWeb(Readable.from(multipart())),
5583
- duplex: "half"
5584
- });
5585
5825
  const r = await parseJson(res);
5586
5826
  const fid = r.data?.file_id ?? r.data?.id;
5587
5827
  if (r.code === 200 && fid)
5588
5828
  return String(fid);
5589
- throw new Error(`上传失败 (code=${r.code ?? "?"}):${r.msg ?? "未知错误"}`);
5829
+ throw new CloudError(r.code, `上传失败 (code=${r.code ?? "?"}):${r.msg ?? "未知错误"}`);
5590
5830
  }
5591
5831
  async function submitTask(cfg, taskType, payload) {
5592
5832
  const res = await fetch(`${cfg.base}/task/${taskType}`, {
@@ -5597,7 +5837,7 @@ async function submitTask(cfg, taskType, payload) {
5597
5837
  const r = await parseJson(res);
5598
5838
  if (r.code === 200 && r.data?.task_id)
5599
5839
  return String(r.data.task_id);
5600
- throw new Error(`提交失败 (code=${r.code ?? "?"}):${r.msg ?? "未知错误"}`);
5840
+ throw new CloudError(r.code, `提交失败 (code=${r.code ?? "?"}):${r.msg ?? "未知错误"}`);
5601
5841
  }
5602
5842
  async function getTaskResult(cfg, taskType, taskId) {
5603
5843
  const res = await fetch(`${cfg.base}/task/${taskType}/${taskId}`, {
@@ -5885,10 +6125,12 @@ async function putPart(cfg, uploadId, idx, view) {
5885
6125
  var CACHE_DIR = gitruckHome();
5886
6126
  var CACHE_FILE = join10(CACHE_DIR, "upload-cache.json");
5887
6127
  var SESSION_FILE = join10(CACHE_DIR, "upload-sessions.json");
5888
- async function fingerprint(path) {
5889
- const s = await stat3(path);
6128
+ function fingerprintFromStat(s) {
5890
6129
  return `${s.size}:${Math.round(s.mtimeMs)}`;
5891
6130
  }
6131
+ async function fingerprint(path) {
6132
+ return fingerprintFromStat(await stat3(path));
6133
+ }
5892
6134
  async function load() {
5893
6135
  if (!existsSync9(CACHE_FILE))
5894
6136
  return {};
@@ -5902,6 +6144,12 @@ async function save(cache) {
5902
6144
  await mkdir(CACHE_DIR, { recursive: true });
5903
6145
  await writeFile2(CACHE_FILE, JSON.stringify(cache, null, 2));
5904
6146
  }
6147
+ var defaultUploadCacheDeps = {
6148
+ stat: stat3,
6149
+ uploadFile,
6150
+ uploadChunked,
6151
+ cacheStore: { load, save }
6152
+ };
5905
6153
  async function invalidateUpload(path) {
5906
6154
  const fp = await fingerprint(path);
5907
6155
  const cache = await load();
@@ -5940,19 +6188,22 @@ var fileSessionStore = {
5940
6188
  }
5941
6189
  }
5942
6190
  };
5943
- async function uploadCached(cfg, path, opts) {
5944
- const fp = await fingerprint(path);
5945
- const cache = await load();
6191
+ async function uploadCached(cfg, path, opts, deps = defaultUploadCacheDeps) {
6192
+ const s0 = await deps.stat(path);
6193
+ const fp = fingerprintFromStat(s0);
6194
+ const cache = await deps.cacheStore.load();
5946
6195
  const hit = cache[fp]?.fileId;
5947
6196
  if (!opts?.force && hit)
5948
6197
  return { fileId: hit, cached: true };
5949
- const s0 = await stat3(path);
5950
- const fileId = s0.size >= CHUNK_THRESHOLD ? await uploadChunked(cfg, path, {
6198
+ const fileId = s0.size >= CHUNK_THRESHOLD ? await deps.uploadChunked(cfg, path, {
5951
6199
  fingerprint: fp,
5952
6200
  store: fileSessionStore,
5953
6201
  force: opts?.force
5954
- }) : await uploadFile(cfg, path);
5955
- const s = await stat3(path);
6202
+ }) : await deps.uploadFile(cfg, path);
6203
+ const s = await deps.stat(path);
6204
+ if (s.size !== s0.size || Math.round(s.mtimeMs) !== Math.round(s0.mtimeMs)) {
6205
+ throw new Error("上传过程中输入文件发生变化,请等待文件写入完成后重试");
6206
+ }
5956
6207
  cache[fp] = {
5957
6208
  fileId,
5958
6209
  size: s.size,
@@ -5960,10 +6211,50 @@ async function uploadCached(cfg, path, opts) {
5960
6211
  path,
5961
6212
  uploadedAt: Date.now()
5962
6213
  };
5963
- await save(cache);
6214
+ await deps.cacheStore.save(cache);
5964
6215
  return { fileId, cached: false };
5965
6216
  }
5966
6217
 
6218
+ // src/lib/upload-submit.ts
6219
+ var MATERIAL_NOT_FOUND = 6004;
6220
+ var DEFAULT_VISIBILITY_BACKOFF_MS = [250, 750, 1500, 3000];
6221
+ var defaultDeps = {
6222
+ uploadCached,
6223
+ invalidateUpload,
6224
+ submitTask,
6225
+ sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms))
6226
+ };
6227
+ async function submitFreshFile(cfg, taskType, fileId, buildPayload, backoffMs, deps) {
6228
+ for (let attempt = 0;; attempt++) {
6229
+ try {
6230
+ return await deps.submitTask(cfg, taskType, buildPayload(fileId));
6231
+ } catch (error) {
6232
+ if (cloudErrorCode(error) !== MATERIAL_NOT_FOUND || attempt >= backoffMs.length)
6233
+ throw error;
6234
+ await deps.sleep(backoffMs[attempt]);
6235
+ }
6236
+ }
6237
+ }
6238
+ async function uploadAndSubmitTask(cfg, path, taskType, buildPayload, options = {}, deps = defaultDeps) {
6239
+ let uploaded = await deps.uploadCached(cfg, path, { force: options.force });
6240
+ options.onUploaded?.(uploaded);
6241
+ const backoffMs = options.visibilityBackoffMs ?? DEFAULT_VISIBILITY_BACKOFF_MS;
6242
+ if (uploaded.cached) {
6243
+ try {
6244
+ const taskId2 = await deps.submitTask(cfg, taskType, buildPayload(uploaded.fileId));
6245
+ return { taskId: taskId2, fileId: uploaded.fileId, cached: true };
6246
+ } catch (error) {
6247
+ if (cloudErrorCode(error) !== MATERIAL_NOT_FOUND)
6248
+ throw error;
6249
+ options.onCacheInvalid?.();
6250
+ await deps.invalidateUpload(path);
6251
+ uploaded = await deps.uploadCached(cfg, path, { force: true });
6252
+ }
6253
+ }
6254
+ const taskId = await submitFreshFile(cfg, taskType, uploaded.fileId, buildPayload, backoffMs, deps);
6255
+ return { taskId, fileId: uploaded.fileId, cached: false };
6256
+ }
6257
+
5967
6258
  // src/lib/media.ts
5968
6259
  import { mkdir as mkdir2, stat as stat4 } from "node:fs/promises";
5969
6260
  import { existsSync as existsSync10 } from "node:fs";
@@ -6456,7 +6747,7 @@ async function runOralCut(input, opts) {
6456
6747
  if (opts.json)
6457
6748
  routeLogsToStderr();
6458
6749
  const cfg = loadConfig();
6459
- const inputAbs = resolve3(input);
6750
+ const inputAbs = resolve4(input);
6460
6751
  if (!existsSync12(inputAbs))
6461
6752
  throw new Error(`毛片不存在:${inputAbs}`);
6462
6753
  const projName = basename5(inputAbs, extname2(inputAbs));
@@ -6464,10 +6755,10 @@ async function runOralCut(input, opts) {
6464
6755
  if (opts.render && !formats.includes("gtrk"))
6465
6756
  formats.push("gtrk");
6466
6757
  const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
6467
- const outDir = resolve3(opts.out ?? join14(dirname2(inputAbs), `${projName}-video-project-${timestamp()}`));
6468
- let scriptPath = opts.script ? resolve3(opts.script) : undefined;
6758
+ const outDir = resolve4(opts.out ?? join14(dirname3(inputAbs), `${projName}-video-project-${timestamp()}`));
6759
+ let scriptPath = opts.script ? resolve4(opts.script) : undefined;
6469
6760
  if (!scriptPath) {
6470
- const sibling = join14(dirname2(inputAbs), `${projName}.txt`);
6761
+ const sibling = join14(dirname3(inputAbs), `${projName}.txt`);
6471
6762
  if (existsSync12(sibling)) {
6472
6763
  scriptPath = sibling;
6473
6764
  log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
@@ -6491,8 +6782,6 @@ async function runOralCut(input, opts) {
6491
6782
  assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
6492
6783
  log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename5(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename5(artifact)}`);
6493
6784
  log.step("② 上传抽出物到云端…");
6494
- let up = await uploadCached(cfg, artifact, { force: opts.reupload });
6495
- log.info(up.cached ? `命中上传缓存,复用 file_id = ${up.fileId}(免二次上传)` : `file_id = ${up.fileId}`);
6496
6785
  const buildPayload = (fid) => {
6497
6786
  const p = {
6498
6787
  file_id: fid,
@@ -6519,19 +6808,16 @@ async function runOralCut(input, opts) {
6519
6808
  }
6520
6809
  return p;
6521
6810
  };
6522
- log.step("③ 提交智能口播剪辑任务…");
6523
- let taskId;
6524
- try {
6525
- taskId = await submitTask(cfg, TASK_TYPE, buildPayload(up.fileId));
6526
- } catch (e) {
6527
- if (up.cached && e instanceof CloudError && e.code === 6004) {
6528
- log.warn("缓存的 file_id 在云端已失效,重新上传后重试…");
6529
- await invalidateUpload(artifact);
6530
- up = await uploadCached(cfg, artifact, { force: true });
6531
- taskId = await submitTask(cfg, TASK_TYPE, buildPayload(up.fileId));
6532
- } else
6533
- throw e;
6534
- }
6811
+ const submitted = await uploadAndSubmitTask(cfg, artifact, TASK_TYPE, buildPayload, {
6812
+ force: opts.reupload,
6813
+ onUploaded: (uploaded) => {
6814
+ log.info(uploaded.cached ? `命中上传缓存,复用 file_id = ${uploaded.fileId}(免二次上传)` : `file_id = ${uploaded.fileId}`);
6815
+ log.step("③ 提交智能口播剪辑任务…");
6816
+ },
6817
+ onCacheInvalid: () => log.warn("缓存的 file_id 在云端已失效,重新上传后重试…")
6818
+ });
6819
+ const { taskId } = submitted;
6820
+ const up = { fileId: submitted.fileId, cached: submitted.cached };
6535
6821
  log.info(`task_id = ${taskId}`);
6536
6822
  await mkdir4(outDir, { recursive: true });
6537
6823
  await writeFile5(join14(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE, fileId: up.fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
@@ -6558,7 +6844,7 @@ async function runOralCut(input, opts) {
6558
6844
  }
6559
6845
 
6560
6846
  // src/commands/oralcut-result.ts
6561
- import { resolve as resolve4, join as join15 } from "node:path";
6847
+ import { resolve as resolve5, join as join15 } from "node:path";
6562
6848
  var TASK_TYPE2 = "cli/video_oral_cut_for_cli";
6563
6849
  function timestamp2() {
6564
6850
  const d = new Date;
@@ -6592,7 +6878,7 @@ async function runOralCutResult(taskId, opts) {
6592
6878
  const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
6593
6879
  throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
6594
6880
  }
6595
- const outDir = resolve4(opts.out ?? join15(process.cwd(), `${taskId}-video-project-${timestamp2()}`));
6881
+ const outDir = resolve5(opts.out ?? join15(process.cwd(), `${taskId}-video-project-${timestamp2()}`));
6596
6882
  const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
6597
6883
  await materializeResult({
6598
6884
  outDir,
@@ -6610,10 +6896,10 @@ async function runOralCutResult(taskId, opts) {
6610
6896
  }
6611
6897
 
6612
6898
  // src/commands/upgrade.ts
6613
- import { spawnSync as spawnSync3 } from "node:child_process";
6899
+ import { spawnSync as spawnSync4 } from "node:child_process";
6614
6900
  var CLIENT_UPGRADE = "irm https://api.ai-mcn.tv:9000/broadcast/exe/install.ps1 | iex";
6615
6901
  function run(cmd) {
6616
- const r = spawnSync3(cmd, { stdio: "inherit", shell: true });
6902
+ const r = spawnSync4(cmd, { stdio: "inherit", shell: true });
6617
6903
  return r.status ?? 1;
6618
6904
  }
6619
6905
  function registerUpgrade(program2) {
@@ -6642,7 +6928,7 @@ function registerUpgrade(program2) {
6642
6928
  process.exitCode = 1;
6643
6929
  return;
6644
6930
  }
6645
- log.step("② 刷新 /gtrk-oralcut skill…");
6931
+ log.step("② 通过通用适配器刷新 Agent Skills…");
6646
6932
  if (run("gtrk skills install") !== 0) {
6647
6933
  log.warn("skill 没刷成,手动跑一次:gtrk skills install");
6648
6934
  }
@@ -6652,16 +6938,16 @@ function registerUpgrade(program2) {
6652
6938
  }
6653
6939
 
6654
6940
  // src/commands/render.ts
6655
- import { resolve as resolve5, dirname as dirname3, join as join16, basename as basename6, extname as extname3 } from "node:path";
6941
+ import { resolve as resolve6, dirname as dirname4, join as join16, basename as basename6, extname as extname3 } from "node:path";
6656
6942
  import { existsSync as existsSync13 } from "node:fs";
6657
6943
  function registerRender(program2) {
6658
6944
  program2.command("render <gtrk>").description("本地渲染:gtrk 工程按 EDL 用本地 ffmpeg 渲染成片 mp4(素材取原片本地路径)").option("-o, --out <file>", "输出 mp4 路径(缺省 = <gtrk 同目录>/<gtrk 名>.mp4)").option("--crf <n>", "视频质量 CRF 14-28(越小越清晰/文件越大,默认 18)").option("--codec <c>", "视频编码(默认 h264)").option("--ffmpeg-path <dir>", "指定 ffmpeg/ffprobe 所在目录(缺省 ~/.gitruck/ffmpeg → 系统)").option("--no-open", "完成后不自动打开产物目录").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (gtrk, opts) => {
6659
6945
  if (opts.json)
6660
6946
  routeLogsToStderr();
6661
- const gtrkAbs = resolve5(gtrk);
6947
+ const gtrkAbs = resolve6(gtrk);
6662
6948
  if (!existsSync13(gtrkAbs))
6663
6949
  throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
6664
- const outMp4 = resolve5(opts.out ?? join16(dirname3(gtrkAbs), `${basename6(gtrkAbs, extname3(gtrkAbs))}.mp4`));
6950
+ const outMp4 = resolve6(opts.out ?? join16(dirname4(gtrkAbs), `${basename6(gtrkAbs, extname3(gtrkAbs))}.mp4`));
6665
6951
  log.step(`▶ 本地渲染:${basename6(gtrkAbs)} → ${basename6(outMp4)}`);
6666
6952
  const project = await readGtrkFile(gtrkAbs);
6667
6953
  const result = await renderGtrk(project, outMp4, {
@@ -6677,7 +6963,7 @@ function registerRender(program2) {
6677
6963
  log.tickEnd();
6678
6964
  log.ok(`渲染完成:${outMp4}(${result.duration.toFixed(1)}s)`);
6679
6965
  if (opts.open)
6680
- openFolder(dirname3(outMp4));
6966
+ openFolder(dirname4(outMp4));
6681
6967
  if (opts.json) {
6682
6968
  console.log(JSON.stringify({ ok: true, output: outMp4, duration: result.duration }));
6683
6969
  }
@@ -6685,7 +6971,7 @@ function registerRender(program2) {
6685
6971
  }
6686
6972
 
6687
6973
  // src/commands/split.ts
6688
- import { resolve as resolve6, join as join18, dirname as dirname5, basename as basename8 } from "node:path";
6974
+ import { resolve as resolve7, join as join18, dirname as dirname6, basename as basename8 } from "node:path";
6689
6975
  import { existsSync as existsSync14 } from "node:fs";
6690
6976
  import { readFile as readFile4, writeFile as writeFile6, mkdir as mkdir5 } from "node:fs/promises";
6691
6977
  import { createHash } from "node:crypto";
@@ -6810,7 +7096,7 @@ function projectTranscript(transcript, gtrk, opts = {}) {
6810
7096
 
6811
7097
  // src/lib/gtrk-writeback.ts
6812
7098
  import { readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
6813
- import { dirname as dirname4, join as join17, basename as basename7 } from "node:path";
7099
+ import { dirname as dirname5, join as join17, basename as basename7 } from "node:path";
6814
7100
  import { randomBytes as randomBytes2 } from "node:crypto";
6815
7101
  function readGtrk(path) {
6816
7102
  const raw = readFileSync4(path, "utf8");
@@ -6837,7 +7123,7 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
6837
7123
  }
6838
7124
  const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
6839
7125
  const next = { ...gtrk, struct_meta: nextStructMeta };
6840
- const tmp = join17(dirname4(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
7126
+ const tmp = join17(dirname5(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
6841
7127
  try {
6842
7128
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
6843
7129
  renameSync(tmp, path);
@@ -6853,7 +7139,7 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
6853
7139
  if (cur !== expectedMtimeMs) {
6854
7140
  throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
6855
7141
  }
6856
- const tmp = join17(dirname4(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
7142
+ const tmp = join17(dirname5(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
6857
7143
  try {
6858
7144
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
6859
7145
  renameSync(tmp, path);
@@ -6876,10 +7162,10 @@ function firstExisting(cands) {
6876
7162
  return cands.find((p) => existsSync14(p));
6877
7163
  }
6878
7164
  function resolvePaths(opts) {
6879
- const project = opts.project ? resolve6(opts.project) : undefined;
7165
+ const project = opts.project ? resolve7(opts.project) : undefined;
6880
7166
  let gtrkPath;
6881
7167
  if (opts.gtrk) {
6882
- gtrkPath = resolve6(opts.gtrk);
7168
+ gtrkPath = resolve7(opts.gtrk);
6883
7169
  } else if (project) {
6884
7170
  gtrkPath = firstExisting([join18(project, "gtrk", "project.gtrk"), join18(project, "project.gtrk")]) ?? join18(project, "gtrk", "project.gtrk");
6885
7171
  } else {
@@ -6889,14 +7175,14 @@ function resolvePaths(opts) {
6889
7175
  throw new Error(`找不到工程文件:${gtrkPath}`);
6890
7176
  let transcriptPath;
6891
7177
  if (opts.transcript)
6892
- transcriptPath = resolve6(opts.transcript);
7178
+ transcriptPath = resolve7(opts.transcript);
6893
7179
  else if (project)
6894
7180
  transcriptPath = firstExisting([
6895
7181
  join18(project, "transcript", "transcript.json"),
6896
7182
  join18(project, "json", "transcript.json"),
6897
7183
  join18(project, "transcript.json")
6898
7184
  ]);
6899
- const baseDir = project ?? dirname5(gtrkPath);
7185
+ const baseDir = project ?? dirname6(gtrkPath);
6900
7186
  return { baseDir, gtrkPath, transcriptPath };
6901
7187
  }
6902
7188
  function slugify(name) {
@@ -6916,7 +7202,7 @@ async function runSplit(splitdoc, opts) {
6916
7202
  if (opts.json)
6917
7203
  routeLogsToStderr();
6918
7204
  const { baseDir, gtrkPath, transcriptPath } = resolvePaths(opts);
6919
- return splitdoc ? runLand(resolve6(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
7205
+ return splitdoc ? runLand(resolve7(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
6920
7206
  }
6921
7207
  async function runView(baseDir, gtrkPath, transcriptPath, opts) {
6922
7208
  if (!transcriptPath || !existsSync14(transcriptPath))
@@ -7022,7 +7308,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
7022
7308
  }
7023
7309
 
7024
7310
  // src/commands/matrix.ts
7025
- import { resolve as resolve7, join as join19, dirname as dirname6, basename as basename9 } from "node:path";
7311
+ import { resolve as resolve8, join as join19, dirname as dirname7, basename as basename9 } from "node:path";
7026
7312
  import { existsSync as existsSync15 } from "node:fs";
7027
7313
  import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir6 } from "node:fs/promises";
7028
7314
 
@@ -7561,10 +7847,10 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
7561
7847
  let dispatchPath;
7562
7848
  let baseDir;
7563
7849
  if (opts.dispatch) {
7564
- dispatchPath = resolve7(opts.dispatch);
7565
- baseDir = dirname6(dirname6(dispatchPath));
7850
+ dispatchPath = resolve8(opts.dispatch);
7851
+ baseDir = dirname7(dirname7(dispatchPath));
7566
7852
  } else if (opts.project) {
7567
- baseDir = resolve7(opts.project);
7853
+ baseDir = resolve8(opts.project);
7568
7854
  dispatchPath = join19(baseDir, "split", "dispatch.json");
7569
7855
  } else {
7570
7856
  throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
@@ -7669,7 +7955,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor) {
7669
7955
  const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
7670
7956
  const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
7671
7957
  log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
7672
- const gtrkDir = dirname6(gtrkPath);
7958
+ const gtrkDir = dirname7(gtrkPath);
7673
7959
  const previewDir = join19(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
7674
7960
  await mkdir6(previewDir, { recursive: true });
7675
7961
  const prevSource = new Map;
@@ -7782,7 +8068,7 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
7782
8068
  counts: { beats: 0, queries: 1, results: results.length, errors: 0 }
7783
8069
  };
7784
8070
  if (opts.out) {
7785
- const outPath = resolve7(opts.out);
8071
+ const outPath = resolve8(opts.out);
7786
8072
  await writeFile7(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
7787
8073
  log.ok(`结果已落盘:${outPath}`);
7788
8074
  result.outPath = outPath;
@@ -7802,7 +8088,7 @@ function slugify2(name) {
7802
8088
  }
7803
8089
 
7804
8090
  // src/commands/mg.ts
7805
- import { resolve as resolve8, join as join20, dirname as dirname7, basename as basename10 } from "node:path";
8091
+ import { resolve as resolve9, join as join20, dirname as dirname8, basename as basename10 } from "node:path";
7806
8092
  import { existsSync as existsSync16 } from "node:fs";
7807
8093
  import { readFile as readFile6, mkdir as mkdir7, copyFile } from "node:fs/promises";
7808
8094
 
@@ -8025,11 +8311,11 @@ async function runMg(words, opts) {
8025
8311
  }
8026
8312
  function resolveDispatch(opts) {
8027
8313
  if (opts.dispatch) {
8028
- const dispatchPath = resolve8(opts.dispatch);
8029
- return { dispatchPath, baseDir: dirname7(dirname7(dispatchPath)) };
8314
+ const dispatchPath = resolve9(opts.dispatch);
8315
+ return { dispatchPath, baseDir: dirname8(dirname8(dispatchPath)) };
8030
8316
  }
8031
8317
  if (opts.project) {
8032
- const baseDir = resolve8(opts.project);
8318
+ const baseDir = resolve9(opts.project);
8033
8319
  return { dispatchPath: join20(baseDir, "split", "dispatch.json"), baseDir };
8034
8320
  }
8035
8321
  throw new Error("需 --project <目录> 或显式 --dispatch <path>");
@@ -8106,7 +8392,7 @@ async function runLay(opts) {
8106
8392
  }
8107
8393
  const { gtrk, mtimeMs } = readGtrk(gtrkPath);
8108
8394
  assertGtrkV1(gtrk);
8109
- const gtrkDir = dirname7(gtrkPath);
8395
+ const gtrkDir = dirname8(gtrkPath);
8110
8396
  await mkdir7(join20(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
8111
8397
  for (const it of items) {
8112
8398
  await copyFile(srcByComp.get(it.composition_id), join20(gtrkDir, ...it.html_rel.split("/")));
@@ -8127,10 +8413,10 @@ async function runLint(args, opts) {
8127
8413
  const file = args[0];
8128
8414
  if (!file)
8129
8415
  throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
8130
- const html = await readFile6(resolve8(file), "utf8");
8416
+ const html = await readFile6(resolve9(file), "utf8");
8131
8417
  let dispatchIds;
8132
- if (opts.dispatch && existsSync16(resolve8(opts.dispatch))) {
8133
- dispatchIds = (await readMgQueue(resolve8(opts.dispatch))).map((q) => q.composition_id);
8418
+ if (opts.dispatch && existsSync16(resolve9(opts.dispatch))) {
8419
+ dispatchIds = (await readMgQueue(resolve9(opts.dispatch))).map((q) => q.composition_id);
8134
8420
  }
8135
8421
  const lint = lintParticle(html, { dispatchIds });
8136
8422
  for (const vv of lint.violations)
@@ -8180,6 +8466,21 @@ import { extname as extname4 } from "node:path";
8180
8466
  var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".heic", ".heif", ".avif"];
8181
8467
  var VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts"];
8182
8468
  var AUDIO_EXTS = [".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg", ".opus", ".wma"];
8469
+ var PUBLIC_VIDEO_EXTS = [
8470
+ ".mp4",
8471
+ ".avi",
8472
+ ".mpg",
8473
+ ".mov",
8474
+ ".flv",
8475
+ ".mxf",
8476
+ ".mpeg",
8477
+ ".ogg",
8478
+ ".3gp",
8479
+ ".wmv",
8480
+ ".h264",
8481
+ ".m4v",
8482
+ ".ts"
8483
+ ];
8183
8484
  function defaultExtsFor(kind) {
8184
8485
  if (kind === "image")
8185
8486
  return IMAGE_EXTS;
@@ -8228,7 +8529,7 @@ var imageMove = {
8228
8529
  description: "把一张静态图生成带运镜的短视频。",
8229
8530
  kind: "cloud",
8230
8531
  input: { kind: "image" },
8231
- billingHint: "2 积分/个",
8532
+ priceKey: "image_move_local",
8232
8533
  outputHint: "运镜视频",
8233
8534
  enabled: true,
8234
8535
  taskType: "image_move",
@@ -8261,7 +8562,7 @@ var imageMatting = {
8261
8562
  description: "把图片主体从背景抠出,产透明背景 png(可经 --param 请求额外背景底板输出)。",
8262
8563
  kind: "cloud",
8263
8564
  input: { kind: "image" },
8264
- billingHint: "免费",
8565
+ priceKey: "image_matting",
8265
8566
  outputHint: "透明 png",
8266
8567
  enabled: true,
8267
8568
  taskType: "image_matting",
@@ -8279,13 +8580,382 @@ var imageMatting = {
8279
8580
  return files;
8280
8581
  }
8281
8582
  };
8583
+ var imageBlackborderRemove = {
8584
+ name: "image_blackborder_remove",
8585
+ title: "图片去黑边",
8586
+ description: "自动检测并裁去单张图片四周的黑边,保留有效画面。",
8587
+ kind: "cloud",
8588
+ input: { kind: "image" },
8589
+ priceKey: "image_blackborder_remove",
8590
+ outputHint: "去黑边图片",
8591
+ enabled: true,
8592
+ taskType: "image_blackborder_remove",
8593
+ buildPayload(fileId) {
8594
+ return { file_id: fileId };
8595
+ },
8596
+ mapOutputs(out, ctx) {
8597
+ const url = pickUrl(out, ["download_url"]);
8598
+ return url ? [{ url, filename: `${ctx.baseName}-blackborder-removed${extFromUrl(url, ".jpg")}` }] : [];
8599
+ }
8600
+ };
8601
+ var imageCanvasAdapt = {
8602
+ name: "image_canvas_adapt",
8603
+ title: "图片比例转换",
8604
+ description: "把单张图片适配到目标画布尺寸,可选适配、矩形裁剪或方形裁剪。",
8605
+ kind: "cloud",
8606
+ input: { kind: "image" },
8607
+ priceKey: "image_canvas_adapt",
8608
+ outputHint: "比例适配图片",
8609
+ enabled: true,
8610
+ taskType: "image_canvas_adapt",
8611
+ options: [
8612
+ { flag: "--canvas-width <px>", desc: "目标画布宽度(像素;未传则使用服务端默认)" },
8613
+ { flag: "--canvas-height <px>", desc: "目标画布高度(像素;未传则使用服务端默认)" },
8614
+ {
8615
+ flag: "--canvas-type <normal|rectangle|square>",
8616
+ desc: "画布模式:normal、rectangle 或 square(未传则使用服务端默认)"
8617
+ }
8618
+ ],
8619
+ buildPayload(fileId, ctx) {
8620
+ const payload = { file_id: fileId };
8621
+ for (const [optKey, payloadKey, flag] of [
8622
+ ["canvasWidth", "canvas_width", "--canvas-width"],
8623
+ ["canvasHeight", "canvas_height", "--canvas-height"]
8624
+ ]) {
8625
+ if (ctx.opts[optKey] == null)
8626
+ continue;
8627
+ const value = Number(ctx.opts[optKey]);
8628
+ if (!Number.isFinite(value))
8629
+ throw new Error(`${flag} 必须是数字`);
8630
+ payload[payloadKey] = value;
8631
+ }
8632
+ if (ctx.opts.canvasType != null) {
8633
+ const value = String(ctx.opts.canvasType);
8634
+ if (value !== "normal" && value !== "rectangle" && value !== "square") {
8635
+ throw new Error("--canvas-type 只支持 normal、rectangle 或 square");
8636
+ }
8637
+ payload.canvas_type = value;
8638
+ }
8639
+ return payload;
8640
+ },
8641
+ mapOutputs(out, ctx) {
8642
+ const url = pickUrl(out, ["download_url"]);
8643
+ return url ? [{ url, filename: `${ctx.baseName}-canvas-adapted${extFromUrl(url, ".jpg")}` }] : [];
8644
+ }
8645
+ };
8646
+ var imagePurify = {
8647
+ name: "image_purify",
8648
+ title: "图片净化",
8649
+ description: "清理你有权处理的图片中的水印、Logo 或叠加元素。",
8650
+ kind: "cloud",
8651
+ input: { kind: "image" },
8652
+ priceKey: "image_purify",
8653
+ outputHint: "净化图片",
8654
+ enabled: true,
8655
+ taskType: "image_purify",
8656
+ buildPayload(fileId) {
8657
+ return { file_id: fileId };
8658
+ },
8659
+ mapOutputs(out, ctx) {
8660
+ const url = pickUrl(out, ["download_url"]);
8661
+ return url ? [{ url, filename: `${ctx.baseName}-purified${extFromUrl(url, ".jpg")}` }] : [];
8662
+ }
8663
+ };
8664
+ var videoBlackborderRemove = {
8665
+ name: "video_blackborder_remove",
8666
+ title: "视频去黑边",
8667
+ description: "自动检测并裁去单条视频四周的黑边,保留有效画面与原音轨。",
8668
+ kind: "cloud",
8669
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
8670
+ priceKey: "video_blackborder_remove",
8671
+ outputHint: "去黑边视频",
8672
+ enabled: true,
8673
+ taskType: "video_blackborder_remove",
8674
+ buildPayload(fileId) {
8675
+ return { file_id: fileId };
8676
+ },
8677
+ mapOutputs(out, ctx) {
8678
+ const url = pickUrl(out, ["download_url"]);
8679
+ return url ? [{ url, filename: `${ctx.baseName}-blackborder-removed${extFromUrl(url, ".mp4")}` }] : [];
8680
+ }
8681
+ };
8682
+ var videoCanvasAdapt = {
8683
+ name: "video_canvas_adapt",
8684
+ title: "视频比例转换",
8685
+ description: "把单条视频适配到目标画布,可选截取时间片段并移除音轨。",
8686
+ kind: "cloud",
8687
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
8688
+ priceKey: "video_canvas_adapt",
8689
+ outputHint: "比例适配视频",
8690
+ enabled: true,
8691
+ taskType: "video_canvas_adapt",
8692
+ options: [
8693
+ { flag: "--canvas-width <px>", desc: "目标画布宽度(像素;未传则使用服务端默认)" },
8694
+ { flag: "--canvas-height <px>", desc: "目标画布高度(像素;未传则使用服务端默认)" },
8695
+ {
8696
+ flag: "--canvas-type <normal|rectangle|square>",
8697
+ desc: "画布模式:normal、rectangle 或 square(未传则使用服务端默认)"
8698
+ },
8699
+ { flag: "--clip-start <frame>", desc: "截取起始帧序号(未传则使用服务端默认)" },
8700
+ { flag: "--clip-end <frame>", desc: "截取结束帧序号(未传则使用服务端默认)" },
8701
+ { flag: "--without-audio", desc: "输出视频不保留音轨" }
8702
+ ],
8703
+ buildPayload(fileId, ctx) {
8704
+ const payload = { file_id: fileId };
8705
+ for (const [optKey, payloadKey, flag] of [
8706
+ ["canvasWidth", "target_width", "--canvas-width"],
8707
+ ["canvasHeight", "target_height", "--canvas-height"]
8708
+ ]) {
8709
+ if (ctx.opts[optKey] == null)
8710
+ continue;
8711
+ const value = Number(ctx.opts[optKey]);
8712
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
8713
+ throw new Error(`${flag} 必须是有限整数`);
8714
+ }
8715
+ payload[payloadKey] = value;
8716
+ }
8717
+ for (const [optKey, payloadKey, flag] of [
8718
+ ["clipStart", "start", "--clip-start"],
8719
+ ["clipEnd", "end", "--clip-end"]
8720
+ ]) {
8721
+ if (ctx.opts[optKey] == null)
8722
+ continue;
8723
+ const value = Number(ctx.opts[optKey]);
8724
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
8725
+ throw new Error(`${flag} 必须是有限整数帧号`);
8726
+ }
8727
+ payload[payloadKey] = value;
8728
+ }
8729
+ if (ctx.opts.canvasType != null) {
8730
+ const value = String(ctx.opts.canvasType);
8731
+ if (value !== "normal" && value !== "rectangle" && value !== "square") {
8732
+ throw new Error("--canvas-type 只支持 normal、rectangle 或 square");
8733
+ }
8734
+ payload.canvas_type = value;
8735
+ }
8736
+ if (ctx.opts.withoutAudio === true)
8737
+ payload.need_audio = false;
8738
+ return payload;
8739
+ },
8740
+ mapOutputs(out, ctx) {
8741
+ const url = pickUrl(out, ["download_url"]);
8742
+ return url ? [{ url, filename: `${ctx.baseName}-canvas-adapted${extFromUrl(url, ".mp4")}` }] : [];
8743
+ }
8744
+ };
8745
+ var videoStabilizer = {
8746
+ name: "video_stabilizer",
8747
+ title: "视频防抖",
8748
+ description: "稳定手持或运动拍摄画面;exp 为实验方式,产物观感需自行检查。",
8749
+ kind: "cloud",
8750
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
8751
+ priceKey: "video_stabilizer",
8752
+ outputHint: "防抖视频",
8753
+ enabled: true,
8754
+ taskType: "video_stabilizer",
8755
+ options: [{ flag: "--stabilizer-method <fast|exp|turbo>", desc: "防抖方式(未传则使用服务端 turbo 默认值)" }],
8756
+ buildPayload(fileId, ctx) {
8757
+ const payload = { file_id: fileId };
8758
+ if (ctx.opts.stabilizerMethod == null)
8759
+ return payload;
8760
+ const method = String(ctx.opts.stabilizerMethod);
8761
+ if (method !== "fast" && method !== "exp" && method !== "turbo") {
8762
+ throw new Error("--stabilizer-method 只支持 fast、exp 或 turbo");
8763
+ }
8764
+ payload.method = method;
8765
+ return payload;
8766
+ },
8767
+ mapOutputs(out, ctx) {
8768
+ const url = pickUrl(out, ["download_url"]);
8769
+ return url ? [{ url, filename: `${ctx.baseName}-stabilized${extFromUrl(url, ".mp4")}` }] : [];
8770
+ }
8771
+ };
8772
+ var videoVaporwave = {
8773
+ name: "video_vaporwave",
8774
+ title: "视频蒸汽波滤镜",
8775
+ description: "按精确预设名称为单条视频应用蒸汽波风格滤镜。",
8776
+ kind: "cloud",
8777
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
8778
+ priceKey: "video_vaporwave",
8779
+ outputHint: "蒸汽波滤镜视频",
8780
+ enabled: true,
8781
+ taskType: "video_vaporwave",
8782
+ options: [{ flag: "--vaporwave-filter <name>", desc: "精确滤镜名称(默认:愈漸升溫)" }],
8783
+ buildPayload(fileId, ctx) {
8784
+ const raw = ctx.opts.vaporwaveFilter;
8785
+ const filter = raw == null ? "愈漸升溫" : String(raw);
8786
+ if (!filter.trim())
8787
+ throw new Error("--vaporwave-filter 不能为空");
8788
+ return { file_id: fileId, filter };
8789
+ },
8790
+ mapOutputs(out, ctx) {
8791
+ const url = pickUrl(out, ["download_url"]);
8792
+ return url ? [{ url, filename: `${ctx.baseName}-vaporwave${extFromUrl(url, ".mp4")}` }] : [];
8793
+ }
8794
+ };
8795
+ function parseNormalizedRoi(value) {
8796
+ let rawValues;
8797
+ let acceptNumericStrings = false;
8798
+ if (typeof value === "string") {
8799
+ acceptNumericStrings = true;
8800
+ const parts = value.split(",");
8801
+ if (parts.length !== 4 || parts.some((part) => !part.trim())) {
8802
+ throw new Error("--purify-roi 必须是 x,y,w,h 四个归一化数字");
8803
+ }
8804
+ rawValues = parts;
8805
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
8806
+ const roi = value;
8807
+ rawValues = [roi.x, roi.y, roi.w, roi.h];
8808
+ } else {
8809
+ throw new Error("ROI 必须包含归一化数字 x、y、w、h");
8810
+ }
8811
+ const numbers = rawValues.map((item) => {
8812
+ if (typeof item === "number")
8813
+ return item;
8814
+ if (acceptNumericStrings && typeof item === "string")
8815
+ return Number(item);
8816
+ return Number.NaN;
8817
+ });
8818
+ if (numbers.some((item) => !Number.isFinite(item))) {
8819
+ throw new Error("ROI 的 x、y、w、h 必须是有限数字");
8820
+ }
8821
+ const [x, y, w, h] = numbers;
8822
+ if (x < 0 || x > 1 || y < 0 || y > 1 || w <= 0 || w > 1 || h <= 0 || h > 1 || x + w > 1 || y + h > 1) {
8823
+ throw new Error("ROI 必须满足 0≤x,y≤1、0<w,h≤1、x+w≤1、y+h≤1");
8824
+ }
8825
+ return { x, y, w, h };
8826
+ }
8827
+ var videoPurify = {
8828
+ name: "video_purify",
8829
+ title: "视频净化",
8830
+ description: "清理你有权处理的视频中的水印、字幕或指定区域;不承诺还原被遮挡的原始内容。",
8831
+ kind: "cloud",
8832
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
8833
+ priceKey: "video_purify",
8834
+ outputHint: "净化视频",
8835
+ enabled: true,
8836
+ taskType: "video_purify",
8837
+ pollTimeoutMs: 4 * 60 * 60 * 1000,
8838
+ options: [
8839
+ {
8840
+ flag: "--purify-scope <full_screen|subtitle|custom>",
8841
+ desc: "净化范围;未传时使用服务端 full_screen 默认值"
8842
+ },
8843
+ {
8844
+ flag: "--purify-method <ffmpeg|raft>",
8845
+ desc: "净化方式;未传时使用服务端 ffmpeg 默认值,raft 仅支持 20 分钟以内视频"
8846
+ },
8847
+ { flag: "--purify-roi <x,y,w,h>", desc: "custom 模式的归一化矩形区域" }
8848
+ ],
8849
+ buildPayload(fileId, ctx) {
8850
+ const payload = { file_id: fileId };
8851
+ const scopeRaw = ctx.opts.purifyScope;
8852
+ const scope = scopeRaw == null ? undefined : String(scopeRaw);
8853
+ if (scope != null && scope !== "full_screen" && scope !== "subtitle" && scope !== "custom") {
8854
+ throw new Error("--purify-scope 只支持 full_screen、subtitle 或 custom");
8855
+ }
8856
+ if (scope != null)
8857
+ payload.purify_scope = scope;
8858
+ const methodRaw = ctx.opts.purifyMethod;
8859
+ const method = methodRaw == null ? undefined : String(methodRaw);
8860
+ if (method != null && method !== "ffmpeg" && method !== "raft") {
8861
+ throw new Error("--purify-method 只支持 ffmpeg 或 raft");
8862
+ }
8863
+ if (method != null)
8864
+ payload.purify_func_type = method;
8865
+ const roiRaw = ctx.opts.purifyRoi;
8866
+ if (roiRaw != null) {
8867
+ if (scope !== "custom") {
8868
+ throw new Error("--purify-roi 只能与 --purify-scope custom 一起使用");
8869
+ }
8870
+ payload.roi = parseNormalizedRoi(roiRaw);
8871
+ } else if (scope === "custom") {
8872
+ if (ctx.extraParams.roi == null) {
8873
+ throw new Error("--purify-scope custom 必须同时提供 --purify-roi 或 params-json.roi");
8874
+ }
8875
+ parseNormalizedRoi(ctx.extraParams.roi);
8876
+ }
8877
+ return payload;
8878
+ },
8879
+ mapOutputs(out, ctx) {
8880
+ const url = pickUrl(out, ["download_url"]);
8881
+ return url ? [{ url, filename: `${ctx.baseName}-purified${extFromUrl(url, ".mp4")}` }] : [];
8882
+ }
8883
+ };
8884
+ var videoUpscale = {
8885
+ name: "video_upscale",
8886
+ title: "视频超分",
8887
+ description: "对一分钟以内的低分辨率视频做实验性 GPU 超分;效果需自行检查。",
8888
+ kind: "cloud",
8889
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS, maxDurationSec: 60 },
8890
+ priceKey: "video_upscale",
8891
+ outputHint: "超分视频",
8892
+ enabled: true,
8893
+ taskType: "video_upscale",
8894
+ pollTimeoutMs: 4 * 60 * 60 * 1000,
8895
+ options: [
8896
+ { flag: "--upscale-times <2|3|4>", desc: "超分倍数;未传时使用服务端 2 倍默认值" },
8897
+ { flag: "--upscale-type <Reality|Anime>", desc: "写实或动漫类型;未传时使用服务端 Reality 默认值" }
8898
+ ],
8899
+ buildPayload(fileId, ctx) {
8900
+ const payload = { file_id: fileId };
8901
+ if (ctx.opts.upscaleTimes != null) {
8902
+ const times = Number(ctx.opts.upscaleTimes);
8903
+ if (!Number.isInteger(times) || times !== 2 && times !== 3 && times !== 4) {
8904
+ throw new Error("--upscale-times 只支持 2、3 或 4");
8905
+ }
8906
+ payload.times = times;
8907
+ }
8908
+ if (ctx.opts.upscaleType != null) {
8909
+ const upscaleType = String(ctx.opts.upscaleType);
8910
+ if (upscaleType !== "Reality" && upscaleType !== "Anime") {
8911
+ throw new Error("--upscale-type 只支持 Reality 或 Anime");
8912
+ }
8913
+ payload.upscale_type = upscaleType;
8914
+ }
8915
+ return payload;
8916
+ },
8917
+ mapOutputs(out, ctx) {
8918
+ const url = pickUrl(out, ["download_url"]);
8919
+ return url ? [{ url, filename: `${ctx.baseName}-upscaled${extFromUrl(url, ".mp4")}` }] : [];
8920
+ }
8921
+ };
8922
+ var videoInterpolate = {
8923
+ name: "video_interpolate",
8924
+ title: "视频插帧",
8925
+ description: "以 GPU 帧插值提升视频流畅度;不附加未经服务端声明的时长限制。",
8926
+ kind: "cloud",
8927
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
8928
+ priceKey: "video_interpolate",
8929
+ outputHint: "插帧视频",
8930
+ enabled: true,
8931
+ taskType: "video_interpolate",
8932
+ pollTimeoutMs: 4 * 60 * 60 * 1000,
8933
+ options: [
8934
+ { flag: "--interpolate-multiplier <2|3|4>", desc: "插帧倍数;未传时使用服务端 2 倍默认值" }
8935
+ ],
8936
+ buildPayload(fileId, ctx) {
8937
+ const payload = { file_id: fileId };
8938
+ if (ctx.opts.interpolateMultiplier != null) {
8939
+ const multiplier = Number(ctx.opts.interpolateMultiplier);
8940
+ if (!Number.isInteger(multiplier) || multiplier !== 2 && multiplier !== 3 && multiplier !== 4) {
8941
+ throw new Error("--interpolate-multiplier 只支持 2、3 或 4");
8942
+ }
8943
+ payload.multiplier = multiplier;
8944
+ }
8945
+ return payload;
8946
+ },
8947
+ mapOutputs(out, ctx) {
8948
+ const url = pickUrl(out, ["download_url"]);
8949
+ return url ? [{ url, filename: `${ctx.baseName}-interpolated${extFromUrl(url, ".mp4")}` }] : [];
8950
+ }
8951
+ };
8282
8952
  var videoMatting = {
8283
8953
  name: "video_matting",
8284
8954
  title: "视频抠像",
8285
8955
  description: "把视频主体从背景抠出,产透明背景 webm(像素级、原片直传不压代理;单片 ≤10 分钟)。",
8286
8956
  kind: "cloud",
8287
8957
  input: { kind: "video", maxDurationSec: 600 },
8288
- billingHint: "免费",
8958
+ priceKey: "video_matting",
8289
8959
  outputHint: "透明 webm",
8290
8960
  enabled: true,
8291
8961
  taskType: "video_matting",
@@ -8304,13 +8974,411 @@ var videoMatting = {
8304
8974
  return files;
8305
8975
  }
8306
8976
  };
8977
+ var audioSeparation = {
8978
+ name: "audio_separation",
8979
+ title: "人声伴奏分离",
8980
+ description: "把单条音频分离为人声与伴奏,可返回其中一项或两项。",
8981
+ kind: "cloud",
8982
+ input: { kind: "audio" },
8983
+ priceKey: "audio_separation",
8984
+ outputHint: "人声与伴奏音频",
8985
+ enabled: true,
8986
+ taskType: "audio_separation",
8987
+ options: [{ flag: "--mode <fast|turbo>", desc: "处理档位(默认 fast,可选 turbo)" }],
8988
+ buildPayload(fileId, ctx) {
8989
+ const raw = ctx.opts.mode;
8990
+ const mode = raw == null ? "fast" : String(raw);
8991
+ if (mode !== "fast" && mode !== "turbo")
8992
+ throw new Error("--mode 只支持 fast 或 turbo");
8993
+ return { file_id: fileId, mode };
8994
+ },
8995
+ mapOutputs(out, ctx) {
8996
+ if (!Array.isArray(out.files))
8997
+ return [];
8998
+ const items = [];
8999
+ for (const raw of out.files) {
9000
+ if (!raw || typeof raw !== "object")
9001
+ continue;
9002
+ const file = raw;
9003
+ const type = file.type;
9004
+ const url = file.download_url;
9005
+ if (type !== "vocals" && type !== "instrumental" || typeof url !== "string" || !url.trim())
9006
+ continue;
9007
+ items.push({ url, filename: `${ctx.baseName}-${type}${extFromUrl(url, ".wav")}` });
9008
+ }
9009
+ return items;
9010
+ }
9011
+ };
9012
+ var audioNoiseReduce = {
9013
+ name: "audio_noise_reduce",
9014
+ title: "音频降噪",
9015
+ description: "对音频或视频中的声音降噪,输出降噪后的音频。",
9016
+ kind: "cloud",
9017
+ input: { kind: "audio", exts: [...AUDIO_EXTS, ...VIDEO_EXTS] },
9018
+ priceKey: "audio_noise_reduce",
9019
+ outputHint: "降噪音频",
9020
+ enabled: true,
9021
+ taskType: "audio_noise_reduce",
9022
+ options: [{ flag: "--prop-decrease <0..1>", desc: "降噪强度(0 到 1;未传则使用服务端默认)" }],
9023
+ buildPayload(fileId, ctx) {
9024
+ const payload = { file_id: fileId };
9025
+ if (ctx.opts.propDecrease != null) {
9026
+ const value = Number(ctx.opts.propDecrease);
9027
+ if (!Number.isFinite(value) || value < 0 || value > 1)
9028
+ throw new Error("--prop-decrease 必须是 0 到 1 的数字");
9029
+ payload.prop_decrease = value;
9030
+ }
9031
+ return payload;
9032
+ },
9033
+ mapOutputs(out, ctx) {
9034
+ const url = pickUrl(out, ["download_url"]);
9035
+ return url ? [{ url, filename: `${ctx.baseName}-denoise${extFromUrl(url, ".wav")}` }] : [];
9036
+ }
9037
+ };
9038
+ var audioSilenceRemove = {
9039
+ name: "audio_silence_remove",
9040
+ title: "静音片段移除",
9041
+ description: "移除音频中过长的静音片段,输出压缩停顿后的音频。",
9042
+ kind: "cloud",
9043
+ input: { kind: "audio" },
9044
+ priceKey: "audio_silence_remove",
9045
+ outputHint: "去静音音频",
9046
+ enabled: true,
9047
+ taskType: "audio_silence_remove",
9048
+ options: [
9049
+ { flag: "--min-silence-len <ms>", desc: "被视为静音片段的最短毫秒数(未传则使用服务端默认)" },
9050
+ { flag: "--desired-silence-len <ms>", desc: "处理后保留的静音毫秒数(未传则使用服务端默认)" }
9051
+ ],
9052
+ buildPayload(fileId, ctx) {
9053
+ const payload = { file_id: fileId };
9054
+ for (const [optKey, payloadKey, flag] of [
9055
+ ["minSilenceLen", "min_silence_len", "--min-silence-len"],
9056
+ ["desiredSilenceLen", "desired_silence_len", "--desired-silence-len"]
9057
+ ]) {
9058
+ if (ctx.opts[optKey] == null)
9059
+ continue;
9060
+ const value = Number(ctx.opts[optKey]);
9061
+ if (!Number.isFinite(value) || value < 0)
9062
+ throw new Error(`${flag} 必须是非负毫秒数`);
9063
+ payload[payloadKey] = value;
9064
+ }
9065
+ return payload;
9066
+ },
9067
+ mapOutputs(out, ctx) {
9068
+ const url = pickUrl(out, ["download_url"]);
9069
+ return url ? [{ url, filename: `${ctx.baseName}-desilence${extFromUrl(url, ".wav")}` }] : [];
9070
+ }
9071
+ };
9072
+ var videoSegment = {
9073
+ name: "video_segment",
9074
+ title: "视频机械分镜",
9075
+ description: "按画面变动率把视频切成分镜区间,输出结构化 JSON(场景数与各段起止/时长),不产切片文件。",
9076
+ kind: "cloud",
9077
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
9078
+ priceKey: "video_segment",
9079
+ outputHint: "分镜区间结构(result-output.json)",
9080
+ enabled: true,
9081
+ taskType: "video_segment",
9082
+ options: [
9083
+ { flag: "--detector <content|adaptive>", desc: "切分算法(未传则使用服务端默认)" },
9084
+ { flag: "--threshold <number>", desc: "切分阈值(未传则使用服务端默认)" }
9085
+ ],
9086
+ buildPayload(fileId, ctx) {
9087
+ const payload = { file_id: fileId, only_struct: true };
9088
+ if (ctx.opts.detector != null) {
9089
+ const detector = String(ctx.opts.detector);
9090
+ if (detector !== "content" && detector !== "adaptive") {
9091
+ throw new Error("--detector 只支持 content 或 adaptive");
9092
+ }
9093
+ payload.detector = detector;
9094
+ }
9095
+ if (ctx.opts.threshold != null) {
9096
+ const value = Number(ctx.opts.threshold);
9097
+ if (!Number.isFinite(value))
9098
+ throw new Error("--threshold 必须是数字");
9099
+ payload.threshold = value;
9100
+ }
9101
+ return payload;
9102
+ },
9103
+ mapResult(out) {
9104
+ return out;
9105
+ }
9106
+ };
9107
+ var videoAiSegment = {
9108
+ name: "video_ai_segment",
9109
+ title: "视频智能分镜",
9110
+ description: "按语义把视频切成带类目、景别、标签、描述的镜头结构,输出结构化 JSON,不产切片文件。",
9111
+ kind: "cloud",
9112
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
9113
+ priceKey: "video_ai_segment",
9114
+ outputHint: "语义分镜结构(result-output.json)",
9115
+ enabled: true,
9116
+ taskType: "video_ai_segment",
9117
+ options: [
9118
+ { flag: "--segment-mode <scene|shot_type|narrative|subject>", desc: "分镜维度(未传则使用服务端默认)" }
9119
+ ],
9120
+ buildPayload(fileId, ctx) {
9121
+ const payload = { file_id: fileId, only_struct: true };
9122
+ if (ctx.opts.segmentMode != null) {
9123
+ const mode = String(ctx.opts.segmentMode);
9124
+ if (mode !== "scene" && mode !== "shot_type" && mode !== "narrative" && mode !== "subject") {
9125
+ throw new Error("--segment-mode 只支持 scene、shot_type、narrative 或 subject");
9126
+ }
9127
+ payload.mode = mode;
9128
+ }
9129
+ return payload;
9130
+ },
9131
+ mapResult(out) {
9132
+ return out;
9133
+ }
9134
+ };
9135
+ var videoMotionCut = {
9136
+ name: "video_motion_cut",
9137
+ title: "视频运镜高光",
9138
+ description: "用光流分析提取运镜/高光片段,输出结构化 JSON(每片帧号、秒级时码与运动特征),不产文件。",
9139
+ kind: "cloud",
9140
+ input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
9141
+ priceKey: "video_motion_cut",
9142
+ outputHint: "运镜高光片段结构(result-output.json)",
9143
+ enabled: true,
9144
+ taskType: "video_motion_cut",
9145
+ buildPayload(fileId) {
9146
+ return { file_id: fileId };
9147
+ },
9148
+ mapResult(out) {
9149
+ return out;
9150
+ }
9151
+ };
9152
+ var audioSpeakerSplit = {
9153
+ name: "audio_speaker_split",
9154
+ title: "按说话人分轨",
9155
+ description: "把多说话人音频按说话人分成独立音轨,并产说话人时间线结构;--only-struct 只出结构不切文件。",
9156
+ kind: "cloud",
9157
+ input: { kind: "audio" },
9158
+ priceKey: "audio_speaker_split",
9159
+ outputHint: "各说话人音轨 + 时间线结构",
9160
+ enabled: true,
9161
+ taskType: "audio_speaker_split",
9162
+ options: [{ flag: "--only-struct", desc: "只输出说话人时间线结构,不切分音轨文件" }],
9163
+ buildPayload(fileId, ctx) {
9164
+ return { file_id: fileId, only_struct: ctx.opts.onlyStruct === true };
9165
+ },
9166
+ mapOutputs(out, ctx) {
9167
+ if (!Array.isArray(out.files))
9168
+ return [];
9169
+ const items = [];
9170
+ for (const raw of out.files) {
9171
+ if (!raw || typeof raw !== "object")
9172
+ continue;
9173
+ const file = raw;
9174
+ const url = file.download_url;
9175
+ if (typeof url !== "string" || !url.trim())
9176
+ continue;
9177
+ const speaker = typeof file.speaker === "string" && file.speaker.trim() ? file.speaker : `speaker_${items.length + 1}`;
9178
+ items.push({ url, filename: `${ctx.baseName}-${speaker}${extFromUrl(url, ".wav")}` });
9179
+ }
9180
+ return items;
9181
+ },
9182
+ mapResult(out) {
9183
+ return { spoken_list: Array.isArray(out.spoken_list) ? out.spoken_list : [] };
9184
+ }
9185
+ };
9186
+ var audioStretch = {
9187
+ name: "audio_stretch",
9188
+ title: "音频变调变速",
9189
+ description: "对音频独立调整音高(半音)与速度(倍率),输出处理后的音频。",
9190
+ kind: "cloud",
9191
+ input: { kind: "audio" },
9192
+ priceKey: "audio_stretch",
9193
+ outputHint: "变调变速音频",
9194
+ enabled: true,
9195
+ taskType: "audio_stretch",
9196
+ options: [
9197
+ { flag: "--semitones <n>", desc: "变调半音(升正降负;未传则不变调)" },
9198
+ { flag: "--speed <n>", desc: "变速倍率(必须 > 0;未传则不变速)" }
9199
+ ],
9200
+ buildPayload(fileId, ctx) {
9201
+ const payload = { file_id: fileId };
9202
+ if (ctx.opts.semitones != null) {
9203
+ const value = Number(ctx.opts.semitones);
9204
+ if (!Number.isFinite(value))
9205
+ throw new Error("--semitones 必须是数字");
9206
+ payload.semitones = value;
9207
+ }
9208
+ if (ctx.opts.speed != null) {
9209
+ const value = Number(ctx.opts.speed);
9210
+ if (!Number.isFinite(value) || value <= 0)
9211
+ throw new Error("--speed 必须是大于 0 的数字");
9212
+ payload.speed = value;
9213
+ }
9214
+ return payload;
9215
+ },
9216
+ mapOutputs(out, ctx) {
9217
+ const url = pickUrl(out, ["download_url"]);
9218
+ return url ? [{ url, filename: `${ctx.baseName}-stretch${extFromUrl(url, ".wav")}` }] : [];
9219
+ }
9220
+ };
9221
+ var pianoAudioToMidi = {
9222
+ name: "piano_audio_to_midi",
9223
+ title: "钢琴音频转 MIDI",
9224
+ description: "把钢琴演奏音频扒谱为 MIDI 文件。",
9225
+ kind: "cloud",
9226
+ input: { kind: "audio" },
9227
+ priceKey: "piano_audio_to_midi",
9228
+ outputHint: "MIDI 文件(.mid)",
9229
+ enabled: true,
9230
+ taskType: "piano_audio_to_midi",
9231
+ buildPayload(fileId) {
9232
+ return { file_id: fileId };
9233
+ },
9234
+ mapOutputs(out, ctx) {
9235
+ const url = pickUrl(out, ["download_url"]);
9236
+ return url ? [{ url, filename: `${ctx.baseName}${extFromUrl(url, ".mid")}` }] : [];
9237
+ }
9238
+ };
9239
+ var pianoAudioEnhance = {
9240
+ name: "piano_audio_enhance",
9241
+ title: "钢琴音频修复增强",
9242
+ description: "修复并增强钢琴录音音质,产高质量 WAV 与配套 MIDI。",
9243
+ kind: "cloud",
9244
+ input: { kind: "audio" },
9245
+ priceKey: "piano_audio_enhance",
9246
+ outputHint: "高质量 WAV + MIDI",
9247
+ enabled: true,
9248
+ taskType: "piano_audio_enhance",
9249
+ buildPayload(fileId) {
9250
+ return { file_id: fileId };
9251
+ },
9252
+ mapOutputs(out, ctx) {
9253
+ const files = [];
9254
+ const main = pickUrl(out, ["download_url"]);
9255
+ if (main)
9256
+ files.push({ url: main, filename: `${ctx.baseName}-enhanced${extFromUrl(main, ".wav")}` });
9257
+ const midi = pickUrl(out, ["midi_download_url"]);
9258
+ if (midi)
9259
+ files.push({ url: midi, filename: `${ctx.baseName}${extFromUrl(midi, ".mid")}` });
9260
+ return files;
9261
+ }
9262
+ };
9263
+ var imageToSquare = {
9264
+ name: "image_to_square",
9265
+ title: "长图转方图",
9266
+ description: "把长图智能转为方形图,可指定最大边长(默认 4000,上限 20000)。",
9267
+ kind: "cloud",
9268
+ input: { kind: "image" },
9269
+ priceKey: "image_to_square",
9270
+ outputHint: "方形图片",
9271
+ enabled: true,
9272
+ taskType: "image_to_square",
9273
+ options: [{ flag: "--max-line <px>", desc: "最大边长像素(默认 4000,上限 20000)" }],
9274
+ buildPayload(fileId, ctx) {
9275
+ const payload = { file_id: fileId };
9276
+ if (ctx.opts.maxLine != null) {
9277
+ const value = Number(ctx.opts.maxLine);
9278
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > 20000) {
9279
+ throw new Error("--max-line 必须是 1 到 20000 之间的整数");
9280
+ }
9281
+ payload.max_line = value;
9282
+ }
9283
+ return payload;
9284
+ },
9285
+ mapOutputs(out, ctx) {
9286
+ const url = pickUrl(out, ["download_url"]);
9287
+ return url ? [{ url, filename: `${ctx.baseName}-square${extFromUrl(url, ".jpg")}` }] : [];
9288
+ }
9289
+ };
9290
+ var imageToLive = {
9291
+ name: "image_to_live",
9292
+ title: "智能 LivePhoto",
9293
+ description: "把一张静态图片生成微动的 LivePhoto 视频(产物是视频)。",
9294
+ kind: "cloud",
9295
+ input: { kind: "image" },
9296
+ priceKey: "image_to_live",
9297
+ outputHint: "微动视频(.mp4)",
9298
+ enabled: true,
9299
+ taskType: "image_to_live",
9300
+ buildPayload(fileId) {
9301
+ return { file_id: fileId };
9302
+ },
9303
+ mapOutputs(out, ctx) {
9304
+ const url = pickUrl(out, ["download_url", "video_download_url", "url"]);
9305
+ return url ? [{ url, filename: `${ctx.baseName}-live${extFromUrl(url, ".mp4")}` }] : [];
9306
+ }
9307
+ };
9308
+ var AI_SUBTITLE_TYPES = ["default", "outline", "cinema_yellow", "immersive_box", "wide_spacing", "deep_shadow", "boxed"];
9309
+ var AI_SUBTITLE_COLORS = ["雅黑", "淡绿", "森林绿", "湖蓝", "道奇蓝", "钢蓝", "浅粉红", "深橙", "珊瑚橙", "橙红", "土豪金"];
9310
+ var videoAiSubtitle = {
9311
+ name: "video_ai_subtitle",
9312
+ title: "智能视频字幕",
9313
+ description: "为视频(或音频)智能生成字幕,可选双语翻译、烧录进视频、去除原字幕;另产 LLM 摘要与字级时间轴。",
9314
+ kind: "cloud",
9315
+ input: { kind: "video", exts: [...PUBLIC_VIDEO_EXTS, ...AUDIO_EXTS] },
9316
+ priceKey: "video_ai_subtitle",
9317
+ outputHint: "字幕 .ass + 可选烧录/去字幕视频 + 摘要/字级时轴结构",
9318
+ enabled: true,
9319
+ taskType: "video_ai_subtitle",
9320
+ pollTimeoutMs: 4 * 60 * 60 * 1000,
9321
+ options: [
9322
+ { flag: "--language <code>", desc: "源语种代码(必填;具体取值由服务端支持列表校验)" },
9323
+ { flag: "--translate-language <code>", desc: "译文目标语种(未传则单语)" },
9324
+ { flag: "--need-render", desc: "把字幕烧录进视频(仅视频输入有效)" },
9325
+ { flag: "--need-pure", desc: "先去除原视频中的字幕" },
9326
+ { flag: "--subtitle-type <style>", desc: `字幕样式:${AI_SUBTITLE_TYPES.join("/")}(未传则用服务端默认)` },
9327
+ { flag: "--subtitle-color <color>", desc: `字幕颜色:${AI_SUBTITLE_COLORS.join("/")}(未传则用服务端默认)` }
9328
+ ],
9329
+ buildPayload(fileId, ctx) {
9330
+ const language = ctx.opts.language == null ? "" : String(ctx.opts.language).trim();
9331
+ if (!language)
9332
+ throw new Error("--language 必填:请指定源语种代码(具体取值见云端 API 文档 / 服务端支持列表)");
9333
+ const payload = { file_id: fileId, language };
9334
+ if (ctx.opts.translateLanguage != null) {
9335
+ const t = String(ctx.opts.translateLanguage).trim();
9336
+ if (t)
9337
+ payload.translate_language = t;
9338
+ }
9339
+ if (ctx.opts.needRender === true)
9340
+ payload.need_render = true;
9341
+ if (ctx.opts.needPure === true)
9342
+ payload.need_pure = true;
9343
+ if (ctx.opts.subtitleType != null) {
9344
+ const v = String(ctx.opts.subtitleType);
9345
+ if (!AI_SUBTITLE_TYPES.includes(v))
9346
+ throw new Error(`--subtitle-type 只支持 ${AI_SUBTITLE_TYPES.join("、")}`);
9347
+ payload.subtitle_type = v;
9348
+ }
9349
+ if (ctx.opts.subtitleColor != null) {
9350
+ const v = String(ctx.opts.subtitleColor);
9351
+ if (!AI_SUBTITLE_COLORS.includes(v))
9352
+ throw new Error(`--subtitle-color 只支持 ${AI_SUBTITLE_COLORS.join("、")}`);
9353
+ payload.subtitle_color = v;
9354
+ }
9355
+ return payload;
9356
+ },
9357
+ mapOutputs(out, ctx) {
9358
+ const files = [];
9359
+ const sub = pickUrl(out, ["subtitle_file_download_url"]);
9360
+ if (sub)
9361
+ files.push({ url: sub, filename: `${ctx.baseName}${extFromUrl(sub, ".ass")}` });
9362
+ const rendered = pickUrl(out, ["rendered_file_download_url"]);
9363
+ if (rendered)
9364
+ files.push({ url: rendered, filename: `${ctx.baseName}-subtitled${extFromUrl(rendered, ".mp4")}` });
9365
+ const pure = pickUrl(out, ["pure_file_download_url"]);
9366
+ if (pure)
9367
+ files.push({ url: pure, filename: `${ctx.baseName}-pure${extFromUrl(pure, ".mp4")}` });
9368
+ return files;
9369
+ },
9370
+ mapResult(out) {
9371
+ return { summary: typeof out.summary === "string" ? out.summary : "", asr: out.asr ?? null };
9372
+ }
9373
+ };
8307
9374
  var mad = {
8308
9375
  name: "mad",
8309
9376
  title: "一键剪 MAD",
8310
9377
  description: "素材文件夹(3~10 条视频)+ 可选 BGM → 自动选技法 → 单一 .jsx,AE 2020+ 跑一遍出 15~30s 卡点成片工程。仅支持 AE。",
8311
9378
  kind: "local",
8312
9379
  input: { kind: "directory" },
8313
- billingHint: "免费(--bgm 卡点时计费一次云端节拍分析 audio_music_analyze",
9380
+ priceKey: "audio_music_analyze",
9381
+ pricingContext: "仅 --bgm 卡点时",
8314
9382
  outputHint: "AE 母合成工程 .jsx",
8315
9383
  enabled: true,
8316
9384
  options: [
@@ -8321,7 +9389,35 @@ var mad = {
8321
9389
  ]
8322
9390
  };
8323
9391
  var RESERVED_NAMES = new Set(["list"]);
8324
- var TOOL_REGISTRY = [imageMove, imageMatting, videoMatting, mad];
9392
+ var TOOL_REGISTRY = [
9393
+ imageMove,
9394
+ imageMatting,
9395
+ imageBlackborderRemove,
9396
+ imageCanvasAdapt,
9397
+ imagePurify,
9398
+ videoMatting,
9399
+ videoBlackborderRemove,
9400
+ videoCanvasAdapt,
9401
+ videoStabilizer,
9402
+ videoVaporwave,
9403
+ videoPurify,
9404
+ videoUpscale,
9405
+ videoInterpolate,
9406
+ audioSeparation,
9407
+ audioNoiseReduce,
9408
+ audioSilenceRemove,
9409
+ videoSegment,
9410
+ videoAiSegment,
9411
+ videoMotionCut,
9412
+ audioSpeakerSplit,
9413
+ audioStretch,
9414
+ pianoAudioToMidi,
9415
+ pianoAudioEnhance,
9416
+ imageToSquare,
9417
+ imageToLive,
9418
+ videoAiSubtitle,
9419
+ mad
9420
+ ];
8325
9421
  function findTool(name, registry = TOOL_REGISTRY) {
8326
9422
  return registry.find((d) => d.name === name);
8327
9423
  }
@@ -8339,15 +9435,101 @@ function validateRegistry(registry = TOOL_REGISTRY) {
8339
9435
  throw new Error(`未启用工具缺 disabledReason:「${d.name}」`);
8340
9436
  if (d.kind === "cloud" && !d.taskType)
8341
9437
  throw new Error(`cloud 型工具缺 taskType:「${d.name}」`);
9438
+ if (d.kind === "cloud" && !d.priceKey)
9439
+ throw new Error(`cloud 型工具缺 priceKey:「${d.name}」`);
9440
+ if (d.kind === "cloud" && !d.mapOutputs && !d.mapResult) {
9441
+ throw new Error(`cloud 型工具须至少声明 mapOutputs 或 mapResult 之一:「${d.name}」`);
9442
+ }
8342
9443
  }
8343
9444
  }
8344
9445
 
8345
9446
  // src/lib/tool-runner.ts
8346
- import { resolve as resolve9, join as join21, dirname as dirname8, basename as basename11, extname as extname5 } from "node:path";
9447
+ import { resolve as resolve10, join as join21, dirname as dirname9, basename as basename11, extname as extname5 } from "node:path";
8347
9448
  import { mkdir as mkdir8, writeFile as writeFile8, stat as stat5 } from "node:fs/promises";
8348
9449
  import { createWriteStream, existsSync as existsSync17 } from "node:fs";
8349
9450
  import { Readable as Readable2 } from "node:stream";
8350
9451
  import { pipeline } from "node:stream/promises";
9452
+
9453
+ // src/lib/tool-pricing.ts
9454
+ var TOOL_PRICE_LIST_URL = "https://cloud.ai-mcn.tv/api/get_price_list";
9455
+ var PRICE_UNAVAILABLE_HINT = "实时价格暂不可用,以服务端结算为准";
9456
+ var PRICE_REQUEST_TIMEOUT_MS = 5000;
9457
+ function validNumber(v) {
9458
+ return typeof v === "number" && Number.isFinite(v);
9459
+ }
9460
+ function parseToolPriceList(value) {
9461
+ if (!Array.isArray(value))
9462
+ throw new Error("价格表响应不是数组");
9463
+ const prices = new Map;
9464
+ for (const raw of value) {
9465
+ if (!raw || typeof raw !== "object")
9466
+ continue;
9467
+ const item = raw;
9468
+ const key = typeof item.key === "string" ? item.key.trim() : "";
9469
+ const measure = typeof item.measure === "string" ? item.measure.trim() : "";
9470
+ if (!key || !measure || !validNumber(item.price) || !validNumber(item.exPrice))
9471
+ continue;
9472
+ prices.set(key, {
9473
+ ...validNumber(item.taskTypeId) ? { taskTypeId: item.taskTypeId } : {},
9474
+ ...typeof item.name === "string" ? { name: item.name } : {},
9475
+ key,
9476
+ price: item.price,
9477
+ exPrice: item.exPrice,
9478
+ measure,
9479
+ ...typeof item.note === "string" ? { note: item.note } : {}
9480
+ });
9481
+ }
9482
+ return prices;
9483
+ }
9484
+ async function fetchToolPrices(fetchFn = fetch) {
9485
+ const res = await fetchFn(TOOL_PRICE_LIST_URL, {
9486
+ method: "GET",
9487
+ headers: { Accept: "application/json" },
9488
+ signal: AbortSignal.timeout(PRICE_REQUEST_TIMEOUT_MS)
9489
+ });
9490
+ if (!res.ok)
9491
+ throw new Error(`价格表请求失败 HTTP ${res.status}`);
9492
+ return parseToolPriceList(await res.json());
9493
+ }
9494
+ function formatToolPrice(item, pricingContext) {
9495
+ let price;
9496
+ if (item.price === 0 && item.exPrice === 0) {
9497
+ price = `免费(0 积分/${item.measure})`;
9498
+ } else if (item.price === item.exPrice) {
9499
+ price = `${item.price} 积分/${item.measure}`;
9500
+ } else {
9501
+ price = `标准价 ${item.price} 积分/${item.measure};超额价 ${item.exPrice} 积分/${item.measure}`;
9502
+ }
9503
+ return pricingContext ? `${pricingContext}:${price}` : price;
9504
+ }
9505
+ function resolveToolPricingFromMap(priceKey, prices, pricingContext) {
9506
+ const item = prices?.get(priceKey);
9507
+ if (!item) {
9508
+ return {
9509
+ billingHint: pricingContext ? `${pricingContext}:${PRICE_UNAVAILABLE_HINT}` : PRICE_UNAVAILABLE_HINT,
9510
+ pricing: { key: priceKey, available: false }
9511
+ };
9512
+ }
9513
+ return {
9514
+ billingHint: formatToolPrice(item, pricingContext),
9515
+ pricing: {
9516
+ key: priceKey,
9517
+ available: true,
9518
+ price: item.price,
9519
+ exPrice: item.exPrice,
9520
+ measure: item.measure
9521
+ }
9522
+ };
9523
+ }
9524
+ async function resolveToolPricing(priceKey, pricingContext, fetchFn = fetch) {
9525
+ try {
9526
+ return resolveToolPricingFromMap(priceKey, await fetchToolPrices(fetchFn), pricingContext);
9527
+ } catch {
9528
+ return resolveToolPricingFromMap(priceKey, undefined, pricingContext);
9529
+ }
9530
+ }
9531
+
9532
+ // src/lib/tool-runner.ts
8351
9533
  var DEFAULT_POLL_TIMEOUT_MS = 30 * 60 * 1000;
8352
9534
  var DEFAULT_POLL_INTERVAL_MS = 5000;
8353
9535
  function coerceValue2(v) {
@@ -8461,10 +9643,10 @@ function timestamp3() {
8461
9643
  }
8462
9644
  function resolveOutDir(descriptor, inputAbs, out) {
8463
9645
  if (out)
8464
- return resolve9(out);
9646
+ return resolve10(out);
8465
9647
  if (inputAbs) {
8466
9648
  const base = basename11(inputAbs, extname5(inputAbs));
8467
- return join21(dirname8(inputAbs), `${base}-${descriptor.name}`);
9649
+ return join21(dirname9(inputAbs), `${base}-${descriptor.name}`);
8468
9650
  }
8469
9651
  return join21(process.cwd(), `${descriptor.name}-${timestamp3()}`);
8470
9652
  }
@@ -8481,7 +9663,7 @@ function emitBilling(hint) {
8481
9663
  `);
8482
9664
  }
8483
9665
  async function runCloudTool(descriptor, inputArg, opts, deps) {
8484
- const inputAbs = inputArg ? resolve9(inputArg) : undefined;
9666
+ const inputAbs = inputArg ? resolve10(inputArg) : undefined;
8485
9667
  const baseName = inputAbs ? basename11(inputAbs, extname5(inputAbs)) : descriptor.name;
8486
9668
  validateToolInput(descriptor, inputAbs);
8487
9669
  const probe = deps.probeDurationSec ?? probeDuration;
@@ -8502,25 +9684,27 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
8502
9684
  uploadPath = await descriptor.preprocess(ctx);
8503
9685
  if (!uploadPath)
8504
9686
  throw new Error(`${descriptor.name} 缺上传物(input=none 的 cloud 型工具需 preprocess 产上传物)`);
8505
- emitBilling(descriptor.billingHint);
8506
- let up = await deps.uploadCached(deps.cfg, uploadPath, { force: opts.reupload });
9687
+ let billingHint;
9688
+ try {
9689
+ billingHint = (await (deps.resolvePricing ?? resolveToolPricing)(descriptor.priceKey, descriptor.pricingContext)).billingHint;
9690
+ } catch {
9691
+ billingHint = "实时价格暂不可用,以服务端结算为准";
9692
+ }
9693
+ emitBilling(billingHint);
8507
9694
  const buildPayload = (fid) => {
8508
9695
  const p = descriptor.buildPayload ? descriptor.buildPayload(fid, ctx) : { file_id: fid };
8509
9696
  mergeParams(p, extraParams);
8510
9697
  return p;
8511
9698
  };
8512
9699
  const taskType = descriptor.taskType;
8513
- let taskId;
8514
- try {
8515
- taskId = await deps.submitTask(deps.cfg, taskType, buildPayload(up.fileId));
8516
- } catch (e) {
8517
- if (up.cached && isCloudErrorCode(e) === 6004) {
8518
- await deps.invalidateUpload(uploadPath);
8519
- up = await deps.uploadCached(deps.cfg, uploadPath, { force: true });
8520
- taskId = await deps.submitTask(deps.cfg, taskType, buildPayload(up.fileId));
8521
- } else
8522
- throw e;
8523
- }
9700
+ const submitted = await uploadAndSubmitTask(deps.cfg, uploadPath, taskType, buildPayload, { force: opts.reupload }, {
9701
+ uploadCached: deps.uploadCached,
9702
+ invalidateUpload: deps.invalidateUpload,
9703
+ submitTask: deps.submitTask,
9704
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
9705
+ });
9706
+ const { taskId } = submitted;
9707
+ const up = { fileId: submitted.fileId, cached: submitted.cached };
8524
9708
  await mkdir8(outDir, { recursive: true });
8525
9709
  const fingerprint2 = inputAbs ? await safeFingerprint(inputAbs) : undefined;
8526
9710
  await writeFile8(join21(outDir, "task.json"), JSON.stringify({ tool: descriptor.name, taskType, taskId, fileId: up.fileId, source: inputAbs, fingerprint: fingerprint2, createdAt: new Date().toISOString() }, null, 2));
@@ -8531,11 +9715,10 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
8531
9715
  sleep: deps.sleep,
8532
9716
  now: deps.now
8533
9717
  });
8534
- const items = descriptor.mapOutputs ? descriptor.mapOutputs(output, ctx) : [];
9718
+ const outputResult = output;
8535
9719
  const files = [];
8536
9720
  const errors = {};
8537
- if (items.length === 0)
8538
- errors["output"] = "任务完成但未解析到产物下载链接(output_result 形态异常)";
9721
+ const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
8539
9722
  for (const it of items) {
8540
9723
  const dest = join21(outDir, it.filename);
8541
9724
  try {
@@ -8545,7 +9728,16 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
8545
9728
  errors[it.filename] = e instanceof Error ? e.message : String(e);
8546
9729
  }
8547
9730
  }
8548
- const ok = files.length > 0 && Object.keys(errors).length === 0;
9731
+ let resultFile;
9732
+ const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
9733
+ if (structured != null) {
9734
+ resultFile = join21(outDir, "result-output.json");
9735
+ await writeFile8(resultFile, JSON.stringify(structured, null, 2));
9736
+ }
9737
+ if (items.length === 0 && resultFile == null) {
9738
+ errors["output"] = "任务完成但未解析到任何产物(下载链接与结构化结果均为空,output_result 形态异常)";
9739
+ }
9740
+ const ok = Object.keys(errors).length === 0 && (files.length > 0 || resultFile != null);
8549
9741
  const result = {
8550
9742
  ok,
8551
9743
  tool: descriptor.name,
@@ -8554,6 +9746,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
8554
9746
  fileId: up.fileId,
8555
9747
  outDir,
8556
9748
  files,
9749
+ ...resultFile ? { resultFile } : {},
8557
9750
  ...Object.keys(errors).length ? { errors } : {}
8558
9751
  };
8559
9752
  await writeFile8(join21(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
@@ -8563,7 +9756,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
8563
9756
  // src/lib/mad/mad.ts
8564
9757
  import { mkdir as mkdir11, writeFile as writeFile10 } from "node:fs/promises";
8565
9758
  import { existsSync as existsSync20, statSync as statSync3 } from "node:fs";
8566
- import { resolve as resolve10, join as join25 } from "node:path";
9759
+ import { resolve as resolve11, join as join25 } from "node:path";
8567
9760
 
8568
9761
  // src/lib/convert/types.ts
8569
9762
  function num(v, d = 0) {
@@ -9733,9 +10926,6 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
9733
10926
 
9734
10927
  // src/lib/mad/cloud-beat.ts
9735
10928
  var ANALYZE_TASK = "audio_music_analyze";
9736
- function isCode(e, code) {
9737
- return !!e && typeof e === "object" && e.code === code;
9738
- }
9739
10929
  function extractAnalysis(output) {
9740
10930
  const o = output ?? {};
9741
10931
  const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "number" && Number.isFinite(x)) : [];
@@ -9746,19 +10936,14 @@ function extractAnalysis(output) {
9746
10936
  };
9747
10937
  }
9748
10938
  async function analyzeBgm(cfg, bgmAbs, deps) {
9749
- let up = await deps.uploadCached(cfg, bgmAbs, {});
9750
10939
  const payload = (fid) => ({ file_id: fid });
9751
- let taskId;
9752
- try {
9753
- taskId = await deps.submitTask(cfg, ANALYZE_TASK, payload(up.fileId));
9754
- } catch (e) {
9755
- if (up.cached && isCode(e, 6004)) {
9756
- await deps.invalidateUpload(bgmAbs);
9757
- up = await deps.uploadCached(cfg, bgmAbs, { force: true });
9758
- taskId = await deps.submitTask(cfg, ANALYZE_TASK, payload(up.fileId));
9759
- } else
9760
- throw e;
9761
- }
10940
+ const submitted = await uploadAndSubmitTask(cfg, bgmAbs, ANALYZE_TASK, payload, {}, {
10941
+ uploadCached: deps.uploadCached,
10942
+ invalidateUpload: deps.invalidateUpload,
10943
+ submitTask: deps.submitTask,
10944
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
10945
+ });
10946
+ const { taskId } = submitted;
9762
10947
  const output = await deps.pollToolTask(cfg, ANALYZE_TASK, taskId, {});
9763
10948
  return extractAnalysis(output);
9764
10949
  }
@@ -9817,7 +11002,7 @@ async function runMad(inputArg, opts, deps = {}) {
9817
11002
  const probeDur = deps.probeDurationFn ?? probeDuration;
9818
11003
  if (!inputArg)
9819
11004
  throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
9820
- const dirAbs = resolve10(inputArg);
11005
+ const dirAbs = resolve11(inputArg);
9821
11006
  if (!existsSync20(dirAbs) || !statSync3(dirAbs).isDirectory()) {
9822
11007
  throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
9823
11008
  }
@@ -9849,7 +11034,7 @@ async function runMad(inputArg, opts, deps = {}) {
9849
11034
  let level = 3;
9850
11035
  let analysis = null;
9851
11036
  let bgmForTrack;
9852
- const bgmAbs = opts.bgm ? resolve10(opts.bgm) : undefined;
11037
+ const bgmAbs = opts.bgm ? resolve11(opts.bgm) : undefined;
9853
11038
  if (bgmAbs) {
9854
11039
  let dur = -1;
9855
11040
  try {
@@ -9872,7 +11057,8 @@ async function runMad(inputArg, opts, deps = {}) {
9872
11057
  level = 2;
9873
11058
  bgmForTrack = bgmAbs;
9874
11059
  } else {
9875
- emitBilling2("BGM 卡点将调用一次云端节拍分析(audio_music_analyze),按现行价计费。");
11060
+ const pricing = await (deps.resolvePricing ?? resolveToolPricing)("audio_music_analyze", "仅 --bgm 卡点时");
11061
+ emitBilling2(pricing.billingHint);
9876
11062
  const beatCloud = deps.beatCloud ?? { uploadCached, invalidateUpload, submitTask, pollToolTask };
9877
11063
  try {
9878
11064
  analysis = await analyzeBgm(cfg, bgmAbs, beatCloud);
@@ -9931,7 +11117,7 @@ async function runMad(inputArg, opts, deps = {}) {
9931
11117
  const header = madHeader(version, now.toISOString());
9932
11118
  const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
9933
11119
  const { jsx } = madJsx({ master, windows, header, bgm });
9934
- const outDir = opts.out ? resolve10(opts.out) : join25(process.cwd(), `mad-${timestamp4(now)}`);
11120
+ const outDir = opts.out ? resolve11(opts.out) : join25(process.cwd(), `mad-${timestamp4(now)}`);
9935
11121
  await mkdir11(outDir, { recursive: true });
9936
11122
  const jsxPath = join25(outDir, "mad.jsx");
9937
11123
  await writeFile10(jsxPath, jsx);
@@ -9984,7 +11170,7 @@ async function runToolCommand(words, opts, registry = TOOL_REGISTRY, deps) {
9984
11170
  throw new Error("用法:`gtrk tool <name> [input]` 跑工具;`gtrk tool list` 查全部工具");
9985
11171
  }
9986
11172
  if (name === "list") {
9987
- runList(opts, registry);
11173
+ await runList(opts, registry);
9988
11174
  return;
9989
11175
  }
9990
11176
  const descriptor = findTool(name, registry);
@@ -9994,16 +11180,26 @@ async function runToolCommand(words, opts, registry = TOOL_REGISTRY, deps) {
9994
11180
  }
9995
11181
  return runTool(descriptor, words[1], opts, deps);
9996
11182
  }
9997
- function runList(opts, registry = TOOL_REGISTRY) {
9998
- const rows = registry.map((d) => ({
9999
- name: d.name,
10000
- title: d.title,
10001
- input: d.input.kind,
10002
- output: d.outputHint,
10003
- billingHint: d.billingHint,
10004
- enabled: d.enabled,
10005
- ...d.disabledReason ? { disabledReason: d.disabledReason } : {}
10006
- }));
11183
+ async function runList(opts, registry = TOOL_REGISTRY, loadPrices = fetchToolPrices) {
11184
+ let prices;
11185
+ try {
11186
+ prices = await loadPrices();
11187
+ } catch {
11188
+ prices = undefined;
11189
+ }
11190
+ const rows = registry.map((d) => {
11191
+ const resolved = resolveToolPricingFromMap(d.priceKey ?? d.name, prices, d.pricingContext);
11192
+ return {
11193
+ name: d.name,
11194
+ title: d.title,
11195
+ input: d.input.kind,
11196
+ output: d.outputHint,
11197
+ billingHint: resolved.billingHint,
11198
+ pricing: resolved.pricing,
11199
+ enabled: d.enabled,
11200
+ ...d.disabledReason ? { disabledReason: d.disabledReason } : {}
11201
+ };
11202
+ });
10007
11203
  if (opts.json) {
10008
11204
  console.log(JSON.stringify(rows));
10009
11205
  return;
@@ -10066,12 +11262,461 @@ async function runMadInTool(inputArg, opts) {
10066
11262
  return { ok: r.ok, tool: r.tool, outDir: r.outDir, files: r.files };
10067
11263
  }
10068
11264
 
11265
+ // src/commands/transcript.ts
11266
+ import { existsSync as existsSync21 } from "node:fs";
11267
+ import { mkdir as mkdir12, rename as rename2, rm as rm2, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
11268
+ import { basename as basename12, dirname as dirname10, extname as extname7, join as join26, resolve as resolve12 } from "node:path";
11269
+
11270
+ // src/lib/transcript.ts
11271
+ var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
11272
+ function finiteNumber(value) {
11273
+ const n = typeof value === "number" ? value : Number(value);
11274
+ return Number.isFinite(n) && n >= 0 ? n : undefined;
11275
+ }
11276
+ function timeFrom(item, secondsKey, msKey, shortKey) {
11277
+ const seconds = finiteNumber(item[secondsKey]);
11278
+ if (seconds != null)
11279
+ return seconds;
11280
+ const milliseconds = finiteNumber(item[msKey]);
11281
+ if (milliseconds != null)
11282
+ return milliseconds / 1000;
11283
+ return finiteNumber(item[shortKey]) ?? 0;
11284
+ }
11285
+ function normalizeTimedList(value) {
11286
+ if (!Array.isArray(value))
11287
+ return [];
11288
+ const items = [];
11289
+ for (const raw of value) {
11290
+ if (!raw || typeof raw !== "object")
11291
+ continue;
11292
+ const item = raw;
11293
+ const text = typeof item.text === "string" ? item.text.trim() : "";
11294
+ if (!text)
11295
+ continue;
11296
+ const start = timeFrom(item, "start_time", "begin_time_ms", "st");
11297
+ const end = timeFrom(item, "end_time", "end_time_ms", "ed");
11298
+ items.push({ text, start, end: Math.max(start, end) });
11299
+ }
11300
+ return items.sort((a, b) => a.start - b.start || a.end - b.end);
11301
+ }
11302
+ function normalizeAsrOutput(output) {
11303
+ let sentences = normalizeTimedList(output.sentence_tc_list ?? output.sentence_list);
11304
+ const words = normalizeTimedList(output.word_tc_list ?? output.word_list);
11305
+ let text = "";
11306
+ for (const key of ["asr_text", "text"]) {
11307
+ const value = output[key];
11308
+ if (typeof value === "string" && value.trim()) {
11309
+ text = value.trim();
11310
+ break;
11311
+ }
11312
+ }
11313
+ if (sentences.length === 0 && words.length > 0) {
11314
+ sentences = [{
11315
+ text: text || words.map((word) => word.text).join(""),
11316
+ start: words[0]?.start ?? 0,
11317
+ end: words[words.length - 1]?.end ?? 0
11318
+ }];
11319
+ }
11320
+ if (!text && sentences.length > 0)
11321
+ text = sentences.map((sentence) => sentence.text).join(`
11322
+ `);
11323
+ if (!text.trim() || sentences.length === 0) {
11324
+ throw new Error("ASR 任务已完成,但没有返回可用的文字或句级时间戳");
11325
+ }
11326
+ return { text: text.trim(), sentences, words };
11327
+ }
11328
+ function formatTimestamp(seconds) {
11329
+ const total = Math.max(0, Math.floor(Number.isFinite(seconds) ? seconds : 0));
11330
+ const h = Math.floor(total / 3600);
11331
+ const m = Math.floor(total % 3600 / 60);
11332
+ const s = total % 60;
11333
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
11334
+ }
11335
+ function ensureTerminalPunctuation(text) {
11336
+ const value = text.trim();
11337
+ if (!value)
11338
+ return value;
11339
+ return /[。!?!?;;::]$/.test(value) ? value : `${value}。`;
11340
+ }
11341
+ function localDateTime(date) {
11342
+ const p = (value) => String(value).padStart(2, "0");
11343
+ return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}`;
11344
+ }
11345
+ function renderTranscriptMarkdown(input) {
11346
+ const timed = input.asr.sentences.flatMap((sentence) => [
11347
+ `**[${formatTimestamp(sentence.start)}]**`,
11348
+ "",
11349
+ ensureTerminalPunctuation(sentence.text),
11350
+ ""
11351
+ ]);
11352
+ return [
11353
+ `# ${input.title}`,
11354
+ "",
11355
+ `> 生成时间:${localDateTime(input.generatedAt)} `,
11356
+ `> 视频时长:${formatTimestamp(input.durationSec)} `,
11357
+ `> 来源:本地视频 \`${input.sourceName}\` `,
11358
+ `> 识别语言:${input.language}`,
11359
+ "",
11360
+ "## 总结",
11361
+ "",
11362
+ AGENT_SUMMARY_PENDING,
11363
+ "> 待驱动 CLI 的 Agent 阅读下方完整文字稿后,在此生成总结。",
11364
+ "",
11365
+ "## 文字记录",
11366
+ "",
11367
+ ...timed,
11368
+ "## 纯文本",
11369
+ "",
11370
+ input.asr.text.trim(),
11371
+ ""
11372
+ ].join(`
11373
+ `);
11374
+ }
11375
+
11376
+ // src/commands/transcript.ts
11377
+ var TASK_TYPE3 = "asr";
11378
+ var PRICE_KEY = "asr";
11379
+ function buildDeps(overrides = {}) {
11380
+ return {
11381
+ cfg: overrides.cfg ?? loadConfig(),
11382
+ probe: overrides.probe ?? probeGeometry,
11383
+ extract: overrides.extract ?? extractAudio,
11384
+ assertDuration: overrides.assertDuration ?? assertDurationConsistent,
11385
+ resolvePricing: overrides.resolvePricing ?? ((key) => resolveToolPricing(key)),
11386
+ upload: overrides.upload ?? uploadCached,
11387
+ invalidate: overrides.invalidate ?? invalidateUpload,
11388
+ submit: overrides.submit ?? submitTask,
11389
+ sleep: overrides.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms))),
11390
+ poll: overrides.poll ?? (async (cfg, taskType, taskId, onTick) => await pollToolTask(cfg, taskType, taskId, { onTick })),
11391
+ writeMarkdown: overrides.writeMarkdown ?? writeMarkdownAtomic,
11392
+ now: overrides.now ?? (() => new Date)
11393
+ };
11394
+ }
11395
+ function looksLikeRemote(value) {
11396
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(value.trim());
11397
+ }
11398
+ async function validateTranscriptInput(input) {
11399
+ if (!input.trim())
11400
+ throw new Error("缺少本地视频路径。用法:gtrk transcript <本地视频>");
11401
+ if (looksLikeRemote(input)) {
11402
+ throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
11403
+ }
11404
+ const inputAbs = resolve12(input);
11405
+ if (!existsSync21(inputAbs))
11406
+ throw new Error(`本地视频不存在:${inputAbs}`);
11407
+ const info = await stat6(inputAbs);
11408
+ if (!info.isFile())
11409
+ throw new Error(`输入不是文件:${inputAbs}`);
11410
+ const extension = extname7(inputAbs).toLowerCase();
11411
+ if (!(defaultExtsFor("video") ?? []).includes(extension)) {
11412
+ throw new Error(`不支持的视频格式「${extension || "无扩展名"}」;请输入本地视频文件`);
11413
+ }
11414
+ return inputAbs;
11415
+ }
11416
+ function resolveTranscriptOutput(inputAbs, out) {
11417
+ const base = basename12(inputAbs, extname7(inputAbs));
11418
+ const output = out ? resolve12(out) : join26(dirname10(inputAbs), `${base}-transcript.md`);
11419
+ if (extname7(output).toLowerCase() !== ".md")
11420
+ throw new Error("--out 必须指向一个 .md 文件");
11421
+ return output;
11422
+ }
11423
+ async function writeMarkdownAtomic(path, markdown) {
11424
+ await mkdir12(dirname10(path), { recursive: true });
11425
+ const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
11426
+ try {
11427
+ await writeFile11(temp, markdown, "utf8");
11428
+ await rename2(temp, path);
11429
+ } finally {
11430
+ await rm2(temp, { force: true });
11431
+ }
11432
+ }
11433
+ async function runTranscript(input, opts = {}, depsOverride) {
11434
+ if (opts.json)
11435
+ routeLogsToStderr();
11436
+ const inputAbs = await validateTranscriptInput(input);
11437
+ const output = resolveTranscriptOutput(inputAbs, opts.out);
11438
+ const deps = buildDeps(depsOverride);
11439
+ const language = opts.lang?.trim() || "zh-CN";
11440
+ const sourceName = basename12(inputAbs);
11441
+ const title = basename12(inputAbs, extname7(inputAbs));
11442
+ log.step(`▶ 视频转文字稿:${sourceName}`);
11443
+ log.step("① 本地探测视频…");
11444
+ const geometry = deps.probe(inputAbs, opts.ffmpegPath);
11445
+ if (!(geometry.duration > 0))
11446
+ throw new Error("未探测到有效视频时长,无法转写");
11447
+ log.info(`视频时长 ${geometry.duration.toFixed(1)}s`);
11448
+ const pricing = await deps.resolvePricing(PRICE_KEY);
11449
+ log.info(`实时计费:${pricing.billingHint}`);
11450
+ log.step("② 本地抽取 16k 单声道音频(原视频不上传)…");
11451
+ const audio = await deps.extract(inputAbs, opts.ffmpegPath);
11452
+ deps.assertDuration(geometry.duration, audio, opts.ffmpegPath);
11453
+ log.info(`上传物:${basename12(audio)}(仅音频衍生物)`);
11454
+ log.step("③ 上传音频并提交 ASR…");
11455
+ const payload = (fileId) => ({ file_id: fileId, language, word_level: true });
11456
+ const submitted = await uploadAndSubmitTask(deps.cfg, audio, TASK_TYPE3, payload, {
11457
+ force: opts.reupload,
11458
+ onCacheInvalid: () => log.warn("缓存的 file_id 已失效,重新上传后重试…")
11459
+ }, {
11460
+ uploadCached: deps.upload,
11461
+ invalidateUpload: deps.invalidate,
11462
+ submitTask: deps.submit,
11463
+ sleep: deps.sleep
11464
+ });
11465
+ const { taskId } = submitted;
11466
+ const uploaded = { fileId: submitted.fileId, cached: submitted.cached };
11467
+ log.info(`task_id = ${taskId}`);
11468
+ log.step("④ 云端识别中…");
11469
+ const raw = await deps.poll(deps.cfg, TASK_TYPE3, taskId, (status, progress) => {
11470
+ log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
11471
+ });
11472
+ log.tickEnd();
11473
+ const asr = normalizeAsrOutput(raw);
11474
+ const markdown = renderTranscriptMarkdown({
11475
+ title,
11476
+ sourceName,
11477
+ durationSec: geometry.duration,
11478
+ language,
11479
+ generatedAt: deps.now(),
11480
+ asr
11481
+ });
11482
+ await deps.writeMarkdown(output, markdown);
11483
+ return { ok: true, taskId, fileId: uploaded.fileId, output, summaryPending: true };
11484
+ }
11485
+ function configureTranscriptCommand(cmd, deps) {
11486
+ return cmd.description("本地视频转文字稿:原视频不上传,只上传抽取音频,生成单个待 Agent 补总结的 Markdown").option("-o, --out <file>", "输出 Markdown 文件(缺省 <视频同目录>/<视频名>-transcript.md)").option("--lang <code>", "识别语言代码(默认 zh-CN)", "zh-CN").option("--ffmpeg-path <dir>", "指定 ffmpeg/ffprobe 所在目录").option("--reupload", "强制重新上传抽取音频,忽略上传缓存").option("--json", "机读模式:stdout 只输出最终结果 JSON").action(async (video, opts) => {
11487
+ const result = await runTranscript(video, opts, deps);
11488
+ if (opts.json)
11489
+ console.log(JSON.stringify(result));
11490
+ else {
11491
+ log.ok(`带时码文字稿已生成:${result.output}`);
11492
+ log.warn("总结仍待驱动 CLI 的 Agent 阅读全文后写回同一个 Markdown");
11493
+ }
11494
+ });
11495
+ }
11496
+ function registerTranscript(program2) {
11497
+ configureTranscriptCommand(program2.command("transcript <video>"));
11498
+ }
11499
+
11500
+ // src/commands/music-visualizer.ts
11501
+ import { resolve as resolve13, join as join27, dirname as dirname11, basename as basename13, extname as extname8 } from "node:path";
11502
+ import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
11503
+ import { existsSync as existsSync22 } from "node:fs";
11504
+ var TASK_TYPE4 = "music_visualizer";
11505
+ var PRICE_KEY2 = "music_visualizer";
11506
+ var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
11507
+ var AUDIO_EXTS2 = defaultExtsFor("audio") ?? [];
11508
+ var IMAGE_EXTS2 = defaultExtsFor("image") ?? [];
11509
+ var VIDEO_EXTS3 = defaultExtsFor("video") ?? [];
11510
+ var collectParam3 = (v, acc) => {
11511
+ acc.push(v);
11512
+ return acc;
11513
+ };
11514
+ function coerceValue3(v) {
11515
+ if (v === "true")
11516
+ return true;
11517
+ if (v === "false")
11518
+ return false;
11519
+ if (v.trim() !== "" && !Number.isNaN(Number(v)))
11520
+ return Number(v);
11521
+ return v;
11522
+ }
11523
+ function parseExtraParams3(pairs, jsonStr) {
11524
+ const out = {};
11525
+ for (const pair of pairs) {
11526
+ const i = pair.indexOf("=");
11527
+ if (i < 0)
11528
+ throw new Error(`--param 需要 key=value 格式:「${pair}」`);
11529
+ out[pair.slice(0, i).trim()] = coerceValue3(pair.slice(i + 1));
11530
+ }
11531
+ if (jsonStr) {
11532
+ let parsed;
11533
+ try {
11534
+ parsed = JSON.parse(jsonStr);
11535
+ } catch {
11536
+ throw new Error(`--params-json 不是合法 JSON:${jsonStr}`);
11537
+ }
11538
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
11539
+ throw new Error("--params-json 必须是一个 JSON 对象");
11540
+ }
11541
+ Object.assign(out, parsed);
11542
+ }
11543
+ return out;
11544
+ }
11545
+ function timestamp5() {
11546
+ const d = new Date;
11547
+ const p = (n) => String(n).padStart(2, "0");
11548
+ return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
11549
+ }
11550
+ function assertExt(pathAbs, exts, label) {
11551
+ if (!existsSync22(pathAbs))
11552
+ throw new Error(`${label}不存在:${pathAbs}`);
11553
+ const e = extname8(pathAbs).toLowerCase();
11554
+ if (exts.length && !exts.includes(e)) {
11555
+ throw new Error(`${label}扩展名不支持:${e}(支持 ${exts.join(" ")})`);
11556
+ }
11557
+ }
11558
+ function buildStyleFields(opts) {
11559
+ const fields = {};
11560
+ if (opts.track != null)
11561
+ fields.track = String(opts.track);
11562
+ if (opts.artist != null)
11563
+ fields.artist = String(opts.artist);
11564
+ if (opts.resolution != null) {
11565
+ const m = /^(\d+)[xX](\d+)$/.exec(opts.resolution.trim());
11566
+ if (!m)
11567
+ throw new Error(`--resolution 必须是 <宽>x<高> 格式(如 1920x1080):${opts.resolution}`);
11568
+ const width = Number(m[1]);
11569
+ const height = Number(m[2]);
11570
+ if (!(width > 0) || !(height > 0))
11571
+ throw new Error("--resolution 的宽高必须为正整数");
11572
+ fields.resolution = { width, height };
11573
+ }
11574
+ if (opts.fps != null) {
11575
+ const fps = Number(opts.fps);
11576
+ if (!Number.isInteger(fps) || fps < 30 || fps > 60)
11577
+ throw new Error("--fps 必须是 30 到 60 的整数");
11578
+ fields.fps = fps;
11579
+ }
11580
+ if (opts.c1 != null) {
11581
+ if (!HEX_RE.test(opts.c1))
11582
+ throw new Error("--c1 必须是十六进制颜色(如 #ff0066 或 #f06)");
11583
+ fields.c1 = opts.c1;
11584
+ }
11585
+ if (opts.c2 != null) {
11586
+ if (!HEX_RE.test(opts.c2))
11587
+ throw new Error("--c2 必须是十六进制颜色(如 #6600ff 或 #60f)");
11588
+ fields.c2 = opts.c2;
11589
+ }
11590
+ if (opts.blur != null) {
11591
+ const blur = Number(opts.blur);
11592
+ if (!Number.isInteger(blur) || blur < 0 || blur > 40)
11593
+ throw new Error("--blur 必须是 0 到 40 的整数");
11594
+ fields.blur = blur;
11595
+ }
11596
+ return fields;
11597
+ }
11598
+ function registerMusicVisualizer(program2) {
11599
+ program2.command("music-visualizer <audio>").description("音乐可视化:一首歌 → 频谱可视化成片(可选背景/封面 + 模板/配色样式)").option("-t, --template <id>", "可视化模板 id(必填;取值见云端 API 文档 / 服务端模板列表)").option("--background <图或视频>", "可选背景素材(图片或视频,独立上传)").option("--cover <图>", "可选封面图(仅图片,独立上传)").option("--track <名>", "曲名(叠加文字)").option("--artist <名>", "歌手名(叠加文字)").option("--resolution <WxH>", "输出分辨率(如 1080x1920;默认服务端 1920x1080)").option("--fps <n>", "帧率 30-60(默认服务端 30)").option("--c1 <hex>", "频谱主色十六进制(如 #ffffff;默认服务端白)").option("--c2 <hex>", "频谱第二色十六进制(给定则双色渐变)").option("--blur <n>", "背景模糊 0-40(默认服务端 16)").option("-o, --out <dir>", "产物目录(缺省 = <音频同目录>/<音频名>-visualizer-<时间戳>)").option("--param <k=v>", "透传任意云端参数(标量、可重复)", collectParam3, []).option("--params-json <json>", "透传任意云端参数(JSON 对象)").option("--reupload", "强制重新上传,忽略本地上传缓存").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (audio, opts) => {
11600
+ await runMusicVisualizer(audio, opts);
11601
+ });
11602
+ }
11603
+ async function runMusicVisualizer(audio, opts) {
11604
+ if (opts.json)
11605
+ routeLogsToStderr();
11606
+ const cfg = loadConfig();
11607
+ const audioAbs = resolve13(audio);
11608
+ assertExt(audioAbs, AUDIO_EXTS2, "音频");
11609
+ const template = opts.template == null ? "" : String(opts.template).trim();
11610
+ if (!template)
11611
+ throw new Error("--template 必填:请指定可视化模板 id(取值见云端 API 文档 / 服务端模板列表)");
11612
+ const styleFields = buildStyleFields(opts);
11613
+ const bgAbs = opts.background ? resolve13(opts.background) : undefined;
11614
+ if (bgAbs)
11615
+ assertExt(bgAbs, [...IMAGE_EXTS2, ...VIDEO_EXTS3], "背景素材");
11616
+ const coverAbs = opts.cover ? resolve13(opts.cover) : undefined;
11617
+ if (coverAbs)
11618
+ assertExt(coverAbs, IMAGE_EXTS2, "封面图");
11619
+ const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
11620
+ const projName = basename13(audioAbs, extname8(audioAbs));
11621
+ const outDir = resolve13(opts.out ?? join27(dirname11(audioAbs), `${projName}-visualizer-${timestamp5()}`));
11622
+ log.step(`▶ 音乐可视化:${basename13(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
11623
+ let billingHint;
11624
+ try {
11625
+ billingHint = (await resolveToolPricing(PRICE_KEY2)).billingHint;
11626
+ } catch {
11627
+ billingHint = "实时价格暂不可用,以服务端结算为准";
11628
+ }
11629
+ process.stderr.write(`\x1B[33m⚠️ 计费提示:${billingHint}\x1B[0m
11630
+ `);
11631
+ let backgroundFileId;
11632
+ if (bgAbs) {
11633
+ log.step("① 上传背景素材…");
11634
+ backgroundFileId = (await uploadCached(cfg, bgAbs, { force: opts.reupload })).fileId;
11635
+ log.info(`背景 file_id = ${backgroundFileId}`);
11636
+ }
11637
+ let coverFileId;
11638
+ if (coverAbs) {
11639
+ log.step("① 上传封面图…");
11640
+ coverFileId = (await uploadCached(cfg, coverAbs, { force: opts.reupload })).fileId;
11641
+ log.info(`封面 file_id = ${coverFileId}`);
11642
+ }
11643
+ const buildPayload = (fid) => {
11644
+ const p = { file_id: fid, template_id: template };
11645
+ if (backgroundFileId)
11646
+ p.background_file_id = backgroundFileId;
11647
+ if (coverFileId)
11648
+ p.cover_file_id = coverFileId;
11649
+ Object.assign(p, styleFields);
11650
+ for (const [k, v] of Object.entries(extraParams)) {
11651
+ const cur = p[k];
11652
+ const bothObj = !!cur && !!v && typeof cur === "object" && typeof v === "object" && !Array.isArray(cur) && !Array.isArray(v);
11653
+ p[k] = bothObj ? { ...cur, ...v } : v;
11654
+ }
11655
+ return p;
11656
+ };
11657
+ const submitted = await uploadAndSubmitTask(cfg, audioAbs, TASK_TYPE4, buildPayload, {
11658
+ force: opts.reupload,
11659
+ onUploaded: (u) => {
11660
+ log.info(u.cached ? `命中上传缓存,复用主音频 file_id = ${u.fileId}` : `主音频 file_id = ${u.fileId}`);
11661
+ log.step("② 提交音乐可视化任务…");
11662
+ },
11663
+ onCacheInvalid: () => log.warn("缓存的 file_id 在云端已失效,重新上传后重试…")
11664
+ });
11665
+ const { taskId } = submitted;
11666
+ log.info(`task_id = ${taskId}`);
11667
+ await mkdir13(outDir, { recursive: true });
11668
+ await writeFile12(join27(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE4, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
11669
+ log.step("③ 云端处理中(每 5s 轮询)…");
11670
+ const result = await pollTask(cfg, TASK_TYPE4, taskId, (status, progress) => {
11671
+ log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
11672
+ });
11673
+ log.tickEnd();
11674
+ const out = result;
11675
+ const url = [out.download_url, out.video_download_url, out.url].find((u) => typeof u === "string" && !!u.trim());
11676
+ const files = [];
11677
+ const errors = {};
11678
+ if (url) {
11679
+ const dest = join27(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
11680
+ try {
11681
+ await downloadStream(url, dest);
11682
+ files.push(dest);
11683
+ } catch (e) {
11684
+ errors["output"] = e instanceof Error ? e.message : String(e);
11685
+ }
11686
+ } else {
11687
+ errors["output"] = "任务完成但未解析到成片下载链接(output_result 形态异常)";
11688
+ }
11689
+ const ok = files.length > 0 && Object.keys(errors).length === 0;
11690
+ const resultJson = {
11691
+ ok,
11692
+ command: "music-visualizer",
11693
+ taskType: TASK_TYPE4,
11694
+ taskId,
11695
+ fileId: submitted.fileId,
11696
+ outDir,
11697
+ files,
11698
+ ...Object.keys(errors).length ? { errors } : {},
11699
+ finishedAt: new Date().toISOString()
11700
+ };
11701
+ await writeFile12(join27(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
11702
+ if (opts.json) {
11703
+ process.stdout.write(`${JSON.stringify(resultJson)}
11704
+ `);
11705
+ }
11706
+ if (!ok) {
11707
+ log.err(`产物下载失败:${JSON.stringify(errors)}。可凭 task.json 的 taskId 稍后恢复。`);
11708
+ process.exitCode = 1;
11709
+ return;
11710
+ }
11711
+ log.ok(`完成。产物目录:${outDir}`);
11712
+ }
11713
+
10069
11714
  // src/index.ts
10070
11715
  try {
10071
11716
  process.loadEnvFile?.();
10072
11717
  } catch {}
10073
11718
  migrateLegacyHome();
10074
- var { version } = JSON.parse(readFileSync5(join26(packageRoot(), "package.json"), "utf8"));
11719
+ var { version } = JSON.parse(readFileSync5(join28(packageRoot(), "package.json"), "utf8"));
10075
11720
  var program2 = new Command;
10076
11721
  program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
10077
11722
  registerInstall(program2);
@@ -10086,6 +11731,8 @@ registerSplit(program2);
10086
11731
  registerMatrix(program2);
10087
11732
  registerMg(program2);
10088
11733
  registerTool(program2);
11734
+ registerTranscript(program2);
11735
+ registerMusicVisualizer(program2);
10089
11736
  program2.parseAsync(process.argv).catch((e) => {
10090
11737
  console.error(`
10091
11738
  ❌ ${e instanceof Error ? e.message : String(e)}`);