siluzan-tso-cli 1.1.35-beta.3 → 1.1.35-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.35-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.35-beta.5),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -5471,7 +5471,8 @@ var init_google_analysis = __esm({
5471
5471
  };
5472
5472
  GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS = [
5473
5473
  "// \u91D1\u989D\uFF1A`campaigns[].budgetAmountYuan` / `campaignTargetCpaYuan` / `maximizeConversionsTargetCpaYuan` \u5747\u4E3A**\u5143**\uFF08CLI \u51FA\u53E3\u7EDF\u4E00\uFF09\uFF1B\u540C\u884C `spend` / `averageCpc` / `costPerConversion` \u4E5F\u662F\u5143\u3002",
5474
- "// \u65E7\u5B57\u6BB5 `budgetAmount`\uFF08\u5206\uFF09/ `*Micros`\uFF08\u5FAE\u5143\uFF09\u5DF2\u4E0D\u518D\u843D\u76D8\uFF0C**\u7981\u6B62**\u518D\u505A \xF7100 / \xF71_000_000 \u6362\u7B97\u3002"
5474
+ "// \u65E7\u5B57\u6BB5 `budgetAmount`\uFF08\u5206\uFF09/ `*Micros`\uFF08\u5FAE\u5143\uFF09\u5DF2\u4E0D\u518D\u843D\u76D8\uFF0C**\u7981\u6B62**\u518D\u505A \xF7100 / \xF71_000_000 \u6362\u7B97\u3002",
5475
+ "// \u72B6\u6001\uFF1A\u6BCF\u884C\u542B `campaignStatusDisplay`\uFF08\u6295\u653E\u4E2D/\u5DF2\u6682\u505C/\u5DF2\u79FB\u9664\uFF09\uFF1B\u62A5\u544A\u72B6\u6001\u5217**\u76F4\u63A5\u8BFB\u6B64\u5B57\u6BB5**\uFF0C\u7981\u6B62\u5BF9 `campaignStatus` \u5199 `MAP[s]||'\u5DF2\u79FB\u9664'`\u3002"
5475
5476
  ];
5476
5477
  GOOGLE_ANALYSIS_ADS_OUTLINE_BUDGET_HINTS = [
5477
5478
  "// \u91D1\u989D\uFF1A`ads[].spend` / `averageCpc` / `costPerConversion` \u843D\u76D8\u524D\u5DF2\u7EDF\u4E00\u4E3A**\u5143**\uFF08`costPerConversion` \u7531\u7F51\u5173\u5FAE\u5143 \xF71e6\uFF09\uFF1B**\u7981\u6B62**\u518D\u5BF9 `costPerConversion` \u505A \xF71_000_000\u3002"
@@ -101445,6 +101446,38 @@ var init_city_to_country = __esm({
101445
101446
  }
101446
101447
  });
101447
101448
 
101449
+ // src/commands/google-analysis/format-campaign-status.ts
101450
+ function formatGoogleCampaignStatusDisplay(status) {
101451
+ if (status == null) return "\u2014";
101452
+ const raw = String(status).trim();
101453
+ if (raw === "") return "\u2014";
101454
+ switch (raw.toUpperCase()) {
101455
+ case "ENABLED":
101456
+ return "\u6295\u653E\u4E2D";
101457
+ case "PAUSED":
101458
+ return "\u5DF2\u6682\u505C";
101459
+ case "REMOVED":
101460
+ return "\u5DF2\u79FB\u9664";
101461
+ default:
101462
+ return "\u672A\u77E5";
101463
+ }
101464
+ }
101465
+ function readGoogleCampaignStatusRaw(row) {
101466
+ if (row.campaignStatusV2 !== void 0 && row.campaignStatusV2 !== null) {
101467
+ return row.campaignStatusV2;
101468
+ }
101469
+ return row.campaignStatus;
101470
+ }
101471
+ function annotateCampaignStatusDisplay(row) {
101472
+ if (row.campaignStatusDisplay !== void 0) return;
101473
+ row.campaignStatusDisplay = formatGoogleCampaignStatusDisplay(readGoogleCampaignStatusRaw(row));
101474
+ }
101475
+ var init_format_campaign_status = __esm({
101476
+ "src/commands/google-analysis/format-campaign-status.ts"() {
101477
+ "use strict";
101478
+ }
101479
+ });
101480
+
101448
101481
  // src/commands/google-analysis/translate-fields.ts
101449
101482
  function lookupZh(dict, key) {
101450
101483
  if (typeof key !== "string" || key === "") return void 0;
@@ -101463,6 +101496,14 @@ function annotateArrayRows(rows, source, target, dict) {
101463
101496
  }
101464
101497
  }
101465
101498
  }
