siluzan-tso-cli 1.1.35-beta.5 → 1.1.36-beta.1

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.5),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.36-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -113676,17 +113676,120 @@ init_cli_table();
113676
113676
  // src/commands/ad/campaign-batch-diff.ts
113677
113677
  init_auth();
113678
113678
  init_cli_json_snapshot();
113679
+
113680
+ // src/commands/ad/campaign-extension-remediate.ts
113681
+ function shellArg(value) {
113682
+ return /[\s"]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
113683
+ }
113684
+ function pickString(...vals) {
113685
+ for (const v of vals) {
113686
+ if (typeof v === "string" && v.trim()) return v.trim();
113687
+ }
113688
+ return "";
113689
+ }
113679
113690
  function asRecord2(v) {
113680
113691
  return v && typeof v === "object" && !Array.isArray(v) ? v : null;
113681
113692
  }
113682
- function pickString(...vals) {
113693
+ function extensionType2(ext) {
113694
+ return pickString(ext["typeV2"], ext["TypeV2"], ext["AssetFieldType"], ext["type"]).toUpperCase();
113695
+ }
113696
+ function extensionLevel(ext) {
113697
+ const level = pickString(ext["level"], ext["Level"]) || "Campaign";
113698
+ return level.charAt(0).toUpperCase() + level.slice(1).toLowerCase();
113699
+ }
113700
+ function buildExtensionRemediateCommandLine(opts) {
113701
+ const { accountId, campaignId, ext } = opts;
113702
+ const jsonOut = shellArg(opts.jsonOutDir ?? "./snap");
113703
+ const type = extensionType2(ext);
113704
+ const level = extensionLevel(ext);
113705
+ const props = asRecord2(ext["Properties"]) ?? {};
113706
+ const levelFlag = `--level ${level}`;
113707
+ const campaignFlag = level === "Campaign" ? `--campaign-id ${campaignId}` : "";
113708
+ const base = ["siluzan-tso", "ad", "extension"];
113709
+ if (type === "SITELINK") {
113710
+ const text = pickString(props["Text"], props["LinkText"]);
113711
+ const url = pickString(props["DestinationUrl"]);
113712
+ if (!text || !url) return null;
113713
+ const parts = [
113714
+ ...base,
113715
+ "sitelink",
113716
+ `-a ${accountId}`,
113717
+ `--text ${shellArg(text)}`,
113718
+ `--url ${shellArg(url)}`
113719
+ ];
113720
+ const line2 = pickString(props["Line2"], props["Description1"]);
113721
+ const line3 = pickString(props["Line3"], props["Description2"]);
113722
+ if (line2) parts.push(`--line2 ${shellArg(line2)}`);
113723
+ if (line3) parts.push(`--line3 ${shellArg(line3)}`);
113724
+ parts.push(levelFlag);
113725
+ if (campaignFlag) parts.push(campaignFlag);
113726
+ parts.push(`--json-out ${jsonOut}`);
113727
+ return parts.join(" ");
113728
+ }
113729
+ if (type === "CALLOUT") {
113730
+ const text = pickString(props["Text"], props["CalloutText"]);
113731
+ if (!text) return null;
113732
+ const parts = [
113733
+ ...base,
113734
+ "callout",
113735
+ `-a ${accountId}`,
113736
+ `--text ${shellArg(text)}`,
113737
+ levelFlag
113738
+ ];
113739
+ if (campaignFlag) parts.push(campaignFlag);
113740
+ parts.push(`--json-out ${jsonOut}`);
113741
+ return parts.join(" ");
113742
+ }
113743
+ if (type === "CALL") {
113744
+ const code = pickString(props["ContryCode"], props["CountryCode"]);
113745
+ const phone = pickString(props["PhoneNumber"], props["phoneNumber"]);
113746
+ if (!code || !phone) return null;
113747
+ const parts = [
113748
+ ...base,
113749
+ "call",
113750
+ `-a ${accountId}`,
113751
+ `--country-code ${shellArg(code)}`,
113752
+ `--phone ${shellArg(phone)}`,
113753
+ levelFlag
113754
+ ];
113755
+ if (campaignFlag) parts.push(campaignFlag);
113756
+ parts.push(`--json-out ${jsonOut}`);
113757
+ return parts.join(" ");
113758
+ }
113759
+ if (type === "STRUCTURED_SNIPPET") {
113760
+ const ssv = asRecord2(ext["StructuredSnippetHeaderValue"]);
113761
+ const header = pickString(ssv?.["Key"]);
113762
+ const values = ssv?.["Value"];
113763
+ if (!header || !Array.isArray(values) || values.length === 0) return null;
113764
+ const valueStr = values.filter((v) => typeof v === "string" && v.trim().length > 0).join(",");
113765
+ if (!valueStr) return null;
113766
+ const parts = [
113767
+ ...base,
113768
+ "snippet",
113769
+ `-a ${accountId}`,
113770
+ `--header ${shellArg(header)}`,
113771
+ `--values ${shellArg(valueStr)}`,
113772
+ levelFlag
113773
+ ];
113774
+ if (campaignFlag) parts.push(campaignFlag);
113775
+ parts.push(`--json-out ${jsonOut}`);
113776
+ return parts.join(" ");
113777
+ }
113778
+ return null;
113779
+ }
113780
+
113781
+ // src/commands/ad/campaign-batch-diff.ts
113782
+ function asRecord3(v) {
113783
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
113784
+ }
113785
+ function pickString2(...vals) {
113683
113786
  for (const v of vals) {
113684
113787
  if (typeof v === "string" && v.trim()) return v.trim();
113685
113788
  }
113686
113789
  return "";
113687
113790
  }
113688
113791
  function normMatchType(v) {
113689
- return pickString(v).toUpperCase() || "BROAD";
113792
+ return pickString2(v).toUpperCase() || "BROAD";
113690
113793
  }
113691
113794
  function keywordKey(core, matchType) {
113692
113795
  return `${normMatchType(matchType)}:${unwrapKeywordDisplayTextForEdit(core).trim().toLowerCase()}`;
@@ -113696,19 +113799,19 @@ function liveKeywordTexts(item) {
113696
113799
  if (Array.isArray(kt)) {
113697
113800
  return kt.filter((t2) => typeof t2 === "string" && t2.trim().length > 0);
113698
113801
  }
113699
- const t = pickString(item["text"], kt);
113802
+ const t = pickString2(item["text"], kt);
113700
113803
  return t ? [t] : [];
113701
113804
  }
113702
113805
  function belongsToCampaign(item, campaignId, campaignName) {
113703
- const cid = pickString(item["campaignId"], item["campaignid"], item["CampaignId"]);
113704
- const cname = pickString(item["campaignName"], item["campaign"], item["CampaignName"]);
113806
+ const cid = pickString2(item["campaignId"], item["campaignid"], item["CampaignId"]);
113807
+ const cname = pickString2(item["campaignName"], item["campaign"], item["CampaignName"]);
113705
113808
  if (campaignId && cid) return cid === campaignId;
113706
113809
  if (campaignName && cname) return cname === campaignName;
113707
113810
  return false;
113708
113811
  }
113709
113812
  function belongsToAdGroup(item, adGroupName, adGroupId) {
113710
- const gid = pickString(item["adGroupId"], item["adgroupId"], item["AdGroupId"]);
113711
- const gname = pickString(item["adGroupName"], item["adGroup"], item["AdGroupName"]);
113813
+ const gid = pickString2(item["adGroupId"], item["adgroupId"], item["AdGroupId"]);
113814
+ const gname = pickString2(item["adGroupName"], item["adGroup"], item["AdGroupName"]);
113712
113815
  if (adGroupId && gid) return gid === adGroupId;
113713
113816
  return gname === adGroupName;
113714
113817
  }
@@ -113729,10 +113832,10 @@ function liveAdHasHeadline(ad, headline) {
113729
113832
  }
113730
113833
  function formatExtensionPlanned(ext) {
113731
113834
  if (!ext) return "\u2014";
113732
- const type = pickString(ext["typeV2"], ext["AssetFieldType"], ext["type"]);
113733
- const props = asRecord2(ext["Properties"]);
113734
- const phone = props ? pickString(props["PhoneNumber"], props["phoneNumber"]) : "";
113735
- const code = props ? pickString(props["ContryCode"], props["CountryCode"]) : "";
113835
+ const type = pickString2(ext["typeV2"], ext["AssetFieldType"], ext["type"]);
113836
+ const props = asRecord3(ext["Properties"]);
113837
+ const phone = props ? pickString2(props["PhoneNumber"], props["phoneNumber"]) : "";
113838
+ const code = props ? pickString2(props["ContryCode"], props["CountryCode"]) : "";
113736
113839
  const parts = [type ? `\u7C7B\u578B: ${type}` : "", code || phone ? `\u7535\u8BDD: ${code}${phone}` : ""].filter(
113737
113840
  Boolean
113738
113841
  );
@@ -113743,15 +113846,15 @@ function listPlannedKeywords(campaign) {
113743
113846
  if (!Array.isArray(groups)) return [];
113744
113847
  const out = [];
113745
113848
  for (let gi = 0; gi < groups.length; gi++) {
113746
- const g = asRecord2(groups[gi]);
113849
+ const g = asRecord3(groups[gi]);
113747
113850
  if (!g) continue;
113748
- const groupName = pickString(g["Name"], g["name"]) || `AdGroupsForBatchJob[${gi}]`;
113851
+ const groupName = pickString2(g["Name"], g["name"]) || `AdGroupsForBatchJob[${gi}]`;
113749
113852
  const blocks = g["KeywordsForBatchJob"];
113750
113853
  if (!Array.isArray(blocks)) continue;
113751
113854
  for (let bi = 0; bi < blocks.length; bi++) {
113752
- const block = asRecord2(blocks[bi]);
113855
+ const block = asRecord3(blocks[bi]);
113753
113856
  if (!block) continue;
113754
- const matchTypeV2 = pickString(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113857
+ const matchTypeV2 = pickString2(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113755
113858
  const texts = block["KeywordText"];
113756
113859
  if (!Array.isArray(texts)) continue;
113757
113860
  for (let ki = 0; ki < texts.length; ki++) {
@@ -113773,9 +113876,9 @@ function listPlannedNegativeKeywords(campaign) {
113773
113876
  if (!Array.isArray(blocks)) return [];
113774
113877
  const out = [];
113775
113878
  for (let bi = 0; bi < blocks.length; bi++) {
113776
- const block = asRecord2(blocks[bi]);
113879
+ const block = asRecord3(blocks[bi]);
113777
113880
  if (!block) continue;
113778
- const matchTypeV2 = pickString(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113881
+ const matchTypeV2 = pickString2(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113779
113882
  const texts = block["KeywordText"];
113780
113883
  if (!Array.isArray(texts)) continue;
113781
113884
  for (let ki = 0; ki < texts.length; ki++) {
@@ -113793,7 +113896,7 @@ function listPlannedNegativeKeywords(campaign) {
113793
113896
  }
113794
113897
  function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113795
113898
  const campaign = cfg.campaign ?? {};
113796
- const campaignName = pickString(cfg.name, campaign["Name"], campaign["name"]);
113899
+ const campaignName = pickString2(cfg.name, campaign["Name"], campaign["name"]);
113797
113900
  const accountId = String(cfg.account ?? "");
113798
113901
  const missing = [];
113799
113902
  if (!live.campaignFound) {
@@ -113807,15 +113910,15 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113807
113910
  }
113808
113911
  const liveGroups = live.adGroups;
113809
113912
  const liveGroupNames = new Set(
113810
- liveGroups.map((g) => pickString(g["name"], g["Name"]).toLowerCase()).filter(Boolean)
113913
+ liveGroups.map((g) => pickString2(g["name"], g["Name"]).toLowerCase()).filter(Boolean)
113811
113914
  );
113812
113915
  const groups = campaign["AdGroupsForBatchJob"];
113813
113916
  const plannedGroupCount = Array.isArray(groups) ? groups.length : 0;
113814
113917
  if (Array.isArray(groups)) {
113815
113918
  for (let gi = 0; gi < groups.length; gi++) {
113816
- const g = asRecord2(groups[gi]);
113919
+ const g = asRecord3(groups[gi]);
113817
113920
  if (!g) continue;
113818
- const groupName = pickString(g["Name"], g["name"]) || `AdGroupsForBatchJob[${gi}]`;
113921
+ const groupName = pickString2(g["Name"], g["name"]) || `AdGroupsForBatchJob[${gi}]`;
113819
113922
  const groupPath = `campaign.AdGroupsForBatchJob[${gi}]`;
113820
113923
  if (!liveGroupNames.has(groupName.toLowerCase())) {
113821
113924
  missing.push({
@@ -113829,9 +113932,9 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113829
113932
  continue;
113830
113933
  }
113831
113934
  const liveGroup = liveGroups.find(
113832
- (lg) => pickString(lg["name"], lg["Name"]).toLowerCase() === groupName.toLowerCase()
113935
+ (lg) => pickString2(lg["name"], lg["Name"]).toLowerCase() === groupName.toLowerCase()
113833
113936
  );
113834
- const liveGroupId = liveGroup ? pickString(liveGroup["id"], liveGroup["Id"]) : "";
113937
+ const liveGroupId = liveGroup ? pickString2(liveGroup["id"], liveGroup["Id"]) : "";
113835
113938
  const liveKwInGroup = live.keywords.filter(
113836
113939
  (k) => belongsToCampaign(k, campaignId, campaignName) && belongsToAdGroup(k, groupName, liveGroupId)
113837
113940
  );
@@ -113844,9 +113947,9 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113844
113947
  const blocks = g["KeywordsForBatchJob"];
113845
113948
  if (Array.isArray(blocks)) {
113846
113949
  for (let bi = 0; bi < blocks.length; bi++) {
113847
- const block = asRecord2(blocks[bi]);
113950
+ const block = asRecord3(blocks[bi]);
113848
113951
  if (!block) continue;
113849
- const matchTypeV2 = pickString(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113952
+ const matchTypeV2 = pickString2(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113850
113953
  const texts = block["KeywordText"];
113851
113954
  if (!Array.isArray(texts)) continue;
113852
113955
  for (let ki = 0; ki < texts.length; ki++) {
@@ -113873,10 +113976,10 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113873
113976
  const ads = g["AdsForBatchJob"];
113874
113977
  if (Array.isArray(ads)) {
113875
113978
  for (let ai = 0; ai < ads.length; ai++) {
113876
- const ad = asRecord2(ads[ai]);
113979
+ const ad = asRecord3(ads[ai]);
113877
113980
  if (!ad) continue;
113878
113981
  const path34 = `${groupPath}.AdsForBatchJob[${ai}]`;
113879
- const primary = pickString(ad["headlinePart1"], ad["AdTitle"]);
113982
+ const primary = pickString2(ad["headlinePart1"], ad["AdTitle"]);
113880
113983
  if (!primary) {
113881
113984
  missing.push({
113882
113985
  layer: "ad",
@@ -113888,7 +113991,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113888
113991
  });
113889
113992
  continue;
113890
113993
  }
113891
- const finalUrl = pickString(ad["Finalurl"], ad["DestinationUrl"], ad["finalUrl"]);
113994
+ const finalUrl = pickString2(ad["Finalurl"], ad["DestinationUrl"], ad["finalUrl"]);
113892
113995
  const found = liveAdsInGroup.some((la) => liveAdHasHeadline(la, primary));
113893
113996
  if (!found) {
113894
113997
  missing.push({
@@ -113929,13 +114032,15 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113929
114032
  ).length;
113930
114033
  if (plannedExt > liveExt && Array.isArray(extBlocks)) {
113931
114034
  for (let ei = liveExt; ei < extBlocks.length; ei++) {
113932
- const ext = asRecord2(extBlocks[ei]);
114035
+ const ext = asRecord3(extBlocks[ei]);
114036
+ const remediateCommand = ext && accountId && campaignId ? buildExtensionRemediateCommandLine({ accountId, campaignId, ext }) ?? void 0 : void 0;
113933
114037
  missing.push({
113934
114038
  layer: "extension",
113935
114039
  path: `campaign.ExtensionsForBatchJob[${ei}]`,
113936
114040
  plannedContent: formatExtensionPlanned(ext),
113937
114041
  liveNote: `\u7CFB\u5217\u7EA7\u9644\u52A0\u4FE1\u606F\u8D26\u6237\u5185 ${liveExt}/${plannedExt} \u6761`,
113938
- summary: `\u9644\u52A0\u4FE1\u606F\u672A\u521B\u5EFA\uFF1A${formatExtensionPlanned(ext)}`
114042
+ summary: `\u9644\u52A0\u4FE1\u606F\u672A\u521B\u5EFA\uFF1A${formatExtensionPlanned(ext)}`,
114043
+ remediateCommand
113939
114044
  });
113940
114045
  }
113941
114046
  }
@@ -113959,7 +114064,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113959
114064
  plannedKeywords: plannedKeywords.length,
113960
114065
  liveKeywords: liveKwCount,
113961
114066
  plannedAds: Array.isArray(groups) ? groups.reduce((n, g) => {
113962
- const gr = asRecord2(g);
114067
+ const gr = asRecord3(g);
113963
114068
  const ads = gr?.["AdsForBatchJob"];
113964
114069
  return n + (Array.isArray(ads) ? ads.length : 0);
113965
114070
  }, 0) : 0,
@@ -114020,7 +114125,7 @@ async function fetchLiveSnapshotForCampaign(config, accountId, campaignId, campa
114020
114125
  )
114021
114126
  ]);
114022
114127
  const campaignFound = campaigns.some(
114023
- (c) => pickString(c["id"], c["Id"]) === campaignId || pickString(c["name"], c["Name"]) === campaignName
114128
+ (c) => pickString2(c["id"], c["Id"]) === campaignId || pickString2(c["name"], c["Name"]) === campaignName
114024
114129
  );
114025
114130
  return {
114026
114131
  campaignFound,
@@ -114035,8 +114140,8 @@ async function fetchLiveSnapshotForCampaign(config, accountId, campaignId, campa
114035
114140
  };
114036
114141
  }
114037
114142
  function resolveCampaignIdFromBatch(record) {
114038
- const campaign = asRecord2(record["campaign"]);
114039
- return pickString(campaign?.["Id"], campaign?.["id"], record["campaignId"]);
114143
+ const campaign = asRecord3(record["campaign"]);
114144
+ return pickString2(campaign?.["Id"], campaign?.["id"], record["campaignId"]);
114040
114145
  }
114041
114146
  function shellArgPath(p) {
114042
114147
  return /[\s"]/.test(p) ? `"${p.replace(/"/g, '\\"')}"` : p;
@@ -114083,7 +114188,9 @@ function printBatchDiffFollowUpHint(opts) {
114083
114188
  if (opts.status === "Successfully") {
114084
114189
  console.log(" \u6838\u5BF9\u8BA1\u5212\u4E0E\u8D26\u6237\u662F\u5426\u4E00\u81F4\uFF08\u65E0\u7F3A\u5931\u5E94\u663E\u793A \u2705\uFF09\u3002\n");
114085
114190
  } else {
114086
- console.log(" \u5217\u51FA\u7F3A\u5931\u9879\u540E\u6309\u8DEF\u5F84\u8865\u5EFA\u3002\n");
114191
+ console.log(
114192
+ " \u5217\u51FA\u7F3A\u5931\u9879\u540E\u81EA\u52A8\u8865\u5EFA\uFF08\u9644\u52A0\u4FE1\u606F\u7528 ad extension *\uFF1B\u7EC4/\u8BCD/RSA \u7528\u5BF9\u5E94 create \u547D\u4EE4\uFF09\u3002\n"
114193
+ );
114087
114194
  }
114088
114195
  console.log(` ${cmd}
114089
114196
  `);
@@ -114111,6 +114218,9 @@ function printMissingEntry(configFile, item) {
114111
114218
  if (item.liveNote) {
114112
114219
  console.log(` \u5B9E\u51B5: ${item.liveNote}`);
114113
114220
  }
114221
+ if (item.remediateCommand) {
114222
+ console.log(` \u8865\u5EFA: ${item.remediateCommand}`);
114223
+ }
114114
114224
  }
114115
114225
  function printMissingGroupByAdGroup(configFile, layer, items) {
114116
114226
  const byGroup = /* @__PURE__ */ new Map();
@@ -114178,6 +114288,11 @@ function formatBatchDiffGrouped(result) {
114178
114288
  for (const m of layerItems) {
114179
114289
  printMissingEntry(configFile, m);
114180
114290
  }
114291
+ if (layer === "extension" && layerItems.some((m) => m.remediateCommand)) {
114292
+ console.log(
114293
+ "\n \u{1F4A1} \u9644\u52A0\u4FE1\u606F BatchJob \u5E38\u5931\u8D25\uFF1AAgent \u5E94**\u76F4\u63A5\u6267\u884C**\u4E0A\u65B9\u300C\u8865\u5EFA\u300D\u547D\u4EE4\uFF08\u7528\u6237\u5DF2\u786E\u8BA4\u8FC7 JSON\uFF09\uFF0C\u52FF\u4EC5\u8BE2\u95EE\u662F\u5426\u8865\u5EFA\u3002"
114294
+ );
114295
+ }
114181
114296
  }
114182
114297
  }
114183
114298
  console.log();
@@ -114190,14 +114305,14 @@ async function runCampaignBatchDiff(opts) {
114190
114305
  `);
114191
114306
  process.exit(1);
114192
114307
  }
114193
- const accountId = pickString(opts.account, cfg.account);
114308
+ const accountId = pickString2(opts.account, cfg.account);
114194
114309
  if (!accountId) {
114195
114310
  console.error("\n\u274C \u7F3A\u5C11\u8D26\u6237 ID\uFF1A\u8BF7\u5728 JSON \u4E2D\u586B\u5199 account \u6216\u4F7F\u7528 -a\n");
114196
114311
  process.exit(1);
114197
114312
  }
114198
114313
  cfg.account = accountId;
114199
114314
  const config = loadConfig(opts.token);
114200
- let campaignId = pickString(opts.campaignId);
114315
+ let campaignId = pickString2(opts.campaignId);
114201
114316
  let batchStatus;
114202
114317
  if (opts.batchId) {
114203
114318
  const batchUrl = `${config.apiBaseUrl}/query/campaign-creation-record/campaign-batch-asyncs/${opts.batchId}`;
@@ -114210,7 +114325,7 @@ async function runCampaignBatchDiff(opts) {
114210
114325
  `);
114211
114326
  process.exit(1);
114212
114327
  }
114213
- batchStatus = pickString(batch["status"]);
114328
+ batchStatus = pickString2(batch["status"]);
114214
114329
  if (batchStatus === "Creating") {
114215
114330
  console.error("\n\u274C \u4EFB\u52A1\u4ECD\u5728\u521B\u5EFA\u4E2D\uFF0C\u8BF7\u7A0D\u540E\u518D\u6267\u884C ad batch diff\n");
114216
114331
  process.exit(1);
@@ -114228,7 +114343,7 @@ async function runCampaignBatchDiff(opts) {
114228
114343
  }
114229
114344
  }
114230
114345
  const campaign = cfg.campaign ?? {};
114231
- const campaignName = pickString(cfg.name, campaign["Name"], campaign["name"]);
114346
+ const campaignName = pickString2(cfg.name, campaign["Name"], campaign["name"]);
114232
114347
  if (!campaignId && campaignName) {
114233
114348
  const googleApiUrl = requireGoogleApi(config);
114234
114349
  const params = new URLSearchParams();
@@ -114240,10 +114355,10 @@ async function runCampaignBatchDiff(opts) {
114240
114355
  opts.verbose
114241
114356
  );
114242
114357
  const hit = campaigns.find(
114243
- (c) => pickString(c["name"], c["Name"]).toLowerCase() === campaignName.toLowerCase()
114358
+ (c) => pickString2(c["name"], c["Name"]).toLowerCase() === campaignName.toLowerCase()
114244
114359
  );
114245
114360
  if (hit) {
114246
- campaignId = pickString(hit["id"], hit["Id"]);
114361
+ campaignId = pickString2(hit["id"], hit["Id"]);
114247
114362
  }
114248
114363
  }
114249
114364
  if (!campaignId) {
@@ -123718,7 +123833,7 @@ function readJsonFile(filePath) {
123718
123833
  }
123719
123834
  });
123720
123835
  }
123721
- function asRecord3(value) {
123836
+ function asRecord4(value) {
123722
123837
  if (value && typeof value === "object" && !Array.isArray(value)) {
123723
123838
  return value;
123724
123839
  }
@@ -123744,10 +123859,10 @@ function injectReportData(html, payload) {
123744
123859
  async function runWebsiteDiagnosisRender(opts) {
123745
123860
  const dataPath = path17.resolve(opts.dataFile);
123746
123861
  const dataRaw = await readJsonFile(dataPath);
123747
- let data = asRecord3(dataRaw);
123862
+ let data = asRecord4(dataRaw);
123748
123863
  if (opts.collectFile) {
123749
123864
  const collectRaw = await readJsonFile(path17.resolve(opts.collectFile));
123750
- data = mergeCollectLighthouse(data, asRecord3(collectRaw));
123865
+ data = mergeCollectLighthouse(data, asRecord4(collectRaw));
123751
123866
  }
123752
123867
  const templatePath = websiteDiagnosisReportTemplatePath();
123753
123868
  let html;
@@ -124388,7 +124503,7 @@ function formatMarketReportContentErrors(result) {
124388
124503
  }
124389
124504
 
124390
124505
  // src/commands/market-analysis/render-report.ts
124391
- function asRecord4(value) {
124506
+ function asRecord5(value) {
124392
124507
  if (value && typeof value === "object" && !Array.isArray(value)) {
124393
124508
  return value;
124394
124509
  }
@@ -124418,7 +124533,7 @@ async function runMarketAnalysisRender(opts) {
124418
124533
  }
124419
124534
  let html;
124420
124535
  try {
124421
- html = resolveMarketAnalysisHtml(asRecord4(dataRaw));
124536
+ html = resolveMarketAnalysisHtml(asRecord5(dataRaw));
124422
124537
  } catch (err) {
124423
124538
  const message = err instanceof Error ? err.message : String(err);
124424
124539
  console.error(`
@@ -124430,7 +124545,7 @@ async function runMarketAnalysisRender(opts) {
124430
124545
  console.error("\n\u274C \u62A5\u544A HTML \u5185\u5BB9\u4E3A\u7A7A\n");
124431
124546
  process.exit(1);
124432
124547
  }
124433
- const reportData = asRecord4(dataRaw);
124548
+ const reportData = asRecord5(dataRaw);
124434
124549
  const contentCheck = validateMarketReportContent(html, validationContextFromReport(reportData));
124435
124550
  if (!contentCheck.ok) {
124436
124551
  console.error(`
@@ -125453,21 +125568,21 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
125453
125568
  }
125454
125569
 
125455
125570
  // src/commands/google-ads-diagnosis/collect-integrity.ts
125456
- function asRecord5(value) {
125571
+ function asRecord6(value) {
125457
125572
  if (value && typeof value === "object" && !Array.isArray(value)) {
125458
125573
  return value;
125459
125574
  }
125460
125575
  return null;
125461
125576
  }
125462
125577
  function itemCount(block) {
125463
- const items = asRecord5(block)?.items;
125578
+ const items = asRecord6(block)?.items;
125464
125579
  return Array.isArray(items) ? items.length : 0;
125465
125580
  }
125466
125581
  function getCollectIntegrityWarnings(reportData) {
125467
125582
  const warnings = [];
125468
125583
  const kwItems = itemCount(reportData.keywords);
125469
125584
  const fullKwItems = itemCount(reportData.fullKeywords);
125470
- const keywordCount = Number(asRecord5(reportData.structure)?.keywordCount ?? 0);
125585
+ const keywordCount = Number(asRecord6(reportData.structure)?.keywordCount ?? 0);
125471
125586
  if (kwItems === 0 && fullKwItems > 0) {
125472
125587
  warnings.push(
125473
125588
  `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`
@@ -125478,7 +125593,7 @@ function getCollectIntegrityWarnings(reportData) {
125478
125593
  );
125479
125594
  }
125480
125595
  const campaignItems = itemCount(reportData.campaigns);
125481
- const campaignCount = Number(asRecord5(reportData.structure)?.campaignCount ?? 0);
125596
+ const campaignCount = Number(asRecord6(reportData.structure)?.campaignCount ?? 0);
125482
125597
  if (campaignItems === 0 && campaignCount > 0) {
125483
125598
  warnings.push(
125484
125599
  `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`
@@ -125682,7 +125797,7 @@ import fs18 from "fs/promises";
125682
125797
  import path23 from "path";
125683
125798
 
125684
125799
  // src/commands/google-ads-diagnosis/report-content.ts
125685
- function asRecord6(value) {
125800
+ function asRecord7(value) {
125686
125801
  if (value && typeof value === "object" && !Array.isArray(value)) {
125687
125802
  return value;
125688
125803
  }
@@ -125691,7 +125806,7 @@ function asRecord6(value) {
125691
125806
  function getNestedValue(root, path34) {
125692
125807
  let cur = root;
125693
125808
  for (const key of path34) {
125694
- const rec = asRecord6(cur);
125809
+ const rec = asRecord7(cur);
125695
125810
  if (!rec) return void 0;
125696
125811
  cur = rec[key];
125697
125812
  }
@@ -125708,7 +125823,7 @@ function validateBudgetCompetitivenessStrategies(data) {
125708
125823
  }
125709
125824
  const warnings = [];
125710
125825
  for (let i = 0; i < items.length; i++) {
125711
- const row = asRecord6(items[i]);
125826
+ const row = asRecord7(items[i]);
125712
125827
  const name2 = String(row?.name ?? row?.metric ?? i);
125713
125828
  if (!hasNarrativeContent(row?.strategy)) {
125714
125829
  warnings.push(`budgetCompetitiveness[${name2}].strategy \u4E3A\u7A7A\uFF08\u987B\u7531 Agent \u586B\u5199\uFF09`);
@@ -125717,13 +125832,13 @@ function validateBudgetCompetitivenessStrategies(data) {
125717
125832
  return warnings;
125718
125833
  }
125719
125834
  function validateNewFeatureRecommendations(data) {
125720
- const nf = asRecord6(data.newFeatures);
125835
+ const nf = asRecord7(data.newFeatures);
125721
125836
  if (!nf) return [];
125722
125837
  const items = nf.items;
125723
125838
  if (!Array.isArray(items)) return [];
125724
125839
  const warnings = [];
125725
125840
  for (let i = 0; i < items.length; i++) {
125726
- const row = asRecord6(items[i]);
125841
+ const row = asRecord7(items[i]);
125727
125842
  const name2 = String(row?.strategyName ?? row?.feature ?? row?.strategy ?? i);
125728
125843
  if (!hasNarrativeContent(row?.optimizerRecommendation)) {
125729
125844
  warnings.push(`newFeatures.items[${name2}].optimizerRecommendation \u4E3A\u7A7A\uFF08\u987B\u7531 Agent \u586B\u5199\uFF09`);
@@ -125732,13 +125847,13 @@ function validateNewFeatureRecommendations(data) {
125732
125847
  return warnings;
125733
125848
  }
125734
125849
  function validateAdCreativeItemSuggestions(data) {
125735
- const creative = asRecord6(data.adCreativeOptimization);
125850
+ const creative = asRecord7(data.adCreativeOptimization);
125736
125851
  if (!creative) return [];
125737
125852
  const items = creative.items;
125738
125853
  if (!Array.isArray(items) || items.length === 0) return [];
125739
125854
  const warnings = [];
125740
125855
  for (let i = 0; i < items.length; i++) {
125741
- const row = asRecord6(items[i]);
125856
+ const row = asRecord7(items[i]);
125742
125857
  const title = String(row?.adTitle ?? i);
125743
125858
  if (!hasNarrativeContent(row?.suggestion ?? row?.suggestions)) {
125744
125859
  warnings.push(
@@ -125753,7 +125868,7 @@ function validateGoogleAdsDiagnosisContent(data) {
125753
125868
  (key) => data[key] === void 0
125754
125869
  );
125755
125870
  const warnings = [];
125756
- const accountInfo = asRecord6(data.accountInfo);
125871
+ const accountInfo = asRecord7(data.accountInfo);
125757
125872
  if (accountInfo && !accountInfo.companyName && !accountInfo.period) {
125758
125873
  warnings.push("accountInfo \u7F3A\u5C11 companyName \u4E0E period\uFF0C\u9875\u7709\u53EF\u80FD\u4E3A\u7A7A");
125759
125874
  }
@@ -125801,7 +125916,7 @@ var GOOGLE_ADS_DIAGNOSIS_RESTORE_ITEM_MODULES = [
125801
125916
  GOOGLE_ADS_DIAGNOSIS_BIDDING_STRATEGY_MODULE,
125802
125917
  GOOGLE_ADS_DIAGNOSIS_NEW_FEATURES_MODULE
125803
125918
  ];
125804
- function asRecord7(value) {
125919
+ function asRecord8(value) {
125805
125920
  if (value && typeof value === "object" && !Array.isArray(value)) {
125806
125921
  return value;
125807
125922
  }
@@ -125812,7 +125927,7 @@ function isValidRateChange(value) {
125812
125927
  return typeof value === "number" && Number.isFinite(value);
125813
125928
  }
125814
125929
  function isComparisonTableRowValid(row) {
125815
- const rec = asRecord7(row);
125930
+ const rec = asRecord8(row);
125816
125931
  if (!rec) return false;
125817
125932
  if (!String(rec.title ?? "").trim()) return false;
125818
125933
  if (!Number.isFinite(Number(rec.currentCost))) return false;
@@ -125824,12 +125939,12 @@ function isComparisonTableRowValid(row) {
125824
125939
  return true;
125825
125940
  }
125826
125941
  function isNewFeatureRowValid(row) {
125827
- const rec = asRecord7(row);
125942
+ const rec = asRecord8(row);
125828
125943
  if (!rec) return false;
125829
125944
  return Boolean(String(rec.strategyName ?? rec.feature ?? rec.strategy ?? "").trim());
125830
125945
  }
125831
125946
  function isBiddingStrategyRowValid(row) {
125832
- const rec = asRecord7(row);
125947
+ const rec = asRecord8(row);
125833
125948
  if (!rec) return false;
125834
125949
  if (!String(rec.campaignName ?? "").trim()) return false;
125835
125950
  if (!Number.isFinite(Number(rec.duration)) || Number(rec.duration) <= 0) return false;
@@ -125867,11 +125982,11 @@ function getFactItemsErrors(data) {
125867
125982
  for (const mod of GOOGLE_ADS_DIAGNOSIS_RESTORE_ITEM_MODULES) {
125868
125983
  const validator = MODULE_ITEM_VALIDATORS[mod];
125869
125984
  const hint = MODULE_ITEM_ERROR_HINT[mod];
125870
- const block = asRecord7(data[mod]);
125985
+ const block = asRecord8(data[mod]);
125871
125986
  const items = block?.items;
125872
125987
  if (!Array.isArray(items) || items.length === 0) {
125873
125988
  if (mod === "keywords") {
125874
- const fullKw = asRecord7(data.fullKeywords)?.items;
125989
+ const fullKw = asRecord8(data.fullKeywords)?.items;
125875
125990
  if (Array.isArray(fullKw) && fullKw.length > 0) {
125876
125991
  errors.push(
125877
125992
  "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"
@@ -125896,15 +126011,15 @@ var LANDING_PAGE_SPEED_KEYS = [
125896
126011
  "speedIndex"
125897
126012
  ];
125898
126013
  function isLandingPageSpeedValid(value) {
125899
- const rec = asRecord7(value);
126014
+ const rec = asRecord8(value);
125900
126015
  if (!rec) return false;
125901
126016
  return LANDING_PAGE_SPEED_KEYS.every(
125902
126017
  (key) => typeof rec[key] === "number" && Number.isFinite(rec[key])
125903
126018
  );
125904
126019
  }
125905
126020
  function restoreLandingPageSpeedFromCollect(finalData, collectReportData) {
125906
- const finalBlock = asRecord7(finalData[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE]);
125907
- const collectBlock = asRecord7(collectReportData[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE]);
126021
+ const finalBlock = asRecord8(finalData[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE]);
126022
+ const collectBlock = asRecord8(collectReportData[GOOGLE_ADS_DIAGNOSIS_LANDING_PAGE_MODULE]);
125908
126023
  if (!collectBlock) return { data: finalData, restored: false };
125909
126024
  const needsDesktop = !isLandingPageSpeedValid(finalBlock?.desktop) && isLandingPageSpeedValid(collectBlock.desktop);
125910
126025
  const needsMobile = !isLandingPageSpeedValid(finalBlock?.mobile) && isLandingPageSpeedValid(collectBlock.mobile);
@@ -125922,8 +126037,8 @@ function mergeComparisonItemsFromCollect(finalData, collectReportData) {
125922
126037
  const restoredModules = [];
125923
126038
  for (const mod of GOOGLE_ADS_DIAGNOSIS_RESTORE_ITEM_MODULES) {
125924
126039
  const validator = MODULE_ITEM_VALIDATORS[mod];
125925
- const finalBlock = asRecord7(data[mod]);
125926
- const collectBlock = asRecord7(collectReportData[mod]);
126040
+ const finalBlock = asRecord8(data[mod]);
126041
+ const collectBlock = asRecord8(collectReportData[mod]);
125927
126042
  const finalItems = finalBlock?.items;
125928
126043
  const collectItems = collectBlock?.items;
125929
126044
  if (!Array.isArray(collectItems) || collectItems.length === 0) continue;
@@ -125939,7 +126054,7 @@ function mergeComparisonItemsFromCollect(finalData, collectReportData) {
125939
126054
  }
125940
126055
 
125941
126056
  // src/commands/google-ads-diagnosis/narrative-consistency-check.ts
125942
- function asRecord8(value) {
126057
+ function asRecord9(value) {
125943
126058
  if (value && typeof value === "object" && !Array.isArray(value)) {
125944
126059
  return value;
125945
126060
  }
@@ -125947,14 +126062,14 @@ function asRecord8(value) {
125947
126062
  }
125948
126063
  function collectNarrativeTexts(data) {
125949
126064
  const texts = [];
125950
- const summary = asRecord8(data.summary);
126065
+ const summary = asRecord9(data.summary);
125951
126066
  if (Array.isArray(summary?.keyIssues)) {
125952
126067
  for (const item of summary.keyIssues) texts.push(String(item ?? ""));
125953
126068
  }
125954
- const overview = asRecord8(data.diagnosisOverview);
126069
+ const overview = asRecord9(data.diagnosisOverview);
125955
126070
  if (Array.isArray(overview?.disadvantages)) {
125956
126071
  for (const item of overview.disadvantages) {
125957
- const rec = asRecord8(item);
126072
+ const rec = asRecord9(item);
125958
126073
  if (rec) {
125959
126074
  texts.push(String(rec.title ?? ""), String(rec.description ?? ""));
125960
126075
  } else {
@@ -125965,16 +126080,16 @@ function collectNarrativeTexts(data) {
125965
126080
  return texts.filter((t) => t.trim().length > 0);
125966
126081
  }
125967
126082
  function newFeatureAccountStatus(data, strategy) {
125968
- const nf = asRecord8(data.newFeatures);
126083
+ const nf = asRecord9(data.newFeatures);
125969
126084
  const items = nf?.items;
125970
126085
  if (!Array.isArray(items)) return null;
125971
- const row = items.find((it) => asRecord8(it)?.strategy === strategy);
125972
- const rec = asRecord8(row);
126086
+ const row = items.find((it) => asRecord9(it)?.strategy === strategy);
126087
+ const rec = asRecord9(row);
125973
126088
  if (!rec || typeof rec.accountStatus !== "boolean") return null;
125974
126089
  return rec.accountStatus;
125975
126090
  }
125976
126091
  function goldAccountFlag(data, key) {
125977
- const gold = asRecord8(data.goldAccount);
126092
+ const gold = asRecord9(data.goldAccount);
125978
126093
  const value = gold?.[key];
125979
126094
  return typeof value === "boolean" ? value : null;
125980
126095
  }
@@ -126005,7 +126120,7 @@ function getNarrativeConsistencyErrors(data) {
126005
126120
  }
126006
126121
 
126007
126122
  // src/commands/google-ads-diagnosis/render-report.ts
126008
- function asRecord9(value) {
126123
+ function asRecord10(value) {
126009
126124
  if (value && typeof value === "object" && !Array.isArray(value)) {
126010
126125
  return value;
126011
126126
  }
@@ -126014,8 +126129,8 @@ function asRecord9(value) {
126014
126129
  async function tryLoadCollectReportData(collectPath) {
126015
126130
  try {
126016
126131
  const raw = JSON.parse(await fs18.readFile(collectPath, "utf8"));
126017
- const root = asRecord9(raw);
126018
- const reportData = asRecord9(root?.reportData);
126132
+ const root = asRecord10(raw);
126133
+ const reportData = asRecord10(root?.reportData);
126019
126134
  return reportData ?? root;
126020
126135
  } catch {
126021
126136
  return null;
@@ -126032,7 +126147,7 @@ async function runGoogleAdsDiagnosisRender(opts) {
126032
126147
  `);
126033
126148
  process.exit(1);
126034
126149
  }
126035
- let data = asRecord9(dataRaw);
126150
+ let data = asRecord10(dataRaw);
126036
126151
  if (!opts.noMergeCollect) {
126037
126152
  const collectPath = path23.resolve(
126038
126153
  opts.collectFile ?? path23.join(path23.dirname(dataPath), GOOGLE_ADS_DIAGNOSIS_COLLECT_BASENAME)
@@ -126545,7 +126660,7 @@ function ensureScorecardFromSnapshot(healthDiagnosis, merged) {
126545
126660
  }
126546
126661
 
126547
126662
  // src/commands/facebook-analysis/merge-snapshot.ts
126548
- function asRecord10(value) {
126663
+ function asRecord11(value) {
126549
126664
  if (value && typeof value === "object" && !Array.isArray(value)) {
126550
126665
  return value;
126551
126666
  }
@@ -126594,7 +126709,7 @@ async function readManifest(snapshotDir) {
126594
126709
  async function loadSectionFile(snapshotDir, file) {
126595
126710
  try {
126596
126711
  const raw = await fs21.readFile(path28.join(snapshotDir, file), "utf8");
126597
- return asRecord10(JSON.parse(raw));
126712
+ return asRecord11(JSON.parse(raw));
126598
126713
  } catch {
126599
126714
  return null;
126600
126715
  }
@@ -126642,7 +126757,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
126642
126757
  if (data) sectionMap.set(art.section, data);
126643
126758
  }
126644
126759
  const overview = sectionMap.get("overview");
126645
- const current = asRecord10(overview?.currentPeriod);
126760
+ const current = asRecord11(overview?.currentPeriod);
126646
126761
  const accountName = typeof overview?.accountName === "string" ? overview.accountName : void 0;
126647
126762
  const accountId = manifest.accountId ?? (typeof overview?.accountId === "string" ? overview.accountId : void 0);
126648
126763
  const kpis = { ...payload.kpis ?? {} };
@@ -126764,7 +126879,7 @@ function isFiniteNumber(value) {
126764
126879
  function asArray2(value) {
126765
126880
  return Array.isArray(value) ? value : [];
126766
126881
  }
126767
- function asRecord11(value) {
126882
+ function asRecord12(value) {
126768
126883
  if (value && typeof value === "object" && !Array.isArray(value)) {
126769
126884
  return value;
126770
126885
  }
@@ -126776,26 +126891,26 @@ function pushMissing(missing, id, chapter, dimension, hint) {
126776
126891
  function countAdSetsWithSpend(tables) {
126777
126892
  const rows = asArray2(tables?.adSets);
126778
126893
  return rows.filter((row) => {
126779
- const r = asRecord11(row);
126894
+ const r = asRecord12(row);
126780
126895
  const spend = r?.spend;
126781
126896
  return isFiniteNumber(spend) && spend > 0;
126782
126897
  }).length;
126783
126898
  }
126784
126899
  function chartLabels(chart) {
126785
- const c = asRecord11(chart);
126900
+ const c = asRecord12(chart);
126786
126901
  return asArray2(c?.labels).length;
126787
126902
  }
126788
126903
  function validateMetaPeriodReportContent(data) {
126789
126904
  const missing = [];
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);
126905
+ const meta = asRecord12(data.meta) ?? {};
126906
+ const kpis = asRecord12(data.kpis) ?? {};
126907
+ const narrative = asRecord12(data.narrative) ?? {};
126908
+ const tables = asRecord12(data.tables);
126909
+ const charts = asRecord12(data.charts);
126910
+ const sections = asRecord12(data.sections);
126911
+ const health = asRecord12(data.healthDiagnosis);
126912
+ const priorityPlan = asRecord12(data.priorityPlan);
126913
+ const actionChecklist = asRecord12(data.actionChecklist);
126799
126914
  const adSetsWithSpend2 = countAdSetsWithSpend(tables ?? void 0);
126800
126915
  const regionalNarratives = asArray2(narrative.regional).length;
126801
126916
  const recommendations = asArray2(narrative.recommendations);
@@ -126867,7 +126982,7 @@ function validateMetaPeriodReportContent(data) {
126867
126982
  "charts.audience.labels \u81F3\u5C11 1 \u9879"
126868
126983
  );
126869
126984
  }
126870
- const funnel = asRecord11(charts?.funnel);
126985
+ const funnel = asRecord12(charts?.funnel);
126871
126986
  if (!isFiniteNumber(funnel?.reach)) {
126872
126987
  pushMissing(
126873
126988
  missing,
@@ -126931,7 +127046,7 @@ function validateMetaPeriodReportContent(data) {
126931
127046
  );
126932
127047
  } else {
126933
127048
  for (let i = 0; i < regionalNarratives; i++) {
126934
- const row = asRecord11(asArray2(narrative.regional)[i]);
127049
+ const row = asRecord12(asArray2(narrative.regional)[i]);
126935
127050
  if (textLen2(row?.text) < 80) {
126936
127051
  pushMissing(
126937
127052
  missing,
@@ -126961,7 +127076,7 @@ function validateMetaPeriodReportContent(data) {
126961
127076
  "\u5FC5\u987B\u6070\u597D 4 \u6761\uFF08\u7B80\u5316\u8868\u5355/\u533A\u57DF\u8C03\u6574/\u9884\u7B97\u91CD\u6784/\u7D20\u6750\u5EFA\u8BAE\uFF09"
126962
127077
  );
126963
127078
  } else {
126964
- const titles = recommendations.map((r) => asRecord11(r)?.title);
127079
+ const titles = recommendations.map((r) => asRecord12(r)?.title);
126965
127080
  for (const title of RECOMMENDATION_TITLES) {
126966
127081
  if (!titles.includes(title)) {
126967
127082
  pushMissing(
@@ -126974,7 +127089,7 @@ function validateMetaPeriodReportContent(data) {
126974
127089
  }
126975
127090
  }
126976
127091
  for (let i = 0; i < recommendations.length; i++) {
126977
- const rec = asRecord11(recommendations[i]);
127092
+ const rec = asRecord12(recommendations[i]);
126978
127093
  if (textLen2(rec?.content) < 150) {
126979
127094
  pushMissing(
126980
127095
  missing,
@@ -126996,7 +127111,7 @@ function validateMetaPeriodReportContent(data) {
126996
127111
  );
126997
127112
  } else {
126998
127113
  for (let i = 0; i < supplementary.length; i++) {
126999
- const row = asRecord11(supplementary[i]);
127114
+ const row = asRecord12(supplementary[i]);
127000
127115
  if (!textLen2(row?.dimension) || !textLen2(row?.issue)) {
127001
127116
  pushMissing(
127002
127117
  missing,
@@ -127078,7 +127193,7 @@ function validateMetaPeriodReportContent(data) {
127078
127193
  );
127079
127194
  } else {
127080
127195
  for (let i = 0; i < fourQuestions.length; i++) {
127081
- const q = asRecord11(fourQuestions[i]);
127196
+ const q = asRecord12(fourQuestions[i]);
127082
127197
  const evidence = asArray2(q?.evidence).filter((e) => textLen2(e) > 0);
127083
127198
  if (evidence.length < 2) {
127084
127199
  pushMissing(
@@ -127102,7 +127217,7 @@ function validateMetaPeriodReportContent(data) {
127102
127217
  }
127103
127218
  let scorecardValidRows = 0;
127104
127219
  for (let i = 0; i < scorecard.length; i++) {
127105
- const row = asRecord11(scorecard[i]);
127220
+ const row = asRecord12(scorecard[i]);
127106
127221
  const itemOk = textLen2(row?.item) > 0;
127107
127222
  const dataOk = textLen2(row?.data) > 0;
127108
127223
  const signalOk = SCORECARD_SIGNALS.includes(row?.signal);
@@ -127144,7 +127259,7 @@ function validateMetaPeriodReportContent(data) {
127144
127259
  );
127145
127260
  }
127146
127261
  for (const key of ["platform", "country", "adSets"]) {
127147
- const insight = textLen2(asRecord11(sections?.[key])?.insight);
127262
+ const insight = textLen2(asRecord12(sections?.[key])?.insight);
127148
127263
  if (insight < 200) {
127149
127264
  pushMissing(
127150
127265
  missing,
@@ -127155,7 +127270,7 @@ function validateMetaPeriodReportContent(data) {
127155
127270
  );
127156
127271
  }
127157
127272
  }
127158
- const audience = asRecord11(sections?.audience);
127273
+ const audience = asRecord12(sections?.audience);
127159
127274
  if (asArray2(audience?.goldenProfile).filter((s) => textLen2(s) > 0).length < 3) {
127160
127275
  pushMissing(
127161
127276
  missing,
@@ -127183,7 +127298,7 @@ function validateMetaPeriodReportContent(data) {
127183
127298
  "\u2265150 \u5B57"
127184
127299
  );
127185
127300
  }
127186
- const landingRows = asArray2(asRecord11(sections?.landingPage)?.rows);
127301
+ const landingRows = asArray2(asRecord12(sections?.landingPage)?.rows);
127187
127302
  if (landingRows.length < 3) {
127188
127303
  pushMissing(
127189
127304
  missing,
@@ -127268,7 +127383,7 @@ async function readJsonFile2(filePath) {
127268
127383
  throw new Error(`\u65E0\u6CD5\u89E3\u6790 JSON\uFF1A${filePath}`);
127269
127384
  }
127270
127385
  }
127271
- function asRecord12(value) {
127386
+ function asRecord13(value) {
127272
127387
  if (value && typeof value === "object" && !Array.isArray(value)) {
127273
127388
  return value;
127274
127389
  }
@@ -127283,7 +127398,7 @@ function injectReportData2(html, payload) {
127283
127398
  }
127284
127399
  async function runFacebookAnalysisRender(opts) {
127285
127400
  const dataPath = path29.resolve(opts.dataFile);
127286
- let data = asRecord12(await readJsonFile2(dataPath));
127401
+ let data = asRecord13(await readJsonFile2(dataPath));
127287
127402
  if (opts.snapshotDir?.trim()) {
127288
127403
  data = await mergeFacebookSnapshotIntoReport(data, path29.resolve(opts.snapshotDir));
127289
127404
  }
@@ -20,7 +20,7 @@ allowed-tools: Bash(siluzan-tso:*) Read Write
20
20
 
21
21
  <!-- 注入到 SKILL.md.tmpl 的 {{AGENT_PREAMBLE}};构建时由 gen-skill-docs.mjs 合并 -->
22
22
 
23
- > **Agent 纪律(每个新任务必读)**:先 Read `references/core/agent-conventions.md`(唯一规则真相源:加载纪律、数据处理协议、时间/币种、批量约束、交付前自检),再按下方路由表 Read「必读文档」与对应工作流卡片。禁止跨话题复用参数记忆;数据类任务一律 `--json-out` + **仅用代码**读落盘 JSON(脚本示例见 `references/core/tips.md`)。
23
+ > **Agent 纪律(每个新任务必读)**:先 Read `references/core/agent-conventions.md`(唯一规则真相源:加载纪律、数据处理协议、时间/币种、批量约束、交付前自检),再按下方路由表 Read「必读文档」与对应工作流卡片。向用户回复时另 Read `references/core/user-communication-guide.md`(伙伴式沟通:结论先行、先执行后解释、禁止机械贴 CLI/编号)。禁止跨话题复用参数记忆;数据类任务一律 `--json-out` + **仅用代码**读落盘 JSON(脚本示例见 `references/core/tips.md`)。
24
24
  >
25
25
  > **报告 / 分析类统一消歧(必读)**:用户话术含「报告 / 诊断 / 分析 / 检测 / 监测 / 月报 / 周报」等,或对象不清(只有「帮我出份报告」)→ **必须先 Read `references/core/intent-routing.md`**。**§零 优先**:**网址/域名/官网/链接 + 诊断类词**(含用户说的「网络诊断/网络检测」)→ **直接 P8**,禁止走 P9/P1。**§零·B 次优先**:未命中 §零 的**行业/市场分析报告**(行业分析、市场分析、行业分析报告、「写一份 XX 行业报告」、战略/KA 市场报告、竞品/GTM 等,**无论是否提及丝路赞/广告**)→ **100% 走 P9 `market-analysis collect`+`render`**,**禁止**纯 WebSearch/WebFetch 在对话里写 Markdown/HTML 当终稿,**禁止**改走 P8/P1/P4/W5。**§零·C 关键词规划**:**拓词 / Keyword Planner / 长尾关键词 / 月搜索量 / 竞争度 / 核心词扩词**(含读 URL/文章后出 Google 词表,**无论是否提及丝路赞/账户**)→ **100% 走 W5 `keyword suggest --google-only --json-out`**,**禁止** WebSearch 编造搜索量。再按 §一 定唯一工作流。**禁止**见「报告」就默认 P4 周期或 P1 诊断。
26
26
  >
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.35-beta.5",
4
- "publishedAt": 1783330558539
3
+ "version": "1.1.36-beta.1",
4
+ "publishedAt": 1783409684521
5
5
  }
@@ -250,4 +250,4 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --js
250
250
  | 否词误填 | `KeywordsForBatchJob` 含常见否词词根或与 `NegativeKeywordsForBatchJob` 重复 → **warnings**(不阻断,须 Agent 修正后再 create) |
251
251
  | 实务 | 日期格式与先后、出价策略与配套字段一致 |
252
252
 
253
- `ad campaign-validate` 通过不保证 BatchJob 成功(例如仅写 `DestinationUrl` 未写 `Finalurl` 时 validate 仍可能 ✅)。异步结果用 `ad batch get` 轮询;`HasFailed` / 部分失败时用 `ad batch diff` 对照 JSON 补缺,系列级失败时改 JSON 重提,勿在半成品上反复整包创建。写操作须 `--commit`,见 `references/google-ads/google-ads.md` § ad campaign-create。
253
+ `ad campaign-validate` 通过不保证 BatchJob 成功(例如仅写 `DestinationUrl` 未写 `Finalurl` 时 validate 仍可能 ✅)。异步结果用 `ad batch get` 轮询;`HasFailed` / 部分失败时用 `ad batch diff` 对照 JSON 补缺,系列级失败时改 JSON 重提,勿在半成品上反复整包创建。**附加信息(Sitelink 等)batch 漏建时**:`batch diff` 后 **自动** `ad extension *` 补挂,勿仅询问用户(见 `references/google-ads/google-ads-campaign-plan.md` § batch diff 后自动补建)。写操作须 `--commit`,见 `references/google-ads/google-ads.md` § ad campaign-create。
@@ -8,6 +8,7 @@
8
8
  | ------------------------------------------- | ------------------------------------------------------------------------------------------- |
9
9
  | `references/core/setup.md` | 安装、登录(手机验证码优先)、配置、更新 |
10
10
  | `references/core/agent-conventions.md` | **Agent 必读 · 唯一规则真相源**:加载纪律、数据处理协议、硬规范、时间/币种/批量、交付前自检 |
11
+ | `references/core/user-communication-guide.md` | **用户回复风格**:结论先行、伙伴语气、自然语言路由;禁止机械贴 CLI/编号 |
11
12
  | `references/core/intent-routing.md` | **报告 / 分析类统一消歧**:P1–P9 与 W3/W7 同义词、信号优先级、一次追问模板 |
12
13
  | `references/core/tips.md` | `--json-out` 脚本食谱(node -e 示例、文件命名、分页) |
13
14
  | `references/core/playbooks.md` | **工作流目录 · 分析/报告类**(P1–P9,统一卡片:触发/必读/步骤/交付) |
@@ -13,6 +13,7 @@
13
13
  | 触发 | 动作 |
14
14
  | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
15
  | **新的用户任务 / 同对话内换话题**(新需求、新账户、新媒体、新报告类型;例:刚查余额 → 改问建系列) | 按 `SKILL.md` 路由表 **重新 Read** 该任务的「必读文档」与工作流卡片后再执行 CLI;**禁止**沿用上一任务的参数记忆——对话会被压缩,「读过」≠ 当前上下文仍含正确字段名与 flags |
16
+ | **向用户回复结论、建议、诊断说明**(非纯静默跑命令) | Read `references/core/user-communication-guide.md`:结论先行、伙伴语气、禁止机械贴 CLI/编号 |
16
17
  | **工作流编号不同**(P1→P3、P4→P5) | 即使刚做过 P1,仍须 Read 新卡片及其必读文档 |
17
18
  | **报告 / 分析类话术模糊**(只说「报告/诊断/分析/检测/监测」、对象不清) | **必须先 Read** `references/core/intent-routing.md`,定唯一 P*/W* 后再 Read 工作流卡片;**禁止**默认 P4 或 P1 |
18
19
  | **专用 report-templates**(OKKI / 询盘分析 / 周期报告纲要) | Read `references/report-templates/<名>.md` **全文**(与 `report-templates/<名>.md` 同源);勿只凭 SKILL 摘要 |
@@ -40,11 +41,11 @@
40
41
 
41
42
  **计划 → 确认 → 执行 → 验证 → 推测下一步**:
42
43
 
43
- 1. 按上表 Read 当次任务 references → 用 `-h` 确认命令 → 向用户输出操作计划。
44
+ 1. 按上表 Read 当次任务 references → 用 `-h` 确认命令 → 向用户输出操作计划(**计划用业务语言一句带过**,详见 `user-communication-guide.md`)。
44
45
  2. 涉及写入/修改/删除的操作必须与用户确认;多数破坏性操作还需 `--commit`。
45
46
  3. 按计划执行,说明每步意图。
46
47
  4. 用成对的读命令复核写入结果;异步任务每 5s 轮询直到完成。
47
- 5. 报告/Excel/含金额话术交付前,按本文件 **§七 交付前自检** 审阅最终产物。
48
+ 5. 报告/Excel/含金额话术交付前,按本文件 **§七 交付前自检** 审阅最终产物;**同时**按 `references/core/user-communication-guide.md` 组织面向用户的摘要(结论先行,禁止机械贴命令输出)。
48
49
  6. 全部完成后预测用户下一步操作。
49
50
 
50
51
  ### 执行模式速查
@@ -0,0 +1,112 @@
1
+ # 用户沟通风格(Agent 回复规范)
2
+
3
+ > **何时必读**:每个面向用户的任务(查数、诊断、报告、开户、优化、异常排查等)在**组织最终回复文字**前 Read 本文件。CLI 纪律、字段口径仍以 `agent-conventions.md` 为准;本文件只管**怎么说**,不管**怎么跑命令**。
4
+ >
5
+ > 产品背景见《丝路赞 TSO Agent 用户全链路引导方案》;此处为 Skill 可执行摘要,**禁止**向用户暴露 P1/W12/playbook 编号。
6
+
7
+ ---
8
+
9
+ ## 一、定位
10
+
11
+ 用户不会按开发者逻辑提问(不会说「执行 P4」或「查 campaignStatus」)。Agent 是**广告运营伙伴**,不是「等指令的工具」。
12
+
13
+ | 避免(机械) | 改为(伙伴感) |
14
+ | ------------ | -------------- |
15
+ | 「请执行 `google-analysis --sections overview`」 | 「我先帮您拉一下账户整体数据,稍等。」 |
16
+ | 「您需要先关联 Google 账户」 | 「我看了一下,您还没绑定 Google 账户。绑好后我就能帮你看投放数据和优化建议,要现在开始吗?」 |
17
+ | 贴一整段 CLI stdout / JSON | **结论先行**,数字来自脚本,用业务语言概括 |
18
+ | 连续反问 5 个技术参数 | **能推断就先做**;缺关键项只问 1~2 个,其余用合理默认并在结论里说明 |
19
+
20
+ ---
21
+
22
+ ## 二、回复结构(默认模板)
23
+
24
+ **共情/确认 → 结论先行 → 数据支撑 → 行动建议 → 下一步**
25
+
26
+ 1. **共情/确认**:用一句话接住用户意图(「好的,我帮您看看…」「收到,我来排查一下…」)。
27
+ 2. **结论先行**:第一句给出可操作的判断(正常 / 有问题 / 原因是什么),不要让用户从表格里自己猜。
28
+ 3. **数据支撑**:引用**当次 CLI/脚本**的真实数字(消耗、CPA、余额、状态等);禁止编造、禁止示例 ID。
29
+ 4. **行动建议**:1~3 条可执行项,点名对象(系列名、账户、关键词)并引用数据。
30
+ 5. **下一步**:一句预测或邀请(「要我帮您调整预算吗?」「还想看关键词明细吗?」)。
31
+
32
+ 长任务(预计 >30s):中间用**进度式**短句(「正在拉取系列数据…」「正在检查余额…」),不要长时间沉默后突然扔一大段表。
33
+
34
+ ---
35
+
36
+ ## 三、先执行后解释(与纪律的平衡)
37
+
38
+ | 场景 | 做法 |
39
+ | ---- | ---- |
40
+ | 查数、诊断、报告、列表 | **先拉数再汇报**;缺账户 ID 才问;缺时间范围按 `agent-conventions.md` §五 处理(能默认则默认并注明区间) |
41
+ | 模糊意图(「帮我看一下」「最近怎么样」) | 选**最可能**的工作流先执行(有 Google 户 → 画像/总览);结论后附「您是不是还想…」 |
42
+ | **campaign-create 后 Sitelink 等附加信息 batch 失败** | **自动** `ad batch diff` → 执行 `ad extension *` 补建 → 汇报已补挂条数;**勿**反问「需要我现在补上吗?」(用户已确认过 JSON) |
43
+ | 写入/删除/充值/开户提交 | **必须先确认**(`agent-conventions.md` §四);话术用业务语言说明「将要做什么」 |
44
+ | 对象不清且无法推断 | **只问一次**,给 2~3 个选项,不要问卷式连问 |
45
+
46
+ ---
47
+
48
+ ## 四、自然语言 ↔ 内部路由(对用户隐藏编号)
49
+
50
+ 对用户只说业务语言;内部仍按 `SKILL.md` + `intent-routing.md` 路由。
51
+
52
+ | 用户可能说 | 内部路由(勿写出) | 对用户怎么说 |
53
+ | ---------- | ------------------ | ------------ |
54
+ | 帮我看一下 Google 广告 / 账户怎么样 | P1 / P3 + 余额 | 「我先看看您 Google 账户的整体情况。」 |
55
+ | 广告怎么不跑了 / 没花费了 | 余额 + 系列状态 + 拒审 + 落地页 | 「我来帮您排查,通常是预算、状态或审核几类原因。」 |
56
+ | 最近效果怎么样 / 出个月报 | P4 / P1 | 「我拉一下这段时间的数据,给您一份完整报告。」 |
57
+ | 帮我优化一下 | W6 / 优化建议 | 「我先看看哪些广告表现偏弱,给您一份优化清单。」 |
58
+ | 哪些账户快没钱了 | P2 balance-scan | 「正在扫描全部账户余额…」 |
59
+ | 帮我开个户 / 建广告 | W2 / W3 | 「没问题,我先告诉您需要准备哪些资料。」 |
60
+ | 网站行不行 | P8 | 「我来给这个网站做个体检,看是否符合投放要求。」 |
61
+
62
+ 完整话术矩阵见产品文档第十章;Skill 内记住**结构**即可,勿背诵全文。
63
+
64
+ ---
65
+
66
+ ## 五、场景话术要点(示例骨架)
67
+
68
+ ### 账户概览(正常)
69
+
70
+ > 帮您看完了!「{账户名}」近 {N} 天花费 {金额},点击 {次数},转化 {次数},CPA {金额},余额 {金额}(预计还能跑 {天数} 天)。整体运行正常。要我深入看看有哪些可以优化的地方吗?
71
+
72
+ ### 投放异常(找到原因)
73
+
74
+ > 帮您排查完了,找到原因了:
75
+ >
76
+ > **问题**:系列「{名称}」当前是「{campaignStatusDisplay}」,{一句话原因,如预算熔断/余额不足/被拒审}。
77
+ >
78
+ > **建议**:{1~2 条具体动作}
79
+ >
80
+ > 要我帮您{下一步动作}吗?
81
+
82
+ ### 数据不足 / 拉数失败
83
+
84
+ > 这部分数据暂时拿不到({简短原因})。我已经把能查到的部分整理好了;{缺什么}需要您{补什么}后我再帮您查。
85
+
86
+ ### 用户说「不是这个意思」
87
+
88
+ > 抱歉理解偏了!您方便再说一下具体想做什么吗?或者从下面选一个:{选项 A} / {选项 B}
89
+
90
+ ---
91
+
92
+ ## 六、禁止事项(机械感来源)
93
+
94
+ - **禁止**把 playbook 编号、CLI 命令名、JSON 字段名当对用户的主回复内容(技术细节放代码块或附录,且仅在用户要「技术细节」时)。
95
+ - **禁止**无结论的数据堆砌(大表前没有一句话总结)。
96
+ - **禁止**每条 bullet 都是「建议优化」「请关注」而无数字、无对象名。
97
+ - **禁止**用冷冰冰的系统提示语气(「操作失败」「参数错误」)而不给下一步。
98
+ - **禁止**在报告/分析类任务里,用户未要求时输出冗长「执行计划」清单;计划可在脑中或简短一句带过,**尽快进入执行**。
99
+ - 交付 HTML/Excel 时:附**3~5 行摘要**(区间、币种、核心 KPI、最大问题、建议动作),不要只丢文件路径。
100
+
101
+ ---
102
+
103
+ ## 七、与现有纪律的关系
104
+
105
+ | 文档 | 分工 |
106
+ | ---- | ---- |
107
+ | `agent-conventions.md` | 怎么拉数、怎么读 outline、怎么自检、硬规范 |
108
+ | `intent-routing.md` | 报告/分析消歧、选哪个 P/W |
109
+ | `playbooks.md` / `workflows.md` | 步骤清单 |
110
+ | **本文件** | **对用户怎么说** |
111
+
112
+ 数值纪律不变:所有金额、状态、ID 仍只来自当次 CLI/`--json-out` 脚本;本文件只约束**表述方式**,不放宽数据真实性要求。
@@ -74,8 +74,9 @@
74
74
  1. 地域 ID:`ad geo search -a <id> -q "United States"` 写入 `campaign.targetedLocations[].id`。
75
75
  2. 门禁:`ad campaign-validate --config-file ./campaign.json`(必跑)。
76
76
  3. 用户确认后创建:`ad campaign-create --config-file ./campaign.json`,记录返回 taskId。
77
- 4. 轮询:`ad batch get --id <taskId>`(Creating Successfully)。
78
- 5. 复核:`ad campaigns -a <id> --json-out ./snap` `campaignId` 供后续精细操作。
77
+ 4. 轮询:`ad batch get --id <taskId>` 直至非 Creating。
78
+ 5. **必做** `ad batch diff`;缺失的附加信息(Sitelink 等)**自动**用 `ad extension *` 补建,勿反问用户(见 `google-ads-campaign-plan.md` § batch diff 后自动补建)。
79
+ 6. 复核:`ad campaigns` / `ad extension list` 确认系列与扩展齐全。
79
80
  - **精细管理与日常运营**:`ad adgroup-create` / `ad keyword-create` / `ad keyword-negative-create` / `ad ad-create`(拓词辅助见 **W5**);调整用 `ad adgroup-status` / `ad campaign-status` / `ad ad-delete` / `ad keyword-negative-delete`。完整参数见 `references/google-ads/google-ads.md`。
80
81
  - **交付/确认**:关键词匹配格式 `running shoes`=广泛 / `"..."`=词组 / `[...]`=精确;结构性写操作(新建/暂停/删除)须用户确认;写后用成对读命令复核。
81
82
 
@@ -44,8 +44,8 @@
44
44
  | 6 | 输出:**JSON 代码块** → **Markdown**(`google-ads-launch-plan-template.md` 正文)→ 待确认 | — |
45
45
  | 7 | 用户确认后 **`ad campaign-create`** | `references/google-ads/google-ads.md` |
46
46
  | 8 | 每隔5s 获取创建结果 | `ad batch get --id <taskId> --config-file ./campaign.json` |
47
- | 9 | 创建失败根据失败原因修改json重新走创建流程,部分成功/成功/部分失败:都调用来做最后一步调整 `ad batch diff --batch-id <taskId> --config-file ./campaign.json` | |
48
- | 10 | 输出所有失败的内容与原因,并询问用户是否需要修改后单独添加到系列中如果用户要求是则读取 `references\references/google-ads/google-ads.md` 来获取对应缺失部分的创建命令 |
47
+ | 9 | 成功或部分成功后 **`ad batch diff`** 对照 JSON 与账户实况 | |
48
+ | 10 | **自动补建缺失项**(见下文「batch diff 后自动补建」):附加信息(Sitelink/Callout 等)**直接**执行 `ad extension *`;组/词/RSA 用对应 create 命令。**勿**仅汇报失败并反问用户是否补建 | `references/google-ads/google-ads.md` § batch diff 后自动补建 |
49
49
 
50
50
  多系列:每系列一个 JSON;可选 `campaign-manifest.json`(`role: brand|competitor|generic`)仅作文件组织参考。
51
51
 
@@ -108,6 +108,34 @@ validate 与 create **共用** `runCampaignCreateValidation`:词面规范化 +
108
108
 
109
109
  ---
110
110
 
111
+ ## batch diff 后自动补建(Agent 必遵)
112
+
113
+ `campaign-create` BatchJob **常漏附加信息**(尤其 Sitelink);系列/组/关键词/RSA 已创建成功时,**不要**停在「需要我现在补上吗?」——用户确认 JSON 时已包含这些内容,Agent **应自动补建**。
114
+
115
+ | 步骤 | 动作 |
116
+ | ---- | ---- |
117
+ | 1 | `ad batch get` 直至 `Successfully` 或 `HasFailed`(Creating 时每 5s 轮询) |
118
+ | 2 | `ad batch diff --batch-id <taskId> --config-file ./campaign.json [--campaign-id <id>]` |
119
+ | 3 | 读缺失明细(或 `--json-out` 落盘中的 `missing[]`);每条 `extension` 层若有 `remediateCommand`,**直接执行** |
120
+ | 4 | 无 `remediateCommand` 时,按 JSON 路径与 `google-ads.md` § 批量创建工作流 映射为 `ad extension sitelink` / `callout` / `call` / `snippet` |
121
+ | 5 | 缺组/词/RSA/否词:用 `ad adgroup-create` / `keyword-create` / `ad-create` / `keyword-negative-create` 补建(补建前 `ad extension list` 等核对,避免重复) |
122
+ | 6 | 全部补建后再 `ad batch diff` 或 `ad extension list --campaign-id <id>` 复核;汇总告诉用户「已自动补上 N 条 Sitelink」 |
123
+ | 7 | 仅当补建命令也失败时,才向用户说明原因并给改写方案 |
124
+
125
+ **Sitelink 单条示例**(字段来自 `ExtensionsForBatchJob[i].Properties`):
126
+
127
+ ```bash
128
+ siluzan-tso ad extension sitelink -a <accountId> \
129
+ --level Campaign --campaign-id <campaignId> \
130
+ --text "<Properties.Text>" --url "<Properties.DestinationUrl>" \
131
+ [--line2 "<Line2>"] [--line3 "<Line3>"] \
132
+ --json-out ./snap
133
+ ```
134
+
135
+ **禁止**:系列主体已按计划建成,却因 Sitelink batch 失败就结束任务、等用户二次确认(除非用户明确说「先不要挂扩展」)。
136
+
137
+ ---
138
+
111
139
  ## 已上线后的修改
112
140
 
113
141
  - **勿**用 `campaign-create` 覆盖已有系列;用 `ad campaign-edit` / `adgroup-*` / `keyword-*` / `ad-edit` 等(见 `references/google-ads/google-ads.md`)。
@@ -371,8 +371,21 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json
371
371
  2. **仅少数组/资产失败**(系列与大部分组已有 `Id`):
372
372
  - 缺组:`ad adgroup-create`(`--json-out` 取 `id`)
373
373
  - 缺词:`ad keyword-create`(词面格式同 JSON `KeywordsForBatchJob`)
374
- - 缺 RSA:`ad ad-create`(`--headlines` / `--descriptions` 从 JSON `AdsForBatchJob` 抄)
375
- 补建前可用 `ad groups` / `ad keywords` 核对账户现状。
374
+ - 缺 RSA:`ad ad-create`(`--headlines` / `--descriptions` 从 JSON `AdsForBatchJob` 抄)
375
+ - **缺附加信息(常见)**:**自动**执行 `ad extension sitelink` / `callout` / `call` / `snippet`(见下节);`batch diff` 输出含 `补建:` 行或 `--json-out` 的 `missing[].remediateCommand`
376
+ 补建前可用 `ad groups` / `ad keywords` / `ad extension list` 核对账户现状。
377
+
378
+ ### batch diff 后自动补建附加信息(Agent 必遵)
379
+
380
+ BatchJob 中 `ExtensionsForBatchJob`(尤其 **Sitelink**)失败率较高,但系列/组/词/RSA 往往已成功。用户确认创建时已认可 JSON 内全部附加信息 → Agent **须直接补建**,**禁止**仅汇报「Sitelink 未创建,需要我现在补上吗?」。
381
+
382
+ 1. 跑 `ad batch diff`;对 `layer=extension` 的缺失项执行输出中的 **`补建:`** 命令(或读落盘 `missing[].remediateCommand`)。
383
+ 2. 字段映射:`Properties.Text` + `DestinationUrl` → `ad extension sitelink`;`CALLOUT` → `callout`;`CALL` → `call`;`STRUCTURED_SNIPPET` → `snippet`(`StructuredSnippetHeaderValue`)。
384
+ 3. 系列级统一 `--level Campaign --campaign-id <id>`;每条 `--json-out ./snap`。
385
+ 4. 补建完成后 `ad extension list -a <accountId> --campaign-id <id> --type SITELINK` 复核条数。
386
+ 5. 向用户汇报:「系列已创建;Sitelink 等 X 条已通过单独命令补挂完成」(附系列 ID 与条数即可)。
387
+
388
+ 完整流水线见 `references/google-ads/google-ads-campaign-plan.md` § batch diff 后自动补建。
376
389
 
377
390
  **`ad batch diff` 比对维度:**系列是否存在 → 各 `AdGroupsForBatchJob[].Name` → 组内关键词(匹配类型+词面)→ RSA 首条标题 → 系列否定词 → 附加信息条数。默认按层级树状输出每条缺失的 **JSON 路径 + 计划内容 + 账户实况**;`--json-out` 可落盘完整结构。
378
391
 
@@ -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.5'
12
+ $PKG_VERSION = '1.1.36-beta.1'
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.5"
12
+ readonly PKG_VERSION="1.1.36-beta.1"
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.5",
3
+ "version": "1.1.36-beta.1",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",