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 +1 -1
- package/dist/index.js +128 -43
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/references/analytics/account-analytics.md +2 -2
- package/dist/skill/references/report-templates/bing-period-report.md +9 -9
- package/dist/skill/report-templates/bing-period-report.html +8 -0
- package/dist/skill/report-templates/bing-period-report.md +9 -9
- package/dist/skill/scripts/install.ps1 +1 -1
- package/dist/skill/scripts/install.sh +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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" ?
|
|
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 ? `\
|
|
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 =
|
|
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 ? `\
|
|
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\
|
|
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" ?
|
|
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 ? `\
|
|
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
|
-
|
|
118357
|
-
|
|
118358
|
-
|
|
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
|
-
|
|
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
|
|
136060
|
-
|
|
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
|
|
136337
|
-
|
|
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
|
|
136986
|
+
function mapAudienceLabelRows(payload, labelKey) {
|
|
136920
136987
|
const obj = asRecord23(payload);
|
|
136921
|
-
|
|
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
|
|
136990
|
+
m[labelKey] = r.audience;
|
|
136927
136991
|
return m;
|
|
136928
136992
|
});
|
|
136929
|
-
|
|
136930
|
-
|
|
136931
|
-
|
|
136932
|
-
|
|
136933
|
-
|
|
136934
|
-
|
|
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
|
-
|
|
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 =
|
|
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=\
|
|
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",
|
package/dist/skill/_meta.json
CHANGED
|
@@ -368,7 +368,7 @@ siluzan-tso tiktok-analysis official-report -a <mediaCustomerId> --json-out ./sn
|
|
|
368
368
|
|
|
369
369
|
| 选项 | 说明 |
|
|
370
370
|
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
371
|
-
| `--sections` / `--exclude` |
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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-
|
|
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
|
|
209
|
+
- **CLI**:默认已含 `--sections age-audience,gender-audience`。不要与 `audience-merged` 同时拉(Bing SDK 会抢同一份受众 csv,merged 常 400)。`render --snapshot-dir` 优先 merged,空则回退拆分维。
|
|
210
210
|
|
|
211
|
-
**数据呈现**:年龄段、性别的展示、点击、消耗、CTR、CPC
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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-
|
|
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
|
|
209
|
+
- **CLI**:默认已含 `--sections age-audience,gender-audience`。不要与 `audience-merged` 同时拉(Bing SDK 会抢同一份受众 csv,merged 常 400)。`render --snapshot-dir` 优先 merged,空则回退拆分维。
|
|
210
210
|
|
|
211
|
-
**数据呈现**:年龄段、性别的展示、点击、消耗、CTR、CPC
|
|
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.
|
|
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.
|
|
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"
|