101499
+ function annotateCampaignStatusDisplayRows(rows) {
101500
+ if (!Array.isArray(rows)) return;
101501
+ for (const r of rows) {
101502
+ if (r && typeof r === "object" && !Array.isArray(r)) {
101503
+ annotateCampaignStatusDisplay(r);
101504
+ }
101505
+ }
101506
+ }
101466
101507
  function annotateCountryRollup(rows) {
101467
101508
  if (!Array.isArray(rows)) return;
101468
101509
  for (const r of rows) {
@@ -101483,9 +101524,15 @@ function annotateCountryRollup(rows) {
101483
101524
  function annotateZhFields(payload, section) {
101484
101525
  if (payload === null || typeof payload !== "object") return payload;
101485
101526
  switch (section) {
101527
+ case "campaigns": {
101528
+ const rows = payload.campaigns;
101529
+ annotateCampaignStatusDisplayRows(rows);
101530
+ break;
101531
+ }
101486
101532
  case "keywords": {
101487
101533
  const rows = payload.keywords;
101488
101534
  annotateArrayRows(rows, "keywordMatchType", "keywordMatchTypeZh", MATCH_TYPE_DICT);
101535
+ annotateCampaignStatusDisplayRows(rows);
101489
101536
  break;
101490
101537
  }
101491
101538
  case "search-terms": {
@@ -101520,6 +101567,7 @@ var init_translate_fields = __esm({
101520
101567
  init_match_type_en2zh();
101521
101568
  init_geo_en2zh();
101522
101569
  init_city_to_country();
101570
+ init_format_campaign_status();
101523
101571
  MATCH_TYPE_DICT = match_type_en2zh_default;
101524
101572
  GEO_DICT = geo_en2zh_default;
101525
101573
  CITY_DICT = city_to_country_default.byName ?? {};
@@ -105346,12 +105394,12 @@ init_cli_json_snapshot();
105346
105394
 
105347
105395
  // src/utils/list-json-envelope.ts
105348
105396
  function wrapListJson(params) {
105349
- const itemCount = Array.isArray(params.items) ? params.items.length : 0;
105397
+ const itemCount2 = Array.isArray(params.items) ? params.items.length : 0;
105350
105398
  return {
105351
105399
  page: params.page,
105352
105400
  pageSize: params.pageSize,
105353
105401
  total: params.total === void 0 || params.total === null ? null : params.total,
105354
- itemCount,
105402
+ itemCount: itemCount2,
105355
105403
  items: params.items
105356
105404
  };
105357
105405
  }
@@ -124534,7 +124582,7 @@ function buildGoogleAdsDiagnosisCollectAgentHint(snapDir) {
124534
124582
  "Google Ads \u8BCA\u65AD collect \u5DF2\u5B8C\u6210\uFF08\u4EC5\u4E8B\u5B9E\u6570\u636E\uFF0C\u4E0D\u542B\u5EFA\u8BAE\u6027\u6587\u6848\uFF09\u3002",
124535
124583
  `1) Read ${dir}/${GOOGLE_ADS_DIAGNOSIS_COLLECT_BASENAME}\uFF08reportData + agentBrief\uFF09\uFF1B`,
124536
124584
  "2) Read references/report-templates/google-ads-diagnosis.md\uFF0C\u7531 Agent \u64B0\u5199\u5168\u90E8 analysis/suggestions\u3001diagnosisOverview\u3001summary\u3001accountInfo.businessModel/industry\u3001budgetCompetitiveness[].strategy\u3001newFeatures[].optimizerRecommendation\u3001adCreativeOptimization.items[].suggestion \u7B49\uFF1B",
124537
- `3) \u5C06 reportData \u4E0E\u53D9\u4E8B\u5B57\u6BB5\u5408\u5E76\u5199\u5165 ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME}\uFF08**\u7981\u6B62\u6539\u5199** campaigns/geographic/keywords \u7684 items \u884C\uFF0C\u53EA\u586B analysis/suggestions\uFF09\uFF1B`,
124585
+ `3) \u5C06 reportData \u4E0E\u53D9\u4E8B\u5B57\u6BB5\u5408\u5E76\u5199\u5165 ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME}\uFF08**\u7981\u6B62\u5199\u5165/\u8986\u76D6** campaigns/geographic/keywords \u7684 items \u884C\uFF0C\u53EA\u586B analysis/suggestions\uFF1Bitems \u4EE5 collect \u4E3A\u51C6\uFF0Crender \u4F1A\u81EA\u52A8\u8986\u76D6 Agent \u6539\u52A8\u7684\u5BF9\u6BD4\u8868\uFF09\uFF1B`,
124538
124586
  `4) siluzan-tso google-ads-diagnosis render --data ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME} --out ${dir}/google-ads-diagnosis-report.html\uFF08\u9ED8\u8BA4\u540C\u76EE\u5F55 collect \u81EA\u52A8\u6062\u590D\u88AB\u6539\u574F\u7684 items\uFF09\u3002`,
124539
124587
  "\u7981\u6B62\u5728\u672A\u7ECF\u8FC7 Agent \u64B0\u5199\u7684\u60C5\u51B5\u4E0B\u76F4\u63A5 render collect \u4EA7\u7269\uFF1B\u7F3A\u4EFB\u4E00\u5EFA\u8BAE\u5B57\u6BB5 render \u4F1A\u5931\u8D25\u3002"
124540
124588
  ].join("\n");
@@ -124587,6 +124635,20 @@ function buildGoogleAdsDiagnosisAgentBrief(input) {
124587
124635
  campaigns: input.campaignHints,
124588
124636
  keywords: input.keywordHints
124589
124637
  },
124638
+ comparisonTableItemCounts: input.comparisonTableItemCounts,
124639
+ agentRules: {
124640
+ doNotModifyItemRows: [
124641
+ "campaigns.items",
124642
+ "geographic.items",
124643
+ "keywords.items"
124644
+ ],
124645
+ renderRestoresComparisonItemsFromCollect: "render \u9ED8\u8BA4\u540C\u76EE\u5F55 google-ads-diagnosis-collect.json \u4F1A\u4EE5 collect \u8986\u76D6\u4E0A\u8FF0 items\uFF08Agent \u53EA\u586B analysis/suggestions\uFF09",
124646
+ mayEditItemFields: [
124647
+ "biddingStrategy.items[].recommendationReason",
124648
+ "newFeatures.items[].optimizerRecommendation",
124649
+ "adCreativeOptimization.items[].suggestion"
124650
+ ]
124651
+ },
124590
124652
  agentDeliverable: {
124591
124653
  outputFile: "google-ads-diagnosis.json",
124592
124654
  structure: "\u4E0E reportData \u540C\u7ED3\u6784\u7684 sectionData\uFF1B\u987B\u586B diagnosisOverview\u3001metrics.analysis/suggestions\u3001\u5404\u6A21\u5757 analysis/suggestions\u3001summary.keyIssues\u3001summary.optimizationRoadmap[]\uFF08priority/focusArea/actionItems/expectedOutcome\uFF09\u3001budgetCompetitiveness[].strategy\u3001newFeatures[].optimizerRecommendation \u7B49",
@@ -125380,11 +125442,58 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
125380
125442
  profile.industry,
125381
125443
  ...campaignCurrent.map((r) => String(r.campaignName ?? ""))
125382
125444
  ].filter(Boolean),
125383
- keywordHints: zeroConvKw.slice(0, 5).map((r) => String(r.keyword ?? ""))
125445
+ keywordHints: zeroConvKw.slice(0, 5).map((r) => String(r.keyword ?? "")),
125446
+ comparisonTableItemCounts: {
125447
+ campaigns: campaignItems.length,
125448
+ geographic: geoItems.length,
125449
+ keywords: kwItems.length
125450
+ }
125384
125451
  });
125385
125452
  return { reportData: data, agentBrief };
125386
125453
  }
125387
125454
 
125455
+ // src/commands/google-ads-diagnosis/collect-integrity.ts
125456
+ function asRecord5(value) {
125457
+ if (value && typeof value === "object" && !Array.isArray(value)) {
125458
+ return value;
125459
+ }
125460
+ return null;
125461
+ }
125462
+ function itemCount(block) {
125463
+ const items = asRecord5(block)?.items;
125464
+ return Array.isArray(items) ? items.length : 0;
125465
+ }
125466
+ function getCollectIntegrityWarnings(reportData) {
125467
+ const warnings = [];
125468
+ const kwItems = itemCount(reportData.keywords);
125469
+ const fullKwItems = itemCount(reportData.fullKeywords);
125470
+ const keywordCount = Number(asRecord5(reportData.structure)?.keywordCount ?? 0);
125471
+ if (kwItems === 0 && fullKwItems > 0) {
125472
+ warnings.push(
125473
+ `keywords.items \u5BF9\u6BD4\u8868\u4E3A\u7A7A\uFF0C\u4F46 fullKeywords.items \u6709 ${fullKwItems} \u884C\uFF1B\u8BF7\u786E\u8BA4 collect \u662F\u5426\u6210\u529F\u62C9\u53D6\u4E0A\u4E00\u5468\u671F keywords \u5FEB\u7167\uFF08\u73AF\u6BD4\u5217\u4F9D\u8D56 prev \u5468\u671F JSON\uFF09\u3002`
125474
+ );
125475
+ } else if (kwItems === 0 && keywordCount > 0) {
125476
+ warnings.push(
125477
+ `keywords.items \u5BF9\u6BD4\u8868\u4E3A\u7A7A\uFF0C\u4F46 structure.keywordCount=${keywordCount}\uFF1B\u8BF7\u786E\u8BA4\u4E0A\u4E00\u5468\u671F keywords \u5FEB\u7167\u662F\u5426\u62C9\u53D6\u6210\u529F\u3002`
125478
+ );
125479
+ }
125480
+ const campaignItems = itemCount(reportData.campaigns);
125481
+ const campaignCount = Number(asRecord5(reportData.structure)?.campaignCount ?? 0);
125482
+ if (campaignItems === 0 && campaignCount > 0) {
125483
+ warnings.push(
125484
+ `campaigns.items \u5BF9\u6BD4\u8868\u4E3A\u7A7A\uFF0C\u4F46 structure.campaignCount=${campaignCount}\uFF1B\u8BF7\u786E\u8BA4\u4E0A\u4E00\u5468\u671F campaigns \u5FEB\u7167\u662F\u5426\u62C9\u53D6\u6210\u529F\u3002`
125485
+ );
125486
+ }
125487
+ const geoItems = itemCount(reportData.geographic);
125488
+ const fullGeoItems = itemCount(reportData.fullGeographic);
125489
+ if (geoItems === 0 && fullGeoItems > 0) {
125490
+ warnings.push(
125491
+ `geographic.items \u5BF9\u6BD4\u8868\u4E3A\u7A7A\uFF0C\u4F46 fullGeographic.items \u6709 ${fullGeoItems} \u884C\uFF1B\u8BF7\u786E\u8BA4\u4E0A\u4E00\u5468\u671F geographic \u5FEB\u7167\u662F\u5426\u62C9\u53D6\u6210\u529F\u3002`
125492
+ );
125493
+ }
125494
+ return warnings;
125495
+ }
125496
+
125388
125497
  // src/commands/google-ads-diagnosis/invoke-google-analysis.ts
125389
125498
  import { spawnSync as spawnSync2 } from "child_process";
125390
125499
  function invokeGoogleAnalysisCli(opts) {
@@ -125527,6 +125636,12 @@ async function runGoogleAdsDiagnosisCollect(opts) {
125527
125636
  website: websiteUrl || opts.website,
125528
125637
  landingPage
125529
125638
  });
125639
+ for (const warning of getCollectIntegrityWarnings(reportData)) {
125640
+ console.error(`
125641
+ \u26A0\uFE0F collect \u5B8C\u6574\u6027\uFF1A${warning}
125642
+ `);
125643
+ degraded = true;
125644
+ }
125530
125645
  const collectPayload = {
125531
125646
  kind: "siluzan-tso-google-ads-diagnosis-collect",
125532
125647
  collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -125567,7 +125682,7 @@ import fs18 from "fs/promises";
125567
125682
  import path23 from "path";
125568
125683
 
125569
125684
  // src/commands/google-ads-diagnosis/report-content.ts
125570
- function asRecord5(value) {
125685
+ function asRecord6(value) {
125571
125686
  if (value && typeof value === "object" && !Array.isArray(value)) {
125572
125687
  return value;
125573
125688
  }
@@ -125576,7 +125691,7 @@ function asRecord5(value) {
125576
125691
  function getNestedValue(root, path34) {
125577
125692
  let cur = root;
125578
125693
  for (const key of path34) {
125579
- const rec = asRecord5(cur);
125694
+ const rec = asRecord6(cur);
125580
125695
  if (!rec) return void 0;
125581
125696
  cur = rec[key];
125582
125697
  }
@@ -125593,7 +125708,7 @@ function validateBudgetCompetitivenessStrategies(data) {
125593
125708
  }
125594
125709
  const warnings = [];
125595
125710
  for (let i = 0; i < items.length; i++) {
125596
- const row = asRecord5(items[i]);
125711
+ const row = asRecord6(items[i]);
125597
125712
  const name2 = String(row?.name ?? row?.metric ?? i);
125598
125713
  if (!hasNarrativeContent(row?.strategy)) {
125599
125714
  warnings.push(`budgetCompetitiveness[${name2}].strategy \u4E3A\u7A7A\uFF08\u987B\u7531 Agent \u586B\u5199\uFF09`);
@@ -125602,13 +125717,13 @@ function validateBudgetCompetitivenessStrategies(data) {
125602
125717
  return warnings;
125603
125718
  }
125604
125719
  function validateNewFeatureRecommendations(data) {
125605
- const nf = asRecord5(data.newFeatures);
125720
+ const nf = asRecord6(data.newFeatures);
125606
125721
  if (!nf) return [];
125607
125722
  const items = nf.items;
125608
125723
  if (!Array.isArray(items)) return [];
125609
125724
  const warnings = [];
125610
125725
  for (let i = 0; i < items.length; i++) {
125611
- const row = asRecord5(items[i]);
125726
+ const row = asRecord6(items[i]);
125612
125727
  const name2 = String(row?.strategyName ?? row?.feature ?? row?.strategy ?? i);
125613
125728
  if (!hasNarrativeContent(row?.optimizerRecommendation)) {
125614
125729
  warnings.push(`newFeatures.items[${name2}].optimizerRecommendation \u4E3A\u7A7A\uFF08\u987B\u7531 Agent \u586B\u5199\uFF09`);
@@ -125617,13 +125732,13 @@ function validateNewFeatureRecommendations(data) {
125617
125732
  return warnings;
125618
125733
  }
125619
125734
  function validateAdCreativeItemSuggestions(data) {
125620
- const creative = asRecord5(data.adCreativeOptimization);
125735
+ const creative = asRecord6(data.adCreativeOptimization);
125621
125736
  if (!creative) return [];
125622
125737
  const items = creative.items;
125623
125738
  if (!Array.isArray(items) || items.length === 0) return [];
125624
125739
  const warnings = [];
125625
125740
  for (let i = 0; i < items.length; i++) {
125626
- const row = asRecord5(items[i]);
125741
+ const row = asRecord6(items[i]);
125627
125742
  const title = String(row?.adTitle ?? i);
125628
125743
  if (!hasNarrativeContent(row?.suggestion ?? row?.suggestions)) {
125629
125744
  warnings.push(
@@ -125638,7 +125753,7 @@ function validateGoogleAdsDiagnosisContent(data) {
125638
125753
  (key) => data[key] === void 0
125639
125754
  );
125640
125755
  const warnings = [];
125641
- const accountInfo = asRecord5(data.accountInfo);
125756
+ const accountInfo = asRecord6(data.accountInfo);
125642
125757
  if (accountInfo && !accountInfo.companyName && !accountInfo.period) {
125643
125758
  warnings.push("accountInfo \u7F3A\u5C11 companyName \u4E0E period\uFF0C\u9875\u7709\u53EF\u80FD\u4E3A\u7A7A");
125644
125759
  }
@@ -125686,7 +125801,7 @@ var GOOGLE_ADS_DIAGNOSIS_RESTORE_ITEM_MODULES = [
125686
125801
  GOOGLE_ADS_DIAGNOSIS_BIDDING_STRATEGY_MODULE,
125687
125802
  GOOGLE_ADS_DIAGNOSIS_NEW_FEATURES_MODULE
125688
125803
  ];
125689
- function asRecord6(value) {
125804
+ function asRecord7(value) {
125690
125805
  if (value && typeof value === "object" && !Array.isArray(value)) {
125691
125806
  return value;
125692
125807
  }
@@ -125697,7 +125812,7 @@ function isValidRateChange(value) {
125697
125812
  return typeof value === "number" && Number.isFinite(value);
125698
125813
  }
125699
125814
  function isComparisonTableRowValid(row) {
125700
- const rec = asRecord6(row);
125815
+ const rec = asRecord7(row);
125701
125816
  if (!rec) return false;
125702
125817
  if (!String(rec.title ?? "").trim()) return false;
125703
125818
  if (!Number.isFinite(Number(rec.currentCost))) return false;
@@ -125709,12 +125824,12 @@ function isComparisonTableRowValid(row) {
125709
125824
  return true;
125710
125825
  }
125711
125826
  function isNewFeatureRowValid(row) {
125712
- const rec = asRecord6(row);
125827
+ const rec = asRecord7(row);
125713
125828
  if (!rec) return false;
125714
125829
  return Boolean(String(rec.strategyName ?? rec.feature ?? rec.strategy ?? "").trim());
125715
125830
  }
125716
125831
  function isBiddingStrategyRowValid(row) {
125717
- const rec = asRecord6(row);
125832
+ const rec = asRecord7(row);
125718
125833
  if (!rec) return false;
125719
125834
  if (!String(rec.campaignName ?? "").trim()) return false;
125720
125835
  if (!Number.isFinite(Number(rec.duration)) || Number(rec.duration) <= 0) return false;
@@ -125739,14 +125854,32 @@ function moduleItemsNeedRestore(items, validator) {
125739
125854
  if (!Array.isArray(items) || items.length === 0) return false;
125740
125855
  return items.some((row) => !validator(row));
125741
125856
  }
125857
+ function isComparisonTableModule(mod) {
125858
+ return GOOGLE_ADS_DIAGNOSIS_COMPARISON_MODULES.includes(mod);
125859
+ }
125860
+ function comparisonItemsDifferFromCollect(finalItems, collectItems) {
125861
+ if (!Array.isArray(finalItems)) return true;
125862
+ if (finalItems.length !== collectItems.length) return true;
125863
+ return JSON.stringify(finalItems) !== JSON.stringify(collectItems);
125864
+ }
125742
125865
  function getFactItemsErrors(data) {
125743
125866
  const errors = [];
125744
125867
  for (const mod of GOOGLE_ADS_DIAGNOSIS_RESTORE_ITEM_MODULES) {
125745
125868
  const validator = MODULE_ITEM_VALIDATORS[mod];
125746
125869
  const hint = MODULE_ITEM_ERROR_HINT[mod];
125747
- const block = asRecord6(data[mod]);
125870
+ const block = asRecord7(data[mod]);
125748
125871
  const items = block?.items;
125749
- if (!Array.isArray(items) || items.length === 0) continue;
125872
+ if (!Array.isArray(items) || items.length === 0) {
125873
+ if (mod === "keywords") {
125874
+ const fullKw = asRecord7(data.fullKeywords)?.items;
125875
+ if (Array.isArray(fullKw) && fullKw.length > 0) {
125876
+ errors.push(
125877
+ "keywords.items \u4E3A\u7A7A\u4F46 fullKeywords.items \u6709\u6570\u636E\uFF1B\u8BF7\u4FDD\u7559\u540C\u76EE\u5F55 google-ads-diagnosis-collect.json \u540E\u91CD\u65B0 render\uFF08\u4F1A\u81EA\u52A8\u6062\u590D\u5BF9\u6BD4\u8868\uFF09"
125878
+ );
125879
+ }
125880
+ }
125881
+ continue;
125882
+ }
125750
125883
  for (let i = 0; i < items.length; i++) {
125751
125884
  if (!validator(items[i])) {
125752
125885
  errors.push(`${mod}.items[${i}] ${hint}`);
@@ -125763,15 +125896,15 @@ var LANDING_PAGE_SPEED_KEYS = [
125763
125896
  "speedIndex"
125764
125897
  ];
125765
125898
  function isLandingPageSpeedValid(value) {
125766
- const rec = asRecord6(value);
125899
+ const rec = asRecord7(value);
125767
125900
  if (!rec) return false;
125768
125901
  return LANDING_PAGE_SPEED_KEYS.every(
125769
125902
  (key) => typeof rec[key] === "number" && Number.isFinite(rec[key])
125770
125903
  );
125771
125904
  }
125772
125905
  function restoreLandingPageSpeedFromCollect(finalData, collectReportData) {
125773
- const finalBlock = asRecord6(finalData[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE]);
125774
- const collectBlock = asRecord6(collectReportData[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE]);
125906
+ const finalBlock = asRecord7(finalData[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE]);
125907
+ const collectBlock = asRecord7(collectReportData[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE]);
125775
125908
  if (!collectBlock) return { data: finalData, restored: false };
125776
125909
  const needsDesktop = !isLandingPageSpeedValid(finalBlock?.desktop) && isLandingPageSpeedValid(collectBlock.desktop);
125777
125910
  const needsMobile = !isLandingPageSpeedValid(finalBlock?.mobile) && isLandingPageSpeedValid(collectBlock.mobile);
@@ -125789,12 +125922,13 @@ function mergeComparisonItemsFromCollect(finalData, collectReportData) {
125789
125922
  const restoredModules = [];
125790
125923
  for (const mod of GOOGLE_ADS_DIAGNOSIS_RESTORE_ITEM_MODULES) {
125791
125924
  const validator = MODULE_ITEM_VALIDATORS[mod];
125792
- const finalBlock = asRecord6(data[mod]);
125793
- const collectBlock = asRecord6(collectReportData[mod]);
125925
+ const finalBlock = asRecord7(data[mod]);
125926
+ const collectBlock = asRecord7(collectReportData[mod]);
125794
125927
  const finalItems = finalBlock?.items;
125795
125928
  const collectItems = collectBlock?.items;
125796
- if (!moduleItemsNeedRestore(finalItems, validator)) continue;
125797
125929
  if (!Array.isArray(collectItems) || collectItems.length === 0) continue;
125930
+ const needsRestore = isComparisonTableModule(mod) ? comparisonItemsDifferFromCollect(finalItems, collectItems) : moduleItemsNeedRestore(finalItems, validator);
125931
+ if (!needsRestore) continue;
125798
125932
  data[mod] = {
125799
125933
  ...finalBlock ?? {},
125800
125934
  items: collectItems
@@ -125805,7 +125939,7 @@ function mergeComparisonItemsFromCollect(finalData, collectReportData) {
125805
125939
  }
125806
125940
 
125807
125941
  // src/commands/google-ads-diagnosis/narrative-consistency-check.ts
125808
- function asRecord7(value) {
125942
+ function asRecord8(value) {
125809
125943
  if (value && typeof value === "object" && !Array.isArray(value)) {
125810
125944
  return value;
125811
125945
  }
@@ -125813,14 +125947,14 @@ function asRecord7(value) {
125813
125947
  }
125814
125948
  function collectNarrativeTexts(data) {
125815
125949
  const texts = [];
125816
- const summary = asRecord7(data.summary);
125950
+ const summary = asRecord8(data.summary);
125817
125951
  if (Array.isArray(summary?.keyIssues)) {
125818
125952
  for (const item of summary.keyIssues) texts.push(String(item ?? ""));
125819
125953
  }
125820
- const overview = asRecord7(data.diagnosisOverview);
125954
+ const overview = asRecord8(data.diagnosisOverview);
125821
125955
  if (Array.isArray(overview?.disadvantages)) {
125822
125956
  for (const item of overview.disadvantages) {
125823
- const rec = asRecord7(item);
125957
+ const rec = asRecord8(item);
125824
125958
  if (rec) {
125825
125959
  texts.push(String(rec.title ?? ""), String(rec.description ?? ""));
125826
125960
  } else {
@@ -125831,16 +125965,16 @@ function collectNarrativeTexts(data) {
125831
125965
  return texts.filter((t) => t.trim().length > 0);
125832
125966
  }
125833
125967
  function newFeatureAccountStatus(data, strategy) {
125834
- const nf = asRecord7(data.newFeatures);
125968
+ const nf = asRecord8(data.newFeatures);
125835
125969
  const items = nf?.items;
125836
125970
  if (!Array.isArray(items)) return null;
125837
- const row = items.find((it) => asRecord7(it)?.strategy === strategy);
125838
- const rec = asRecord7(row);
125971
+ const row = items.find((it) => asRecord8(it)?.strategy === strategy);
125972
+ const rec = asRecord8(row);
125839
125973
  if (!rec || typeof rec.accountStatus !== "boolean") return null;
125840
125974
  return rec.accountStatus;
125841
125975
  }
125842
125976
  function goldAccountFlag(data, key) {
125843
- const gold = asRecord7(data.goldAccount);
125977
+ const gold = asRecord8(data.goldAccount);
125844
125978
  const value = gold?.[key];
125845
125979
  return typeof value === "boolean" ? value : null;
125846
125980
  }
@@ -125871,7 +126005,7 @@ function getNarrativeConsistencyErrors(data) {
125871
126005
  }
125872
126006
 
125873
126007
  // src/commands/google-ads-diagnosis/render-report.ts
125874
- function asRecord8(value) {
126008
+ function asRecord9(value) {
125875
126009
  if (value && typeof value === "object" && !Array.isArray(value)) {
125876
126010
  return value;
125877
126011
  }
@@ -125880,8 +126014,8 @@ function asRecord8(value) {
125880
126014
  async function tryLoadCollectReportData(collectPath) {
125881
126015
  try {
125882
126016
  const raw = JSON.parse(await fs18.readFile(collectPath, "utf8"));
125883
- const root = asRecord8(raw);
125884
- const reportData = asRecord8(root?.reportData);
126017
+ const root = asRecord9(raw);
126018
+ const reportData = asRecord9(root?.reportData);
125885
126019
  return reportData ?? root;
125886
126020
  } catch {
125887
126021
  return null;
@@ -125898,7 +126032,7 @@ async function runGoogleAdsDiagnosisRender(opts) {
125898
126032
  `);
125899
126033
  process.exit(1);
125900
126034
  }
125901
- let data = asRecord8(dataRaw);
126035
+ let data = asRecord9(dataRaw);
125902
126036
  if (!opts.noMergeCollect) {
125903
126037
  const collectPath = path23.resolve(
125904
126038
  opts.collectFile ?? path23.join(path23.dirname(dataPath), GOOGLE_ADS_DIAGNOSIS_COLLECT_BASENAME)
@@ -126411,7 +126545,7 @@ function ensureScorecardFromSnapshot(healthDiagnosis, merged) {
126411
126545
  }
126412
126546
 
126413
126547
  // src/commands/facebook-analysis/merge-snapshot.ts
126414
- function asRecord9(value) {
126548
+ function asRecord10(value) {
126415
126549
  if (value && typeof value === "object" && !Array.isArray(value)) {
126416
126550
  return value;
126417
126551
  }
@@ -126460,7 +126594,7 @@ async function readManifest(snapshotDir) {
126460
126594
  async function loadSectionFile(snapshotDir, file) {
126461
126595
  try {
126462
126596
  const raw = await fs21.readFile(path28.join(snapshotDir, file), "utf8");
126463
- return asRecord9(JSON.parse(raw));
126597
+ return asRecord10(JSON.parse(raw));
126464
126598
  } catch {
126465
126599
  return null;
126466
126600
  }
@@ -126508,7 +126642,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
126508
126642
  if (data) sectionMap.set(art.section, data);
126509
126643
  }
126510
126644
  const overview = sectionMap.get("overview");
126511
- const current = asRecord9(overview?.currentPeriod);
126645
+ const current = asRecord10(overview?.currentPeriod);
126512
126646
  const accountName = typeof overview?.accountName === "string" ? overview.accountName : void 0;
126513
126647
  const accountId = manifest.accountId ?? (typeof overview?.accountId === "string" ? overview.accountId : void 0);
126514
126648
  const kpis = { ...payload.kpis ?? {} };
@@ -126630,7 +126764,7 @@ function isFiniteNumber(value) {
126630
126764
  function asArray2(value) {
126631
126765
  return Array.isArray(value) ? value : [];
126632
126766
  }
126633
- function asRecord10(value) {
126767
+ function asRecord11(value) {
126634
126768
  if (value && typeof value === "object" && !Array.isArray(value)) {
126635
126769
  return value;
126636
126770
  }
@@ -126642,26 +126776,26 @@ function pushMissing(missing, id, chapter, dimension, hint) {
126642
126776
  function countAdSetsWithSpend(tables) {
126643
126777
  const rows = asArray2(tables?.adSets);
126644
126778
  return rows.filter((row) => {
126645
- const r = asRecord10(row);
126779
+ const r = asRecord11(row);
126646
126780
  const spend = r?.spend;
126647
126781
  return isFiniteNumber(spend) && spend > 0;
126648
126782
  }).length;
126649
126783
  }
126650
126784
  function chartLabels(chart) {
126651
- const c = asRecord10(chart);
126785
+ const c = asRecord11(chart);
126652
126786
  return asArray2(c?.labels).length;
126653
126787
  }
126654
126788
  function validateMetaPeriodReportContent(data) {
126655
126789
  const missing = [];
126656
- const meta = asRecord10(data.meta) ?? {};
126657
- const kpis = asRecord10(data.kpis) ?? {};
126658
- const narrative = asRecord10(data.narrative) ?? {};
126659
- const tables = asRecord10(data.tables);
126660
- const charts = asRecord10(data.charts);
126661
- const sections = asRecord10(data.sections);
126662
- const health = asRecord10(data.healthDiagnosis);
126663
- const priorityPlan = asRecord10(data.priorityPlan);
126664
- const actionChecklist = asRecord10(data.actionChecklist);
126790
+ const meta = asRecord11(data.meta) ?? {};
126791
+ const kpis = asRecord11(data.kpis) ?? {};
126792
+ const narrative = asRecord11(data.narrative) ?? {};
126793
+ const tables = asRecord11(data.tables);
126794
+ const charts = asRecord11(data.charts);
126795
+ const sections = asRecord11(data.sections);
126796
+ const health = asRecord11(data.healthDiagnosis);
126797
+ const priorityPlan = asRecord11(data.priorityPlan);
126798
+ const actionChecklist = asRecord11(data.actionChecklist);
126665
126799
  const adSetsWithSpend2 = countAdSetsWithSpend(tables ?? void 0);
126666
126800
  const regionalNarratives = asArray2(narrative.regional).length;
126667
126801
  const recommendations = asArray2(narrative.recommendations);
@@ -126733,7 +126867,7 @@ function validateMetaPeriodReportContent(data) {
126733
126867
  "charts.audience.labels \u81F3\u5C11 1 \u9879"
126734
126868
  );
126735
126869
  }
126736
- const funnel = asRecord10(charts?.funnel);
126870
+ const funnel = asRecord11(charts?.funnel);
126737
126871
  if (!isFiniteNumber(funnel?.reach)) {
126738
126872
  pushMissing(
126739
126873
  missing,
@@ -126797,7 +126931,7 @@ function validateMetaPeriodReportContent(data) {
126797
126931
  );
126798
126932
  } else {
126799
126933
  for (let i = 0; i < regionalNarratives; i++) {
126800
- const row = asRecord10(asArray2(narrative.regional)[i]);
126934
+ const row = asRecord11(asArray2(narrative.regional)[i]);
126801
126935
  if (textLen2(row?.text) < 80) {
126802
126936
  pushMissing(
126803
126937
  missing,
@@ -126827,7 +126961,7 @@ function validateMetaPeriodReportContent(data) {
126827
126961
  "\u5FC5\u987B\u6070\u597D 4 \u6761\uFF08\u7B80\u5316\u8868\u5355/\u533A\u57DF\u8C03\u6574/\u9884\u7B97\u91CD\u6784/\u7D20\u6750\u5EFA\u8BAE\uFF09"
126828
126962
  );
126829
126963
  } else {
126830
- const titles = recommendations.map((r) => asRecord10(r)?.title);
126964
+ const titles = recommendations.map((r) => asRecord11(r)?.title);
126831
126965
  for (const title of RECOMMENDATION_TITLES) {
126832
126966
  if (!titles.includes(title)) {
126833
126967
  pushMissing(
@@ -126840,7 +126974,7 @@ function validateMetaPeriodReportContent(data) {
126840
126974
  }
126841
126975
  }
126842
126976
  for (let i = 0; i < recommendations.length; i++) {
126843
- const rec = asRecord10(recommendations[i]);
126977
+ const rec = asRecord11(recommendations[i]);
126844
126978
  if (textLen2(rec?.content) < 150) {
126845
126979
  pushMissing(
126846
126980
  missing,
@@ -126862,7 +126996,7 @@ function validateMetaPeriodReportContent(data) {
126862
126996
  );
126863
126997
  } else {
126864
126998
  for (let i = 0; i < supplementary.length; i++) {
126865
- const row = asRecord10(supplementary[i]);
126999
+ const row = asRecord11(supplementary[i]);
126866
127000
  if (!textLen2(row?.dimension) || !textLen2(row?.issue)) {
126867
127001
  pushMissing(
126868
127002
  missing,
@@ -126944,7 +127078,7 @@ function validateMetaPeriodReportContent(data) {
126944
127078
  );
126945
127079
  } else {
126946
127080
  for (let i = 0; i < fourQuestions.length; i++) {
126947
- const q = asRecord10(fourQuestions[i]);
127081
+ const q = asRecord11(fourQuestions[i]);
126948
127082
  const evidence = asArray2(q?.evidence).filter((e) => textLen2(e) > 0);
126949
127083
  if (evidence.length < 2) {
126950
127084
  pushMissing(
@@ -126968,7 +127102,7 @@ function validateMetaPeriodReportContent(data) {
126968
127102
  }
126969
127103
  let scorecardValidRows = 0;
126970
127104
  for (let i = 0; i < scorecard.length; i++) {
126971
- const row = asRecord10(scorecard[i]);
127105
+ const row = asRecord11(scorecard[i]);
126972
127106
  const itemOk = textLen2(row?.item) > 0;
126973
127107
  const dataOk = textLen2(row?.data) > 0;
126974
127108
  const signalOk = SCORECARD_SIGNALS.includes(row?.signal);
@@ -127010,7 +127144,7 @@ function validateMetaPeriodReportContent(data) {
127010
127144
  );
127011
127145
  }
127012
127146
  for (const key of ["platform", "country", "adSets"]) {
127013
- const insight = textLen2(asRecord10(sections?.[key])?.insight);
127147
+ const insight = textLen2(asRecord11(sections?.[key])?.insight);
127014
127148
  if (insight < 200) {
127015
127149
  pushMissing(
127016
127150
  missing,
@@ -127021,7 +127155,7 @@ function validateMetaPeriodReportContent(data) {
127021
127155
  );
127022
127156
  }
127023
127157
  }
127024
- const audience = asRecord10(sections?.audience);
127158
+ const audience = asRecord11(sections?.audience);
127025
127159
  if (asArray2(audience?.goldenProfile).filter((s) => textLen2(s) > 0).length < 3) {
127026
127160
  pushMissing(
127027
127161
  missing,
@@ -127049,7 +127183,7 @@ function validateMetaPeriodReportContent(data) {
127049
127183
  "\u2265150 \u5B57"
127050
127184
  );
127051
127185
  }
127052
- const landingRows = asArray2(asRecord10(sections?.landingPage)?.rows);
127186
+ const landingRows = asArray2(asRecord11(sections?.landingPage)?.rows);
127053
127187
  if (landingRows.length < 3) {
127054
127188
  pushMissing(
127055
127189
  missing,
@@ -127134,7 +127268,7 @@ async function readJsonFile2(filePath) {
127134
127268
  throw new Error(`\u65E0\u6CD5\u89E3\u6790 JSON\uFF1A${filePath}`);
127135
127269
  }
127136
127270
  }
127137
- function asRecord11(value) {
127271
+ function asRecord12(value) {
127138
127272
  if (value && typeof value === "object" && !Array.isArray(value)) {
127139
127273
  return value;
127140
127274
  }
@@ -127149,7 +127283,7 @@ function injectReportData2(html, payload) {
127149
127283
  }
127150
127284
  async function runFacebookAnalysisRender(opts) {
127151
127285
  const dataPath = path29.resolve(opts.dataFile);
127152
- let data = asRecord11(await readJsonFile2(dataPath));
127286
+ let data = asRecord12(await readJsonFile2(dataPath));
127153
127287
  if (opts.snapshotDir?.trim()) {
127154
127288
  data = await mergeFacebookSnapshotIntoReport(data, path29.resolve(opts.snapshotDir));
127155
127289
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.35-beta.3",
4
- "publishedAt": 1783323427829
3
+ "version": "1.1.35-beta.5",
4
+ "publishedAt": 1783330558539
5
5
  }
@@ -42,6 +42,8 @@
42
42
 
43
43
  报告中「系列状态列」不得来自账户接口(账户状态 ≠ 系列状态,见 `references/core/agent-conventions.md` §四):系列是否启用**必须**来自 `ad campaigns`(含 `statusDisplay`)或 `google-analysis --sections campaigns` 落盘的系列数据。
44
44
 
45
+ **报告脚本禁止自行映射 `campaignStatus` 枚举**:网关值为 PascalCase(`Enabled`/`Paused`/`Removed`),Agent 若写 `STATUS_MAP[s] || '已移除'` 且未归一大小写,会把「投放中」误显示为「已移除」。`campaigns-*.json` 的 `items[]` 已注入 **`campaignStatusDisplay`**(`投放中` / `已暂停` / `已移除` / `—` / `未知`),HTML/Excel 状态列**直接读该字段**。
46
+
45
47
  ### 金额单位
46
48
 
47
49
  CLI 出口的所有 JSON / 表格金额已统一为**元**,关键字段:
@@ -197,6 +199,11 @@ const data = d.record ?? d.items; // record 非空=汇总维度;否则=列表
197
199
 
198
200
  `items[]` 每行额外包含:`conversionsValue`、`conversionsValuePerCost`(`spend ≤ 0` 时为 0)、`campaignTargetCpaYuan`、`maximizeConversionsTargetCpaYuan`、`manualCpcEnhancedCpcEnabled`、`percentCpcEnhancedCpcEnabled`。所有金额字段(`*Yuan` 后缀)已统一为元,可直接展示,无需换算。
199
201
 
202
+ | 字段 | 含义 |
203
+ | ---- | ---- |
204
+ | `campaignStatus` / `campaignStatusV2` | 网关原始枚举(`Enabled` / `Paused` / `Removed`) |
205
+ | `campaignStatusDisplay` | CLI 注入的中文状态文案;**报告状态列读此字段** |
206
+
200
207
  日预算字段为 `budgetAmountYuan`(元),脚本可直接 `row.budgetAmountYuan`;网关原始 `budgetAmount`(分)已不再落盘。
201
208
 
202
209
  #### 竞争指标 `competitiveMetrics`
@@ -47,7 +47,7 @@ siluzan-tso google-ads-diagnosis render \
47
47
  - 若环比列全为 `null`,检查落盘目录是否存在上一周期文件(如 `campaigns-<id>_20260503-20260602.json`);不存在则**重新执行 collect**(勿用手动拼装或 Python 脚本绕过 CLI)。
48
48
 
49
49
  - **render 自动合并(默认开启)**:若同目录存在 `google-ads-diagnosis-collect.json`,且 Agent 改坏了 `campaigns` / `geographic` / `keywords` 的 `items`(缺 `title`/`currentCost` 或环比写成字符串)或整体覆盖丢弃了 `landingPageAnalysis.desktop`/`mobile`,CLI **自动从 collect.reportData 恢复**,保留 Agent 的 `analysis`/`suggestions`。可用 `--no-merge-collect` 关闭;`--collect <file>` 指定 collect 路径。
50
- - **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空);`summary.keyIssues` / `diagnosisOverview.disadvantages` 与 `newFeatures`/`goldAccount` 等事实字段出现已知矛盾模式(见「撰写硬约束」第 5 条)时同样 **exit 1**。
50
+ - **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空);`campaigns/geographic/keywords.items` 默认同目录 collect **整表覆盖** Agent 改动(Agent 只填 analysis/suggestions);`summary.keyIssues` / `diagnosisOverview.disadvantages` 与 `newFeatures`/`goldAccount` 等事实字段出现已知矛盾模式(见「撰写硬约束」第 5 条)时同样 **exit 1**。
51
51
 
52
52
  - **禁止**在 `collect` 阶段生成或写入 `analysis` / `suggestions` / `diagnosisOverview` / `summary` 等建议性文案。
53
53
  - **禁止**跳过 Agent 直接 `render` collect 产物(`render` 会校验全部叙事 / 建议字段;调试可加 `--lenient`)。
@@ -130,6 +130,13 @@ siluzan-tso google-ads-diagnosis render \
130
130
  - `render` 会在同目录存在 `google-ads-diagnosis-collect.json` 时自动从 collect 恢复缺失/损坏的 `desktop`/`mobile`(保留 Agent 写的 `analysis`/`suggestions`),但**不能依赖这个兜底**——终稿应本就保留完整字段。
131
131
  - 若 `collect` 里 `desktop`/`mobile` 本身就是空/全 0(说明 Lighthouse 没有抓到数据),`analysis` 必须写「着陆页测速数据未获取」,**禁止**编造评分或分析文字。
132
132
 
133
+ ### 7. 环比对比表 `campaigns/geographic/keywords.items` — Agent 禁止写入
134
+
135
+ - `collect` 已根据当期 + 上一周期快照生成对比行(`title`、`currentCost`/`previousCost`、`costRateChange`、`currentClicks`/`previousClicks`、`clicksRateChange`、`currentCvr`/`previousCvr`、`cvrRateChange`)。Agent 撰写终稿时**只填** `analysis` / `suggestions`,**禁止**写入、清空、删减或「手工重写」`items` 数组
136
+ - `agentBrief.comparisonTableItemCounts` 给出 collect 侧行数(如 `keywords: 15`);终稿 `keywords.items.length` 须与之一致。若 Agent 误改,`render` 会**始终以**同目录 `google-ads-diagnosis-collect.json` **覆盖**上述三个模块的 `items`(保留 Agent 的 narrative)。
137
+ - 分析文案须引用对比表中的具体词/系列/国家名与环比数字(如「fitness mat 消耗 $230.60,环比下降 39.6%」),**禁止**在 `items` 有数据时写「关键词对比表为空」类结论。
138
+ - `biddingStrategy.items` / `newFeatures.items` 行结构由 collect 生成,Agent 可在合法行上填写 `recommendationReason` / `optimizerRecommendation`,但勿篡改 `duration`(须为天数 number)、`biddingStrategyType`、`strategyName` 等事实字段。
139
+
133
140
  ---
134
141
 
135
142
  ## 页眉信息(报告头)
@@ -246,17 +253,19 @@ siluzan-tso google-ads-diagnosis render \
246
253
  - 指标列:花费(上期/本期/环比)、点击(上期/本期/环比)、转化率(上期/本期/环比)
247
254
  - **数据**:`campaigns.items`(`title`、`previousCost`、`currentCost`、`costRateChange`、`previousClicks`、`currentClicks`、`clicksRateChange`、`previousCvr`、`currentCvr`、`cvrRateChange`)
248
255
  - **分析 / 建议**:`campaigns.analysis`、`campaigns.suggestions`
249
- - **Agent 禁止改写 `items` 行**(须原样保留 collect `reportData`);`render` 会从同目录 collect 自动恢复被改坏的 items
256
+ - **Agent 禁止写入 `items` 行**(只填 `analysis`/`suggestions`);`render` 会以 collect 为准**自动覆盖** Agent 改动的对比表 items
250
257
 
251
258
  #### 国家地区分析
252
259
 
253
260
  - 维度列:**国家/地区**
254
261
  - **数据**:`geographic.items`(同上结构)
262
+ - **Agent 禁止写入 `items` 行**;`render` 自动从 collect 恢复
255
263
 
256
264
  #### 关键词分析
257
265
 
258
266
  - 维度列:**关键词**
259
- - **数据**:`keywords.items`
267
+ - **数据**:`keywords.items`(字段同上;行数见 `agentBrief.comparisonTableItemCounts.keywords`)
268
+ - **Agent 禁止写入/清空 `items`**;分析须引用表中具体词名与环比,勿写「对比表为空」
260
269
 
261
270
  #### 转化成本 / 按日趋势(若页面已渲染)
262
271
 
@@ -58,3 +58,16 @@ siluzan-tso google-analysis -a <id> --start <s> --end <e> --json-out ./snap-goog
58
58
  如果用户**追加**了维度(按上文「可选追加」表),把它们加入 `--sections` 即可;要全 21 维则省略 `--sections`。
59
59
 
60
60
  > 仅在调试单个维度或需要传 `all` 暂未透传的特殊参数时,才回落到逐条 `siluzan-tso google-analysis <子命令> -a <id> …` 调用。
61
+
62
+ ---
63
+
64
+ ## 广告系列状态列(必读)
65
+
66
+ `campaigns-*.json` → `items[]` 每行含 CLI 注入的 **`campaignStatusDisplay`**(`投放中` / `已暂停` / `已移除`)。
67
+
68
+ | 正确 | 错误 |
69
+ | ---- | ---- |
70
+ | 直接展示 `row.campaignStatusDisplay` | 读 `campaignStatus` 后自行 `MAP[s] \|\| '已移除'`(`Enabled` 未命中键时会全变「已移除」) |
71
+ | 需要英文枚举时读 `campaignStatus` 或 `campaignStatusV2`,展示前 `.upper()` 再映射 | 用 `stats` / `list-accounts` 的账户 `status` 充当系列状态 |
72
+
73
+ 原始枚举为 PascalCase(`Enabled`),与 `ad campaigns` 的 `statusDisplay`(有效/未投放/已结束使用)口径不同;周期报告「状态」列统一用 **`campaignStatusDisplay`**。
@@ -47,7 +47,7 @@ siluzan-tso google-ads-diagnosis render \
47
47
  - 若环比列全为 `null`,检查落盘目录是否存在上一周期文件(如 `campaigns-<id>_20260503-20260602.json`);不存在则**重新执行 collect**(勿用手动拼装或 Python 脚本绕过 CLI)。
48
48
 
49
49
  - **render 自动合并(默认开启)**:若同目录存在 `google-ads-diagnosis-collect.json`,且 Agent 改坏了 `campaigns` / `geographic` / `keywords` 的 `items`(缺 `title`/`currentCost` 或环比写成字符串)或整体覆盖丢弃了 `landingPageAnalysis.desktop`/`mobile`,CLI **自动从 collect.reportData 恢复**,保留 Agent 的 `analysis`/`suggestions`。可用 `--no-merge-collect` 关闭;`--collect <file>` 指定 collect 路径。
50
- - **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空);`summary.keyIssues` / `diagnosisOverview.disadvantages` 与 `newFeatures`/`goldAccount` 等事实字段出现已知矛盾模式(见「撰写硬约束」第 5 条)时同样 **exit 1**。
50
+ - **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空);`campaigns/geographic/keywords.items` 默认同目录 collect **整表覆盖** Agent 改动(Agent 只填 analysis/suggestions);`summary.keyIssues` / `diagnosisOverview.disadvantages` 与 `newFeatures`/`goldAccount` 等事实字段出现已知矛盾模式(见「撰写硬约束」第 5 条)时同样 **exit 1**。
51
51
 
52
52
  - **禁止**在 `collect` 阶段生成或写入 `analysis` / `suggestions` / `diagnosisOverview` / `summary` 等建议性文案。
53
53
  - **禁止**跳过 Agent 直接 `render` collect 产物(`render` 会校验全部叙事 / 建议字段;调试可加 `--lenient`)。
@@ -130,6 +130,13 @@ siluzan-tso google-ads-diagnosis render \
130
130
  - `render` 会在同目录存在 `google-ads-diagnosis-collect.json` 时自动从 collect 恢复缺失/损坏的 `desktop`/`mobile`(保留 Agent 写的 `analysis`/`suggestions`),但**不能依赖这个兜底**——终稿应本就保留完整字段。
131
131
  - 若 `collect` 里 `desktop`/`mobile` 本身就是空/全 0(说明 Lighthouse 没有抓到数据),`analysis` 必须写「着陆页测速数据未获取」,**禁止**编造评分或分析文字。
132
132
 
133
+ ### 7. 环比对比表 `campaigns/geographic/keywords.items` — Agent 禁止写入
134
+
135
+ - `collect` 已根据当期 + 上一周期快照生成对比行(`title`、`currentCost`/`previousCost`、`costRateChange`、`currentClicks`/`previousClicks`、`clicksRateChange`、`currentCvr`/`previousCvr`、`cvrRateChange`)。Agent 撰写终稿时**只填** `analysis` / `suggestions`,**禁止**写入、清空、删减或「手工重写」`items` 数组
136
+ - `agentBrief.comparisonTableItemCounts` 给出 collect 侧行数(如 `keywords: 15`);终稿 `keywords.items.length` 须与之一致。若 Agent 误改,`render` 会**始终以**同目录 `google-ads-diagnosis-collect.json` **覆盖**上述三个模块的 `items`(保留 Agent 的 narrative)。
137
+ - 分析文案须引用对比表中的具体词/系列/国家名与环比数字(如「fitness mat 消耗 $230.60,环比下降 39.6%」),**禁止**在 `items` 有数据时写「关键词对比表为空」类结论。
138
+ - `biddingStrategy.items` / `newFeatures.items` 行结构由 collect 生成,Agent 可在合法行上填写 `recommendationReason` / `optimizerRecommendation`,但勿篡改 `duration`(须为天数 number)、`biddingStrategyType`、`strategyName` 等事实字段。
139
+
133
140
  ---
134
141
 
135
142
  ## 页眉信息(报告头)
@@ -246,17 +253,19 @@ siluzan-tso google-ads-diagnosis render \
246
253
  - 指标列:花费(上期/本期/环比)、点击(上期/本期/环比)、转化率(上期/本期/环比)
247
254
  - **数据**:`campaigns.items`(`title`、`previousCost`、`currentCost`、`costRateChange`、`previousClicks`、`currentClicks`、`clicksRateChange`、`previousCvr`、`currentCvr`、`cvrRateChange`)
248
255
  - **分析 / 建议**:`campaigns.analysis`、`campaigns.suggestions`
249
- - **Agent 禁止改写 `items` 行**(须原样保留 collect `reportData`);`render` 会从同目录 collect 自动恢复被改坏的 items
256
+ - **Agent 禁止写入 `items` 行**(只填 `analysis`/`suggestions`);`render` 会以 collect 为准**自动覆盖** Agent 改动的对比表 items
250
257
 
251
258
  #### 国家地区分析
252
259
 
253
260
  - 维度列:**国家/地区**
254
261
  - **数据**:`geographic.items`(同上结构)
262
+ - **Agent 禁止写入 `items` 行**;`render` 自动从 collect 恢复
255
263
 
256
264
  #### 关键词分析
257
265
 
258
266
  - 维度列:**关键词**
259
- - **数据**:`keywords.items`
267
+ - **数据**:`keywords.items`(字段同上;行数见 `agentBrief.comparisonTableItemCounts.keywords`)
268
+ - **Agent 禁止写入/清空 `items`**;分析须引用表中具体词名与环比,勿写「对比表为空」
260
269
 
261
270
  #### 转化成本 / 按日趋势(若页面已渲染)
262
271
 
@@ -58,3 +58,16 @@ siluzan-tso google-analysis -a <id> --start <s> --end <e> --json-out ./snap-goog
58
58
  如果用户**追加**了维度(按上文「可选追加」表),把它们加入 `--sections` 即可;要全 21 维则省略 `--sections`。
59
59
 
60
60
  > 仅在调试单个维度或需要传 `all` 暂未透传的特殊参数时,才回落到逐条 `siluzan-tso google-analysis <子命令> -a <id> …` 调用。
61
+
62
+ ---
63
+
64
+ ## 广告系列状态列(必读)
65
+
66
+ `campaigns-*.json` → `items[]` 每行含 CLI 注入的 **`campaignStatusDisplay`**(`投放中` / `已暂停` / `已移除`)。
67
+
68
+ | 正确 | 错误 |
69
+ | ---- | ---- |
70
+ | 直接展示 `row.campaignStatusDisplay` | 读 `campaignStatus` 后自行 `MAP[s] \|\| '已移除'`(`Enabled` 未命中键时会全变「已移除」) |
71
+ | 需要英文枚举时读 `campaignStatus` 或 `campaignStatusV2`,展示前 `.upper()` 再映射 | 用 `stats` / `list-accounts` 的账户 `status` 充当系列状态 |
72
+
73
+ 原始枚举为 PascalCase(`Enabled`),与 `ad campaigns` 的 `statusDisplay`(有效/未投放/已结束使用)口径不同;周期报告「状态」列统一用 **`campaignStatusDisplay`**。
@@ -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.35-beta.3'
12
+ $PKG_VERSION = '1.1.35-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.35-beta.3"
12
+ readonly PKG_VERSION="1.1.35-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.35-beta.3",
3
+ "version": "1.1.35-beta.5",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",