siluzan-tso-cli 1.1.49-beta.2 → 1.1.49-beta.4

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.49-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.49-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -117511,6 +117511,35 @@ function liveAdHasHeadline(ad, headline) {
117511
117511
  if (!h) return false;
117512
117512
  return collectRsaHeadlines(ad).includes(h);
117513
117513
  }
117514
+ function overlapHeadlineCount(planned, live) {
117515
+ const liveSet = new Set(collectRsaHeadlines(live));
117516
+ let n = 0;
117517
+ for (const h of planned) {
117518
+ const key = h.trim().toLowerCase();
117519
+ if (key && liveSet.has(key)) n += 1;
117520
+ }
117521
+ return n;
117522
+ }
117523
+ function pickLiveAdForPlannedRsa(plannedHeadlines, primary, unmatchedLiveAds) {
117524
+ if (unmatchedLiveAds.length === 0) return null;
117525
+ const primaryKey = primary.trim().toLowerCase();
117526
+ let bestIdx = -1;
117527
+ let bestScore = 0;
117528
+ for (let i = 0; i < unmatchedLiveAds.length; i++) {
117529
+ const live = unmatchedLiveAds[i];
117530
+ const overlap = overlapHeadlineCount(plannedHeadlines, live);
117531
+ if (overlap <= 0) continue;
117532
+ const hasPrimary = primaryKey.length > 0 && liveAdHasHeadline(live, primary);
117533
+ const score = (hasPrimary ? 1e3 : 0) + overlap;
117534
+ if (score > bestScore) {
117535
+ bestScore = score;
117536
+ bestIdx = i;
117537
+ }
117538
+ }
117539
+ if (bestIdx < 0) return null;
117540
+ const [picked] = unmatchedLiveAds.splice(bestIdx, 1);
117541
+ return picked ?? null;
117542
+ }
117514
117543
  function livePoolHasText(pool, text2) {
117515
117544
  const t = text2.trim().toLowerCase();
117516
117545
  return t.length > 0 && pool.has(t);
@@ -117768,12 +117797,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
117768
117797
  const liveAdsInGroup = live.ads.filter(
117769
117798
  (a) => belongsToCampaign(a, campaignId, campaignName) && belongsToAdGroup(a, groupName, liveGroupId)
117770
117799
  );
117771
- const liveHeadlinePool = /* @__PURE__ */ new Set();
117772
- const liveDescPool = /* @__PURE__ */ new Set();
117773
- for (const la of liveAdsInGroup) {
117774
- for (const h of collectRsaHeadlines(la)) liveHeadlinePool.add(h);
117775
- for (const d of collectRsaDescriptions(la)) liveDescPool.add(d);
117776
- }
117800
+ const unmatchedLiveAds = [...liveAdsInGroup];
117777
117801
  const ads = g["AdsForBatchJob"];
117778
117802
  if (Array.isArray(ads)) {
117779
117803
  for (let ai = 0; ai < ads.length; ai++) {
@@ -117783,11 +117807,14 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
117783
117807
  const primary = pickString2(ad["headlinePart1"], ad["AdTitle"]);
117784
117808
  const finalUrl = pickString2(ad["Finalurl"], ad["DestinationUrl"], ad["finalUrl"]);
117785
117809
  const rsaAssets = listPlannedRsaAssets(ad, groupPath, ai, groupName);
117810
+ const plannedHeadlines = rsaAssets.filter((a) => a.kind === "headline").map((a) => a.text);
117811
+ const adLabel = primary || `AdsForBatchJob[${ai}]`;
117786
117812
  if (groupAbsent) {
117787
117813
  pushItem(items3, {
117788
117814
  layer: "ad",
117789
117815
  path: path50,
117790
117816
  adGroupName: groupName,
117817
+ adLabel,
117791
117818
  plannedContent: primary ? `RSA \u9996\u6807\u9898: ${primary}${finalUrl ? ` | \u843D\u5730\u9875: ${finalUrl}` : ""}` : `AdsForBatchJob[${ai}]\uFF08\u7F3A\u5C11 headlinePart1\uFF09`,
117792
117819
  status: "skipped",
117793
117820
  liveNote: "\u5E7F\u544A\u7EC4\u672A\u521B\u5EFA\uFF0C\u8DF3\u8FC7\u521B\u610F\u6BD4\u5BF9",
@@ -117799,6 +117826,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
117799
117826
  layer: "ad",
117800
117827
  path: asset.path,
117801
117828
  adGroupName: groupName,
117829
+ adLabel,
117802
117830
  plannedContent: `${label}: ${asset.text}`,
117803
117831
  status: "skipped",
117804
117832
  liveNote: "\u5E7F\u544A\u7EC4\u672A\u521B\u5EFA\uFF0C\u8DF3\u8FC7\u6587\u6848\u6BD4\u5BF9",
@@ -117807,11 +117835,15 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
117807
117835
  }
117808
117836
  continue;
117809
117837
  }
117838
+ const matchedLive = pickLiveAdForPlannedRsa(plannedHeadlines, primary, unmatchedLiveAds);
117839
+ const liveHeadlineSet = new Set(matchedLive ? collectRsaHeadlines(matchedLive) : []);
117840
+ const liveDescSet = new Set(matchedLive ? collectRsaDescriptions(matchedLive) : []);
117810
117841
  if (!primary) {
117811
117842
  pushItem(items3, {
117812
117843
  layer: "ad",
117813
117844
  path: path50,
117814
117845
  adGroupName: groupName,
117846
+ adLabel,
117815
117847
  plannedContent: `AdsForBatchJob[${ai}]\uFF08\u7F3A\u5C11 headlinePart1\uFF0C\u65E0\u6CD5\u6BD4\u5BF9\u6574\u6761 RSA\uFF09`,
117816
117848
  status: "missing",
117817
117849
  liveNote: `\u540C\u7EC4\u5DF2\u6709 ${liveAdsInGroup.length} \u6761\u521B\u610F`,
@@ -117819,41 +117851,44 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
117819
117851
  });
117820
117852
  for (const asset of rsaAssets) {
117821
117853
  const label = asset.kind === "headline" ? "\u6807\u9898" : "\u63CF\u8FF0";
117822
- const pool = asset.kind === "headline" ? liveHeadlinePool : liveDescPool;
117854
+ const pool = asset.kind === "headline" ? liveHeadlineSet : liveDescSet;
117823
117855
  const found = livePoolHasText(pool, asset.text);
117824
117856
  pushItem(items3, {
117825
117857
  layer: "ad",
117826
117858
  path: asset.path,
117827
117859
  adGroupName: groupName,
117860
+ adLabel,
117828
117861
  plannedContent: `${label}: ${asset.text}`,
117829
117862
  status: found ? "ok" : "missing",
117830
- liveNote: found ? `\u540C\u7EC4 RSA \u6587\u6848\u6C60\u5DF2\u542B\u6B64${label}` : `\u540C\u7EC4 RSA \u6587\u6848\u6C60\u672A\u89C1\u6B64${label}`,
117863
+ liveNote: found ? `\u5DF2\u5728\u5BF9\u5E94 RSA \u4E0A\u627E\u5230\u6B64${label}` : `\u5BF9\u5E94 RSA \u672A\u89C1\u6B64${label}`,
117831
117864
  summary: found ? `RSA ${label}\u5DF2\u751F\u6548\uFF1A${asset.text}` : `RSA ${label}\u672A\u751F\u6548\uFF1A${asset.text}`
117832
117865
  });
117833
117866
  }
117834
117867
  continue;
117835
117868
  }
117836
- const rsaFound = liveAdsInGroup.some((la) => liveAdHasHeadline(la, primary));
117869
+ const rsaFound = matchedLive != null && liveAdHasHeadline(matchedLive, primary);
117837
117870
  pushItem(items3, {
117838
117871
  layer: "ad",
117839
117872
  path: path50,
117840
117873
  adGroupName: groupName,
117874
+ adLabel,
117841
117875
  plannedContent: `RSA \u9996\u6807\u9898: ${primary}${finalUrl ? ` | \u843D\u5730\u9875: ${finalUrl}` : ""}`,
117842
117876
  status: rsaFound ? "ok" : "missing",
117843
- liveNote: rsaFound ? `\u540C\u7EC4\u5DF2\u5339\u914D\u9996\u6807\u9898\uFF08\u5171 ${liveAdsInGroup.length} \u6761 RSA\uFF09` : `\u540C\u7EC4\u5DF2\u6709 ${liveAdsInGroup.length} \u6761 RSA\uFF0C\u65E0\u6B64\u9996\u6807\u9898`,
117844
- summary: rsaFound ? `RSA \u5DF2\u521B\u5EFA\uFF08\u9996\u6761\u6807\u9898\u300C${primary}\u300D\uFF09` : `RSA \u672A\u521B\u5EFA\uFF08\u9996\u6761\u6807\u9898\u300C${primary}\u300D\u5728\u8D26\u6237\u5185\u672A\u627E\u5230\uFF09`
117877
+ liveNote: rsaFound ? `\u5DF2\u4E00\u5BF9\u4E00\u5339\u914D\u5230\u540C\u7EC4 RSA\uFF08\u6807\u9898\u91CD\u53E0 ${overlapHeadlineCount(plannedHeadlines, matchedLive)}\uFF09` : `\u540C\u7EC4 ${liveAdsInGroup.length} \u6761 RSA \u4E2D\u65E0\u6B64\u9996\u6807\u9898\uFF0C\u6216\u5DF2\u88AB\u5176\u5B83\u8BA1\u5212\u5E7F\u544A\u8BA4\u9886`,
117878
+ summary: rsaFound ? `RSA \u5DF2\u521B\u5EFA\uFF08\u9996\u6761\u6807\u9898\u300C${primary}\u300D\uFF09` : `RSA \u672A\u521B\u5EFA\uFF08\u9996\u6761\u6807\u9898\u300C${primary}\u300D\u5728\u5BF9\u5E94\u5E7F\u544A\u4E0A\u672A\u627E\u5230\uFF09`
117845
117879
  });
117846
117880
  for (const asset of rsaAssets) {
117847
117881
  const label = asset.kind === "headline" ? "\u6807\u9898" : "\u63CF\u8FF0";
117848
- const pool = asset.kind === "headline" ? liveHeadlinePool : liveDescPool;
117882
+ const pool = asset.kind === "headline" ? liveHeadlineSet : liveDescSet;
117849
117883
  const found = livePoolHasText(pool, asset.text);
117850
117884
  pushItem(items3, {
117851
117885
  layer: "ad",
117852
117886
  path: asset.path,
117853
117887
  adGroupName: groupName,
117888
+ adLabel,
117854
117889
  plannedContent: `${label}: ${asset.text}`,
117855
117890
  status: found ? "ok" : "missing",
117856
- liveNote: found ? `\u540C\u7EC4 RSA \u6587\u6848\u6C60\u5DF2\u542B\u6B64${label}` : `\u540C\u7EC4 RSA \u6587\u6848\u6C60\u672A\u89C1\u6B64${label}\uFF08\u6C60\u5185 ${pool.size} \u6761\uFF09`,
117891
+ liveNote: found ? `\u5DF2\u5728\u5BF9\u5E94 RSA \u4E0A\u627E\u5230\u6B64${label}` : `\u5BF9\u5E94 RSA \u672A\u89C1\u6B64${label}\uFF08\u8BE5\u5E7F\u544A\u6587\u6848 ${pool.size} \u6761\uFF09`,
117857
117892
  summary: found ? `RSA ${label}\u5DF2\u751F\u6548\uFF1A${asset.text}` : `RSA ${label}\u672A\u751F\u6548\uFF1A${asset.text}`
117858
117893
  });
117859
117894
  }
@@ -118035,6 +118070,7 @@ async function fetchLiveSnapshotForCampaign(config, accountId, campaignId, campa
118035
118070
  const params = new URLSearchParams();
118036
118071
  params.set("startDate", toGoogleDate(void 0, -30));
118037
118072
  params.set("endDate", toGoogleDate(void 0, 0));
118073
+ params.set("newest", "true");
118038
118074
  const qs = params.toString();
118039
118075
  const [campaigns, adGroups, keywords, negativeKeywords, ads, extensions, targetedLocations] = await Promise.all([
118040
118076
  fetchList(
@@ -118353,13 +118389,31 @@ function buildCampaignCreateStatusMarkdown(result) {
118353
118389
  byGroup.set(key, list);
118354
118390
  }
118355
118391
  for (const [groupName, groupItems] of byGroup) {
118356
- const title = layer === "keyword" ? `## \u5E7F\u544A\u7EC4\u300C${groupName}\u300D\xB7 \u5173\u952E\u8BCD` : `## \u5E7F\u544A\u7EC4\u300C${groupName}\u300D\xB7 RSA \u6587\u6848`;
118357
- lines.push(title, ``);
118358
- lines.push(`| \u72B6\u6001 | \u8BA1\u5212\u5185\u5BB9 |`, `| --- | --- |`);
118392
+ if (layer === "keyword") {
118393
+ lines.push(`## \u5E7F\u544A\u7EC4\u300C${groupName}\u300D\xB7 \u5173\u952E\u8BCD`, ``);
118394
+ lines.push(`| \u72B6\u6001 | \u8BA1\u5212\u5185\u5BB9 |`, `| --- | --- |`);
118395
+ for (const item of groupItems) {
118396
+ lines.push(`| ${statusLabel(item.status)} | ${mdCell(item.plannedContent)} |`);
118397
+ }
118398
+ lines.push(``);
118399
+ continue;
118400
+ }
118401
+ lines.push(`## \u5E7F\u544A\u7EC4\u300C${groupName}\u300D\xB7 RSA \u6587\u6848`, ``);
118402
+ const byAd = /* @__PURE__ */ new Map();
118359
118403
  for (const item of groupItems) {
118360
- lines.push(`| ${statusLabel(item.status)} | ${mdCell(item.plannedContent)} |`);
118404
+ const key = item.adLabel ?? "\u2014";
118405
+ const list = byAd.get(key) ?? [];
118406
+ list.push(item);
118407
+ byAd.set(key, list);
118408
+ }
118409
+ for (const [adLabel, adItems] of byAd) {
118410
+ lines.push(`### \u5E7F\u544A\u300C${adLabel}\u300D`, ``);
118411
+ lines.push(`| \u72B6\u6001 | \u8BA1\u5212\u5185\u5BB9 |`, `| --- | --- |`);
118412
+ for (const item of adItems) {
118413
+ lines.push(`| ${statusLabel(item.status)} | ${mdCell(item.plannedContent)} |`);
118414
+ }
118415
+ lines.push(``);
118361
118416
  }
118362
- lines.push(``);
118363
118417
  }
118364
118418
  continue;
118365
118419
  }
@@ -136024,6 +136078,7 @@ var BING_SECTIONS = [
136024
136078
  }
136025
136079
  ];
136026
136080
  var BING_SECTION_NAMES = BING_SECTIONS.map((s) => s.name);
136081
+ var BING_DEFAULT_SECTION_NAMES = BING_SECTION_NAMES.filter((name2) => name2 !== "audience-merged");
136027
136082
  var BING_SECTION_ALIASES = {
136028
136083
  devices: "device",
136029
136084
  geo: "geographic",
@@ -136031,6 +136086,19 @@ var BING_SECTION_ALIASES = {
136031
136086
  };
136032
136087
 
136033
136088
  // src/commands/bing-analysis/resolve-sections.ts
136089
+ var AUDIENCE_SPLIT_NAMES = [
136090
+ "age-audience",
136091
+ "gender-audience"
136092
+ ];
136093
+ function dropMergedWhenSplitPresent(defs) {
136094
+ const names = new Set(defs.map((d) => d.name));
136095
+ const hasSplit = AUDIENCE_SPLIT_NAMES.some((n) => names.has(n));
136096
+ if (!hasSplit || !names.has("audience-merged")) return defs;
136097
+ console.error(
136098
+ "\u5DF2\u8DF3\u8FC7 audience-merged\uFF08\u4E0E age-audience/gender-audience \u5E76\u884C\u4F1A\u89E6\u53D1 Bing SDK \u53D7\u4F17 csv \u6587\u4EF6\u9501\uFF09\u3002"
136099
+ );
136100
+ return defs.filter((d) => d.name !== "audience-merged");
136101
+ }
136034
136102
  function normalizeSectionToken2(raw) {
136035
136103
  const t = raw.trim();
136036
136104
  if (!t) return null;
@@ -136056,8 +136124,9 @@ function resolveSectionList3(sections, exclude) {
136056
136124
  }
136057
136125
  const includeCanonical = new Set(include.map((n) => normalizeSectionToken2(n)).filter(Boolean));
136058
136126
  const dropCanonical = new Set(dropRaw.map((n) => normalizeSectionToken2(n)).filter(Boolean));
136059
- const base = include.length > 0 ? BING_SECTIONS.filter((s) => includeCanonical.has(s.name)) : [...BING_SECTIONS];
136060
- return base.filter((s) => !dropCanonical.has(s.name));
136127
+ const defaultSet = new Set(BING_DEFAULT_SECTION_NAMES);
136128
+ const base = include.length > 0 ? BING_SECTIONS.filter((s) => includeCanonical.has(s.name)) : BING_SECTIONS.filter((s) => defaultSet.has(s.name));
136129
+ return dropMergedWhenSplitPresent(base.filter((s) => !dropCanonical.has(s.name)));
136061
136130
  }
136062
136131
 
136063
136132
  // src/commands/bing-analysis/fetch.ts
@@ -136333,10 +136402,8 @@ async function fetchBingAudienceMergedPayload(config, id, startDate, endDate, ve
136333
136402
  const params = new URLSearchParams({ startDate, endDate });
136334
136403
  const ageUrl = reportingUrl(config, id, "AgeAudienceData", params.toString());
136335
136404
  const genderUrl = reportingUrl(config, id, "GenderAudienceData", params.toString());
136336
- const [ageData, genderData] = await Promise.all([
136337
- fetchBingJson(config, ageUrl, verbose),
136338
- fetchBingJson(config, genderUrl, verbose)
136339
- ]);
136405
+ const ageData = await fetchBingJson(config, ageUrl, verbose);
136406
+ const genderData = await fetchBingJson(config, genderUrl, verbose);
136340
136407
  return normalizeBingSectionPayload(
136341
136408
  {
136342
136409
  mergedParts: ["AgeAudienceData", "GenderAudienceData"],
@@ -136916,22 +136983,29 @@ function mapGeographicRows(payload) {
136916
136983
  (r) => pickMetrics(r, ["countryOrRegion", "countryCriteriaId", "regionName", "cityName"])
136917
136984
  );
136918
136985
  }
136919
- function mapAudienceRows2(payload) {
136986
+ function mapAudienceLabelRows(payload, labelKey) {
136920
136987
  const obj = asRecord23(payload);
136921
- const data = asRecord23(obj?.data);
136922
- const ageAudience = asRecord23(data?.ageAudience);
136923
- const genderAudience = asRecord23(data?.genderAudience);
136924
- const audienceAge = asRows2(ageAudience?.audience).map((r) => {
136988
+ return asRows2(obj?.audience).map((r) => {
136925
136989
  const m = pickMetrics(r, ["audience", "bidModifier"]);
136926
- m.ageRange = r.audience;
136990
+ m[labelKey] = r.audience;
136927
136991
  return m;
136928
136992
  });
136929
- const audienceGender = asRows2(genderAudience?.audience).map((r) => {
136930
- const m = pickMetrics(r, ["audience", "bidModifier"]);
136931
- m.gender = r.audience;
136932
- return m;
136933
- });
136934
- return { audienceAge, audienceGender };
136993
+ }
136994
+ function mapAudienceFromMerged(payload) {
136995
+ const data = asRecord23(asRecord23(payload)?.data);
136996
+ return {
136997
+ audienceAge: mapAudienceLabelRows(data?.ageAudience, "ageRange"),
136998
+ audienceGender: mapAudienceLabelRows(data?.genderAudience, "gender")
136999
+ };
137000
+ }
137001
+ function resolveAudienceTables(sectionMap) {
137002
+ const fromMerged = mapAudienceFromMerged(sectionMap.get("audience-merged"));
137003
+ const fromAge = mapAudienceLabelRows(sectionMap.get("age-audience"), "ageRange");
137004
+ const fromGender = mapAudienceLabelRows(sectionMap.get("gender-audience"), "gender");
137005
+ return {
137006
+ audienceAge: fromMerged.audienceAge.length > 0 ? fromMerged.audienceAge : fromAge,
137007
+ audienceGender: fromMerged.audienceGender.length > 0 ? fromMerged.audienceGender : fromGender
137008
+ };
136935
137009
  }
136936
137010
  function mapCampaignRows2(payload) {
136937
137011
  return asRows2(payload).map((r) => ({
@@ -136954,19 +137028,30 @@ function mapAdGroupRows(payload) {
136954
137028
  ])
136955
137029
  );
136956
137030
  }
137031
+ function displayAdTitle(row) {
137032
+ const raw = String(row.adTitle ?? "").trim();
137033
+ if (raw) return { adTitle: raw, adTitleFallback: false };
137034
+ const camp = String(row.campaignName ?? "").trim();
137035
+ const group = String(row.adGroupName ?? "").trim();
137036
+ const fallback = [camp, group].filter(Boolean).join(" / ");
137037
+ return { adTitle: fallback, adTitleFallback: fallback.length > 0 };
137038
+ }
136957
137039
  function mapAdRows(payload) {
136958
- return asRows2(payload).map(
136959
- (r) => pickMetrics(r, [
137040
+ return asRows2(payload).map((r) => {
137041
+ const mapped = pickMetrics(r, [
136960
137042
  "campaignId",
136961
137043
  "campaignName",
136962
137044
  "adGroupId",
136963
137045
  "adGroupName",
136964
137046
  "adId",
136965
- "adTitle",
136966
137047
  "adType",
136967
137048
  "adStatus"
136968
- ])
136969
- );
137049
+ ]);
137050
+ const title = displayAdTitle(r);
137051
+ mapped.adTitle = title.adTitle;
137052
+ mapped.adTitleFallback = title.adTitleFallback;
137053
+ return mapped;
137054
+ });
136970
137055
  }
136971
137056
  function mapKeywordRows(payload) {
136972
137057
  return asRows2(payload).map((r) => {
@@ -137082,7 +137167,7 @@ async function mergeBingAnalysisSnapshotIntoReport(payload, snapshotDir) {
137082
137167
  const rows = mapGeographicRows(sectionMap.get("geographic"));
137083
137168
  if (rows.length > 0) tables.geographic = rows;
137084
137169
  }
137085
- const audienceMapped = mapAudienceRows2(sectionMap.get("audience-merged"));
137170
+ const audienceMapped = resolveAudienceTables(sectionMap);
137086
137171
  if (audienceMapped.audienceAge.length > 0) tables.audienceAge = audienceMapped.audienceAge;
137087
137172
  if (audienceMapped.audienceGender.length > 0) {
137088
137173
  tables.audienceGender = audienceMapped.audienceGender;
@@ -137340,7 +137425,7 @@ function registerBatchCommand2(sectionHelp, aliasHelp) {
137340
137425
  "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4E0E --end \u540C\u4F20\u6216\u540C\u7701\u7565\uFF1B\u53EF\u542B\u6628\u5929/\u4ECA\u5929\uFF0C\u6570\u636E\u53EF\u80FD\u4E0D\u5B8C\u6574\uFF1B\u7701\u7565=\u622A\u81F3\u6628\u5929\u8FD1 7 \u5929\uFF09"
137341
137426
  ).option("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option(
137342
137427
  "--sections <list>",
137343
- `\u4EC5\u6267\u884C\u6307\u5B9A\u7EF4\u5EA6\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u5982 overview,campaigns,keywords\uFF1B\u7701\u7565=\u5168\u90E8 11 \u4E2A\u3002\u53EF\u9009\uFF1A${sectionHelp}\uFF1B\u522B\u540D\uFF1A${aliasHelp}`
137428
+ `\u4EC5\u6267\u884C\u6307\u5B9A\u7EF4\u5EA6\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u5982 overview,campaigns,keywords\uFF1B\u7701\u7565=\u9ED8\u8BA4 10 \u4E2A\uFF08\u4E0D\u542B audience-merged\uFF0C\u907F\u514D\u4E0E\u5E74\u9F84/\u6027\u522B\u7EF4\u62A2 Bing SDK \u53D7\u4F17 csv\uFF09\u3002\u53EF\u9009\uFF1A${sectionHelp}\uFF1B\u522B\u540D\uFF1A${aliasHelp}`
137344
137429
  ).option("--exclude <list>", "\u6392\u9664\u6307\u5B9A\u7EF4\u5EA6\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u4E0E --sections \u53EF\u53E0\u52A0").option(
137345
137430
  "--limit <n>",
137346
137431
  "\u4EC5 keywords / search-terms\uFF1A\u6761\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4 100\uFF0C\u4E0E\u524D\u7AEF KeywordReport/SearchQueryReport \u4E00\u81F4\uFF09",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.49-beta.2",
4
- "publishedAt": 1788233809334
3
+ "version": "1.1.49-beta.4",
4
+ "publishedAt": 1788313426269
5
5
  }
@@ -368,7 +368,7 @@ siluzan-tso tiktok-analysis official-report -a <mediaCustomerId> --json-out ./sn
368
368
 
369
369
  | 选项 | 说明 |
370
370
  | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
371
- | `--sections` / `--exclude` | `overview` `device` `geographic` `age-audience` `gender-audience` `audience-merged` `campaigns` `ad-groups` `ads` `keywords` `search-terms`(11 个);**别名**:`devices`→`device`,`geo`→`geographic`,`search-queries`→`search-terms` |
371
+ | `--sections` / `--exclude` | 可选 11 个:`overview` `device` `geographic` `age-audience` `gender-audience` `audience-merged` `campaigns` `ad-groups` `ads` `keywords` `search-terms`;**默认 10 个**(不含 `audience-merged`,勿与年龄/性别维同时拉);**别名**:`devices`→`device`,`geo`→`geographic`,`search-queries`→`search-terms` |
372
372
  | `--limit` | 仅 `keywords` / `search-terms`:条数上限,默认 100 |
373
373
  | `--start` / `--end` | 同传或同省略;可含昨天/今天(可能不完整);省略=截至昨天的近 7 天 |
374
374
  | `--concurrency` | 默认 5,上限 16 |
@@ -376,7 +376,7 @@ siluzan-tso tiktok-analysis official-report -a <mediaCustomerId> --json-out ./sn
376
376
  ```bash
377
377
  mkdir -p ./snap-bing
378
378
 
379
- # 11
379
+ # 默认 10 维(含年龄/性别受众,不含 audience-merged)
380
380
  siluzan-tso bing-analysis -a <mediaCustomerId> --json-out ./snap-bing
381
381
 
382
382
  # 仅拉总览 + 关键词(limit 50)
@@ -13,7 +13,7 @@
13
13
  | `overview` | 对象:`currentPeriod` / `previousPeriod` / `balance` / `averageDailyCost` … | → `meta` + `kpis`(环比、余额、日均) |
14
14
  | `device` | `{ devices: Row[] }` | → `tables.devices[]` |
15
15
  | `geographic` | `{ countries: Row[] }` | → `tables.geographic[]` |
16
- | `audience-merged`(或 `age-audience`/`gender-audience`) | `{ data: { ageAudience.audience[], genderAudience.audience[] } }` | → `tables.audienceAge[]` / `audienceGender[]` |
16
+ | `age-audience` / `gender-audience`(默认)或 `audience-merged` | 拆分维 `{ audience: [] }`;merged `{ data: { ageAudience, genderAudience } }` | → `tables.audienceAge[]` / `audienceGender[]`(merged 空则回退拆分维) |
17
17
  | `campaigns` | **根节点数组** | → `tables.campaigns[]`;**本期 KPI 优先由此累加** |
18
18
  | `ad-groups` | **根节点数组** | → `tables.adGroups[]`(含 `qualityScore`) |
19
19
  | `ads` | **根节点数组** | → `tables.ads[]` |
@@ -28,7 +28,7 @@
28
28
 
29
29
  | 步骤 | 执行者 | 动作 |
30
30
  | ---------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
31
- | **1. 拉数** | Agent 调 CLI | `bing-analysis -a <id> --start <s> --end <e> --json-out ./snap-bing`(见下方「日期规则」,全 11 维或 `--sections` 指定子集) |
31
+ | **1. 拉数** | Agent 调 CLI | `bing-analysis -a <id> --start <s> --end <e> --json-out ./snap-bing`(见下方「日期规则」,默认 10 维含年龄/性别、不含 `audience-merged`) |
32
32
  | **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`),把网关原始字段映射为下方 `tables.*` 契约,完成聚合与洞察 |
33
33
  | **3. 写 JSON** | Agent | 撰写 `bing-period-report.json`:仅 `meta.accountId` + `narrative`(9 个分析小节)为必填;`kpis`/`tables` 可省略由 `--snapshot-dir` 自动合并 |
34
34
  | **4. 渲染 HTML** | CLI | `bing-analysis render` — **校验 narrative 9 个分析小节必含字段**,缺项报错不生成 HTML;**禁止** Agent 手写/拼接 HTML |
@@ -55,7 +55,7 @@ siluzan-tso bing-analysis render \
55
55
  - `audienceGender[]`:`{gender, spend, impressions, clicks, ctr, averageCpc, conversions}`
56
56
  - `campaigns[]`:`{campaignName, campaignStatus, campaignStatusDisplay, spend, impressions, clicks, ctr, conversions, costPerConversion}`
57
57
  - `adGroups[]`:`{adGroupName, campaignName, qualityScore, spend, clicks, ctr, conversions, costPerConversion}`
58
- - `ads[]`:`{adTitle, adGroupName, adType, spend, clicks, ctr, conversions}`
58
+ - `ads[]`:`{adTitle, adGroupName, adType, spend, clicks, ctr, conversions}`(RSA 的报表 `adTitle` 常为空,`render` 用「系列 / 广告组」兜底展示并在表上方注明)
59
59
  - `keywords[]`:`{keyword, matchType, qualityScore, spend, ctr, averageCpc, conversions, costPerConversion}`
60
60
  - `searchTerms[]`:`{searchQuery, keyword, deliveredMatchType, spend, ctr, conversions, costPerConversion}`
61
61
  - `narrative`(**Agent 必填,唯一由 Agent 撰写的叙事内容**):
@@ -107,13 +107,13 @@ mkdir -p ./snap-bing
107
107
 
108
108
  siluzan-tso list-accounts -m BingV2 -k <mediaCustomerId> --json-out ./snap-bing
109
109
 
110
- # 一次批跑全部 11 维(等价于逐个 --sections 拉取,见下方速查表)
110
+ # 一次批跑默认 10 维(含 age-audience / gender-audience,不含 audience-merged)
111
111
  siluzan-tso bing-analysis -a <mediaCustomerId> --start <S> --end <E> --json-out ./snap-bing
112
112
  siluzan-tso balance -m BingV2 --accounts <mediaCustomerId> --json-out ./snap-bing
113
113
 
114
- # 或仅拉本次报告所需维度(--sections 逗号分隔)
114
+ # 或仅拉本次报告所需维度(--sections 逗号分隔;不要同时写拆分维和 audience-merged)
115
115
  siluzan-tso bing-analysis -a <mediaCustomerId> --start <S> --end <E> \
116
- --sections overview,campaigns,device,geographic,audience-merged,ad-groups,ads,keywords,search-terms \
116
+ --sections overview,campaigns,device,geographic,age-audience,gender-audience,ad-groups,ads,keywords,search-terms \
117
117
  --limit 100 --json-out ./snap-bing
118
118
  ```
119
119
 
@@ -206,9 +206,9 @@ Bing 网关常不返回 `averageDailyCost`、`activeDays`(或为 0)。CLI
206
206
 
207
207
  ## 4. 受众
208
208
 
209
- - **CLI**:推荐 `bing-analysis --sections audience-merged`(年龄+性别合并 JSON);或分别 `--sections age-audience` / `--sections gender-audience`
209
+ - **CLI**:默认已含 `--sections age-audience,gender-audience`。不要与 `audience-merged` 同时拉(Bing SDK 会抢同一份受众 csv,merged 400)。`render --snapshot-dir` 优先 merged,空则回退拆分维。
210
210
 
211
- **数据呈现**:年龄段、性别的展示、点击、消耗、CTR、CPC(按 `data.ageAudience.audience` / `data.genderAudience.audience`)。写入 JSON `tables.audienceAge[]` / `tables.audienceGender[]`。
211
+ **数据呈现**:年龄段、性别的展示、点击、消耗、CTR、CPC。拆分维读 `age-audience-*.json` / `gender-audience-*.json` 的 `audience[]`(标签字段 `audience`)。写入 JSON `tables.audienceAge[]`(`ageRange`)/ `tables.audienceGender[]`(`gender`)。
212
212
 
213
213
  **分析(必写)** → 写入 `narrative.sections.audience.{analysis,suggestions}`:
214
214
 
@@ -225,7 +225,7 @@ Bing 网关常不返回 `averageDailyCost`、`activeDays`(或为 0)。CLI
225
225
  | 广告组 | `bing-analysis --sections ad-groups` | `ad-groups-*.json` |
226
226
  | 广告 | `bing-analysis --sections ads` | `ads-*.json` |
227
227
 
228
- **数据呈现**:各表按消耗降序;系列含 `campaignStatus`;广告组可含质量分相关字段(以 outline 为准)。写入 JSON `tables.campaigns[]` / `tables.adGroups[]` / `tables.ads[]`。
228
+ **数据呈现**:各表按消耗降序;系列含 `campaignStatus`;广告组可含质量分相关字段(以 outline 为准)。写入 JSON `tables.campaigns[]` / `tables.adGroups[]` / `tables.ads[]`。RSA 无创意标题时 HTML 仍展示「广告标题」列,内容为「系列 / 广告组」。
229
229
 
230
230
  **分析(必写,三个子块各写一段,不可合并为一句带过)**:
231
231
 
@@ -472,6 +472,9 @@
472
472
  color: var(--muted);
473
473
  flex: none;
474
474
  }
475
+ .empty-note + .table-wrap {
476
+ margin-top: 10px;
477
+ }
475
478
 
476
479
  @keyframes fadeInUp {
477
480
  from {
@@ -1022,6 +1025,10 @@
1022
1025
  const currency = currencyOf(data);
1023
1026
  const section = data.narrative?.sections?.ads;
1024
1027
  const sorted = topBySpend(rows, 10);
1028
+ const usedFallback = sorted.some((r) => r.adTitleFallback);
1029
+ const titleNotice = usedFallback
1030
+ ? `<div class="empty-note">Bing 响应式搜索广告的报表不返回创意标题,本列用「系列 / 广告组」名称代替。</div>`
1031
+ : "";
1025
1032
  const body = sorted
1026
1033
  .map(
1027
1034
  (r) => `<tr>
@@ -1038,6 +1045,7 @@
1038
1045
  const table =
1039
1046
  sorted.length > 0
1040
1047
  ? `<div class="card-title-row"><h3>广告 Top 10</h3><span class="hint">共 ${rows.length} 条广告,按消耗排序</span></div>
1048
+ ${titleNotice}
1041
1049
  <div class="table-wrap"><table>
1042
1050
  <thead><tr><th>广告标题</th><th>广告组</th><th>广告类型</th><th class="num">消耗</th><th class="num">点击</th><th class="num">CTR</th><th class="num">转化</th></tr></thead>
1043
1051
  <tbody>${body}</tbody>
@@ -13,7 +13,7 @@
13
13
  | `overview` | 对象:`currentPeriod` / `previousPeriod` / `balance` / `averageDailyCost` … | → `meta` + `kpis`(环比、余额、日均) |
14
14
  | `device` | `{ devices: Row[] }` | → `tables.devices[]` |
15
15
  | `geographic` | `{ countries: Row[] }` | → `tables.geographic[]` |
16
- | `audience-merged`(或 `age-audience`/`gender-audience`) | `{ data: { ageAudience.audience[], genderAudience.audience[] } }` | → `tables.audienceAge[]` / `audienceGender[]` |
16
+ | `age-audience` / `gender-audience`(默认)或 `audience-merged` | 拆分维 `{ audience: [] }`;merged `{ data: { ageAudience, genderAudience } }` | → `tables.audienceAge[]` / `audienceGender[]`(merged 空则回退拆分维) |
17
17
  | `campaigns` | **根节点数组** | → `tables.campaigns[]`;**本期 KPI 优先由此累加** |
18
18
  | `ad-groups` | **根节点数组** | → `tables.adGroups[]`(含 `qualityScore`) |
19
19
  | `ads` | **根节点数组** | → `tables.ads[]` |
@@ -28,7 +28,7 @@
28
28
 
29
29
  | 步骤 | 执行者 | 动作 |
30
30
  | ---------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
31
- | **1. 拉数** | Agent 调 CLI | `bing-analysis -a <id> --start <s> --end <e> --json-out ./snap-bing`(见下方「日期规则」,全 11 维或 `--sections` 指定子集) |
31
+ | **1. 拉数** | Agent 调 CLI | `bing-analysis -a <id> --start <s> --end <e> --json-out ./snap-bing`(见下方「日期规则」,默认 10 维含年龄/性别、不含 `audience-merged`) |
32
32
  | **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`),把网关原始字段映射为下方 `tables.*` 契约,完成聚合与洞察 |
33
33
  | **3. 写 JSON** | Agent | 撰写 `bing-period-report.json`:仅 `meta.accountId` + `narrative`(9 个分析小节)为必填;`kpis`/`tables` 可省略由 `--snapshot-dir` 自动合并 |
34
34
  | **4. 渲染 HTML** | CLI | `bing-analysis render` — **校验 narrative 9 个分析小节必含字段**,缺项报错不生成 HTML;**禁止** Agent 手写/拼接 HTML |
@@ -55,7 +55,7 @@ siluzan-tso bing-analysis render \
55
55
  - `audienceGender[]`:`{gender, spend, impressions, clicks, ctr, averageCpc, conversions}`
56
56
  - `campaigns[]`:`{campaignName, campaignStatus, campaignStatusDisplay, spend, impressions, clicks, ctr, conversions, costPerConversion}`
57
57
  - `adGroups[]`:`{adGroupName, campaignName, qualityScore, spend, clicks, ctr, conversions, costPerConversion}`
58
- - `ads[]`:`{adTitle, adGroupName, adType, spend, clicks, ctr, conversions}`
58
+ - `ads[]`:`{adTitle, adGroupName, adType, spend, clicks, ctr, conversions}`(RSA 的报表 `adTitle` 常为空,`render` 用「系列 / 广告组」兜底展示并在表上方注明)
59
59
  - `keywords[]`:`{keyword, matchType, qualityScore, spend, ctr, averageCpc, conversions, costPerConversion}`
60
60
  - `searchTerms[]`:`{searchQuery, keyword, deliveredMatchType, spend, ctr, conversions, costPerConversion}`
61
61
  - `narrative`(**Agent 必填,唯一由 Agent 撰写的叙事内容**):
@@ -107,13 +107,13 @@ mkdir -p ./snap-bing
107
107
 
108
108
  siluzan-tso list-accounts -m BingV2 -k <mediaCustomerId> --json-out ./snap-bing
109
109
 
110
- # 一次批跑全部 11 维(等价于逐个 --sections 拉取,见下方速查表)
110
+ # 一次批跑默认 10 维(含 age-audience / gender-audience,不含 audience-merged)
111
111
  siluzan-tso bing-analysis -a <mediaCustomerId> --start <S> --end <E> --json-out ./snap-bing
112
112
  siluzan-tso balance -m BingV2 --accounts <mediaCustomerId> --json-out ./snap-bing
113
113
 
114
- # 或仅拉本次报告所需维度(--sections 逗号分隔)
114
+ # 或仅拉本次报告所需维度(--sections 逗号分隔;不要同时写拆分维和 audience-merged)
115
115
  siluzan-tso bing-analysis -a <mediaCustomerId> --start <S> --end <E> \
116
- --sections overview,campaigns,device,geographic,audience-merged,ad-groups,ads,keywords,search-terms \
116
+ --sections overview,campaigns,device,geographic,age-audience,gender-audience,ad-groups,ads,keywords,search-terms \
117
117
  --limit 100 --json-out ./snap-bing
118
118
  ```
119
119
 
@@ -206,9 +206,9 @@ Bing 网关常不返回 `averageDailyCost`、`activeDays`(或为 0)。CLI
206
206
 
207
207
  ## 4. 受众
208
208
 
209
- - **CLI**:推荐 `bing-analysis --sections audience-merged`(年龄+性别合并 JSON);或分别 `--sections age-audience` / `--sections gender-audience`
209
+ - **CLI**:默认已含 `--sections age-audience,gender-audience`。不要与 `audience-merged` 同时拉(Bing SDK 会抢同一份受众 csv,merged 400)。`render --snapshot-dir` 优先 merged,空则回退拆分维。
210
210
 
211
- **数据呈现**:年龄段、性别的展示、点击、消耗、CTR、CPC(按 `data.ageAudience.audience` / `data.genderAudience.audience`)。写入 JSON `tables.audienceAge[]` / `tables.audienceGender[]`。
211
+ **数据呈现**:年龄段、性别的展示、点击、消耗、CTR、CPC。拆分维读 `age-audience-*.json` / `gender-audience-*.json` 的 `audience[]`(标签字段 `audience`)。写入 JSON `tables.audienceAge[]`(`ageRange`)/ `tables.audienceGender[]`(`gender`)。
212
212
 
213
213
  **分析(必写)** → 写入 `narrative.sections.audience.{analysis,suggestions}`:
214
214
 
@@ -225,7 +225,7 @@ Bing 网关常不返回 `averageDailyCost`、`activeDays`(或为 0)。CLI
225
225
  | 广告组 | `bing-analysis --sections ad-groups` | `ad-groups-*.json` |
226
226
  | 广告 | `bing-analysis --sections ads` | `ads-*.json` |
227
227
 
228
- **数据呈现**:各表按消耗降序;系列含 `campaignStatus`;广告组可含质量分相关字段(以 outline 为准)。写入 JSON `tables.campaigns[]` / `tables.adGroups[]` / `tables.ads[]`。
228
+ **数据呈现**:各表按消耗降序;系列含 `campaignStatus`;广告组可含质量分相关字段(以 outline 为准)。写入 JSON `tables.campaigns[]` / `tables.adGroups[]` / `tables.ads[]`。RSA 无创意标题时 HTML 仍展示「广告标题」列,内容为「系列 / 广告组」。
229
229
 
230
230
  **分析(必写,三个子块各写一段,不可合并为一句带过)**:
231
231
 
@@ -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.49-beta.2'
12
+ $PKG_VERSION = '1.1.49-beta.4'
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.49-beta.2"
12
+ readonly PKG_VERSION="1.1.49-beta.4"
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.49-beta.2",
3
+ "version": "1.1.49-beta.4",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",