siluzan-tso-cli 1.1.49-beta.3 → 1.1.49-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +88 -23
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/references/report-templates/bing-period-report.md +2 -2
- package/dist/skill/report-templates/bing-period-report.html +17 -16
- package/dist/skill/report-templates/bing-period-report.md +2 -2
- package/dist/skill/report-templates/meta-period-report.html +8 -8
- 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.5),供内部测试使用。正式发布后安装命令将改为 `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
|
}
|
|
@@ -136974,19 +137028,30 @@ function mapAdGroupRows(payload) {
|
|
|
136974
137028
|
])
|
|
136975
137029
|
);
|
|
136976
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
|
+
}
|
|
136977
137039
|
function mapAdRows(payload) {
|
|
136978
|
-
return asRows2(payload).map(
|
|
136979
|
-
|
|
137040
|
+
return asRows2(payload).map((r) => {
|
|
137041
|
+
const mapped = pickMetrics(r, [
|
|
136980
137042
|
"campaignId",
|
|
136981
137043
|
"campaignName",
|
|
136982
137044
|
"adGroupId",
|
|
136983
137045
|
"adGroupName",
|
|
136984
137046
|
"adId",
|
|
136985
|
-
"adTitle",
|
|
136986
137047
|
"adType",
|
|
136987
137048
|
"adStatus"
|
|
136988
|
-
])
|
|
136989
|
-
|
|
137049
|
+
]);
|
|
137050
|
+
const title = displayAdTitle(r);
|
|
137051
|
+
mapped.adTitle = title.adTitle;
|
|
137052
|
+
mapped.adTitleFallback = title.adTitleFallback;
|
|
137053
|
+
return mapped;
|
|
137054
|
+
});
|
|
136990
137055
|
}
|
|
136991
137056
|
function mapKeywordRows(payload) {
|
|
136992
137057
|
return asRows2(payload).map((r) => {
|
package/dist/skill/_meta.json
CHANGED
|
@@ -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}`(RSA
|
|
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 撰写的叙事内容**):
|
|
@@ -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[]`。RSA
|
|
228
|
+
**数据呈现**:各表按消耗降序;系列含 `campaignStatus`;广告组可含质量分相关字段(以 outline 为准)。写入 JSON `tables.campaigns[]` / `tables.adGroups[]` / `tables.ads[]`。RSA 无创意标题时 HTML 仍展示「广告标题」列,内容为「系列 / 广告组」。
|
|
229
229
|
|
|
230
230
|
**分析(必写,三个子块各写一段,不可合并为一句带过)**:
|
|
231
231
|
|
|
@@ -306,11 +306,11 @@
|
|
|
306
306
|
}
|
|
307
307
|
.chart-box {
|
|
308
308
|
position: relative;
|
|
309
|
-
height:
|
|
310
|
-
margin:
|
|
309
|
+
height: 400px;
|
|
310
|
+
margin: 16px 0 8px;
|
|
311
311
|
}
|
|
312
312
|
.chart-box.sm {
|
|
313
|
-
height:
|
|
313
|
+
height: 320px;
|
|
314
314
|
}
|
|
315
315
|
.chart-grid-2 {
|
|
316
316
|
display: grid;
|
|
@@ -538,7 +538,7 @@
|
|
|
538
538
|
border-radius: 12px;
|
|
539
539
|
}
|
|
540
540
|
.chart-box {
|
|
541
|
-
height: clamp(
|
|
541
|
+
height: clamp(280px, 62vw, 360px);
|
|
542
542
|
}
|
|
543
543
|
.chart-grid-2 {
|
|
544
544
|
grid-template-columns: 1fr;
|
|
@@ -836,8 +836,8 @@
|
|
|
836
836
|
const table =
|
|
837
837
|
sorted.length > 0
|
|
838
838
|
? `<div class="chart-grid-2">
|
|
839
|
-
<div id="chart-devices-pie" class="chart-box" style="height:
|
|
840
|
-
<div id="chart-devices-bar" class="chart-box" style="height:
|
|
839
|
+
<div id="chart-devices-pie" class="chart-box" style="height:380px"></div>
|
|
840
|
+
<div id="chart-devices-bar" class="chart-box" style="height:380px"></div>
|
|
841
841
|
</div>
|
|
842
842
|
<div class="table-wrap"><table>
|
|
843
843
|
<thead><tr><th>设备</th><th class="num">消耗</th><th class="num">展示</th><th class="num">点击</th><th class="num">CTR</th><th class="num">平均CPC</th><th class="num">转化</th><th class="num">CPA</th></tr></thead>
|
|
@@ -875,7 +875,7 @@
|
|
|
875
875
|
.join("");
|
|
876
876
|
const table =
|
|
877
877
|
sorted.length > 0
|
|
878
|
-
? `<div id="chart-geographic" class="chart-box" style="height:
|
|
878
|
+
? `<div id="chart-geographic" class="chart-box" style="height:420px"></div>
|
|
879
879
|
<div class="card-title-row"><h3>地域 Top 10</h3><span class="hint">共 ${rows.length} 个国家/地区,按消耗排序</span></div>
|
|
880
880
|
<div class="table-wrap"><table>
|
|
881
881
|
<thead><tr><th>国家/地区</th><th class="num">消耗</th><th class="num">展示</th><th class="num">点击</th><th class="num">CTR</th><th class="num">平均CPC</th><th class="num">转化</th><th class="num">CPA</th></tr></thead>
|
|
@@ -966,7 +966,7 @@
|
|
|
966
966
|
.join("");
|
|
967
967
|
const table =
|
|
968
968
|
sorted.length > 0
|
|
969
|
-
? `<div id="chart-campaigns" class="chart-box" style="height:
|
|
969
|
+
? `<div id="chart-campaigns" class="chart-box" style="height:380px"></div>
|
|
970
970
|
<div class="card-title-row"><h3>系列 Top 10</h3><span class="hint">共 ${rows.length} 个系列,按消耗排序</span></div>
|
|
971
971
|
<div class="table-wrap"><table>
|
|
972
972
|
<thead><tr><th>广告系列</th><th>状态</th><th class="num">消耗</th><th class="num">展示</th><th class="num">点击</th><th class="num">CTR</th><th class="num">转化</th><th class="num">CPA</th></tr></thead>
|
|
@@ -1025,14 +1025,14 @@
|
|
|
1025
1025
|
const currency = currencyOf(data);
|
|
1026
1026
|
const section = data.narrative?.sections?.ads;
|
|
1027
1027
|
const sorted = topBySpend(rows, 10);
|
|
1028
|
-
const
|
|
1029
|
-
const titleNotice =
|
|
1030
|
-
? `<div class="empty-note">Bing
|
|
1028
|
+
const usedFallback = sorted.some((r) => r.adTitleFallback);
|
|
1029
|
+
const titleNotice = usedFallback
|
|
1030
|
+
? `<div class="empty-note">Bing 响应式搜索广告的报表不返回创意标题,本列用「系列 / 广告组」名称代替。</div>`
|
|
1031
1031
|
: "";
|
|
1032
1032
|
const body = sorted
|
|
1033
1033
|
.map(
|
|
1034
1034
|
(r) => `<tr>
|
|
1035
|
-
|
|
1035
|
+
<td>${escapeHtml(r.adTitle || "—")}</td>
|
|
1036
1036
|
<td>${escapeHtml(r.adGroupName || "—")}</td>
|
|
1037
1037
|
<td>${escapeHtml(r.adType || "—")}</td>
|
|
1038
1038
|
<td class="num">${money(r.spend, currency)}</td>
|
|
@@ -1047,7 +1047,7 @@
|
|
|
1047
1047
|
? `<div class="card-title-row"><h3>广告 Top 10</h3><span class="hint">共 ${rows.length} 条广告,按消耗排序</span></div>
|
|
1048
1048
|
${titleNotice}
|
|
1049
1049
|
<div class="table-wrap"><table>
|
|
1050
|
-
<thead><tr
|
|
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>
|
|
1051
1051
|
<tbody>${body}</tbody>
|
|
1052
1052
|
</table></div>`
|
|
1053
1053
|
: emptyNoteHtml();
|
|
@@ -1180,11 +1180,12 @@ ${titleNotice}
|
|
|
1180
1180
|
{ trigger: "item", formatter: (p) => `${p.name}: ${money(p.value, currency)}` },
|
|
1181
1181
|
TOOLTIP_STYLE,
|
|
1182
1182
|
),
|
|
1183
|
-
legend: { bottom:
|
|
1183
|
+
legend: { bottom: 4, left: "center", textStyle: { color: AXIS_COLOR } },
|
|
1184
1184
|
series: [
|
|
1185
1185
|
{
|
|
1186
1186
|
type: "pie",
|
|
1187
|
-
radius: ["
|
|
1187
|
+
radius: ["36%", "54%"],
|
|
1188
|
+
center: ["50%", "44%"],
|
|
1188
1189
|
itemStyle: { borderColor: "#ffffff", borderWidth: 3, borderRadius: 8 },
|
|
1189
1190
|
data: items.map((d, i) => ({
|
|
1190
1191
|
name: d[nameKey] || "—",
|
|
@@ -1208,7 +1209,7 @@ ${titleNotice}
|
|
|
1208
1209
|
{ trigger: "axis", formatter: (p) => money(p[0].value, currency) },
|
|
1209
1210
|
TOOLTIP_STYLE,
|
|
1210
1211
|
),
|
|
1211
|
-
grid: { left: 140, right:
|
|
1212
|
+
grid: { left: 140, right: 36, top: 28, bottom: 36 },
|
|
1212
1213
|
xAxis: styleAxis({ type: "value" }),
|
|
1213
1214
|
yAxis: styleAxis({
|
|
1214
1215
|
type: "category",
|
|
@@ -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}`(RSA
|
|
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 撰写的叙事内容**):
|
|
@@ -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[]`。RSA
|
|
228
|
+
**数据呈现**:各表按消耗降序;系列含 `campaignStatus`;广告组可含质量分相关字段(以 outline 为准)。写入 JSON `tables.campaigns[]` / `tables.adGroups[]` / `tables.ads[]`。RSA 无创意标题时 HTML 仍展示「广告标题」列,内容为「系列 / 广告组」。
|
|
229
229
|
|
|
230
230
|
**分析(必写,三个子块各写一段,不可合并为一句带过)**:
|
|
231
231
|
|
|
@@ -347,12 +347,12 @@ window.__META_PERIOD_REPORT__ = window.__META_PERIOD_REPORT__ || null;
|
|
|
347
347
|
var daily = charts.daily || {};
|
|
348
348
|
bindChart(document.getElementById("chartDaily"), {
|
|
349
349
|
tooltip: { trigger: "axis" },
|
|
350
|
-
legend: { data: ["线索", "花费 (" + currency + ")"], textStyle: { color: "#334155" } },
|
|
351
|
-
grid: { left:
|
|
350
|
+
legend: { top: 4, left: "center", data: ["线索", "花费 (" + currency + ")"], textStyle: { color: "#334155" } },
|
|
351
|
+
grid: { left: 56, right: 56, top: 48, bottom: 76 },
|
|
352
352
|
xAxis: { type: "category", data: daily.labels || [], axisLabel: { color: "#64748b", rotate: 45, fontSize: 10 } },
|
|
353
353
|
yAxis: [
|
|
354
|
-
Object.assign({ type: "value", name: "线索", nameTextStyle: { color: "#64748b" } }, axis),
|
|
355
|
-
Object.assign({ type: "value", name: "花费", nameTextStyle: { color: "#64748b" }, splitLine: { show: false } }, axis),
|
|
354
|
+
Object.assign({ type: "value", name: "线索", nameLocation: "middle", nameGap: 40, nameTextStyle: { color: "#64748b" } }, axis),
|
|
355
|
+
Object.assign({ type: "value", name: "花费", nameLocation: "middle", nameGap: 40, nameTextStyle: { color: "#64748b" }, splitLine: { show: false } }, axis),
|
|
356
356
|
],
|
|
357
357
|
series: [
|
|
358
358
|
{
|
|
@@ -368,8 +368,8 @@ window.__META_PERIOD_REPORT__ = window.__META_PERIOD_REPORT__ || null;
|
|
|
368
368
|
var country = charts.country || {};
|
|
369
369
|
bindChart(document.getElementById("chartCountry"), {
|
|
370
370
|
tooltip: { trigger: "axis" },
|
|
371
|
-
legend: { data: ["CPL (" + currency + ")", "花费 (" + currency + ")"], textStyle: { color: "#334155" } },
|
|
372
|
-
grid: { left: 88, right: 48, top:
|
|
371
|
+
legend: { top: 4, left: "center", data: ["CPL (" + currency + ")", "花费 (" + currency + ")"], textStyle: { color: "#334155" } },
|
|
372
|
+
grid: { left: 88, right: 48, top: 56, bottom: 40 },
|
|
373
373
|
yAxis: { type: "category", data: country.labels || [], axisLabel: { color: "#334155" }, inverse: true },
|
|
374
374
|
xAxis: [Object.assign({ type: "value" }, axis), Object.assign({ type: "value", splitLine: { show: false } }, axis)],
|
|
375
375
|
series: [
|
|
@@ -386,8 +386,8 @@ window.__META_PERIOD_REPORT__ = window.__META_PERIOD_REPORT__ || null;
|
|
|
386
386
|
var aud = charts.audience || {};
|
|
387
387
|
bindChart(document.getElementById("chartAudience"), {
|
|
388
388
|
tooltip: { trigger: "axis" },
|
|
389
|
-
legend: { data: ["CPL", "线索"], textStyle: { color: "#334155" } },
|
|
390
|
-
grid: { left:
|
|
389
|
+
legend: { top: 4, left: "center", data: ["CPL", "线索"], textStyle: { color: "#334155" } },
|
|
390
|
+
grid: { left: 56, right: 56, top: 56, bottom: 80 },
|
|
391
391
|
xAxis: { type: "category", data: aud.labels || [], axisLabel: { color: "#64748b", rotate: 40, fontSize: 10 } },
|
|
392
392
|
yAxis: [Object.assign({ type: "value" }, axis), Object.assign({ type: "value", splitLine: { show: false } }, axis)],
|
|
393
393
|
series: [
|
|
@@ -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.5'
|
|
13
13
|
$CLI_BIN = 'siluzan-tso'
|
|
14
14
|
$SKILL_LABEL = 'Siluzan TSO'
|
|
15
15
|
$INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
|
|
@@ -9,7 +9,7 @@ set -euo pipefail
|
|
|
9
9
|
# -- Package info (injected at build time) ------------------------------------
|
|
10
10
|
readonly PKG_NAME="siluzan-tso-cli"
|
|
11
11
|
# PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
|
|
12
|
-
readonly PKG_VERSION="1.1.49-beta.
|
|
12
|
+
readonly PKG_VERSION="1.1.49-beta.5"
|
|
13
13
|
readonly CLI_BIN="siluzan-tso"
|
|
14
14
|
readonly SKILL_LABEL="Siluzan TSO"
|
|
15
15
|
readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
|