@gitruck/cli 0.2.10 → 0.2.12

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 join27 } 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 = {
@@ -4285,40 +4286,247 @@ var SKILL_NAMES = [
4285
4286
  "gtrk-ai-drama",
4286
4287
  "gtrk-style-maker",
4287
4288
  "gtrk-transcript",
4288
- "gtrk-tools"
4289
+ "gtrk-tools",
4290
+ "gtrk-music-visualizer"
4289
4291
  ];
4290
- function installSkill(opts = {}) {
4291
- 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) {
4292
4420
  let allOk = true;
4293
4421
  for (const name of SKILL_NAMES) {
4294
- const src = join2(packageRoot(), "skills", name);
4295
- if (!existsSync2(join2(src, "SKILL.md"))) {
4296
- 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},不影响命令行)`);
4297
4425
  allOk = false;
4298
- continue;
4299
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;
4300
4437
  const dest = join2(destRoot, name);
4301
4438
  try {
4302
4439
  mkdirSync2(dest, { recursive: true });
4303
4440
  cpSync2(src, dest, { recursive: true });
4304
- log.ok(`已安装 /${name} → ${join2(dest, "SKILL.md")}`);
4305
- } catch (e) {
4441
+ log.ok(`已安装 ${name}${targetLabel ? ` → ${targetLabel}` : ""}:${join2(dest, "SKILL.md")}`);
4442
+ } catch (error) {
4306
4443
  allOk = false;
4307
- log.warn(`skill 安装失败(${name},不影响命令行使用):${e instanceof Error ? e.message : String(e)}`);
4444
+ log.warn(`skill 安装失败(${name},不影响命令行使用):${error instanceof Error ? error.message : String(error)}`);
4308
4445
  }
4309
4446
  }
4310
- log.info("在 Claude Code 里打 /gtrk-oralcut、/gtrk-transcript 或 /gtrk-tools,也可直接说「帮我剪个口播 / 把本地视频转成文字稿 / 给视频声音降噪」触发(可能需重载会话)。");
4447
+ if (targetLabel) {
4448
+ log.info(`已写入 ${targetLabel};若当前会话未出现新 Skill,请重启或刷新该 Agent。`);
4449
+ } else {
4450
+ log.info("已写入自定义 skills 目录;具体调用入口以该 Agent 的界面为准。");
4451
+ }
4311
4452
  return allOk;
4312
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
+ }
4313
4520
  function registerSkills(program2) {
4314
- const skills = program2.command("skills").description("管理 agent skill(安装到 Claude Code)");
4315
- skills.command("install").description("把 gtrk 全家框架 skill 安装到 ~/.claude/skills(对标飞书 skills add)").option("--dir <dir>", "自定义 skills 目录(缺省 ~/.claude/skills)").action((opts) => {
4316
- 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;
4317
4525
  });
4318
4526
  }
4319
4527
 
4320
4528
  // src/commands/init.ts
4321
- import { join as join9, resolve as resolve2 } from "node:path";
4529
+ import { join as join9, resolve as resolve3 } from "node:path";
4322
4530
  import { existsSync as existsSync8 } from "node:fs";
4323
4531
 
4324
4532
  // src/lib/user-config.ts
@@ -4346,7 +4554,7 @@ function writeUserConfig(patch) {
4346
4554
  }
4347
4555
 
4348
4556
  // src/lib/jianying.ts
4349
- import { join as join4, resolve } from "node:path";
4557
+ import { join as join4, resolve as resolve2 } from "node:path";
4350
4558
  import { existsSync as existsSync4 } from "node:fs";
4351
4559
  function probeJianyingDraftDir() {
4352
4560
  const local = process.env.LOCALAPPDATA;
@@ -4360,7 +4568,7 @@ function probeJianyingDraftDir() {
4360
4568
  }
4361
4569
  function resolveJianyingDraftDir(opt) {
4362
4570
  if (opt && opt !== "auto")
4363
- return resolve(opt);
4571
+ return resolve2(opt);
4364
4572
  const saved = readUserConfig().jianyingDraftDir;
4365
4573
  if (saved && existsSync4(saved))
4366
4574
  return saved;
@@ -4387,7 +4595,7 @@ function launch(cmd) {
4387
4595
 
4388
4596
  // src/lib/prompt.ts
4389
4597
  import { stdin, stdout } from "node:process";
4390
- import { spawnSync } from "node:child_process";
4598
+ import { spawnSync as spawnSync2 } from "node:child_process";
4391
4599
  var c2 = {
4392
4600
  dim: (s) => `\x1B[2m${s}\x1B[0m`,
4393
4601
  cyan: (s) => `\x1B[36m${s}\x1B[0m`
@@ -4395,24 +4603,24 @@ var c2 = {
4395
4603
  function readClipboard() {
4396
4604
  try {
4397
4605
  if (process.platform === "win32") {
4398
- const r = spawnSync("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], {
4606
+ const r = spawnSync2("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], {
4399
4607
  encoding: "utf8"
4400
4608
  });
4401
4609
  return (r.stdout ?? "").replace(/\r?\n$/, "");
4402
4610
  }
4403
4611
  if (process.platform === "darwin") {
4404
- return spawnSync("pbpaste", { encoding: "utf8" }).stdout ?? "";
4612
+ return spawnSync2("pbpaste", { encoding: "utf8" }).stdout ?? "";
4405
4613
  }
4406
- const x = spawnSync("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf8" });
4614
+ const x = spawnSync2("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf8" });
4407
4615
  if (x.status === 0)
4408
4616
  return x.stdout ?? "";
4409
- return spawnSync("wl-paste", ["-n"], { encoding: "utf8" }).stdout ?? "";
4617
+ return spawnSync2("wl-paste", ["-n"], { encoding: "utf8" }).stdout ?? "";
4410
4618
  } catch {
4411
4619
  return "";
4412
4620
  }
4413
4621
  }
4414
4622
  function ask(message, opts = {}) {
4415
- return new Promise((resolve2) => {
4623
+ return new Promise((resolve3) => {
4416
4624
  const hint = opts.defaultValue ? c2.dim(` (${opts.defaultValue})`) : "";
4417
4625
  stdout.write(`${c2.cyan("?")} ${message}${hint} `);
4418
4626
  let value = "";
@@ -4433,7 +4641,7 @@ function ask(message, opts = {}) {
4433
4641
  stdout.write(`
4434
4642
  `);
4435
4643
  cleanup();
4436
- resolve2(value || opts.defaultValue || "");
4644
+ resolve3(value || opts.defaultValue || "");
4437
4645
  return;
4438
4646
  }
4439
4647
  if (ch === "\x03") {
@@ -5140,7 +5348,7 @@ function effectiveVocab(config) {
5140
5348
  }
5141
5349
 
5142
5350
  // src/lib/ffmpeg.ts
5143
- import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
5351
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
5144
5352
  import { existsSync as existsSync6 } from "node:fs";
5145
5353
  import { join as join6 } from "node:path";
5146
5354
  var isWin = process.platform === "win32";
@@ -5149,7 +5357,7 @@ var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。请把二者放到 ${ffmpe
5149
5357
  var _cache = new Map;
5150
5358
  function onSystemPath(cmd) {
5151
5359
  try {
5152
- return spawnSync2(cmd, ["-version"], { stdio: "ignore" }).status === 0;
5360
+ return spawnSync3(cmd, ["-version"], { stdio: "ignore" }).status === 0;
5153
5361
  } catch {
5154
5362
  return false;
5155
5363
  }
@@ -5184,14 +5392,14 @@ function requireFfmpeg(ffmpegPath) {
5184
5392
  return r;
5185
5393
  }
5186
5394
  function ffprobeJson(ffprobePath, args) {
5187
- const r = spawnSync2(ffprobePath, args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
5395
+ const r = spawnSync3(ffprobePath, args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
5188
5396
  if (r.status !== 0) {
5189
5397
  throw new Error(`ffprobe 失败(code=${r.status}):${(r.stderr || "").slice(-300)}`);
5190
5398
  }
5191
5399
  return JSON.parse(r.stdout || "{}");
5192
5400
  }
5193
5401
  function runFfmpeg(ffmpegPath, args, onLine) {
5194
- return new Promise((resolve2, reject) => {
5402
+ return new Promise((resolve3, reject) => {
5195
5403
  const p = spawn2(ffmpegPath, args, { env: process.env });
5196
5404
  let tail = "";
5197
5405
  p.stderr.on("data", (buf) => {
@@ -5206,20 +5414,20 @@ function runFfmpeg(ffmpegPath, args, onLine) {
5206
5414
  p.on("error", (e) => reject(e));
5207
5415
  p.on("close", (code) => {
5208
5416
  if (code === 0)
5209
- resolve2();
5417
+ resolve3();
5210
5418
  else
5211
5419
  reject(new Error(`ffmpeg 退出码 ${code}:${tail.slice(-600)}`));
5212
5420
  });
5213
5421
  });
5214
5422
  }
5215
5423
  function probeCapabilities(res) {
5216
- const ver = spawnSync2(res.ffmpeg, ["-version"], { encoding: "utf8" });
5424
+ const ver = spawnSync3(res.ffmpeg, ["-version"], { encoding: "utf8" });
5217
5425
  const version = (ver.stdout || "").split(/\r?\n/)[0]?.trim() || "unknown";
5218
- const enc = spawnSync2(res.ffmpeg, ["-hide_banner", "-encoders"], {
5426
+ const enc = spawnSync3(res.ffmpeg, ["-hide_banner", "-encoders"], {
5219
5427
  encoding: "utf8",
5220
5428
  maxBuffer: 16 * 1024 * 1024
5221
5429
  });
5222
- const fil = spawnSync2(res.ffmpeg, ["-hide_banner", "-filters"], {
5430
+ const fil = spawnSync3(res.ffmpeg, ["-hide_banner", "-filters"], {
5223
5431
  encoding: "utf8",
5224
5432
  maxBuffer: 16 * 1024 * 1024
5225
5433
  });
@@ -5453,7 +5661,7 @@ async function runInit(opts) {
5453
5661
  const manual = (await promptText("剪映草稿根目录(…\\com.lveditor.draft),留空跳过:")).trim();
5454
5662
  if (manual) {
5455
5663
  if (existsSync8(manual))
5456
- jianyingDraftDir = resolve2(manual);
5664
+ jianyingDraftDir = resolve3(manual);
5457
5665
  else
5458
5666
  log.warn(`目录不存在,已跳过:${manual}`);
5459
5667
  }
@@ -5476,7 +5684,7 @@ async function afterConfigDoctor() {
5476
5684
  if (healthy) {
5477
5685
  log.step("装好了!两种用法任选:");
5478
5686
  log.info('① 命令行直接剪:gtrk oralcut "<毛片.mp4>" --script "<文字稿.txt>"(无稿就别加 --script)');
5479
- log.info("② 重启你常用的 AI agent(Claude / Codex / Trae / WorkBuddy 等),用 /gtrk-oralcut <你的口播剪辑需求>,一句话交给它,体验更智能的剪辑~");
5687
+ log.info("② 刷新你常用的 AI Agent,在它的 Skills 入口选择或点名 gtrk-oralcut;不同客户端也都可以直接描述剪辑需求触发。");
5480
5688
  log.info("想有自己栏目的风格体系?在 agent 里跑 /gtrk-style-maker 建一次「你的厨房」(skill 家族 + 栏目配置);不建就直接用默认,照常开剪。");
5481
5689
  } else {
5482
5690
  log.warn("上面体检有项没通,按提示处理好再开剪(多半是 API Key 或剪映目录)。");
@@ -5494,7 +5702,7 @@ async function runInitNonInteractive(opts) {
5494
5702
  let jianyingDraftDir;
5495
5703
  const dirOpt = opts.jianyingDraftDir;
5496
5704
  if (dirOpt && dirOpt !== "auto")
5497
- jianyingDraftDir = resolve2(dirOpt);
5705
+ jianyingDraftDir = resolve3(dirOpt);
5498
5706
  else if (dirOpt === "auto" || !existing.jianyingDraftDir)
5499
5707
  jianyingDraftDir = probeJianyingDraftDir();
5500
5708
  else
@@ -5510,16 +5718,23 @@ async function runInitNonInteractive(opts) {
5510
5718
 
5511
5719
  // src/commands/install.ts
5512
5720
  function registerInstall(program2) {
5513
- 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) => {
5514
5722
  log.step("① 安装 / 刷新 agent skill…");
5515
- 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;
5516
5731
  log.step("② 配置 + 体检…");
5517
5732
  await runInit(opts);
5518
5733
  });
5519
5734
  }
5520
5735
 
5521
5736
  // src/commands/oralcut.ts
5522
- 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";
5523
5738
  import { mkdir as mkdir4, writeFile as writeFile5, readFile as readFile3 } from "node:fs/promises";
5524
5739
  import { existsSync as existsSync12 } from "node:fs";
5525
5740
 
@@ -6007,7 +6222,7 @@ var defaultDeps = {
6007
6222
  uploadCached,
6008
6223
  invalidateUpload,
6009
6224
  submitTask,
6010
- sleep: (ms) => new Promise((resolve3) => setTimeout(resolve3, ms))
6225
+ sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms))
6011
6226
  };
6012
6227
  async function submitFreshFile(cfg, taskType, fileId, buildPayload, backoffMs, deps) {
6013
6228
  for (let attempt = 0;; attempt++) {
@@ -6532,7 +6747,7 @@ async function runOralCut(input, opts) {
6532
6747
  if (opts.json)
6533
6748
  routeLogsToStderr();
6534
6749
  const cfg = loadConfig();
6535
- const inputAbs = resolve3(input);
6750
+ const inputAbs = resolve4(input);
6536
6751
  if (!existsSync12(inputAbs))
6537
6752
  throw new Error(`毛片不存在:${inputAbs}`);
6538
6753
  const projName = basename5(inputAbs, extname2(inputAbs));
@@ -6540,10 +6755,10 @@ async function runOralCut(input, opts) {
6540
6755
  if (opts.render && !formats.includes("gtrk"))
6541
6756
  formats.push("gtrk");
6542
6757
  const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
6543
- const outDir = resolve3(opts.out ?? join14(dirname2(inputAbs), `${projName}-video-project-${timestamp()}`));
6544
- 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;
6545
6760
  if (!scriptPath) {
6546
- const sibling = join14(dirname2(inputAbs), `${projName}.txt`);
6761
+ const sibling = join14(dirname3(inputAbs), `${projName}.txt`);
6547
6762
  if (existsSync12(sibling)) {
6548
6763
  scriptPath = sibling;
6549
6764
  log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
@@ -6629,7 +6844,7 @@ async function runOralCut(input, opts) {
6629
6844
  }
6630
6845
 
6631
6846
  // src/commands/oralcut-result.ts
6632
- import { resolve as resolve4, join as join15 } from "node:path";
6847
+ import { resolve as resolve5, join as join15 } from "node:path";
6633
6848
  var TASK_TYPE2 = "cli/video_oral_cut_for_cli";
6634
6849
  function timestamp2() {
6635
6850
  const d = new Date;
@@ -6663,7 +6878,7 @@ async function runOralCutResult(taskId, opts) {
6663
6878
  const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
6664
6879
  throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
6665
6880
  }
6666
- 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()}`));
6667
6882
  const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
6668
6883
  await materializeResult({
6669
6884
  outDir,
@@ -6681,10 +6896,10 @@ async function runOralCutResult(taskId, opts) {
6681
6896
  }
6682
6897
 
6683
6898
  // src/commands/upgrade.ts
6684
- import { spawnSync as spawnSync3 } from "node:child_process";
6899
+ import { spawnSync as spawnSync4 } from "node:child_process";
6685
6900
  var CLIENT_UPGRADE = "irm https://api.ai-mcn.tv:9000/broadcast/exe/install.ps1 | iex";
6686
6901
  function run(cmd) {
6687
- const r = spawnSync3(cmd, { stdio: "inherit", shell: true });
6902
+ const r = spawnSync4(cmd, { stdio: "inherit", shell: true });
6688
6903
  return r.status ?? 1;
6689
6904
  }
6690
6905
  function registerUpgrade(program2) {
@@ -6713,7 +6928,7 @@ function registerUpgrade(program2) {
6713
6928
  process.exitCode = 1;
6714
6929
  return;
6715
6930
  }
6716
- log.step("② 刷新 /gtrk-oralcut skill…");
6931
+ log.step("② 通过通用适配器刷新 Agent Skills…");
6717
6932
  if (run("gtrk skills install") !== 0) {
6718
6933
  log.warn("skill 没刷成,手动跑一次:gtrk skills install");
6719
6934
  }
@@ -6723,16 +6938,16 @@ function registerUpgrade(program2) {
6723
6938
  }
6724
6939
 
6725
6940
  // src/commands/render.ts
6726
- 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";
6727
6942
  import { existsSync as existsSync13 } from "node:fs";
6728
6943
  function registerRender(program2) {
6729
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) => {
6730
6945
  if (opts.json)
6731
6946
  routeLogsToStderr();
6732
- const gtrkAbs = resolve5(gtrk);
6947
+ const gtrkAbs = resolve6(gtrk);
6733
6948
  if (!existsSync13(gtrkAbs))
6734
6949
  throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
6735
- 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`));
6736
6951
  log.step(`▶ 本地渲染:${basename6(gtrkAbs)} → ${basename6(outMp4)}`);
6737
6952
  const project = await readGtrkFile(gtrkAbs);
6738
6953
  const result = await renderGtrk(project, outMp4, {
@@ -6748,7 +6963,7 @@ function registerRender(program2) {
6748
6963
  log.tickEnd();
6749
6964
  log.ok(`渲染完成:${outMp4}(${result.duration.toFixed(1)}s)`);
6750
6965
  if (opts.open)
6751
- openFolder(dirname3(outMp4));
6966
+ openFolder(dirname4(outMp4));
6752
6967
  if (opts.json) {
6753
6968
  console.log(JSON.stringify({ ok: true, output: outMp4, duration: result.duration }));
6754
6969
  }
@@ -6756,7 +6971,7 @@ function registerRender(program2) {
6756
6971
  }
6757
6972
 
6758
6973
  // src/commands/split.ts
6759
- 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";
6760
6975
  import { existsSync as existsSync14 } from "node:fs";
6761
6976
  import { readFile as readFile4, writeFile as writeFile6, mkdir as mkdir5 } from "node:fs/promises";
6762
6977
  import { createHash } from "node:crypto";
@@ -6881,7 +7096,7 @@ function projectTranscript(transcript, gtrk, opts = {}) {
6881
7096
 
6882
7097
  // src/lib/gtrk-writeback.ts
6883
7098
  import { readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
6884
- 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";
6885
7100
  import { randomBytes as randomBytes2 } from "node:crypto";
6886
7101
  function readGtrk(path) {
6887
7102
  const raw = readFileSync4(path, "utf8");
@@ -6908,7 +7123,7 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
6908
7123
  }
6909
7124
  const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
6910
7125
  const next = { ...gtrk, struct_meta: nextStructMeta };
6911
- 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`);
6912
7127
  try {
6913
7128
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
6914
7129
  renameSync(tmp, path);
@@ -6924,7 +7139,7 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
6924
7139
  if (cur !== expectedMtimeMs) {
6925
7140
  throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
6926
7141
  }
6927
- 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`);
6928
7143
  try {
6929
7144
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
6930
7145
  renameSync(tmp, path);
@@ -6947,10 +7162,10 @@ function firstExisting(cands) {
6947
7162
  return cands.find((p) => existsSync14(p));
6948
7163
  }
6949
7164
  function resolvePaths(opts) {
6950
- const project = opts.project ? resolve6(opts.project) : undefined;
7165
+ const project = opts.project ? resolve7(opts.project) : undefined;
6951
7166
  let gtrkPath;
6952
7167
  if (opts.gtrk) {
6953
- gtrkPath = resolve6(opts.gtrk);
7168
+ gtrkPath = resolve7(opts.gtrk);
6954
7169
  } else if (project) {
6955
7170
  gtrkPath = firstExisting([join18(project, "gtrk", "project.gtrk"), join18(project, "project.gtrk")]) ?? join18(project, "gtrk", "project.gtrk");
6956
7171
  } else {
@@ -6960,14 +7175,14 @@ function resolvePaths(opts) {
6960
7175
  throw new Error(`找不到工程文件:${gtrkPath}`);
6961
7176
  let transcriptPath;
6962
7177
  if (opts.transcript)
6963
- transcriptPath = resolve6(opts.transcript);
7178
+ transcriptPath = resolve7(opts.transcript);
6964
7179
  else if (project)
6965
7180
  transcriptPath = firstExisting([
6966
7181
  join18(project, "transcript", "transcript.json"),
6967
7182
  join18(project, "json", "transcript.json"),
6968
7183
  join18(project, "transcript.json")
6969
7184
  ]);
6970
- const baseDir = project ?? dirname5(gtrkPath);
7185
+ const baseDir = project ?? dirname6(gtrkPath);
6971
7186
  return { baseDir, gtrkPath, transcriptPath };
6972
7187
  }
6973
7188
  function slugify(name) {
@@ -6987,7 +7202,7 @@ async function runSplit(splitdoc, opts) {
6987
7202
  if (opts.json)
6988
7203
  routeLogsToStderr();
6989
7204
  const { baseDir, gtrkPath, transcriptPath } = resolvePaths(opts);
6990
- 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);
6991
7206
  }
6992
7207
  async function runView(baseDir, gtrkPath, transcriptPath, opts) {
6993
7208
  if (!transcriptPath || !existsSync14(transcriptPath))
@@ -7093,7 +7308,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
7093
7308
  }
7094
7309
 
7095
7310
  // src/commands/matrix.ts
7096
- 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";
7097
7312
  import { existsSync as existsSync15 } from "node:fs";
7098
7313
  import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir6 } from "node:fs/promises";
7099
7314
 
@@ -7632,10 +7847,10 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
7632
7847
  let dispatchPath;
7633
7848
  let baseDir;
7634
7849
  if (opts.dispatch) {
7635
- dispatchPath = resolve7(opts.dispatch);
7636
- baseDir = dirname6(dirname6(dispatchPath));
7850
+ dispatchPath = resolve8(opts.dispatch);
7851
+ baseDir = dirname7(dirname7(dispatchPath));
7637
7852
  } else if (opts.project) {
7638
- baseDir = resolve7(opts.project);
7853
+ baseDir = resolve8(opts.project);
7639
7854
  dispatchPath = join19(baseDir, "split", "dispatch.json");
7640
7855
  } else {
7641
7856
  throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
@@ -7740,7 +7955,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor) {
7740
7955
  const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
7741
7956
  const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
7742
7957
  log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
7743
- const gtrkDir = dirname6(gtrkPath);
7958
+ const gtrkDir = dirname7(gtrkPath);
7744
7959
  const previewDir = join19(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
7745
7960
  await mkdir6(previewDir, { recursive: true });
7746
7961
  const prevSource = new Map;
@@ -7853,7 +8068,7 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
7853
8068
  counts: { beats: 0, queries: 1, results: results.length, errors: 0 }
7854
8069
  };
7855
8070
  if (opts.out) {
7856
- const outPath = resolve7(opts.out);
8071
+ const outPath = resolve8(opts.out);
7857
8072
  await writeFile7(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
7858
8073
  log.ok(`结果已落盘:${outPath}`);
7859
8074
  result.outPath = outPath;
@@ -7873,7 +8088,7 @@ function slugify2(name) {
7873
8088
  }
7874
8089
 
7875
8090
  // src/commands/mg.ts
7876
- 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";
7877
8092
  import { existsSync as existsSync16 } from "node:fs";
7878
8093
  import { readFile as readFile6, mkdir as mkdir7, copyFile } from "node:fs/promises";
7879
8094
 
@@ -8096,11 +8311,11 @@ async function runMg(words, opts) {
8096
8311
  }
8097
8312
  function resolveDispatch(opts) {
8098
8313
  if (opts.dispatch) {
8099
- const dispatchPath = resolve8(opts.dispatch);
8100
- return { dispatchPath, baseDir: dirname7(dirname7(dispatchPath)) };
8314
+ const dispatchPath = resolve9(opts.dispatch);
8315
+ return { dispatchPath, baseDir: dirname8(dirname8(dispatchPath)) };
8101
8316
  }
8102
8317
  if (opts.project) {
8103
- const baseDir = resolve8(opts.project);
8318
+ const baseDir = resolve9(opts.project);
8104
8319
  return { dispatchPath: join20(baseDir, "split", "dispatch.json"), baseDir };
8105
8320
  }
8106
8321
  throw new Error("需 --project <目录> 或显式 --dispatch <path>");
@@ -8177,7 +8392,7 @@ async function runLay(opts) {
8177
8392
  }
8178
8393
  const { gtrk, mtimeMs } = readGtrk(gtrkPath);
8179
8394
  assertGtrkV1(gtrk);
8180
- const gtrkDir = dirname7(gtrkPath);
8395
+ const gtrkDir = dirname8(gtrkPath);
8181
8396
  await mkdir7(join20(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
8182
8397
  for (const it of items) {
8183
8398
  await copyFile(srcByComp.get(it.composition_id), join20(gtrkDir, ...it.html_rel.split("/")));
@@ -8198,10 +8413,10 @@ async function runLint(args, opts) {
8198
8413
  const file = args[0];
8199
8414
  if (!file)
8200
8415
  throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
8201
- const html = await readFile6(resolve8(file), "utf8");
8416
+ const html = await readFile6(resolve9(file), "utf8");
8202
8417
  let dispatchIds;
8203
- if (opts.dispatch && existsSync16(resolve8(opts.dispatch))) {
8204
- 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);
8205
8420
  }
8206
8421
  const lint = lintParticle(html, { dispatchIds });
8207
8422
  for (const vv of lint.violations)
@@ -8854,6 +9069,308 @@ var audioSilenceRemove = {
8854
9069
  return url ? [{ url, filename: `${ctx.baseName}-desilence${extFromUrl(url, ".wav")}` }] : [];
8855
9070
  }
8856
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
+ };
8857
9374
  var mad = {
8858
9375
  name: "mad",
8859
9376
  title: "一键剪 MAD",
@@ -8889,6 +9406,16 @@ var TOOL_REGISTRY = [
8889
9406
  audioSeparation,
8890
9407
  audioNoiseReduce,
8891
9408
  audioSilenceRemove,
9409
+ videoSegment,
9410
+ videoAiSegment,
9411
+ videoMotionCut,
9412
+ audioSpeakerSplit,
9413
+ audioStretch,
9414
+ pianoAudioToMidi,
9415
+ pianoAudioEnhance,
9416
+ imageToSquare,
9417
+ imageToLive,
9418
+ videoAiSubtitle,
8892
9419
  mad
8893
9420
  ];
8894
9421
  function findTool(name, registry = TOOL_REGISTRY) {
@@ -8910,11 +9437,14 @@ function validateRegistry(registry = TOOL_REGISTRY) {
8910
9437
  throw new Error(`cloud 型工具缺 taskType:「${d.name}」`);
8911
9438
  if (d.kind === "cloud" && !d.priceKey)
8912
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
+ }
8913
9443
  }
8914
9444
  }
8915
9445
 
8916
9446
  // src/lib/tool-runner.ts
8917
- 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";
8918
9448
  import { mkdir as mkdir8, writeFile as writeFile8, stat as stat5 } from "node:fs/promises";
8919
9449
  import { createWriteStream, existsSync as existsSync17 } from "node:fs";
8920
9450
  import { Readable as Readable2 } from "node:stream";
@@ -9113,10 +9643,10 @@ function timestamp3() {
9113
9643
  }
9114
9644
  function resolveOutDir(descriptor, inputAbs, out) {
9115
9645
  if (out)
9116
- return resolve9(out);
9646
+ return resolve10(out);
9117
9647
  if (inputAbs) {
9118
9648
  const base = basename11(inputAbs, extname5(inputAbs));
9119
- return join21(dirname8(inputAbs), `${base}-${descriptor.name}`);
9649
+ return join21(dirname9(inputAbs), `${base}-${descriptor.name}`);
9120
9650
  }
9121
9651
  return join21(process.cwd(), `${descriptor.name}-${timestamp3()}`);
9122
9652
  }
@@ -9133,7 +9663,7 @@ function emitBilling(hint) {
9133
9663
  `);
9134
9664
  }
9135
9665
  async function runCloudTool(descriptor, inputArg, opts, deps) {
9136
- const inputAbs = inputArg ? resolve9(inputArg) : undefined;
9666
+ const inputAbs = inputArg ? resolve10(inputArg) : undefined;
9137
9667
  const baseName = inputAbs ? basename11(inputAbs, extname5(inputAbs)) : descriptor.name;
9138
9668
  validateToolInput(descriptor, inputAbs);
9139
9669
  const probe = deps.probeDurationSec ?? probeDuration;
@@ -9171,7 +9701,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9171
9701
  uploadCached: deps.uploadCached,
9172
9702
  invalidateUpload: deps.invalidateUpload,
9173
9703
  submitTask: deps.submitTask,
9174
- sleep: deps.sleep ?? ((ms) => new Promise((resolve10) => setTimeout(resolve10, ms)))
9704
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
9175
9705
  });
9176
9706
  const { taskId } = submitted;
9177
9707
  const up = { fileId: submitted.fileId, cached: submitted.cached };
@@ -9185,11 +9715,10 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9185
9715
  sleep: deps.sleep,
9186
9716
  now: deps.now
9187
9717
  });
9188
- const items = descriptor.mapOutputs ? descriptor.mapOutputs(output, ctx) : [];
9718
+ const outputResult = output;
9189
9719
  const files = [];
9190
9720
  const errors = {};
9191
- if (items.length === 0)
9192
- errors["output"] = "任务完成但未解析到产物下载链接(output_result 形态异常)";
9721
+ const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
9193
9722
  for (const it of items) {
9194
9723
  const dest = join21(outDir, it.filename);
9195
9724
  try {
@@ -9199,7 +9728,16 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9199
9728
  errors[it.filename] = e instanceof Error ? e.message : String(e);
9200
9729
  }
9201
9730
  }
9202
- 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);
9203
9741
  const result = {
9204
9742
  ok,
9205
9743
  tool: descriptor.name,
@@ -9208,6 +9746,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9208
9746
  fileId: up.fileId,
9209
9747
  outDir,
9210
9748
  files,
9749
+ ...resultFile ? { resultFile } : {},
9211
9750
  ...Object.keys(errors).length ? { errors } : {}
9212
9751
  };
9213
9752
  await writeFile8(join21(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
@@ -9217,7 +9756,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9217
9756
  // src/lib/mad/mad.ts
9218
9757
  import { mkdir as mkdir11, writeFile as writeFile10 } from "node:fs/promises";
9219
9758
  import { existsSync as existsSync20, statSync as statSync3 } from "node:fs";
9220
- import { resolve as resolve10, join as join25 } from "node:path";
9759
+ import { resolve as resolve11, join as join25 } from "node:path";
9221
9760
 
9222
9761
  // src/lib/convert/types.ts
9223
9762
  function num(v, d = 0) {
@@ -10402,7 +10941,7 @@ async function analyzeBgm(cfg, bgmAbs, deps) {
10402
10941
  uploadCached: deps.uploadCached,
10403
10942
  invalidateUpload: deps.invalidateUpload,
10404
10943
  submitTask: deps.submitTask,
10405
- sleep: deps.sleep ?? ((ms) => new Promise((resolve10) => setTimeout(resolve10, ms)))
10944
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
10406
10945
  });
10407
10946
  const { taskId } = submitted;
10408
10947
  const output = await deps.pollToolTask(cfg, ANALYZE_TASK, taskId, {});
@@ -10463,7 +11002,7 @@ async function runMad(inputArg, opts, deps = {}) {
10463
11002
  const probeDur = deps.probeDurationFn ?? probeDuration;
10464
11003
  if (!inputArg)
10465
11004
  throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
10466
- const dirAbs = resolve10(inputArg);
11005
+ const dirAbs = resolve11(inputArg);
10467
11006
  if (!existsSync20(dirAbs) || !statSync3(dirAbs).isDirectory()) {
10468
11007
  throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
10469
11008
  }
@@ -10495,7 +11034,7 @@ async function runMad(inputArg, opts, deps = {}) {
10495
11034
  let level = 3;
10496
11035
  let analysis = null;
10497
11036
  let bgmForTrack;
10498
- const bgmAbs = opts.bgm ? resolve10(opts.bgm) : undefined;
11037
+ const bgmAbs = opts.bgm ? resolve11(opts.bgm) : undefined;
10499
11038
  if (bgmAbs) {
10500
11039
  let dur = -1;
10501
11040
  try {
@@ -10578,7 +11117,7 @@ async function runMad(inputArg, opts, deps = {}) {
10578
11117
  const header = madHeader(version, now.toISOString());
10579
11118
  const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
10580
11119
  const { jsx } = madJsx({ master, windows, header, bgm });
10581
- 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)}`);
10582
11121
  await mkdir11(outDir, { recursive: true });
10583
11122
  const jsxPath = join25(outDir, "mad.jsx");
10584
11123
  await writeFile10(jsxPath, jsx);
@@ -10726,7 +11265,7 @@ async function runMadInTool(inputArg, opts) {
10726
11265
  // src/commands/transcript.ts
10727
11266
  import { existsSync as existsSync21 } from "node:fs";
10728
11267
  import { mkdir as mkdir12, rename as rename2, rm as rm2, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
10729
- import { basename as basename12, dirname as dirname9, extname as extname7, join as join26, resolve as resolve11 } from "node:path";
11268
+ import { basename as basename12, dirname as dirname10, extname as extname7, join as join26, resolve as resolve12 } from "node:path";
10730
11269
 
10731
11270
  // src/lib/transcript.ts
10732
11271
  var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
@@ -10847,7 +11386,7 @@ function buildDeps(overrides = {}) {
10847
11386
  upload: overrides.upload ?? uploadCached,
10848
11387
  invalidate: overrides.invalidate ?? invalidateUpload,
10849
11388
  submit: overrides.submit ?? submitTask,
10850
- sleep: overrides.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms))),
11389
+ sleep: overrides.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms))),
10851
11390
  poll: overrides.poll ?? (async (cfg, taskType, taskId, onTick) => await pollToolTask(cfg, taskType, taskId, { onTick })),
10852
11391
  writeMarkdown: overrides.writeMarkdown ?? writeMarkdownAtomic,
10853
11392
  now: overrides.now ?? (() => new Date)
@@ -10862,7 +11401,7 @@ async function validateTranscriptInput(input) {
10862
11401
  if (looksLikeRemote(input)) {
10863
11402
  throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
10864
11403
  }
10865
- const inputAbs = resolve11(input);
11404
+ const inputAbs = resolve12(input);
10866
11405
  if (!existsSync21(inputAbs))
10867
11406
  throw new Error(`本地视频不存在:${inputAbs}`);
10868
11407
  const info = await stat6(inputAbs);
@@ -10876,13 +11415,13 @@ async function validateTranscriptInput(input) {
10876
11415
  }
10877
11416
  function resolveTranscriptOutput(inputAbs, out) {
10878
11417
  const base = basename12(inputAbs, extname7(inputAbs));
10879
- const output = out ? resolve11(out) : join26(dirname9(inputAbs), `${base}-transcript.md`);
11418
+ const output = out ? resolve12(out) : join26(dirname10(inputAbs), `${base}-transcript.md`);
10880
11419
  if (extname7(output).toLowerCase() !== ".md")
10881
11420
  throw new Error("--out 必须指向一个 .md 文件");
10882
11421
  return output;
10883
11422
  }
10884
11423
  async function writeMarkdownAtomic(path, markdown) {
10885
- await mkdir12(dirname9(path), { recursive: true });
11424
+ await mkdir12(dirname10(path), { recursive: true });
10886
11425
  const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
10887
11426
  try {
10888
11427
  await writeFile11(temp, markdown, "utf8");
@@ -10958,12 +11497,226 @@ function registerTranscript(program2) {
10958
11497
  configureTranscriptCommand(program2.command("transcript <video>"));
10959
11498
  }
10960
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
+
10961
11714
  // src/index.ts
10962
11715
  try {
10963
11716
  process.loadEnvFile?.();
10964
11717
  } catch {}
10965
11718
  migrateLegacyHome();
10966
- var { version } = JSON.parse(readFileSync5(join27(packageRoot(), "package.json"), "utf8"));
11719
+ var { version } = JSON.parse(readFileSync5(join28(packageRoot(), "package.json"), "utf8"));
10967
11720
  var program2 = new Command;
10968
11721
  program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
10969
11722
  registerInstall(program2);
@@ -10979,6 +11732,7 @@ registerMatrix(program2);
10979
11732
  registerMg(program2);
10980
11733
  registerTool(program2);
10981
11734
  registerTranscript(program2);
11735
+ registerMusicVisualizer(program2);
10982
11736
  program2.parseAsync(process.argv).catch((e) => {
10983
11737
  console.error(`
10984
11738
  ❌ ${e instanceof Error ? e.message : String(e)}`);