siluzan-tso-cli 1.1.29-beta.4 → 1.1.29-beta.5

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/README.md CHANGED
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.29-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.29-beta.5),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -258,9 +258,9 @@ var require_semver = __commonJS({
258
258
  } else {
259
259
  this.prerelease = m[4].split(".").map((id) => {
260
260
  if (/^[0-9]+$/.test(id)) {
261
- const num2 = +id;
262
- if (num2 >= 0 && num2 < MAX_SAFE_INTEGER) {
263
- return num2;
261
+ const num4 = +id;
262
+ if (num4 >= 0 && num4 < MAX_SAFE_INTEGER) {
263
+ return num4;
264
264
  }
265
265
  }
266
266
  return id;
@@ -123395,13 +123395,297 @@ import { fileURLToPath as fileURLToPath5 } from "url";
123395
123395
  // src/commands/facebook-analysis/merge-snapshot.ts
123396
123396
  import * as fs16 from "fs/promises";
123397
123397
  import * as path23 from "path";
123398
+
123399
+ // src/commands/facebook-analysis/lifecycle-autofill.ts
123400
+ var LIFECYCLE_PHASES = ["test-market", "find-winner", "scale"];
123401
+ var PHASE_LABEL = {
123402
+ "test-market": "\u6D4B\u5E02\u573A",
123403
+ "find-winner": "\u627E\u8D62\u5BB6",
123404
+ scale: "\u653E\u91CF"
123405
+ };
123406
+ function num(value) {
123407
+ const n = Number(value);
123408
+ return Number.isFinite(n) ? n : void 0;
123409
+ }
123410
+ function money(value, currency = "USD") {
123411
+ if (value == null) return "\u2014";
123412
+ const sym = currency === "USD" ? "$" : "";
123413
+ return `${sym}${value.toFixed(2)}`;
123414
+ }
123415
+ function adSetsWithSpend(tables) {
123416
+ const rows = Array.isArray(tables?.adSets) ? tables.adSets : [];
123417
+ return rows.filter((r) => (num(r.spend) ?? 0) > 0);
123418
+ }
123419
+ function topAdSetConcentration(adSets) {
123420
+ if (!adSets.length) return null;
123421
+ const totalSpend = adSets.reduce((s, r) => s + (num(r.spend) ?? 0), 0);
123422
+ const totalResults = adSets.reduce((s, r) => s + (num(r.results) ?? 0), 0);
123423
+ let best = adSets[0];
123424
+ let bestSpend = num(best.spend) ?? 0;
123425
+ for (const r of adSets) {
123426
+ const sp = num(r.spend) ?? 0;
123427
+ if (sp > bestSpend) {
123428
+ best = r;
123429
+ bestSpend = sp;
123430
+ }
123431
+ }
123432
+ const results = num(best.results) ?? 0;
123433
+ return {
123434
+ name: String(best.adGroupName ?? best.name ?? "\u2014"),
123435
+ spendShare: totalSpend > 0 ? bestSpend / totalSpend : 0,
123436
+ resultsShare: totalResults > 0 ? results / totalResults : 0,
123437
+ cpl: num(best.costPerResult)
123438
+ };
123439
+ }
123440
+ function winnerSpendShare(adSets, avgCpl, totalSpend) {
123441
+ if (totalSpend <= 0 || avgCpl <= 0) return 0;
123442
+ const winnerSpend = adSets.filter((r) => {
123443
+ const cpl = num(r.costPerResult);
123444
+ return cpl != null && cpl <= avgCpl * 0.9;
123445
+ }).reduce((s, r) => s + (num(r.spend) ?? 0), 0);
123446
+ return winnerSpend / totalSpend;
123447
+ }
123448
+ function countActiveDimensions(charts, tables) {
123449
+ let n = 0;
123450
+ const platform = charts?.platform;
123451
+ if (asArray(platform?.labels).length > 0) n += 1;
123452
+ const country = charts?.country;
123453
+ if (asArray(country?.labels).length > 0) n += 1;
123454
+ n += adSetsWithSpend(tables).length;
123455
+ return n;
123456
+ }
123457
+ function asArray(value) {
123458
+ return Array.isArray(value) ? value : [];
123459
+ }
123460
+ function inferLifecyclePhase(input) {
123461
+ const { avgCpl, currency = "USD" } = input;
123462
+ const spend = num(input.spend) ?? 0;
123463
+ const adSets = adSetsWithSpend(input.tables);
123464
+ const activeDims = countActiveDimensions(input.charts, input.tables);
123465
+ const winShare = winnerSpendShare(adSets, avgCpl, spend);
123466
+ const top = topAdSetConcentration(adSets);
123467
+ const reasons = [];
123468
+ reasons.push(`\u603B\u82B1\u8D39 ${money(spend, currency)}\uFF0C\u6D3B\u8DC3\u6295\u653E\u7EF4\u5EA6\u7EA6 ${activeDims} \u4E2A`);
123469
+ if (winShare > 0) reasons.push(`CPL \u4F18\u4E8E\u5747\u503C\u7684\u7EF4\u5EA6\u82B1\u8D39\u5360\u6BD4\u7EA6 ${Math.round(winShare * 100)}%`);
123470
+ if (top) {
123471
+ reasons.push(
123472
+ `Top \u5E7F\u544A\u7EC4\u300C${top.name}\u300D\u82B1\u8D39\u5360\u6BD4 ${Math.round(top.spendShare * 100)}%\u3001CPL ${money(top.cpl, currency)}`
123473
+ );
123474
+ }
123475
+ let phase = "find-winner";
123476
+ const lowSpendProbe = spend > 0 && spend < 200;
123477
+ const dispersedNoWinner = activeDims >= 4 && winShare < 0.35;
123478
+ const concentratedWinner = top != null && (top.spendShare >= 0.55 || top.resultsShare >= 0.55) && top.cpl != null && avgCpl > 0 && top.cpl <= avgCpl * 1.05;
123479
+ if (lowSpendProbe || dispersedNoWinner) {
123480
+ phase = "test-market";
123481
+ if (lowSpendProbe) reasons.push("\u603B\u82B1\u8D39\u4ECD\u5904\u4E8E\u8BD5\u63A2\u533A\u95F4");
123482
+ if (dispersedNoWinner) reasons.push("\u591A\u56FD/\u591A\u7EC4\u5206\u6563\u4E14\u5C1A\u672A\u5F62\u6210\u7A33\u5B9A\u8D62\u5BB6");
123483
+ } else if (concentratedWinner) {
123484
+ phase = "scale";
123485
+ reasons.push("\u9884\u7B97\u5DF2\u660E\u663E\u5411\u9AD8\u6548\u5E7F\u544A\u7EC4\u96C6\u4E2D\uFF0C\u9002\u5408\u653E\u91CF");
123486
+ } else {
123487
+ phase = "find-winner";
123488
+ reasons.push("\u5DF2\u51FA\u73B0\u4F18\u52A3\u5206\u5316\uFF0C\u4F46\u9884\u7B97\u5C1A\u672A\u9AD8\u5EA6\u96C6\u4E2D");
123489
+ }
123490
+ const label = PHASE_LABEL[phase];
123491
+ const verdict = `${reasons.join("\uFF1B")}\u3002\u7EFC\u5408\u5224\u65AD\u8D26\u6237\u5904\u4E8E\u300C${label}\u300D\u9636\u6BB5\uFF0C\u5EFA\u8BAE\u6309\u8BE5\u9636\u6BB5\u7B56\u7565\u8C03\u6574\u9884\u7B97\u4E0E\u7ED3\u6784\u3002`;
123492
+ return { phase, verdict, reasons };
123493
+ }
123494
+ function textLen(value) {
123495
+ return typeof value === "string" ? value.trim().length : 0;
123496
+ }
123497
+ function isValidPhase(value) {
123498
+ return typeof value === "string" && LIFECYCLE_PHASES.includes(value);
123499
+ }
123500
+ function ensureLifecycleFromSnapshot(healthDiagnosis, merged) {
123501
+ const hd = { ...healthDiagnosis ?? {} };
123502
+ const inferred = inferLifecyclePhase(merged);
123503
+ hd.lifecyclePhaseInferred = inferred.phase;
123504
+ const agentPhase = hd.lifecyclePhase;
123505
+ const agentVerdict = hd.lifecycleVerdict;
123506
+ if (!isValidPhase(agentPhase)) {
123507
+ hd.lifecyclePhase = inferred.phase;
123508
+ hd.lifecyclePhaseAutofilled = true;
123509
+ if (textLen(agentVerdict) < 60) {
123510
+ hd.lifecycleVerdict = inferred.verdict;
123511
+ hd.lifecycleVerdictAutofilled = true;
123512
+ }
123513
+ } else if (textLen(agentVerdict) < 60) {
123514
+ hd.lifecycleVerdict = inferred.verdict;
123515
+ hd.lifecycleVerdictAutofilled = true;
123516
+ }
123517
+ return hd;
123518
+ }
123519
+
123520
+ // src/commands/facebook-analysis/scorecard-autofill.ts
123521
+ function num2(value) {
123522
+ const n = Number(value);
123523
+ return Number.isFinite(n) ? n : void 0;
123524
+ }
123525
+ function money2(value, currency = "USD") {
123526
+ if (value == null) return "\u2014";
123527
+ const sym = currency === "USD" ? "$" : "";
123528
+ return `${sym}${value.toFixed(2)}`;
123529
+ }
123530
+ function cplSignal(cpl, avg) {
123531
+ if (cpl == null || avg <= 0) {
123532
+ return { signal: "yellow", signalLabel: "\u89C2", advice: "\u6837\u672C\u4E0D\u8DB3\uFF0C\u89C2\u5BDF 7 \u5929" };
123533
+ }
123534
+ if (cpl <= avg * 0.9) {
123535
+ return { signal: "green", signalLabel: "\u52A0", advice: "\u52A0\u7801\u9884\u7B97\u6216\u6269\u91CF" };
123536
+ }
123537
+ if (cpl >= avg * 1.15) {
123538
+ return { signal: "red", signalLabel: "\u51CF", advice: "\u9650\u6D41\u3001\u51CF\u9884\u7B97\u6216\u6682\u505C" };
123539
+ }
123540
+ return { signal: "yellow", signalLabel: "\u89C2", advice: "\u7EF4\u6301\u89C2\u5BDF\uFF0C7 \u5929\u540E\u590D\u76D8" };
123541
+ }
123542
+ function pushRow(rows, seen, row) {
123543
+ const key = row.item.trim();
123544
+ if (!key || seen.has(key)) return;
123545
+ seen.add(key);
123546
+ rows.push(row);
123547
+ }
123548
+ function buildScorecardFromMergedData(input) {
123549
+ const { avgCpl, currency = "USD" } = input;
123550
+ const charts = input.charts ?? {};
123551
+ const tables = input.tables ?? {};
123552
+ const rows = [];
123553
+ const seen = /* @__PURE__ */ new Set();
123554
+ const platform = charts.platform;
123555
+ const pLabels = Array.isArray(platform?.labels) ? platform.labels : [];
123556
+ const pCpl = Array.isArray(platform?.cpl) ? platform.cpl : [];
123557
+ const pSpend = Array.isArray(platform?.spend) ? platform.spend : [];
123558
+ if (pLabels.length) {
123559
+ const indexed = pLabels.map((label, i) => ({
123560
+ label: String(label),
123561
+ cpl: num2(pCpl[i]),
123562
+ spend: num2(pSpend[i]) ?? 0
123563
+ })).filter((x) => x.spend > 0 || x.cpl != null && x.cpl > 0);
123564
+ const sorted = [...indexed].sort((a, b) => (a.cpl ?? Infinity) - (b.cpl ?? Infinity));
123565
+ if (sorted[0]) {
123566
+ const s = cplSignal(sorted[0].cpl, avgCpl);
123567
+ pushRow(rows, seen, {
123568
+ item: `\u5E73\u53F0\xB7${sorted[0].label}`,
123569
+ data: `CPL ${money2(sorted[0].cpl, currency)}`,
123570
+ ...s
123571
+ });
123572
+ }
123573
+ if (sorted.length > 1) {
123574
+ const worst = sorted[sorted.length - 1];
123575
+ const s = cplSignal(worst.cpl, avgCpl);
123576
+ pushRow(rows, seen, {
123577
+ item: `\u5E73\u53F0\xB7${worst.label}`,
123578
+ data: `CPL ${money2(worst.cpl, currency)}`,
123579
+ ...s
123580
+ });
123581
+ }
123582
+ }
123583
+ const country = charts.country;
123584
+ const cLabels = Array.isArray(country?.labels) ? country.labels : [];
123585
+ const cCpl = Array.isArray(country?.cpl) ? country.cpl : [];
123586
+ if (cLabels.length) {
123587
+ const indexed = cLabels.map((label, i) => ({ label: String(label), cpl: num2(cCpl[i]) })).filter((x) => x.cpl != null && x.cpl > 0);
123588
+ const sorted = [...indexed].sort((a, b) => (a.cpl ?? 0) - (b.cpl ?? 0));
123589
+ if (sorted[0]) {
123590
+ const s = cplSignal(sorted[0].cpl, avgCpl);
123591
+ pushRow(rows, seen, {
123592
+ item: String(sorted[0].label),
123593
+ data: `CPL ${money2(sorted[0].cpl, currency)}`,
123594
+ ...s
123595
+ });
123596
+ }
123597
+ if (sorted.length > 1) {
123598
+ const worst = sorted[sorted.length - 1];
123599
+ const s = cplSignal(worst.cpl, avgCpl);
123600
+ pushRow(rows, seen, {
123601
+ item: String(worst.label),
123602
+ data: `CPL ${money2(worst.cpl, currency)}`,
123603
+ ...s
123604
+ });
123605
+ }
123606
+ }
123607
+ const adSets = Array.isArray(tables.adSets) ? tables.adSets : [];
123608
+ const adWithSpend = adSets.filter((g) => (num2(g.spend) ?? 0) > 0);
123609
+ if (adWithSpend.length) {
123610
+ const sorted = [...adWithSpend].sort(
123611
+ (a, b) => (num2(a.costPerResult) ?? Infinity) - (num2(b.costPerResult) ?? Infinity)
123612
+ );
123613
+ const best = sorted[0];
123614
+ const sBest = cplSignal(num2(best.costPerResult), avgCpl);
123615
+ pushRow(rows, seen, {
123616
+ item: String(best.name ?? "\u5E7F\u544A\u7EC4"),
123617
+ data: `CPL ${money2(num2(best.costPerResult), currency)}`,
123618
+ ...sBest
123619
+ });
123620
+ if (sorted.length > 1) {
123621
+ const worst = sorted[sorted.length - 1];
123622
+ const sWorst = cplSignal(num2(worst.costPerResult), avgCpl);
123623
+ pushRow(rows, seen, {
123624
+ item: String(worst.name ?? "\u5E7F\u544A\u7EC4"),
123625
+ data: `CPL ${money2(num2(worst.costPerResult), currency)}`,
123626
+ ...sWorst
123627
+ });
123628
+ }
123629
+ }
123630
+ const topAud = Array.isArray(tables.audienceTop) ? tables.audienceTop : [];
123631
+ const bottomAud = Array.isArray(tables.audienceBottom) ? tables.audienceBottom : [];
123632
+ if (topAud[0]) {
123633
+ const s = cplSignal(num2(topAud[0].costPerResult), avgCpl);
123634
+ pushRow(rows, seen, {
123635
+ item: `\u53D7\u4F17\xB7${String(topAud[0].label ?? "Top")}`,
123636
+ data: `CPL ${money2(num2(topAud[0].costPerResult), currency)}`,
123637
+ ...s
123638
+ });
123639
+ }
123640
+ if (bottomAud[0]) {
123641
+ const s = cplSignal(num2(bottomAud[0].costPerResult), avgCpl);
123642
+ pushRow(rows, seen, {
123643
+ item: `\u53D7\u4F17\xB7${String(bottomAud[0].label ?? "Bottom")}`,
123644
+ data: `CPL ${money2(num2(bottomAud[0].costPerResult), currency)}`,
123645
+ ...s
123646
+ });
123647
+ }
123648
+ if (rows.length < 6 && avgCpl > 0) {
123649
+ pushRow(rows, seen, {
123650
+ item: "\u8D26\u6237\u5747\u503C",
123651
+ data: `CPL ${money2(avgCpl, currency)}`,
123652
+ signal: "yellow",
123653
+ signalLabel: "\u89C2",
123654
+ advice: "\u4EE5\u8D26\u6237 CPL \u4E3A\u57FA\u51C6\u5BF9\u6BD4\u5404\u7EF4\u5EA6"
123655
+ });
123656
+ }
123657
+ return rows;
123658
+ }
123659
+ var MIN_SCORECARD_ROWS = 6;
123660
+ function ensureScorecardFromSnapshot(healthDiagnosis, merged) {
123661
+ const hd = { ...healthDiagnosis ?? {} };
123662
+ const existing = Array.isArray(hd.scorecard) ? hd.scorecard : [];
123663
+ if (existing.length >= MIN_SCORECARD_ROWS) return hd;
123664
+ const auto = buildScorecardFromMergedData(merged);
123665
+ const seen = new Set(existing.map((r) => String(r.item ?? "").trim()).filter(Boolean));
123666
+ const combined = [...existing];
123667
+ for (const row of auto) {
123668
+ if (combined.length >= MIN_SCORECARD_ROWS) break;
123669
+ const key = row.item.trim();
123670
+ if (!key || seen.has(key)) continue;
123671
+ seen.add(key);
123672
+ combined.push(row);
123673
+ }
123674
+ hd.scorecard = combined;
123675
+ if (combined.length > 0) {
123676
+ hd.scorecardAutofilled = existing.length < MIN_SCORECARD_ROWS;
123677
+ }
123678
+ return hd;
123679
+ }
123680
+
123681
+ // src/commands/facebook-analysis/merge-snapshot.ts
123398
123682
  function asRecord5(value) {
123399
123683
  if (value && typeof value === "object" && !Array.isArray(value)) {
123400
123684
  return value;
123401
123685
  }
123402
123686
  return null;
123403
123687
  }
123404
- function num(value) {
123688
+ function num3(value) {
123405
123689
  const n = Number(value);
123406
123690
  return Number.isFinite(n) ? n : void 0;
123407
123691
  }
@@ -123453,8 +123737,8 @@ function aggregatePlatform(networks) {
123453
123737
  const platform = String(row.publisherPlatform ?? row.network ?? "unknown");
123454
123738
  const prev = map.get(platform) ?? { spend: 0, results: 0 };
123455
123739
  map.set(platform, {
123456
- spend: prev.spend + (num(row.spend) ?? 0),
123457
- results: prev.results + (num(row.results) ?? 0)
123740
+ spend: prev.spend + (num3(row.spend) ?? 0),
123741
+ results: prev.results + (num3(row.results) ?? 0)
123458
123742
  });
123459
123743
  }
123460
123744
  const labels = [];
@@ -123470,10 +123754,10 @@ function aggregatePlatform(networks) {
123470
123754
  function buildAudienceRows(audiences) {
123471
123755
  return audiences.map((a) => ({
123472
123756
  label: audienceLabel(a.age, a.gender),
123473
- spend: num(a.spend) ?? 0,
123474
- results: num(a.results) ?? 0,
123475
- costPerResult: num(a.costPerResult) ?? (num(a.results) ? (num(a.spend) ?? 0) / (num(a.results) ?? 1) : 0),
123476
- frequency: num(a.frequency)
123757
+ spend: num3(a.spend) ?? 0,
123758
+ results: num3(a.results) ?? 0,
123759
+ costPerResult: num3(a.costPerResult) ?? (num3(a.results) ? (num3(a.spend) ?? 0) / (num3(a.results) ?? 1) : 0),
123760
+ frequency: num3(a.frequency)
123477
123761
  })).filter((r) => r.spend > 0 || r.results > 0);
123478
123762
  }
123479
123763
  async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
@@ -123510,7 +123794,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
123510
123794
  }
123511
123795
  if (!meta.resultType && current?.resultType) meta.resultType = String(current.resultType);
123512
123796
  const charts = { ...payload.charts ?? {} };
123513
- const avgCpl = num(kpis.costPerResult) ?? 0;
123797
+ const avgCpl = num3(kpis.costPerResult) ?? 0;
123514
123798
  if (!charts.platform) {
123515
123799
  const platform = sectionMap.get("platform");
123516
123800
  const networks = Array.isArray(platform?.networks) ? platform.networks : [];
@@ -123520,10 +123804,10 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
123520
123804
  const country = sectionMap.get("country");
123521
123805
  const countries = Array.isArray(country?.countries) ? country.countries : [];
123522
123806
  if (countries.length) {
123523
- const sorted = [...countries].sort((a, b) => (num(b.spend) ?? 0) - (num(a.spend) ?? 0));
123807
+ const sorted = [...countries].sort((a, b) => (num3(b.spend) ?? 0) - (num3(a.spend) ?? 0));
123524
123808
  charts.country = {
123525
123809
  labels: sorted.map((c) => String(c.countryOrRegion ?? "\u2014")),
123526
- cpl: sorted.map((c) => num(c.costPerResult) ?? 0)
123810
+ cpl: sorted.map((c) => num3(c.costPerResult) ?? 0)
123527
123811
  };
123528
123812
  }
123529
123813
  }
@@ -123539,7 +123823,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
123539
123823
  };
123540
123824
  }
123541
123825
  }
123542
- if (!charts.funnel && num(kpis.reach) != null) {
123826
+ if (!charts.funnel && num3(kpis.reach) != null) {
123543
123827
  charts.funnel = { reach: kpis.reach, results: kpis.results ?? 0 };
123544
123828
  }
123545
123829
  const tables = { ...payload.tables ?? {} };
@@ -123548,14 +123832,14 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
123548
123832
  const groups = Array.isArray(adSets?.adGroups) ? adSets.adGroups : [];
123549
123833
  if (groups.length) {
123550
123834
  tables.adSets = groups.map((g) => {
123551
- const cpl = num(g.costPerResult);
123552
- const freq = num(g.frequency);
123835
+ const cpl = num3(g.costPerResult);
123836
+ const freq = num3(g.frequency);
123553
123837
  const fatigue = fatigueFromFrequency(freq);
123554
123838
  const status = statusFromCpl(cpl, avgCpl);
123555
123839
  return {
123556
123840
  name: String(g.adGroupName ?? g.campaignName ?? "\u2014"),
123557
- spend: num(g.spend) ?? 0,
123558
- results: num(g.results) ?? 0,
123841
+ spend: num3(g.spend) ?? 0,
123842
+ results: num3(g.results) ?? 0,
123559
123843
  costPerResult: cpl ?? 0,
123560
123844
  frequency: freq,
123561
123845
  fatigueLevel: fatigue.level,
@@ -123577,25 +123861,37 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
123577
123861
  if (!tables.audienceBottom) tables.audienceBottom = bottomN;
123578
123862
  }
123579
123863
  }
123864
+ const currency = typeof meta.currency === "string" ? meta.currency : "USD";
123865
+ const mergeCtx = {
123866
+ spend: num3(kpis.spend),
123867
+ avgCpl,
123868
+ currency,
123869
+ charts,
123870
+ tables
123871
+ };
123872
+ let healthDiagnosis = ensureScorecardFromSnapshot(payload.healthDiagnosis, mergeCtx);
123873
+ healthDiagnosis = ensureLifecycleFromSnapshot(healthDiagnosis, mergeCtx);
123580
123874
  return {
123581
123875
  ...payload,
123582
123876
  meta,
123583
123877
  kpis,
123584
123878
  charts,
123585
- tables
123879
+ tables,
123880
+ healthDiagnosis
123586
123881
  };
123587
123882
  }
123588
123883
 
123589
123884
  // src/commands/facebook-analysis/report-content.ts
123590
123885
  var RECOMMENDATION_TITLES = ["\u7B80\u5316\u8868\u5355\u95EE\u9898", "\u533A\u57DF\u8C03\u6574", "\u9884\u7B97\u91CD\u6784", "\u7D20\u6750\u5EFA\u8BAE"];
123591
- var LIFECYCLE_PHASES = ["test-market", "find-winner", "scale"];
123592
- function textLen(value) {
123886
+ var SCORECARD_SIGNALS = ["green", "yellow", "red"];
123887
+ var SNAPSHOT_HEALTH_HINT = "\u53EF\u7701\u7565\u5BF9\u5E94\u5B57\u6BB5\uFF0Crender \u65F6\u52A0 --snapshot-dir \u7531 CLI \u4ECE\u5FEB\u7167\u81EA\u52A8\u8865\u5168\uFF1B\u6216\u6309 meta-period-report-rules.md \u624B\u5199";
123888
+ function textLen2(value) {
123593
123889
  return typeof value === "string" ? value.trim().length : 0;
123594
123890
  }
123595
123891
  function isFiniteNumber(value) {
123596
123892
  return typeof value === "number" && Number.isFinite(value);
123597
123893
  }
123598
- function asArray(value) {
123894
+ function asArray2(value) {
123599
123895
  return Array.isArray(value) ? value : [];
123600
123896
  }
123601
123897
  function asRecord6(value) {
@@ -123608,7 +123904,7 @@ function pushMissing(missing, id, chapter, dimension, hint) {
123608
123904
  missing.push({ id, chapter, dimension, hint });
123609
123905
  }
123610
123906
  function countAdSetsWithSpend(tables) {
123611
- const rows = asArray(tables?.adSets);
123907
+ const rows = asArray2(tables?.adSets);
123612
123908
  return rows.filter((row) => {
123613
123909
  const r = asRecord6(row);
123614
123910
  const spend = r?.spend;
@@ -123617,7 +123913,7 @@ function countAdSetsWithSpend(tables) {
123617
123913
  }
123618
123914
  function chartLabels(chart) {
123619
123915
  const c = asRecord6(chart);
123620
- return asArray(c?.labels).length;
123916
+ return asArray2(c?.labels).length;
123621
123917
  }
123622
123918
  function validateMetaPeriodReportContent(data) {
123623
123919
  const missing = [];
@@ -123630,15 +123926,15 @@ function validateMetaPeriodReportContent(data) {
123630
123926
  const health = asRecord6(data.healthDiagnosis);
123631
123927
  const priorityPlan = asRecord6(data.priorityPlan);
123632
123928
  const actionChecklist = asRecord6(data.actionChecklist);
123633
- const adSetsWithSpend = countAdSetsWithSpend(tables ?? void 0);
123634
- const regionalNarratives = asArray(narrative.regional).length;
123635
- const recommendations = asArray(narrative.recommendations);
123636
- const supplementary = asArray(data.supplementaryRecommendations);
123637
- const executiveSummary = asArray(data.executiveSummary);
123638
- const fourQuestions = asArray(health?.fourQuestions);
123639
- const scorecard = asArray(health?.scorecard);
123640
- const abTests = asArray(data.abTests);
123641
- if (!textLen(meta.accountName) && !textLen(meta.accountId)) {
123929
+ const adSetsWithSpend2 = countAdSetsWithSpend(tables ?? void 0);
123930
+ const regionalNarratives = asArray2(narrative.regional).length;
123931
+ const recommendations = asArray2(narrative.recommendations);
123932
+ const supplementary = asArray2(data.supplementaryRecommendations);
123933
+ const executiveSummary = asArray2(data.executiveSummary);
123934
+ const fourQuestions = asArray2(health?.fourQuestions);
123935
+ const scorecard = asArray2(health?.scorecard);
123936
+ const abTests = asArray2(data.abTests);
123937
+ if (!textLen2(meta.accountName) && !textLen2(meta.accountId)) {
123642
123938
  pushMissing(
123643
123939
  missing,
123644
123940
  "meta-account",
@@ -123647,7 +123943,7 @@ function validateMetaPeriodReportContent(data) {
123647
123943
  "meta.accountName \u6216 meta.accountId \u81F3\u5C11\u4E00\u9879\uFF1B\u53EF\u7531 --snapshot-dir \u5408\u5E76 overview"
123648
123944
  );
123649
123945
  }
123650
- if (!textLen(meta.periodLabel) && !(textLen(meta.startDate) && textLen(meta.endDate))) {
123946
+ if (!textLen2(meta.periodLabel) && !(textLen2(meta.startDate) && textLen2(meta.endDate))) {
123651
123947
  pushMissing(
123652
123948
  missing,
123653
123949
  "meta-period",
@@ -123680,7 +123976,7 @@ function validateMetaPeriodReportContent(data) {
123680
123976
  if (!isFiniteNumber(funnel?.reach)) {
123681
123977
  pushMissing(missing, "chart-funnel", "\u6570\u636E\u56FE\u8868", "\u6F0F\u6597\u56FE reach", "charts.funnel.reach \u987B\u4E3A\u6709\u6548\u6570\u503C");
123682
123978
  }
123683
- if (adSetsWithSpend < 1) {
123979
+ if (adSetsWithSpend2 < 1) {
123684
123980
  pushMissing(
123685
123981
  missing,
123686
123982
  "table-adsets",
@@ -123689,10 +123985,10 @@ function validateMetaPeriodReportContent(data) {
123689
123985
  "tables.adSets \u81F3\u5C11 1 \u884C\u82B1\u8D39>0\uFF1B\u53EF\u7531 --snapshot-dir \u5408\u5E76 ad-sets"
123690
123986
  );
123691
123987
  }
123692
- if (asArray(tables?.audienceTop).length < 1) {
123988
+ if (asArray2(tables?.audienceTop).length < 1) {
123693
123989
  pushMissing(missing, "table-audience-top", "\u6570\u636E\u8868\u683C", "\u53D7\u4F17 Top", "tables.audienceTop \u81F3\u5C11 1 \u884C");
123694
123990
  }
123695
- if (asArray(tables?.audienceBottom).length < 1) {
123991
+ if (asArray2(tables?.audienceBottom).length < 1) {
123696
123992
  pushMissing(
123697
123993
  missing,
123698
123994
  "table-audience-bottom",
@@ -123701,7 +123997,7 @@ function validateMetaPeriodReportContent(data) {
123701
123997
  "tables.audienceBottom \u81F3\u5C11 1 \u884C"
123702
123998
  );
123703
123999
  }
123704
- if (textLen(narrative.overall) < 120) {
124000
+ if (textLen2(narrative.overall) < 120) {
123705
124001
  pushMissing(
123706
124002
  missing,
123707
124003
  "narrative-overall",
@@ -123710,13 +124006,13 @@ function validateMetaPeriodReportContent(data) {
123710
124006
  "\u2265120 \u5B57\uFF0C\u542B\u82B1\u8D39/\u7EBF\u7D22/CPL/\u8986\u76D6/\u5C55\u793A/\u9891\u6B21\u7B49\u81F3\u5C11 5 \u9879\u6570\u5B57"
123711
124007
  );
123712
124008
  }
123713
- if (adSetsWithSpend > 0 && regionalNarratives < adSetsWithSpend) {
124009
+ if (adSetsWithSpend2 > 0 && regionalNarratives < adSetsWithSpend2) {
123714
124010
  pushMissing(
123715
124011
  missing,
123716
124012
  "narrative-regional",
123717
124013
  "\u603B\u6570\u636E\u53D9\u4E8B",
123718
124014
  "narrative.regional",
123719
- `\u6BCF\u4E2A\u82B1\u8D39>0 \u7684\u5E7F\u544A\u7EC4\u5404 1 \u6BB5\uFF08\u5F53\u524D ${regionalNarratives}/${adSetsWithSpend}\uFF09`
124015
+ `\u6BCF\u4E2A\u82B1\u8D39>0 \u7684\u5E7F\u544A\u7EC4\u5404 1 \u6BB5\uFF08\u5F53\u524D ${regionalNarratives}/${adSetsWithSpend2}\uFF09`
123720
124016
  );
123721
124017
  } else if (regionalNarratives < 1) {
123722
124018
  pushMissing(
@@ -123728,8 +124024,8 @@ function validateMetaPeriodReportContent(data) {
123728
124024
  );
123729
124025
  } else {
123730
124026
  for (let i = 0; i < regionalNarratives; i++) {
123731
- const row = asRecord6(asArray(narrative.regional)[i]);
123732
- if (textLen(row?.text) < 80) {
124027
+ const row = asRecord6(asArray2(narrative.regional)[i]);
124028
+ if (textLen2(row?.text) < 80) {
123733
124029
  pushMissing(
123734
124030
  missing,
123735
124031
  `narrative-regional-${i}`,
@@ -123740,7 +124036,7 @@ function validateMetaPeriodReportContent(data) {
123740
124036
  }
123741
124037
  }
123742
124038
  }
123743
- if (textLen(narrative.country) < 80) {
124039
+ if (textLen2(narrative.country) < 80) {
123744
124040
  pushMissing(
123745
124041
  missing,
123746
124042
  "narrative-country",
@@ -123772,7 +124068,7 @@ function validateMetaPeriodReportContent(data) {
123772
124068
  }
123773
124069
  for (let i = 0; i < recommendations.length; i++) {
123774
124070
  const rec = asRecord6(recommendations[i]);
123775
- if (textLen(rec?.content) < 150) {
124071
+ if (textLen2(rec?.content) < 150) {
123776
124072
  pushMissing(
123777
124073
  missing,
123778
124074
  `narrative-rec-content-${i}`,
@@ -123794,7 +124090,7 @@ function validateMetaPeriodReportContent(data) {
123794
124090
  } else {
123795
124091
  for (let i = 0; i < supplementary.length; i++) {
123796
124092
  const row = asRecord6(supplementary[i]);
123797
- if (!textLen(row?.dimension) || !textLen(row?.issue)) {
124093
+ if (!textLen2(row?.dimension) || !textLen2(row?.issue)) {
123798
124094
  pushMissing(
123799
124095
  missing,
123800
124096
  `supplementary-fields-${i}`,
@@ -123803,7 +124099,7 @@ function validateMetaPeriodReportContent(data) {
123803
124099
  "\u987B\u542B dimension\u3001issue\u3001suggestion"
123804
124100
  );
123805
124101
  }
123806
- if (textLen(row?.suggestion) < 60) {
124102
+ if (textLen2(row?.suggestion) < 60) {
123807
124103
  pushMissing(
123808
124104
  missing,
123809
124105
  `supplementary-suggestion-${i}`,
@@ -123815,7 +124111,7 @@ function validateMetaPeriodReportContent(data) {
123815
124111
  }
123816
124112
  }
123817
124113
  for (const level of ["high", "medium", "low"]) {
123818
- const items = asArray(priorityPlan?.[level]).filter((s) => textLen(s) >= 40);
124114
+ const items = asArray2(priorityPlan?.[level]).filter((s) => textLen2(s) >= 40);
123819
124115
  if (items.length < 2) {
123820
124116
  pushMissing(
123821
124117
  missing,
@@ -123826,7 +124122,7 @@ function validateMetaPeriodReportContent(data) {
123826
124122
  );
123827
124123
  }
123828
124124
  }
123829
- const summaryOk = executiveSummary.filter((p) => textLen(p) >= 80);
124125
+ const summaryOk = executiveSummary.filter((p) => textLen2(p) >= 80);
123830
124126
  if (summaryOk.length < 3) {
123831
124127
  pushMissing(
123832
124128
  missing,
@@ -123836,22 +124132,33 @@ function validateMetaPeriodReportContent(data) {
123836
124132
  "3\uFF5E5 \u6BB5\uFF0C\u6BCF\u6BB5 \u226580 \u5B57\uFF0C\u89E3\u91CA\u300C\u4E3A\u4EC0\u4E48\u300D"
123837
124133
  );
123838
124134
  }
123839
- if (!LIFECYCLE_PHASES.includes(health?.lifecyclePhase)) {
124135
+ const lifecyclePhase = health?.lifecyclePhase;
124136
+ const lifecyclePhaseInferred = health?.lifecyclePhaseInferred;
124137
+ const phaseValid = LIFECYCLE_PHASES.includes(lifecyclePhase);
124138
+ if (!phaseValid) {
123840
124139
  pushMissing(
123841
124140
  missing,
123842
124141
  "health-phase",
123843
- "\u5065\u5EB7\u8BCA\u65AD",
124142
+ "\u5065\u5EB7\u8BCA\u65AD \u2460",
123844
124143
  "healthDiagnosis.lifecyclePhase",
123845
- "\u987B\u4E3A test-market / find-winner / scale"
124144
+ `\u987B\u4E3A test-market / find-winner / scale\uFF1B${SNAPSHOT_HEALTH_HINT}`
124145
+ );
124146
+ } else if (typeof lifecyclePhaseInferred === "string" && LIFECYCLE_PHASES.includes(lifecyclePhaseInferred) && lifecyclePhase !== lifecyclePhaseInferred && health?.lifecyclePhaseAutofilled !== true) {
124147
+ pushMissing(
124148
+ missing,
124149
+ "health-phase-mismatch",
124150
+ "\u5065\u5EB7\u8BCA\u65AD \u2460",
124151
+ "healthDiagnosis.lifecyclePhase",
124152
+ `Agent \u586B\u300C${lifecyclePhase}\u300D\u4F46\u5FEB\u7167\u6570\u636E\u63A8\u65AD\u4E3A\u300C${lifecyclePhaseInferred}\u300D\uFF1B\u5220\u9664 lifecyclePhase \u7531 --snapshot-dir \u81EA\u52A8\u63A8\u65AD\uFF0C\u6216\u6309\u89C4\u5219\u4FEE\u6B63`
123846
124153
  );
123847
124154
  }
123848
- if (textLen(health?.lifecycleVerdict) < 60) {
124155
+ if (textLen2(health?.lifecycleVerdict) < 60) {
123849
124156
  pushMissing(
123850
124157
  missing,
123851
124158
  "health-verdict",
123852
- "\u5065\u5EB7\u8BCA\u65AD",
124159
+ "\u5065\u5EB7\u8BCA\u65AD \u2460",
123853
124160
  "healthDiagnosis.lifecycleVerdict",
123854
- "\u226560 \u5B57\uFF0C\u7ED3\u5408\u603B\u82B1\u8D39\u4E0E\u7EF4\u5EA6\u5206\u6563\u5EA6"
124161
+ `\u226560 \u5B57\uFF0C\u7ED3\u5408\u603B\u82B1\u8D39\u4E0E\u7EF4\u5EA6\u5206\u6563\u5EA6\uFF1B${SNAPSHOT_HEALTH_HINT}`
123855
124162
  );
123856
124163
  }
123857
124164
  if (fourQuestions.length !== 4) {
@@ -123865,7 +124172,7 @@ function validateMetaPeriodReportContent(data) {
123865
124172
  } else {
123866
124173
  for (let i = 0; i < fourQuestions.length; i++) {
123867
124174
  const q = asRecord6(fourQuestions[i]);
123868
- const evidence = asArray(q?.evidence).filter((e) => textLen(e) > 0);
124175
+ const evidence = asArray2(q?.evidence).filter((e) => textLen2(e) > 0);
123869
124176
  if (evidence.length < 2) {
123870
124177
  pushMissing(
123871
124178
  missing,
@@ -123875,7 +124182,7 @@ function validateMetaPeriodReportContent(data) {
123875
124182
  "\u6BCF\u6761 evidence \u22652 \u6761\u4E14\u542B\u6570\u5B57"
123876
124183
  );
123877
124184
  }
123878
- if (textLen(q?.action) < 40) {
124185
+ if (textLen2(q?.action) < 40) {
123879
124186
  pushMissing(
123880
124187
  missing,
123881
124188
  `health-action-${i}`,
@@ -123886,17 +124193,51 @@ function validateMetaPeriodReportContent(data) {
123886
124193
  }
123887
124194
  }
123888
124195
  }
124196
+ let scorecardValidRows = 0;
124197
+ for (let i = 0; i < scorecard.length; i++) {
124198
+ const row = asRecord6(scorecard[i]);
124199
+ const itemOk = textLen2(row?.item) > 0;
124200
+ const dataOk = textLen2(row?.data) > 0;
124201
+ const signalOk = SCORECARD_SIGNALS.includes(row?.signal);
124202
+ const labelOk = textLen2(row?.signalLabel) > 0;
124203
+ const adviceOk = textLen2(row?.advice) > 0;
124204
+ if (itemOk && dataOk && signalOk && labelOk && adviceOk) {
124205
+ scorecardValidRows += 1;
124206
+ continue;
124207
+ }
124208
+ const parts = [];
124209
+ if (!itemOk) parts.push("item");
124210
+ if (!dataOk) parts.push("data");
124211
+ if (!signalOk) parts.push("signal(green|yellow|red)");
124212
+ if (!labelOk) parts.push("signalLabel");
124213
+ if (!adviceOk) parts.push("advice");
124214
+ pushMissing(
124215
+ missing,
124216
+ `health-scorecard-row-${i}`,
124217
+ "\u5065\u5EB7\u8BCA\u65AD \u2462",
124218
+ `healthDiagnosis.scorecard[${i}]`,
124219
+ `\u6BCF\u884C\u987B\u542B ${parts.join("\u3001")}`
124220
+ );
124221
+ }
123889
124222
  if (scorecard.length < 6) {
123890
124223
  pushMissing(
123891
124224
  missing,
123892
- "health-scorecard",
123893
- "\u5065\u5EB7\u8BCA\u65AD",
124225
+ "health-scorecard-count",
124226
+ "\u5065\u5EB7\u8BCA\u65AD \u2462",
124227
+ "healthDiagnosis.scorecard",
124228
+ `\u7EA2\u7EFF\u706F\u8868 \u22656 \u884C\uFF08\u5E73\u53F0/\u56FD\u5BB6/\u5E7F\u544A\u7EC4/\u53D7\u4F17\u7B49\uFF09\uFF1B\u5F53\u524D ${scorecard.length} \u884C\uFF1B${SNAPSHOT_HEALTH_HINT}`
124229
+ );
124230
+ } else if (scorecardValidRows < 6) {
124231
+ pushMissing(
124232
+ missing,
124233
+ "health-scorecard-valid",
124234
+ "\u5065\u5EB7\u8BCA\u65AD \u2462",
123894
124235
  "healthDiagnosis.scorecard",
123895
- "\u7EA2\u7EFF\u706F\u8868 \u22656 \u884C\uFF08\u5E73\u53F0/\u56FD\u5BB6/\u5E7F\u544A\u7EC4/\u53D7\u4F17\u7B49\uFF09"
124236
+ `\u6709\u6548\u884C ${scorecardValidRows}/6\uFF08\u6BCF\u884C item/data/signal/signalLabel/advice \u9F50\u5168\uFF09`
123896
124237
  );
123897
124238
  }
123898
124239
  for (const key of ["platform", "country", "adSets"]) {
123899
- const insight = textLen(asRecord6(sections?.[key])?.insight);
124240
+ const insight = textLen2(asRecord6(sections?.[key])?.insight);
123900
124241
  if (insight < 200) {
123901
124242
  pushMissing(
123902
124243
  missing,
@@ -123908,7 +124249,7 @@ function validateMetaPeriodReportContent(data) {
123908
124249
  }
123909
124250
  }
123910
124251
  const audience = asRecord6(sections?.audience);
123911
- if (asArray(audience?.goldenProfile).filter((s) => textLen(s) > 0).length < 3) {
124252
+ if (asArray2(audience?.goldenProfile).filter((s) => textLen2(s) > 0).length < 3) {
123912
124253
  pushMissing(
123913
124254
  missing,
123914
124255
  "sections-audience-golden",
@@ -123917,7 +124258,7 @@ function validateMetaPeriodReportContent(data) {
123917
124258
  "\u22653 \u6761\u9EC4\u91D1\u53D7\u4F17\u753B\u50CF"
123918
124259
  );
123919
124260
  }
123920
- if (asArray(audience?.antiProfile).filter((s) => textLen(s) > 0).length < 2) {
124261
+ if (asArray2(audience?.antiProfile).filter((s) => textLen2(s) > 0).length < 2) {
123921
124262
  pushMissing(
123922
124263
  missing,
123923
124264
  "sections-audience-anti",
@@ -123926,7 +124267,7 @@ function validateMetaPeriodReportContent(data) {
123926
124267
  "\u22652 \u6761\u53CD\u753B\u50CF"
123927
124268
  );
123928
124269
  }
123929
- if (textLen(audience?.insight) < 150) {
124270
+ if (textLen2(audience?.insight) < 150) {
123930
124271
  pushMissing(
123931
124272
  missing,
123932
124273
  "sections-audience-insight",
@@ -123935,7 +124276,7 @@ function validateMetaPeriodReportContent(data) {
123935
124276
  "\u2265150 \u5B57"
123936
124277
  );
123937
124278
  }
123938
- const landingRows = asArray(asRecord6(sections?.landingPage)?.rows);
124279
+ const landingRows = asArray2(asRecord6(sections?.landingPage)?.rows);
123939
124280
  if (landingRows.length < 3) {
123940
124281
  pushMissing(
123941
124282
  missing,
@@ -123954,9 +124295,9 @@ function validateMetaPeriodReportContent(data) {
123954
124295
  "\u22653 \u4E2A\u5B9E\u9A8C\uFF08\u53D8\u91CF\u3001\u5047\u8BBE\u3001\u6210\u529F\u6807\u51C6\uFF09"
123955
124296
  );
123956
124297
  }
123957
- const today = asArray(actionChecklist?.today).filter((s) => textLen(s) > 0);
123958
- const thisWeek = asArray(actionChecklist?.thisWeek).filter((s) => textLen(s) > 0);
123959
- const thisMonth = asArray(actionChecklist?.thisMonth).filter((s) => textLen(s) > 0);
124298
+ const today = asArray2(actionChecklist?.today).filter((s) => textLen2(s) > 0);
124299
+ const thisWeek = asArray2(actionChecklist?.thisWeek).filter((s) => textLen2(s) > 0);
124300
+ const thisMonth = asArray2(actionChecklist?.thisMonth).filter((s) => textLen2(s) > 0);
123960
124301
  if (today.length < 2 || thisWeek.length < 3 || thisMonth.length < 3) {
123961
124302
  pushMissing(
123962
124303
  missing,
@@ -123974,8 +124315,11 @@ function validateMetaPeriodReportContent(data) {
123974
124315
  supplementaryCount: supplementary.length,
123975
124316
  fourQuestionsCount: fourQuestions.length,
123976
124317
  scorecardRows: scorecard.length,
124318
+ scorecardValidRows,
124319
+ lifecyclePhase: phaseValid ? String(lifecyclePhase) : void 0,
124320
+ lifecyclePhaseInferred: typeof lifecyclePhaseInferred === "string" ? lifecyclePhaseInferred : void 0,
123977
124321
  regionalNarratives,
123978
- adSetsWithSpend
124322
+ adSetsWithSpend: adSetsWithSpend2
123979
124323
  }
123980
124324
  };
123981
124325
  }
@@ -123987,7 +124331,7 @@ function formatMetaPeriodReportErrors(result) {
123987
124331
  ...lines,
123988
124332
  "",
123989
124333
  "\u8BF7 Read assets/meta-period-report-rules.md\uFF0C\u8865\u5168 meta-period-report.json \u540E\u91CD\u65B0\u6267\u884C render\u3002",
123990
- "\u6570\u636E\u7C7B\u5B57\u6BB5\u53EF\u5148 facebook-analysis --json-out\uFF0Crender \u65F6\u52A0 --snapshot-dir \u81EA\u52A8\u5408\u5E76 KPI/\u56FE\u8868/\u8868\u683C\u3002"
124334
+ "\u6570\u636E\u7C7B\u5B57\u6BB5\u53EF\u5148 facebook-analysis --json-out\uFF0Crender \u65F6\u52A0 --snapshot-dir \u81EA\u52A8\u5408\u5E76 KPI/\u56FE\u8868/\u8868\u683C\uFF0C\u5E76\u8865\u5168 \u2460 \u9636\u6BB5\u4E0E \u2462 \u7EA2\u7EFF\u706F\u8868\u3002"
123991
124335
  ].join("\n");
123992
124336
  }
123993
124337
 
@@ -124049,7 +124393,7 @@ async function runFacebookAnalysisRender(opts) {
124049
124393
  `);
124050
124394
  const { stats: stats2 } = contentCheck;
124051
124395
  console.error(
124052
- ` \u7EDF\u8BA1\uFF1A\u5EFA\u8BAE ${stats2.recommendationCount}/4\uFF5C\u8865\u5145 ${stats2.supplementaryCount}/7\uFF5C\u56DB\u95EE ${stats2.fourQuestionsCount}/4\uFF5C\u7EA2\u7EFF\u706F ${stats2.scorecardRows}/6\uFF5C\u533A\u57DF\u53D9\u4E8B ${stats2.regionalNarratives}/${stats2.adSetsWithSpend}
124396
+ ` \u7EDF\u8BA1\uFF1A\u5EFA\u8BAE ${stats2.recommendationCount}/4\uFF5C\u8865\u5145 ${stats2.supplementaryCount}/7\uFF5C\u56DB\u95EE ${stats2.fourQuestionsCount}/4\uFF5C\u9636\u6BB5 ${stats2.lifecyclePhase ?? "\u2014"}${stats2.lifecyclePhaseInferred ? `\uFF08\u63A8\u65AD ${stats2.lifecyclePhaseInferred}\uFF09` : ""}\uFF5C\u7EA2\u7EFF\u706F ${stats2.scorecardValidRows}/${stats2.scorecardRows} \u6709\u6548\u884C\uFF5C\u533A\u57DF\u53D9\u4E8B ${stats2.regionalNarratives}/${stats2.adSetsWithSpend}
124053
124397
  `
124054
124398
  );
124055
124399
  process.exit(1);
@@ -124084,7 +124428,7 @@ async function runFacebookAnalysisRender(opts) {
124084
124428
  \u2705 Meta/Facebook \u5468\u671F\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
124085
124429
  `);
124086
124430
  console.log(
124087
- ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u5EFA\u8BAE ${stats.recommendationCount}\uFF5C\u8865\u5145 ${stats.supplementaryCount}\uFF5C\u56DB\u95EE ${stats.fourQuestionsCount}\uFF5C\u7EA2\u7EFF\u706F ${stats.scorecardRows}
124431
+ ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u5EFA\u8BAE ${stats.recommendationCount}\uFF5C\u8865\u5145 ${stats.supplementaryCount}\uFF5C\u56DB\u95EE ${stats.fourQuestionsCount}\uFF5C\u9636\u6BB5 ${stats.lifecyclePhase ?? "\u2014"}\uFF5C\u7EA2\u7EFF\u706F ${stats.scorecardValidRows} \u6709\u6548\u884C
124088
124432
  `
124089
124433
  );
124090
124434
  if (runtimeCopied) {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.29-beta.4",
4
- "publishedAt": 1781076395156
3
+ "version": "1.1.29-beta.5",
4
+ "publishedAt": 1781081318464
5
5
  }
@@ -120,11 +120,12 @@
120
120
  | 字段 | 最低要求 |
121
121
  | ---- | -------- |
122
122
  | `executiveSummary` | **3~5 段**,每段 **≥ 80 字**;解释「为什么」而不只报数;可拆自 `narrative.overall` 但须加深因果 |
123
- | `healthDiagnosis.lifecyclePhase` | `test-market` / `find-winner` / `scale` 三选一 |
124
- | `healthDiagnosis.lifecycleVerdict` | **≥ 60 字**,结合总花费与维度分散度 |
123
+ | `healthDiagnosis.lifecyclePhase` | `test-market` / `find-winner` / `scale` 三选一。**可省略**:`render --snapshot-dir` 按花费与集中度自动推断;若手写须与数据一致(render 校验 `lifecyclePhase` vs `lifecyclePhaseInferred`) |
124
+ | `healthDiagnosis.lifecycleVerdict` | **≥ 60 字**,结合总花费与维度分散度;可随 phase 由 CLI 补全 |
125
+ | **阶段判定(CLI 推断口径)** | **测市场**:总花费 &lt; $200,或活跃维度 ≥4 且赢家花费占比 &lt;35%;**放量**:Top 广告组花费/结果占比 ≥55% 且 CPL ≤ 账户均值;**找赢家**:其余有优劣分化但未高度集中 |
125
126
  | `healthDiagnosis.fourQuestions` | **恰好 4 张卡片**(钱花得值不值 / 赢在哪 / 输在哪 / 下月重点) |
126
127
  | 每张 `fourQuestions[]` | `verdict` + `evidence` **≥2 条**(含数字)+ `action` **≥ 40 字** |
127
- | `healthDiagnosis.scorecard` | **≥ 6 行**(平台、国家、广告组、受众等分项红绿灯) |
128
+ | `healthDiagnosis.scorecard` | **≥ 6 行**有效行;每行须 `item` / `data` / `signal`(green\|yellow\|red) / `signalLabel` / `advice`。**可省略**:`render --snapshot-dir` 自动补全;render 校验行数与字段完整性 |
128
129
  | `sections.platform.insight` | **≥ 200 字** |
129
130
  | `sections.country.insight` | **≥ 200 字** |
130
131
  | `sections.adSets.insight` | **≥ 200 字** |
@@ -152,7 +153,7 @@
152
153
  - [ ] `narrative.recommendations` 4 条,每条 content ≥150 字且含真实数字
153
154
  - [ ] `supplementaryRecommendations` 7 维齐全
154
155
  - [ ] `priorityPlan` high/medium/low 各 ≥2 条
155
- - [ ] HTML:`executiveSummary` ≥3 段、`fourQuestions` =4、`scorecard` ≥6
156
+ - [ ] HTML:`executiveSummary` ≥3 段、`fourQuestions` =4、`lifecyclePhase` 与快照推断一致(或省略由 CLI 补全)、`scorecard` ≥6 行且每行五字段齐全
156
157
  - [ ] HTML:`sections.platform/country/adSets.insight` 各 ≥200 字
157
158
  - [ ] HTML:`actionChecklist` 三列非空;`abTests` ≥3
158
159
  - [ ] Excel:总数据 Sheet 叙事块 4 节齐全,无 1 句话敷衍
@@ -16,19 +16,30 @@ siluzan-tso list-accounts [选项]
16
16
  | `-p, --page <n>` | 页码(默认 1) |
17
17
  | `--page-size <n>` | 每页数量(默认 20) |
18
18
  | `--json-out` | 输出原始 JSON |
19
- | `--detail` | **合并余额与近期消耗**(及展示/点击/转化/CPC、Arit 得分);**未传时 JSON 不含这些字段**,列表更快 |
19
+ | `--detail` | **合并余额与近期消耗**(及展示/点击/转化/CPC、Arit 得分);**未传时 JSON 不含这些字段**,列表更快。**仅适合少量账号**:每户额外并行请求,账号多时明显变慢;全量余额/消耗汇总改用 `balance-scan` / `accounts-digest` |
20
20
  | `--unicode` | 表格使用 Unicode 线框;**默认**为 ASCII `+- | ` 线框(兼容各类终端) |
21
21
  | `--plain` | 已默认 ASCII,无需再传;保留兼容旧脚本 |
22
22
  | `--refresh-dp` | 强制重拉 Datapermission(排查「本页全部 OAuth 失效」类会话异常) |
23
23
 
24
+ **命令定位(Agent 必读)**:`list-accounts` 主打**精准查询账号信息**(列表、计数、按名称/ID 找户、`entityId` / `mediaCustomerId` / 币种 / 状态等元数据)。**它不是余额/消耗汇总工具**——`--detail` 只是给「少量账号」顺带带出余额与近期消耗的增强项。
25
+
24
26
  **`--detail` 说明(Agent 必读):**
25
27
 
26
- | 模式 | 行为 |
27
- | ---- | ---- |
28
- | 默认(无 `--detail`) | 只拉账户列表;JSON **不含**余额、消耗、展示、点击、转化、CPC、Arit;表格对应列为「-」 |
29
- | `--detail` | 额外并行请求余额与投放概览;JSON 含上述字段的真实数值,**响应明显变慢** |
28
+ | 模式 | 行为 | 适用 |
29
+ | ---- | ---- | ---- |
30
+ | 默认(无 `--detail`) | 只拉账户列表;JSON **不含**余额、消耗、展示、点击、转化、CPC、Arit;表格对应列为「-」 | 任意规模的账号列表/计数/元数据 |
31
+ | `--detail` | 每户额外并行请求余额与投放概览;JSON 含上述字段真实数值,**响应明显变慢** | **仅少量账号**(单户 / 数个 `-k` 命中) |
32
+
33
+ **何时不要用 `--detail`(改用专用命令):**
34
+
35
+ | 需求 | 推荐命令 |
36
+ | ---- | -------- |
37
+ | **全部账号**的余额、续航/充值预警 | `balance-scan`(见 `SKILL.md` Playbook P2) |
38
+ | **全部/多账号**的消耗、点击、转化、CTR/CPC/CPA 投放画像汇总 | `accounts-digest`(本文下方) |
39
+ | 单户/少量户精确余额或消耗 | `balance` / `stats`(`-a` 指定 ID) |
40
+ | 列表顺带看少量户余额/消耗 | `list-accounts --detail` |
30
41
 
31
- 需要真实余额或消耗时:单户优先 `balance` / `stats`;列表顺带看余额/消耗时加 `--detail`。**禁止**在 JSON 无余额/消耗字段时臆造数值。
42
+ **禁止**在 JSON 无余额/消耗字段时臆造数值;也**禁止**用 `--detail` 全量扫描所有账号去拼余额/消耗汇总(改用上表专用命令)。
32
43
 
33
44
  ### Agent 意图速查(**必读 · 避免多次试探**)
34
45
 
@@ -40,7 +51,8 @@ siluzan-tso list-accounts [选项]
40
51
  | 有多少个 Google 账户 | 同上 | **`total`**(无需翻页;`itemCount < total` 时说明 page-size 不够大) |
41
52
  | 列出全部某媒体(TikTok / MetaAd 等) | `list-accounts -m <媒体> --page-size 999 --json-out <dir>` | 同上 |
42
53
  | 只查某一个户 | `list-accounts -m <媒体> -k <id或名称> --json-out <dir>` | 无需大 page-size |
43
- | 列表顺带看余额/消耗 | 在上述命令加 `--detail`(**明显变慢**;仅用户要余额/消耗时) | 含 `ma.balance` 等 enrichment 字段 |
54
+ | **少量账号**顺带看余额/消耗 | 在上述命令加 `--detail`(**明显变慢**;仅少量账号且用户要余额/消耗时) | 含 `ma.balance` 等 enrichment 字段 |
55
+ | **全部账号**的余额/消耗汇总 | **勿用 `--detail` 全量扫描** → 余额预警用 `balance-scan`、消耗画像用 `accounts-digest` | 见各命令章节 |
44
56
 
45
57
  **执行纪律**:
46
58
 
@@ -71,8 +83,9 @@ siluzan-tso list-accounts -m Google -k "品牌A" --json-out ./snap
71
83
  # 只看正常状态
72
84
  siluzan-tso list-accounts -m TikTok -s normal --page-size 999 --json-out ./snap
73
85
 
74
- # 列表同时带出真实余额与近期消耗(较慢,仅用户要余额/消耗时)
75
- siluzan-tso list-accounts -m Google --page-size 999 --detail --json-out ./snap
86
+ # 列表同时带出真实余额与近期消耗(较慢,仅少量账号且用户要余额/消耗时)
87
+ # 注意:全部账号的余额/消耗汇总请改用 balance-scan / accounts-digest,勿用 --detail 全量扫描
88
+ siluzan-tso list-accounts -m Google -k "品牌A" --detail --json-out ./snap
76
89
 
77
90
  # 极少数账户超过 999 条时才翻页(先确认读盘 total > itemCount)
78
91
  siluzan-tso list-accounts -m Google --page 2 --page-size 999 --json-out ./snap-p2
@@ -263,10 +263,10 @@ ${planHtml ? `<h3 style="margin-top:24px">优先级改进计划</h3>${planHtml}`
263
263
  function renderHealthDiagnosis(hd) {
264
264
  if (!hd) return "";
265
265
 
266
- const phase = hd.lifecyclePhase || "find-winner";
266
+ const phase = hd.lifecyclePhase || "";
267
267
  const lifecycle = LIFECYCLE_PHASES.map(
268
268
  (p) => `
269
- <div class="phase${p.id === phase ? " active" : ""}">
269
+ <div class="phase${phase && p.id === phase ? " active" : ""}">
270
270
  <div class="step">${escapeHtml(p.step)}</div>
271
271
  <div class="name">${escapeHtml(p.name)}</div>
272
272
  <div class="desc">${escapeHtml(p.desc)}</div>
@@ -166,7 +166,7 @@ siluzan-tso facebook-analysis -a <mediaCustomerId> --start <s> --end <e> --json-
166
166
  用户未指定 Excel 时,除 `narrative` 外 **必须** 填写(字数见 `meta-period-report-rules.md` §四):
167
167
 
168
168
  - `executiveSummary[]`:**3~5 段**「为什么」解读(每段 ≥80 字)
169
- - `healthDiagnosis`:三阶段 + **4 张四问卡片** + **≥6 行**红绿灯表
169
+ - `healthDiagnosis`:三阶段 + **4 张四问卡片** + **≥6 行**红绿灯表(③ 该加还是该减;可省略 `scorecard`,render 时由 `--snapshot-dir` 自动补全)
170
170
  - `sections.platform/country/adSets.insight`:各 **≥200 字**
171
171
  - `sections.audience`:`goldenProfile` ≥3 条 + `antiProfile` ≥2 条
172
172
  - `sections.landingPage.rows`:≥3 行
@@ -201,7 +201,7 @@ siluzan-tso facebook-analysis -a <mediaCustomerId> --start <s> --end <e> --json-
201
201
  | **`priorityPlan`** | **推荐** | 高/中/低各 ≥2 条;HTML 强烈建议填写 |
202
202
  | `tables` / `charts` | 可省略 | Sheet2~5;由快照自动汇总 |
203
203
  | `executiveSummary` | **HTML 必填** | 3~5 段深度摘要 |
204
- | `healthDiagnosis` | **HTML 必填** | 四问 + 红绿灯表 |
204
+ | `healthDiagnosis` | **HTML 必填** | 四问 + 红绿灯表(`scorecard` 可由 CLI 从快照自动补全) |
205
205
  | `sections.*.insight` | **HTML 必填** | 各维度 ≥200 字 |
206
206
  | `abTests` / `actionChecklist` | **HTML 必填** | ≥3 实验 + 三列行动清单 |
207
207
 
@@ -9,7 +9,7 @@ $ErrorActionPreference = 'Stop'
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  $PKG_NAME = 'siluzan-tso-cli'
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- $PKG_VERSION = '1.1.29-beta.4'
12
+ $PKG_VERSION = '1.1.29-beta.5'
13
13
  $CLI_BIN = 'siluzan-tso'
14
14
  $SKILL_LABEL = 'Siluzan TSO'
15
15
  $INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
@@ -9,7 +9,7 @@ set -euo pipefail
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  readonly PKG_NAME="siluzan-tso-cli"
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- readonly PKG_VERSION="1.1.29-beta.4"
12
+ readonly PKG_VERSION="1.1.29-beta.5"
13
13
  readonly CLI_BIN="siluzan-tso"
14
14
  readonly SKILL_LABEL="Siluzan TSO"
15
15
  readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.29-beta.4",
3
+ "version": "1.1.29-beta.5",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",