@gitruck/cli 0.2.6 → 0.2.8

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
@@ -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 join20 } from "node:path";
4200
+ import { join as join21 } from "node:path";
4201
4201
 
4202
4202
  // src/lib/paths.ts
4203
4203
  import { dirname, join } from "node:path";
@@ -4274,7 +4274,14 @@ var log = {
4274
4274
  };
4275
4275
 
4276
4276
  // src/commands/skills.ts
4277
- var SKILL_NAMES = ["gtrk-oralcut", "gtrk-splitter", "gtrk-style-maker"];
4277
+ var SKILL_NAMES = [
4278
+ "gtrk-oralcut",
4279
+ "gtrk-splitter",
4280
+ "gtrk-matrix",
4281
+ "gtrk-mg",
4282
+ "gtrk-ai-drama",
4283
+ "gtrk-style-maker"
4284
+ ];
4278
4285
  function installSkill(opts = {}) {
4279
4286
  const destRoot = opts.dir ?? join2(homedir2(), ".claude", "skills");
4280
4287
  let allOk = true;
@@ -4300,7 +4307,7 @@ function installSkill(opts = {}) {
4300
4307
  }
4301
4308
  function registerSkills(program2) {
4302
4309
  const skills = program2.command("skills").description("管理 agent skill(安装到 Claude Code)");
4303
- skills.command("install").description("把 /gtrk-oralcut /gtrk-splitter 安装到 ~/.claude/skills(对标飞书 skills add)").option("--dir <dir>", "自定义 skills 目录(缺省 ~/.claude/skills)").action((opts) => {
4310
+ skills.command("install").description("把 gtrk 全家框架 skill 安装到 ~/.claude/skills(对标飞书 skills add)").option("--dir <dir>", "自定义 skills 目录(缺省 ~/.claude/skills)").action((opts) => {
4304
4311
  installSkill({ dir: opts.dir });
4305
4312
  });
4306
4313
  }
@@ -4475,7 +4482,7 @@ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:f
4475
4482
 
4476
4483
  // src/lib/splitdoc.ts
4477
4484
  var BASE_TRACKS = ["真人出镜", "口播继续", "旁白主导"];
4478
- var LANES = ["A_ROLL", "RRV_MG", "AI_DRAMA", "FILM_BROLL"];
4485
+ var LANES = ["A_ROLL", "MG", "AI_DRAMA", "FILM_BROLL"];
4479
4486
  var NARRATIVES = [
4480
4487
  "mirror-hook",
4481
4488
  "demolition",
@@ -4503,8 +4510,17 @@ var AUX_TYPES = [
4503
4510
  "archive-caption",
4504
4511
  "pause-card",
4505
4512
  "data-annotation",
4506
- "timeline-tag"
4513
+ "timeline-tag",
4514
+ "overlay"
4507
4515
  ];
4516
+ var LEGACY_LANE_ALIASES = { RRV_MG: "MG" };
4517
+ function normalizeLane(v) {
4518
+ if (typeof v !== "string")
4519
+ return;
4520
+ if (LANES.includes(v))
4521
+ return v;
4522
+ return LEGACY_LANE_ALIASES[v];
4523
+ }
4508
4524
  function isNonEmptyStr(v) {
4509
4525
  return typeof v === "string" && v.trim().length > 0;
4510
4526
  }
@@ -4573,7 +4589,7 @@ function validateSplitDoc(doc, ctx) {
4573
4589
  if (!enumOk(b.container_stage, vocab.container_stage))
4574
4590
  errors.push(`${tag}:container_stage 非法(不在栏目词表内)`);
4575
4591
  }
4576
- if (!enumOk(b.lane, LANES))
4592
+ if (!normalizeLane(b.lane))
4577
4593
  errors.push(`${tag}:lane 非法(四选一:${LANES.join(" | ")})`);
4578
4594
  if (!enumOk(b.irreplaceability, IRREPLACEABILITY))
4579
4595
  errors.push(`${tag}:irreplaceability 非法(四枚举之一)`);
@@ -4607,7 +4623,7 @@ function validateSplitDoc(doc, ctx) {
4607
4623
  if (!Array.isArray(b.aux_layers))
4608
4624
  errors.push(`${tag}:aux_layers 必须是数组`);
4609
4625
  else
4610
- b.aux_layers.forEach((a, ai) => validateAux(`${tag}.aux[${ai}]`, a, idIndex, errors));
4626
+ b.aux_layers.forEach((a, ai) => validateAux(`${tag}.aux[${ai}]`, a, idIndex, errors, warnings));
4611
4627
  }
4612
4628
  });
4613
4629
  const sorted = [...ranges].sort((a, b) => a.from - b.from || a.to - b.to);
@@ -4628,9 +4644,12 @@ function validateHandoff(tag, b, errors, warnings) {
4628
4644
  warnings.push(`${tag}:A_ROLL 不应带 handoff,已忽略`);
4629
4645
  return;
4630
4646
  }
4631
- if (lane === "RRV_MG") {
4647
+ if (lane === "MG" || lane === "RRV_MG") {
4632
4648
  if (!handoff || typeof handoff.duration_hint !== "number") {
4633
- errors.push(`${tag}:RRV_MG 的 handoff.duration_hint 必填(秒,数值)`);
4649
+ errors.push(`${tag}:MG 的 handoff.duration_hint 必填(秒,数值)`);
4650
+ }
4651
+ if (handoff && handoff.category !== undefined && !isKnownCategory(handoff.category)) {
4652
+ warnings.push(`${tag}:handoff.category「${String(handoff.category)}」非已知品类(${MG_CATEGORIES.join("/")}),已透传但下游按 opaque 反推`);
4634
4653
  }
4635
4654
  return;
4636
4655
  }
@@ -4642,16 +4661,25 @@ function validateHandoff(tag, b, errors, warnings) {
4642
4661
  return;
4643
4662
  }
4644
4663
  }
4645
- function validateAux(tag, raw, idIndex, errors) {
4664
+ function validateAux(tag, raw, idIndex, errors, warnings) {
4646
4665
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
4647
4666
  errors.push(`${tag}:辅助层必须是对象`);
4648
4667
  return;
4649
4668
  }
4650
4669
  const a = raw;
4651
4670
  if (!enumOk(a.type, AUX_TYPES))
4652
- errors.push(`${tag}:type 非法(七类之一)`);
4671
+ errors.push(`${tag}:type 非法(八类之一)`);
4653
4672
  if (!isNonEmptyStr(a.role))
4654
4673
  errors.push(`${tag}:缺 role(职责)`);
4674
+ if (a.type === "overlay") {
4675
+ const handoff = a.handoff;
4676
+ if (!handoff || typeof handoff.duration_hint !== "number" || !(handoff.duration_hint > 0)) {
4677
+ errors.push(`${tag}:overlay aux 缺颗粒时长(handoff.duration_hint 必填且须为正数)`);
4678
+ }
4679
+ if (handoff && handoff.category !== undefined && !isKnownCategory(handoff.category)) {
4680
+ warnings.push(`${tag}:handoff.category「${String(handoff.category)}」非已知品类(${MG_CATEGORIES.join("/")}),已透传但下游按 opaque 反推`);
4681
+ }
4682
+ }
4655
4683
  const m = a.mount;
4656
4684
  if (m === "same_beat")
4657
4685
  return;
@@ -4675,6 +4703,20 @@ function validateAux(tag, raw, idIndex, errors) {
4675
4703
  }
4676
4704
  errors.push(`${tag}:mount 非法(应为 "same_beat" | {from,to} | {trigger})`);
4677
4705
  }
4706
+ var MG_CATEGORIES = ["overlay", "fullscreen", "subtitle", "title"];
4707
+ var CATEGORY_EXPECTED_OPAQUE = {
4708
+ overlay: false,
4709
+ fullscreen: true,
4710
+ subtitle: false,
4711
+ title: true,
4712
+ "rrv-overlay": false,
4713
+ "mg-fullscreen": true,
4714
+ "explain-subtitle": false,
4715
+ "op-ed-title": true
4716
+ };
4717
+ function isKnownCategory(v) {
4718
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(CATEGORY_EXPECTED_OPAQUE, v);
4719
+ }
4678
4720
  function r3(n) {
4679
4721
  return Math.round(n * 1000) / 1000;
4680
4722
  }
@@ -4698,7 +4740,7 @@ function buildLanding(doc, view, opts) {
4698
4740
  ...opts.sourceIndex ? { material_id: opts.sourceIndex.materialId } : {},
4699
4741
  beats: []
4700
4742
  };
4701
- const dispatch = { rrv_mg: [], film_broll: [], ai_drama: [] };
4743
+ const dispatch = { mg: [], film_broll: [], ai_drama: [] };
4702
4744
  const skipped = [];
4703
4745
  const shrunk = [];
4704
4746
  const unhandledLanes = new Set;
@@ -4715,7 +4757,8 @@ function buildLanding(doc, view, opts) {
4715
4757
  const track_st = Math.min(...instances.map((x) => x.track_st));
4716
4758
  const track_ed = Math.max(...instances.map((x) => x.track_ed));
4717
4759
  const isShrunk = droppedCount > 0;
4718
- const metaBeat = { id: beat.id, lane: beat.lane, span: beat.span, track_st, track_ed };
4760
+ const lane = normalizeLane(beat.lane) ?? beat.lane;
4761
+ const metaBeat = { id: beat.id, lane, span: beat.span, track_st, track_ed };
4719
4762
  if (opts.sourceIndex) {
4720
4763
  const from = opts.sourceIndex.utterances.get(beat.span.from);
4721
4764
  const to = opts.sourceIndex.utterances.get(beat.span.to);
@@ -4731,7 +4774,9 @@ function buildLanding(doc, view, opts) {
4731
4774
  metaBeat.container_stage = beat.container_stage;
4732
4775
  if (beat.visual_task)
4733
4776
  metaBeat.visual_task = beat.visual_task;
4734
- if (beat.lane !== "A_ROLL" && beat.handoff)
4777
+ if (lane === "MG" && typeof beat.handoff?.category === "string")
4778
+ metaBeat.category = beat.handoff.category;
4779
+ if (lane !== "A_ROLL" && beat.handoff)
4735
4780
  metaBeat.handoff = beat.handoff;
4736
4781
  split.beats.push(metaBeat);
4737
4782
  if (isShrunk) {
@@ -4739,18 +4784,19 @@ function buildLanding(doc, view, opts) {
4739
4784
  }
4740
4785
  const h = beat.handoff ?? {};
4741
4786
  const compositionId = `${opts.projectSlug}-${beat.id}`;
4742
- if (beat.lane === "RRV_MG") {
4743
- dispatch.rrv_mg.push({
4787
+ if (lane === "MG") {
4788
+ dispatch.mg.push({
4744
4789
  beat: beat.id,
4745
4790
  composition_id: compositionId,
4746
4791
  duration: typeof h.duration_hint === "number" ? h.duration_hint : null,
4792
+ ...h.category !== undefined ? { category: h.category } : {},
4747
4793
  theme: h.theme,
4748
4794
  bg: h.bg,
4749
4795
  slug_hint: h.slug_hint,
4750
4796
  track_st,
4751
4797
  track_ed
4752
4798
  });
4753
- } else if (beat.lane === "FILM_BROLL") {
4799
+ } else if (lane === "FILM_BROLL") {
4754
4800
  dispatch.film_broll.push({
4755
4801
  beat: beat.id,
4756
4802
  queries: Array.isArray(h.queries) ? h.queries : [],
@@ -4760,10 +4806,69 @@ function buildLanding(doc, view, opts) {
4760
4806
  track_st,
4761
4807
  track_ed
4762
4808
  });
4763
- } else if (beat.lane === "AI_DRAMA") {
4809
+ } else if (lane === "AI_DRAMA") {
4764
4810
  dispatch.ai_drama.push({ beat: beat.id, ...h, track_st, track_ed });
4765
- } else if (beat.lane !== "A_ROLL") {
4766
- unhandledLanes.add(beat.lane);
4811
+ } else if (lane !== "A_ROLL") {
4812
+ unhandledLanes.add(lane);
4813
+ }
4814
+ let auxN = 0;
4815
+ for (const aux of beat.aux_layers ?? []) {
4816
+ if (aux.type !== "overlay")
4817
+ continue;
4818
+ auxN += 1;
4819
+ const auxTag = `${beat.id}-aux${auxN}`;
4820
+ const mount = aux.mount;
4821
+ let auxFromId;
4822
+ let auxToId;
4823
+ if (mount === "same_beat") {
4824
+ auxFromId = beat.span.from;
4825
+ auxToId = beat.span.to;
4826
+ } else if ("from" in mount && "to" in mount) {
4827
+ auxFromId = mount.from;
4828
+ auxToId = mount.to;
4829
+ } else {
4830
+ skipped.push({ beat: auxTag, reason: "overlay aux 使用 {trigger} 点挂载,一期不支持(二期补合成窗口)" });
4831
+ continue;
4832
+ }
4833
+ const auxFromIdx = idIndex.get(auxFromId);
4834
+ const auxToIdx = idIndex.get(auxToId);
4835
+ const auxSpanIds = opts.utteranceIds.slice(auxFromIdx, auxToIdx + 1);
4836
+ const auxInstances = auxSpanIds.flatMap((id) => byId.get(id) ?? []);
4837
+ if (auxInstances.length === 0) {
4838
+ skipped.push({ beat: auxTag, reason: "overlay aux 源区间 utterance 全被剪,未落轨" });
4839
+ continue;
4840
+ }
4841
+ const auxTrackSt = Math.min(...auxInstances.map((x) => x.track_st));
4842
+ const auxTrackEd = Math.max(...auxInstances.map((x) => x.track_ed));
4843
+ const auxCompositionId = `${opts.projectSlug}-${beat.id}-aux${auxN}`;
4844
+ const ah = aux.handoff;
4845
+ dispatch.mg.push({
4846
+ beat: beat.id,
4847
+ composition_id: auxCompositionId,
4848
+ duration: ah && typeof ah.duration_hint === "number" ? ah.duration_hint : null,
4849
+ category: "overlay",
4850
+ theme: ah?.theme,
4851
+ bg: ah?.bg,
4852
+ slug_hint: ah?.slug_hint,
4853
+ track_st: auxTrackSt,
4854
+ track_ed: auxTrackEd
4855
+ });
4856
+ const auxMetaBeat = {
4857
+ id: auxTag,
4858
+ lane: "MG",
4859
+ span: { from: auxFromId, to: auxToId },
4860
+ track_st: auxTrackSt,
4861
+ track_ed: auxTrackEd,
4862
+ category: "overlay"
4863
+ };
4864
+ if (opts.sourceIndex) {
4865
+ const sfrom = opts.sourceIndex.utterances.get(auxFromId);
4866
+ const sto = opts.sourceIndex.utterances.get(auxToId);
4867
+ if (sfrom && sto && sto.ed > sfrom.st) {
4868
+ auxMetaBeat.source_ranges = [{ st: r3(sfrom.st), ed: r3(sto.ed) }];
4869
+ }
4870
+ }
4871
+ split.beats.push(auxMetaBeat);
4767
4872
  }
4768
4873
  }
4769
4874
  return { split, dispatch, skipped, shrunk, unhandledLanes: [...unhandledLanes] };
@@ -4810,8 +4915,8 @@ function renderSplitMarkdown(doc, landing, meta) {
4810
4915
  L.push(`- \`${b.id}\` ${b.visual_task}`);
4811
4916
  }
4812
4917
  L.push("");
4813
- L.push("## RRV_MG Queue");
4814
- for (const r of landing.dispatch.rrv_mg) {
4918
+ L.push("## MG Queue");
4919
+ for (const r of landing.dispatch.mg) {
4815
4920
  L.push(`- \`${r.beat}\` composition_id=\`${r.composition_id}\`${r.duration != null ? ` · ${r.duration}s` : ""}`);
4816
4921
  }
4817
4922
  L.push("");
@@ -4928,6 +5033,13 @@ function resolveColumnConfig(opts = {}) {
4928
5033
  return { config, warnings };
4929
5034
  }
4930
5035
  var HANDOFF_REGISTRY = LANES;
5036
+ var LEGACY_HANDOFF_ALIASES = { RRV_MG: "MG" };
5037
+ function normalizeHandoffType(v) {
5038
+ return LEGACY_HANDOFF_ALIASES[v] ?? v;
5039
+ }
5040
+ function isRegisteredHandoff(v) {
5041
+ return HANDOFF_REGISTRY.includes(normalizeHandoffType(v));
5042
+ }
4931
5043
  function normalizeEntries(list, kind, warnings) {
4932
5044
  if (list === undefined)
4933
5045
  return;
@@ -5002,7 +5114,7 @@ function producesNotices(style) {
5002
5114
  if (e.routing === "none")
5003
5115
  continue;
5004
5116
  for (const v of producesValues(e)) {
5005
- if (HANDOFF_REGISTRY.includes(v) || seen.has(v))
5117
+ if (isRegisteredHandoff(v) || seen.has(v))
5006
5118
  continue;
5007
5119
  const near = HANDOFF_REGISTRY.find((r) => r.toLowerCase() === v.toLowerCase() || editDistance(r.toUpperCase(), v.toUpperCase()) <= 2);
5008
5120
  if (near) {
@@ -6877,7 +6989,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
6877
6989
  mdPath = join18(splitDir, "visual-split.md");
6878
6990
  await writeFile6(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
6879
6991
  }
6880
- log.ok(`落地完成:${landing.split.beats.length}/${doc.beats.length} beat 落轨` + `(RRV_MG ${landing.dispatch.rrv_mg.length} · FILM_BROLL ${landing.dispatch.film_broll.length} · AI_DRAMA ${landing.dispatch.ai_drama.length})`);
6992
+ log.ok(`落地完成:${landing.split.beats.length}/${doc.beats.length} beat 落轨` + `(MG ${landing.dispatch.mg.length} · FILM_BROLL ${landing.dispatch.film_broll.length} · AI_DRAMA ${landing.dispatch.ai_drama.length})`);
6881
6993
  for (const s of landing.skipped)
6882
6994
  log.warn(`跳过 ${s.beat}:${s.reason}`);
6883
6995
  for (const s of landing.shrunk)
@@ -6895,7 +7007,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
6895
7007
  projected_at: projectedAt,
6896
7008
  beats: { total: doc.beats.length, landed: landing.split.beats.length, skipped: landing.skipped, shrunk: landing.shrunk },
6897
7009
  queues: {
6898
- rrv_mg: landing.dispatch.rrv_mg.length,
7010
+ mg: landing.dispatch.mg.length,
6899
7011
  film_broll: landing.dispatch.film_broll.length,
6900
7012
  ai_drama: landing.dispatch.ai_drama.length
6901
7013
  }
@@ -7685,12 +7797,386 @@ function slugify2(name) {
7685
7797
  return s || "project";
7686
7798
  }
7687
7799
 
7800
+ // src/commands/mg.ts
7801
+ import { resolve as resolve8, join as join20, dirname as dirname7, basename as basename10 } from "node:path";
7802
+ import { existsSync as existsSync16 } from "node:fs";
7803
+ import { readFile as readFile6, mkdir as mkdir7, copyFile } from "node:fs/promises";
7804
+
7805
+ // src/lib/mg-lint.ts
7806
+ var CDN_OK = [/lib\.baomitu\.com/i, /cdnjs\.cloudflare\.com/i, /unpkg\.com/i];
7807
+ var CDN_WARN = [/jsdelivr\.net/i];
7808
+ function rootTag(html) {
7809
+ const m = html.match(/<[a-zA-Z][^>]*\bdata-composition-id\s*=[^>]*>/);
7810
+ return m ? m[0] : null;
7811
+ }
7812
+ function attr(tag, name) {
7813
+ const m = tag.match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i"));
7814
+ return m ? m[1] : undefined;
7815
+ }
7816
+ function deriveOpaque(rootTagStr) {
7817
+ if (!rootTagStr)
7818
+ return { opaque: false, declared: false };
7819
+ const style = attr(rootTagStr, "style") ?? "";
7820
+ const bg = style.match(/background(?:-color)?\s*:\s*([^;"']+)/i);
7821
+ if (!bg)
7822
+ return { opaque: false, declared: false };
7823
+ const val = bg[1].trim().toLowerCase();
7824
+ const transparent = val === "transparent" || val === "none" || /rgba\([^)]*,\s*0\s*\)/.test(val);
7825
+ return { opaque: !transparent, declared: true };
7826
+ }
7827
+ var CATEGORY_EXPECTED_OPAQUE2 = {
7828
+ overlay: false,
7829
+ fullscreen: true,
7830
+ subtitle: false,
7831
+ title: true,
7832
+ "rrv-overlay": false,
7833
+ "mg-fullscreen": true,
7834
+ "explain-subtitle": false,
7835
+ "op-ed-title": true
7836
+ };
7837
+ function lintParticle(html, opts = {}) {
7838
+ const v = [];
7839
+ const push = (law, fatal, msg) => v.push({ law, fatal, msg });
7840
+ if (!/<template[\s>]/i.test(html))
7841
+ push("1-template", true, "缺 <template> 包裹根元素(裸 div 整片渲染失败)");
7842
+ const root = rootTag(html);
7843
+ const cid = root ? attr(root, "data-composition-id") : undefined;
7844
+ if (!root || !cid)
7845
+ push("1-composition-id", true, "根元素缺 data-composition-id");
7846
+ if (root) {
7847
+ if (attr(root, "data-width") !== "1920")
7848
+ push("1-width", true, `根 data-width 应为 "1920"(实为 ${attr(root, "data-width") ?? "缺"})`);
7849
+ if (attr(root, "data-height") !== "1080")
7850
+ push("1-height", true, `根 data-height 应为 "1080"(实为 ${attr(root, "data-height") ?? "缺"})`);
7851
+ }
7852
+ if (!/gsap\.timeline\s*\(\s*\{[^}]*\bpaused\s*:\s*true\b[^}]*\}\s*\)/.test(html))
7853
+ push("2-paused", true, "缺 gsap.timeline({ paused: true })");
7854
+ const regMatch = html.match(/window\.__timelines\s*\[\s*(["']([^"']+)["']|[A-Za-z_$][\w$]*)\s*\]\s*=/);
7855
+ if (!regMatch)
7856
+ push("2-register", true, "缺 window.__timelines[<id>] = 注册");
7857
+ else {
7858
+ let regId = regMatch[2];
7859
+ if (regId === undefined) {
7860
+ const ident = regMatch[1];
7861
+ const vm = html.match(new RegExp(`(?:var|const|let)\\s+${ident}\\s*=\\s*["']([^"']+)["']`));
7862
+ regId = vm?.[1];
7863
+ if (!regId)
7864
+ push("2-register", true, `__timelines[${ident}] 的 ${ident} 未见字面串赋值,无法静态判定注册 id`);
7865
+ }
7866
+ if (cid && regId !== undefined && regId !== cid)
7867
+ push("2-id-match", true, `__timelines 注册 id "${regId}" 与 data-composition-id "${cid}" 不一致`);
7868
+ }
7869
+ if (/Math\.random\s*\(/.test(html))
7870
+ push("3-random", true, "含 Math.random()(破 StaticGuard,逐帧不确定)");
7871
+ if (/Date\.now\s*\(/.test(html))
7872
+ push("3-date-now", true, "含 Date.now()");
7873
+ if (/new\s+Date\s*\(\s*\)/.test(html))
7874
+ push("3-new-date", true, "含无参 new Date()");
7875
+ for (const m of html.matchAll(/<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi)) {
7876
+ const src = m[1];
7877
+ if (!/^https?:\/\//i.test(src))
7878
+ push("4-script-rel", true, `<script src> 非 http(s) 绝对 url:${src}`);
7879
+ else if (CDN_WARN.some((r) => r.test(src)))
7880
+ push("5-cdn-jsdelivr", false, `CDN 用 jsdelivr(国内渲染机不稳,建议 lib.baomitu.com):${src}`);
7881
+ else if (!CDN_OK.some((r) => r.test(src)))
7882
+ push("5-cdn-unknown", false, `CDN 不在已知可达白名单(渲染机可能拉不到):${src}`);
7883
+ }
7884
+ for (const [re, tag] of [
7885
+ [/<img\b[^>]*\bsrc\s*=\s*["'](?!https?:|data:)[^"']+["']/gi, "<img src>"],
7886
+ [/<link\b[^>]*\bhref\s*=\s*["'](?!https?:|data:)[^"']+["']/gi, "<link href>"],
7887
+ [/<use\b[^>]*\b(?:xlink:)?href\s*=\s*["'](?!#|https?:|data:)[^"']+["']/gi, "<use href>"],
7888
+ [/\burl\(\s*["']?(?!https?:|data:|#)[^)"']+["']?\s*\)/gi, "css url()"]
7889
+ ]) {
7890
+ if (re.test(html))
7891
+ push("4-rel-asset", true, `含相对外链 ${tag}(违反自包含,渲染机读不到)`);
7892
+ }
7893
+ const { opaque, declared } = deriveOpaque(root);
7894
+ if (root && !declared)
7895
+ push("4-bg-explicit", false, "根未显式声明 background(透明与否应明确;缺省按透明叠加 opaque=false 处理)");
7896
+ if (/var\(\s*--/.test(html))
7897
+ push("6-css-var", true, "含 CSS var(--...)(Hyperframes 不解析→整片全黑,须字面值)");
7898
+ const effectiveCid = opts.compositionId ?? cid;
7899
+ if (opts.dispatchIds && effectiveCid && !opts.dispatchIds.includes(effectiveCid))
7900
+ push("x-dispatch", false, `composition_id "${effectiveCid}" 不在 dispatch.mg 派单中`);
7901
+ if (opts.category && opts.category in CATEGORY_EXPECTED_OPAQUE2) {
7902
+ const expect = CATEGORY_EXPECTED_OPAQUE2[opts.category];
7903
+ if (expect !== opaque)
7904
+ push("x-category-opaque", false, `category「${opts.category}」期望${expect ? "不透明满屏" : "透明叠加"},但颗粒 HTML 反推为${opaque ? "不透明满屏" : "透明叠加"}(以 HTML 为准落 clip.opaque=${opaque})`);
7905
+ }
7906
+ return { ok: !v.some((x) => x.fatal), violations: v, opaque, compositionId: cid };
7907
+ }
7908
+
7909
+ // src/lib/mg-lay.ts
7910
+ var MG_MATERIAL_PREFIX = "mg-";
7911
+ var LEGACY_MATERIAL_PREFIX = "rrv-";
7912
+ var isOwnMaterialId = (id) => id.startsWith(MG_MATERIAL_PREFIX) || id.startsWith(LEGACY_MATERIAL_PREFIX);
7913
+ var r34 = (n) => Math.round(n * 1000) / 1000;
7914
+ function layTracksOf(prev) {
7915
+ return Array.isArray(prev?.lay_tracks) ? prev.lay_tracks.filter((x) => typeof x === "number") : [];
7916
+ }
7917
+ function layMgTracks(opts) {
7918
+ const { gtrk, items, generatedAt } = opts;
7919
+ const beatTracks = [...gtrk.beat_track ?? []];
7920
+ const materials = [...gtrk.materials ?? []];
7921
+ const structMeta = { ...gtrk.struct_meta ?? {} };
7922
+ const prevMg = structMeta.mg;
7923
+ const prevRrv = structMeta.rrv;
7924
+ const prevIndices = new Set([...layTracksOf(prevMg), ...layTracksOf(prevRrv)]);
7925
+ const removedTracks = beatTracks.filter((t) => typeof t.track_index === "number" && prevIndices.has(t.track_index));
7926
+ const keptTracks = beatTracks.filter((t) => !(typeof t.track_index === "number" && prevIndices.has(t.track_index)));
7927
+ const removedMaterialIds = new Set;
7928
+ for (const t of removedTracks) {
7929
+ for (const c3 of t.track_timeline ?? []) {
7930
+ const hm = c3.html_material;
7931
+ if (typeof hm === "string" && isOwnMaterialId(hm))
7932
+ removedMaterialIds.add(hm);
7933
+ }
7934
+ }
7935
+ const keptMaterials = materials.filter((m) => !(typeof m.id === "string" && removedMaterialIds.has(m.id)));
7936
+ const newMaterials = [];
7937
+ const clips = [];
7938
+ const metaBeats = [];
7939
+ const allIndices = [
7940
+ ...keptTracks,
7941
+ ...gtrk.video_track ?? [],
7942
+ ...gtrk.audio_track ?? []
7943
+ ].map((t) => typeof t.track_index === "number" ? t.track_index : 0);
7944
+ const newIndex = Math.max(9, ...allIndices) + 1;
7945
+ for (const it of items) {
7946
+ if (!(it.duration > 0)) {
7947
+ metaBeats.push({ ...toMetaBeat(it), laid: null });
7948
+ continue;
7949
+ }
7950
+ const materialId = `${MG_MATERIAL_PREFIX}${it.composition_id}`;
7951
+ newMaterials.push({ id: materialId, path: it.html_rel });
7952
+ clips.push({
7953
+ clip_id: it.composition_id,
7954
+ material: it.composition_id,
7955
+ html_material: materialId,
7956
+ opaque: it.opaque,
7957
+ track_st: it.track_st,
7958
+ duration: it.duration
7959
+ });
7960
+ metaBeats.push({ ...toMetaBeat(it), laid: { track_index: newIndex } });
7961
+ }
7962
+ const createdTracks = clips.length > 0 ? [
7963
+ {
7964
+ track_index: newIndex,
7965
+ track_timeline: clips.sort((a, b) => a.track_st - b.track_st)
7966
+ }
7967
+ ] : [];
7968
+ const mg = {
7969
+ contract_version: "v1",
7970
+ generated_at: generatedAt,
7971
+ lay_tracks: createdTracks.map((t) => t.track_index),
7972
+ beats: metaBeats
7973
+ };
7974
+ const nextStructMeta = { ...structMeta, mg };
7975
+ delete nextStructMeta.rrv;
7976
+ const next = {
7977
+ ...gtrk,
7978
+ materials: [...keptMaterials, ...newMaterials],
7979
+ beat_track: [...keptTracks, ...createdTracks],
7980
+ struct_meta: nextStructMeta
7981
+ };
7982
+ return {
7983
+ next,
7984
+ summary: { laidTrack: createdTracks[0]?.track_index ?? null, laidParticles: clips.length },
7985
+ mg
7986
+ };
7987
+ }
7988
+ function toMetaBeat(it) {
7989
+ return {
7990
+ beat: it.beat,
7991
+ composition_id: it.composition_id,
7992
+ track_st: it.track_st,
7993
+ track_ed: r34(it.track_ed),
7994
+ duration: it.duration,
7995
+ html_path: it.html_rel,
7996
+ ...it.category ? { category: it.category } : {}
7997
+ };
7998
+ }
7999
+
8000
+ // src/commands/mg.ts
8001
+ var MG_ASSET_DIR = "assets/mg";
8002
+ var MG_SRC_DIRS = ["mg", "rrv"];
8003
+ function registerMg(program2) {
8004
+ program2.command("mg [words...]").alias("rrv").description("MG 颗粒铺轨:无 positional=消费 dispatch.mg 铺 html-particle;`mg lint <file>`=单文件 lint;`mg status`=看板").option("--project <dir>", "oralcut 产物目录(定位 split/dispatch.json 与工程)").option("--dispatch <path>", "显式指定 dispatch.json(非标准布局兜底)").option("--only <beat>", "只跑单 beat").option("--lint-only", "只 lint 校验,不铺轨不写回").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (words, opts) => {
8005
+ if (process.argv[2] === "rrv")
8006
+ log.warn("`gtrk rrv` 已更名为 `gtrk mg`(去品牌化),别名仍可用但建议改用 `gtrk mg`。");
8007
+ await runMg(words ?? [], opts);
8008
+ });
8009
+ }
8010
+ async function runMg(words, opts) {
8011
+ if (opts.json)
8012
+ routeLogsToStderr();
8013
+ const sub = words[0];
8014
+ if (sub === "lint")
8015
+ return runLint(words.slice(1), opts);
8016
+ if (sub === "status")
8017
+ return runStatus(opts);
8018
+ if (sub)
8019
+ throw new Error(`未知子命令「${sub}」——铺轨:gtrk mg --project <dir>;lint:gtrk mg lint <file>;看板:gtrk mg status`);
8020
+ return runLay(opts);
8021
+ }
8022
+ function resolveDispatch(opts) {
8023
+ if (opts.dispatch) {
8024
+ const dispatchPath = resolve8(opts.dispatch);
8025
+ return { dispatchPath, baseDir: dirname7(dirname7(dispatchPath)) };
8026
+ }
8027
+ if (opts.project) {
8028
+ const baseDir = resolve8(opts.project);
8029
+ return { dispatchPath: join20(baseDir, "split", "dispatch.json"), baseDir };
8030
+ }
8031
+ throw new Error("需 --project <目录> 或显式 --dispatch <path>");
8032
+ }
8033
+ function locateGtrk2(baseDir) {
8034
+ return [join20(baseDir, "gtrk", "project.gtrk"), join20(baseDir, "project.gtrk")].find((p) => existsSync16(p));
8035
+ }
8036
+ function locateSrcHtml(baseDir, compositionId) {
8037
+ for (const d of MG_SRC_DIRS) {
8038
+ const p = join20(baseDir, d, `${compositionId}.html`);
8039
+ if (existsSync16(p))
8040
+ return p;
8041
+ }
8042
+ return;
8043
+ }
8044
+ async function readMgQueue(dispatchPath) {
8045
+ if (!existsSync16(dispatchPath))
8046
+ throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
8047
+ const dispatch = JSON.parse(await readFile6(dispatchPath, "utf8"));
8048
+ const queue = dispatch.mg ?? dispatch.rrv_mg;
8049
+ return Array.isArray(queue) ? queue : [];
8050
+ }
8051
+ async function runLay(opts) {
8052
+ const { dispatchPath, baseDir } = resolveDispatch(opts);
8053
+ let queue = await readMgQueue(dispatchPath);
8054
+ if (opts.only)
8055
+ queue = queue.filter((q) => q.beat === opts.only);
8056
+ log.step(`▶ MG 颗粒铺轨:${queue.length} 个 beat…`);
8057
+ const dispatchIds = queue.map((q) => q.composition_id);
8058
+ const items = [];
8059
+ const srcByComp = new Map;
8060
+ const skipped = [];
8061
+ for (const q of queue) {
8062
+ const srcPath = locateSrcHtml(baseDir, q.composition_id);
8063
+ if (!srcPath) {
8064
+ skipped.push({ beat: q.beat, reason: "缺颗粒 HTML(未产出)" });
8065
+ log.warn(`${q.beat}:缺 ${join20(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
8066
+ continue;
8067
+ }
8068
+ const html = await readFile6(srcPath, "utf8");
8069
+ const category = typeof q.category === "string" ? q.category : undefined;
8070
+ const lint = lintParticle(html, { compositionId: q.composition_id, dispatchIds, category });
8071
+ for (const vv of lint.violations)
8072
+ (vv.fatal ? log.warn : log.info)(`${q.beat} lint ${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
8073
+ if (!lint.ok) {
8074
+ skipped.push({ beat: q.beat, reason: "lint 未过" });
8075
+ log.warn(`${q.beat}:lint 未过,跳过`);
8076
+ continue;
8077
+ }
8078
+ if (typeof q.duration !== "number" || !(q.duration > 0)) {
8079
+ skipped.push({ beat: q.beat, reason: "duration 非正数" });
8080
+ continue;
8081
+ }
8082
+ srcByComp.set(q.composition_id, srcPath);
8083
+ items.push({
8084
+ beat: q.beat,
8085
+ composition_id: q.composition_id,
8086
+ track_st: q.track_st,
8087
+ track_ed: q.track_ed,
8088
+ duration: q.duration,
8089
+ opaque: lint.opaque,
8090
+ html_rel: `${MG_ASSET_DIR}/${q.composition_id}.html`,
8091
+ ...category ? { category } : {}
8092
+ });
8093
+ }
8094
+ if (opts.lintOnly) {
8095
+ log.ok(`lint-only:${items.length}/${queue.length} 通过,${skipped.length} 跳过(不铺轨)`);
8096
+ return done(opts, { ok: skipped.length === 0, mode: "lay", lintOnly: true, passed: items.length, skipped });
8097
+ }
8098
+ const gtrkPath = locateGtrk2(baseDir);
8099
+ if (!gtrkPath) {
8100
+ log.warn(`未找到工程文件(${join20(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
8101
+ return done(opts, { ok: true, mode: "lay", laid: 0, skipped, note: "工程缺失,未铺轨" });
8102
+ }
8103
+ const { gtrk, mtimeMs } = readGtrk(gtrkPath);
8104
+ assertGtrkV1(gtrk);
8105
+ const gtrkDir = dirname7(gtrkPath);
8106
+ await mkdir7(join20(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
8107
+ for (const it of items) {
8108
+ await copyFile(srcByComp.get(it.composition_id), join20(gtrkDir, ...it.html_rel.split("/")));
8109
+ }
8110
+ const { next, summary } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString() });
8111
+ writeGtrkAtomic(gtrkPath, next, mtimeMs);
8112
+ log.ok(`铺轨完成:${summary.laidParticles} 颗粒 → beat_track ${summary.laidTrack ?? "-"}` + `${skipped.length ? `(${skipped.length} beat 跳过)` : ""}`);
8113
+ log.info("opencut 打开工程即见 MG overlay 轨(预览需 add-particle-project-folder-preview 上线);出片时客户端云渲。");
8114
+ return done(opts, {
8115
+ ok: true,
8116
+ mode: "lay",
8117
+ laid: summary.laidParticles,
8118
+ laidTrack: summary.laidTrack,
8119
+ skipped
8120
+ });
8121
+ }
8122
+ async function runLint(args, opts) {
8123
+ const file = args[0];
8124
+ if (!file)
8125
+ throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
8126
+ const html = await readFile6(resolve8(file), "utf8");
8127
+ let dispatchIds;
8128
+ if (opts.dispatch && existsSync16(resolve8(opts.dispatch))) {
8129
+ dispatchIds = (await readMgQueue(resolve8(opts.dispatch))).map((q) => q.composition_id);
8130
+ }
8131
+ const lint = lintParticle(html, { dispatchIds });
8132
+ for (const vv of lint.violations)
8133
+ (vv.fatal ? log.err : log.warn)(`${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
8134
+ if (lint.ok)
8135
+ log.ok(`lint 通过(${basename10(file)};opaque=${lint.opaque})`);
8136
+ else
8137
+ log.err(`lint 未过(${lint.violations.filter((v) => v.fatal).length} 项致命)`);
8138
+ const result = { mode: "lint", ...lint, ok: lint.ok };
8139
+ if (opts.json)
8140
+ console.log(JSON.stringify(result));
8141
+ if (!lint.ok)
8142
+ process.exitCode = 1;
8143
+ return result;
8144
+ }
8145
+ async function runStatus(opts) {
8146
+ const { dispatchPath, baseDir } = resolveDispatch(opts);
8147
+ const queue = await readMgQueue(dispatchPath);
8148
+ const gtrkPath = locateGtrk2(baseDir);
8149
+ let laidIds = new Set;
8150
+ if (gtrkPath) {
8151
+ const { gtrk } = readGtrk(gtrkPath);
8152
+ const structMeta = gtrk.struct_meta;
8153
+ const meta = structMeta?.mg ?? structMeta?.rrv;
8154
+ laidIds = new Set((meta?.beats ?? []).filter((b) => b.laid).map((b) => b.composition_id));
8155
+ }
8156
+ const rows = queue.map((q) => {
8157
+ const authored2 = locateSrcHtml(baseDir, q.composition_id) !== undefined;
8158
+ const laid2 = laidIds.has(q.composition_id);
8159
+ return { beat: q.beat, composition_id: q.composition_id, authored: authored2, laid: laid2, state: laid2 ? "已铺" : authored2 ? "已产未铺" : "缺 HTML" };
8160
+ });
8161
+ const authored = rows.filter((r) => r.authored).length;
8162
+ const laid = rows.filter((r) => r.laid).length;
8163
+ log.step(`▶ MG 看板:${queue.length} beat · ${authored} 已产 · ${laid} 已铺`);
8164
+ for (const r of rows)
8165
+ log.info(`${r.beat}(${r.composition_id})→ ${r.state}`);
8166
+ return done(opts, { ok: true, mode: "status", total: queue.length, authored, laid, rows });
8167
+ }
8168
+ function done(opts, result) {
8169
+ if (opts.json)
8170
+ console.log(JSON.stringify(result));
8171
+ return result;
8172
+ }
8173
+
7688
8174
  // src/index.ts
7689
8175
  try {
7690
8176
  process.loadEnvFile?.();
7691
8177
  } catch {}
7692
8178
  migrateLegacyHome();
7693
- var { version } = JSON.parse(readFileSync5(join20(packageRoot(), "package.json"), "utf8"));
8179
+ var { version } = JSON.parse(readFileSync5(join21(packageRoot(), "package.json"), "utf8"));
7694
8180
  var program2 = new Command;
7695
8181
  program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
7696
8182
  registerInstall(program2);
@@ -7703,6 +8189,7 @@ registerUpgrade(program2);
7703
8189
  registerRender(program2);
7704
8190
  registerSplit(program2);
7705
8191
  registerMatrix(program2);
8192
+ registerMg(program2);
7706
8193
  program2.parseAsync(process.argv).catch((e) => {
7707
8194
  console.error(`
7708
8195
  ❌ ${e instanceof Error ? e.message : String(e)}`);