siluzan-tso-cli 1.1.45-beta.2 → 1.1.45-beta.3
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 +61 -53
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/references/accounts/accounts-balance-stats.md +2 -2
- package/dist/skill/references/analytics/account-analytics.md +1 -1
- package/dist/skill/references/operations/hosted-automation-monitoring-json.md +1 -1
- package/dist/skill/report-templates/yandex-period-report.html +128 -88
- 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.45-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.45-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -4503,17 +4503,57 @@ var init_currency_display = __esm({
|
|
|
4503
4503
|
}
|
|
4504
4504
|
});
|
|
4505
4505
|
|
|
4506
|
+
// src/commands/google-analysis/daily-metrics-range.ts
|
|
4507
|
+
function pad2(n) {
|
|
4508
|
+
return String(n).padStart(2, "0");
|
|
4509
|
+
}
|
|
4510
|
+
function formatChinaOffsetDateTime(d) {
|
|
4511
|
+
const sh = new Date(d.getTime() + CHINA_OFFSET_MS);
|
|
4512
|
+
return `${sh.getUTCFullYear()}-${pad2(sh.getUTCMonth() + 1)}-${pad2(sh.getUTCDate())}T${pad2(sh.getUTCHours())}:${pad2(sh.getUTCMinutes())}:${pad2(sh.getUTCSeconds())}+08:00`;
|
|
4513
|
+
}
|
|
4514
|
+
function defaultChinaDateRange(now = /* @__PURE__ */ new Date()) {
|
|
4515
|
+
const chinaNow = new Date(now.getTime() + CHINA_OFFSET_MS);
|
|
4516
|
+
chinaNow.setUTCDate(chinaNow.getUTCDate() - 1);
|
|
4517
|
+
const endY = chinaNow.getUTCFullYear();
|
|
4518
|
+
const endM = chinaNow.getUTCMonth();
|
|
4519
|
+
const endD = chinaNow.getUTCDate();
|
|
4520
|
+
const startChina = new Date(Date.UTC(endY, endM, endD - 6));
|
|
4521
|
+
const endChina = new Date(Date.UTC(endY, endM, endD));
|
|
4522
|
+
const fmt = (utcCal) => `${utcCal.getUTCFullYear()}-${pad2(utcCal.getUTCMonth() + 1)}-${pad2(utcCal.getUTCDate())}`;
|
|
4523
|
+
return { startDate: fmt(startChina), endDate: fmt(endChina) };
|
|
4524
|
+
}
|
|
4525
|
+
function resolveDailyMetricsQueryTimes(startDate, endDate, now = /* @__PURE__ */ new Date()) {
|
|
4526
|
+
const startInstant = `${startDate}T00:00:00+08:00`;
|
|
4527
|
+
const endOfDay = /* @__PURE__ */ new Date(`${endDate}T23:59:59+08:00`);
|
|
4528
|
+
const endInstant = endOfDay.getTime() > now.getTime() ? formatChinaOffsetDateTime(now) : formatChinaOffsetDateTime(endOfDay);
|
|
4529
|
+
return { startInstant, endInstant };
|
|
4530
|
+
}
|
|
4531
|
+
function resolveChinaQueryTimes(startDate, endDate, _now = /* @__PURE__ */ new Date()) {
|
|
4532
|
+
return { startInstant: startDate, endInstant: endDate };
|
|
4533
|
+
}
|
|
4534
|
+
var CHINA_OFFSET_MS;
|
|
4535
|
+
var init_daily_metrics_range = __esm({
|
|
4536
|
+
"src/commands/google-analysis/daily-metrics-range.ts"() {
|
|
4537
|
+
"use strict";
|
|
4538
|
+
CHINA_OFFSET_MS = 8 * 36e5;
|
|
4539
|
+
}
|
|
4540
|
+
});
|
|
4541
|
+
|
|
4506
4542
|
// src/utils/balance.ts
|
|
4507
|
-
function
|
|
4508
|
-
return
|
|
4543
|
+
function pad22(n) {
|
|
4544
|
+
return String(n).padStart(2, "0");
|
|
4509
4545
|
}
|
|
4510
|
-
function dateRangeLastNDays(n) {
|
|
4546
|
+
function dateRangeLastNDays(n, now = /* @__PURE__ */ new Date()) {
|
|
4511
4547
|
const days = Math.max(1, Math.floor(Number(n) || 1));
|
|
4512
|
-
const
|
|
4513
|
-
|
|
4514
|
-
const
|
|
4515
|
-
|
|
4516
|
-
|
|
4548
|
+
const chinaNow = new Date(now.getTime() + CHINA_OFFSET_MS2);
|
|
4549
|
+
chinaNow.setUTCDate(chinaNow.getUTCDate() - 1);
|
|
4550
|
+
const endY = chinaNow.getUTCFullYear();
|
|
4551
|
+
const endM = chinaNow.getUTCMonth();
|
|
4552
|
+
const endD = chinaNow.getUTCDate();
|
|
4553
|
+
const startChina = new Date(Date.UTC(endY, endM, endD - (days - 1)));
|
|
4554
|
+
const endChina = new Date(Date.UTC(endY, endM, endD));
|
|
4555
|
+
const fmtChina = (utcCal) => `${utcCal.getUTCFullYear()}-${pad22(utcCal.getUTCMonth() + 1)}-${pad22(utcCal.getUTCDate())}`;
|
|
4556
|
+
return { startDate: fmtChina(startChina), endDate: fmtChina(endChina) };
|
|
4517
4557
|
}
|
|
4518
4558
|
function defaultDateRange() {
|
|
4519
4559
|
return dateRangeLastNDays(7);
|
|
@@ -4709,9 +4749,10 @@ async function fetchOverviewMap(route, accountIds, config, startDate, endDate, v
|
|
|
4709
4749
|
const end = endDate ?? range.endDate;
|
|
4710
4750
|
try {
|
|
4711
4751
|
if (route === "Google") {
|
|
4752
|
+
const { startInstant, endInstant } = resolveDailyMetricsQueryTimes(start, end);
|
|
4712
4753
|
const params = new URLSearchParams({
|
|
4713
|
-
startDate:
|
|
4714
|
-
endDate:
|
|
4754
|
+
startDate: startInstant,
|
|
4755
|
+
endDate: endInstant,
|
|
4715
4756
|
mediaCustomerIds: accountIds.join("|")
|
|
4716
4757
|
});
|
|
4717
4758
|
const url = `${config.apiBaseUrl}/report/media-account/google/account-spend-overview?${params}`;
|
|
@@ -4797,10 +4838,11 @@ async function fetchOverviewMap(route, accountIds, config, startDate, endDate, v
|
|
|
4797
4838
|
}
|
|
4798
4839
|
return result;
|
|
4799
4840
|
}
|
|
4800
|
-
var BALANCE_SUPPORTED_MEDIA;
|
|
4841
|
+
var BALANCE_SUPPORTED_MEDIA, CHINA_OFFSET_MS2;
|
|
4801
4842
|
var init_balance = __esm({
|
|
4802
4843
|
"src/utils/balance.ts"() {
|
|
4803
4844
|
"use strict";
|
|
4845
|
+
init_daily_metrics_range();
|
|
4804
4846
|
init_auth();
|
|
4805
4847
|
BALANCE_SUPPORTED_MEDIA = [
|
|
4806
4848
|
"Google",
|
|
@@ -4810,6 +4852,7 @@ var init_balance = __esm({
|
|
|
4810
4852
|
"Kwai",
|
|
4811
4853
|
"MetaAd"
|
|
4812
4854
|
];
|
|
4855
|
+
CHINA_OFFSET_MS2 = 8 * 36e5;
|
|
4813
4856
|
}
|
|
4814
4857
|
});
|
|
4815
4858
|
|
|
@@ -5525,42 +5568,6 @@ var init_accounts_digest2 = __esm({
|
|
|
5525
5568
|
}
|
|
5526
5569
|
});
|
|
5527
5570
|
|
|
5528
|
-
// src/commands/google-analysis/daily-metrics-range.ts
|
|
5529
|
-
function pad2(n) {
|
|
5530
|
-
return String(n).padStart(2, "0");
|
|
5531
|
-
}
|
|
5532
|
-
function formatChinaOffsetDateTime(d) {
|
|
5533
|
-
const sh = new Date(d.getTime() + CHINA_OFFSET_MS);
|
|
5534
|
-
return `${sh.getUTCFullYear()}-${pad2(sh.getUTCMonth() + 1)}-${pad2(sh.getUTCDate())}T${pad2(sh.getUTCHours())}:${pad2(sh.getUTCMinutes())}:${pad2(sh.getUTCSeconds())}+08:00`;
|
|
5535
|
-
}
|
|
5536
|
-
function defaultChinaDateRange(now = /* @__PURE__ */ new Date()) {
|
|
5537
|
-
const chinaNow = new Date(now.getTime() + CHINA_OFFSET_MS);
|
|
5538
|
-
chinaNow.setUTCDate(chinaNow.getUTCDate() - 1);
|
|
5539
|
-
const endY = chinaNow.getUTCFullYear();
|
|
5540
|
-
const endM = chinaNow.getUTCMonth();
|
|
5541
|
-
const endD = chinaNow.getUTCDate();
|
|
5542
|
-
const startChina = new Date(Date.UTC(endY, endM, endD - 6));
|
|
5543
|
-
const endChina = new Date(Date.UTC(endY, endM, endD));
|
|
5544
|
-
const fmt2 = (utcCal) => `${utcCal.getUTCFullYear()}-${pad2(utcCal.getUTCMonth() + 1)}-${pad2(utcCal.getUTCDate())}`;
|
|
5545
|
-
return { startDate: fmt2(startChina), endDate: fmt2(endChina) };
|
|
5546
|
-
}
|
|
5547
|
-
function resolveDailyMetricsQueryTimes(startDate, endDate, now = /* @__PURE__ */ new Date()) {
|
|
5548
|
-
const startInstant = `${startDate}T00:00:00+08:00`;
|
|
5549
|
-
const endOfDay = /* @__PURE__ */ new Date(`${endDate}T23:59:59+08:00`);
|
|
5550
|
-
const endInstant = endOfDay.getTime() > now.getTime() ? formatChinaOffsetDateTime(now) : formatChinaOffsetDateTime(endOfDay);
|
|
5551
|
-
return { startInstant, endInstant };
|
|
5552
|
-
}
|
|
5553
|
-
function resolveChinaQueryTimes(startDate, endDate, _now = /* @__PURE__ */ new Date()) {
|
|
5554
|
-
return { startInstant: startDate, endInstant: endDate };
|
|
5555
|
-
}
|
|
5556
|
-
var CHINA_OFFSET_MS;
|
|
5557
|
-
var init_daily_metrics_range = __esm({
|
|
5558
|
-
"src/commands/google-analysis/daily-metrics-range.ts"() {
|
|
5559
|
-
"use strict";
|
|
5560
|
-
CHINA_OFFSET_MS = 8 * 36e5;
|
|
5561
|
-
}
|
|
5562
|
-
});
|
|
5563
|
-
|
|
5564
5571
|
// src/utils/snapshot/async-lock.ts
|
|
5565
5572
|
async function withAsyncLock(key, fn) {
|
|
5566
5573
|
const prev = chains.get(key) ?? Promise.resolve();
|
|
@@ -108630,8 +108637,8 @@ function resolveFacebookDateRange(start, end) {
|
|
|
108630
108637
|
endD.setDate(endD.getDate() - 1);
|
|
108631
108638
|
const startD = new Date(endD);
|
|
108632
108639
|
startD.setDate(startD.getDate() - 6);
|
|
108633
|
-
const
|
|
108634
|
-
return { startDate:
|
|
108640
|
+
const fmt = (d) => d.toISOString().slice(0, 10);
|
|
108641
|
+
return { startDate: fmt(startD), endDate: fmt(endD) };
|
|
108635
108642
|
}
|
|
108636
108643
|
console.error("\n\u274C --start \u4E0E --end \u987B\u540C\u65F6\u4F20\u5165\uFF0C\u6216\u540C\u65F6\u7701\u7565\u4EE5\u4F7F\u7528\u9ED8\u8BA4\u8FD1 7 \u5929\uFF08\u622A\u81F3\u6628\u5929\uFF09\u3002\n");
|
|
108637
108644
|
process.exit(1);
|
|
@@ -108729,8 +108736,8 @@ function resolveMetaOverviewDateRange(start, end) {
|
|
|
108729
108736
|
endD.setDate(endD.getDate() - 1);
|
|
108730
108737
|
const startD = new Date(endD);
|
|
108731
108738
|
startD.setDate(startD.getDate() - 6);
|
|
108732
|
-
const
|
|
108733
|
-
return { startDate:
|
|
108739
|
+
const fmt = (d) => d.toISOString().slice(0, 10);
|
|
108740
|
+
return { startDate: fmt(startD), endDate: fmt(endD) };
|
|
108734
108741
|
}
|
|
108735
108742
|
console.error("\n\u274C --start \u4E0E --end \u987B\u540C\u65F6\u4F20\u5165\uFF0C\u6216\u540C\u65F6\u7701\u7565\u4EE5\u4F7F\u7528\u9ED8\u8BA4\u8FD1 7 \u5929\uFF08\u622A\u81F3\u6628\u5929\uFF09\u3002\n");
|
|
108736
108743
|
process.exit(1);
|
|
@@ -133252,8 +133259,8 @@ function resolveDateRange2(start, end) {
|
|
|
133252
133259
|
endD.setDate(endD.getDate() - 1);
|
|
133253
133260
|
const startD = new Date(endD);
|
|
133254
133261
|
startD.setDate(startD.getDate() - 6);
|
|
133255
|
-
const
|
|
133256
|
-
return { startDate:
|
|
133262
|
+
const fmt = (d) => d.toISOString().slice(0, 10);
|
|
133263
|
+
return { startDate: fmt(startD), endDate: fmt(endD) };
|
|
133257
133264
|
}
|
|
133258
133265
|
console.error("\n\u274C --start \u4E0E --end \u987B\u540C\u65F6\u4F20\u5165\uFF0C\u6216\u540C\u65F6\u7701\u7565\u4EE5\u4F7F\u7528\u9ED8\u8BA4\u8FD1 7 \u5929\uFF08\u622A\u81F3\u6628\u5929\uFF09\u3002\n");
|
|
133259
133266
|
process.exit(1);
|
|
@@ -134320,6 +134327,7 @@ function asRecord26(value) {
|
|
|
134320
134327
|
return null;
|
|
134321
134328
|
}
|
|
134322
134329
|
function num10(value) {
|
|
134330
|
+
if (value === null || value === void 0 || value === "") return void 0;
|
|
134323
134331
|
const n = Number(value);
|
|
134324
134332
|
return Number.isFinite(n) ? n : void 0;
|
|
134325
134333
|
}
|
package/dist/skill/_meta.json
CHANGED
|
@@ -24,7 +24,7 @@ siluzan-tso balance-scan -m <媒体类型> [选项]
|
|
|
24
24
|
| `-m, --media <type>` | 必填:`Google \| TikTok \| Yandex \| MetaAd \| BingV2 \| Kwai` | — |
|
|
25
25
|
| `-a, --accounts <ids>` | 指定 `mediaCustomerId`(逗号分隔);**跳过清单翻页**,对这些 ID 拉数后全部输出 | — |
|
|
26
26
|
| `--threshold-days <n>` | 剩余续航天数阈值 | `7` |
|
|
27
|
-
| `--spend-days <n>` |
|
|
27
|
+
| `--spend-days <n>` | 日均消耗回看自然日数(截至昨天不含今天,**北京时间**;日均 = 窗口合计 / N) | `7` |
|
|
28
28
|
| `--min-balance <n>` | 绝对余额阈值(与 threshold 取并集) | — |
|
|
29
29
|
| `--min-daily-spend <n>` | 日均消耗低于此值视为僵尸账户,不做续航估算 | `0.01` |
|
|
30
30
|
| `--target-days <n>` | 建议充值目标续航天数 | `30` |
|
|
@@ -47,7 +47,7 @@ siluzan-tso balance-scan -m BingV2 -a id1,id2,id3 --json-out ./snap-p2-subset
|
|
|
47
47
|
siluzan-tso balance-scan -m MetaAd --json-out ./snap-p2-meta
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
**读盘要点**:先看 `meta.dataIssue`(非空则失败)。`data.items` = **全部已检查账户**(OAuth 失效户不在 items,见 `meta.skippedInvalidOAuth`);`meta.hitCount` = 触阈值条数。预警筛 `hitReason !== "none"`;`hitReason="none"` 表示已检查但未触阈值。按 `remainingDays` 升序交付。用 **`dailySpend`** 做日均(口径见 `meta.spendDays` / `meta.spendWindow`),勿把 `stats` 区间合计 `spend`
|
|
50
|
+
**读盘要点**:先看 `meta.dataIssue`(非空则失败)。`data.items` = **全部已检查账户**(OAuth 失效户不在 items,见 `meta.skippedInvalidOAuth`);`meta.hitCount` = 触阈值条数。预警筛 `hitReason !== "none"`;`hitReason="none"` 表示已检查但未触阈值。按 `remainingDays` 升序交付。用 **`dailySpend`** 做日均(口径见 `meta.spendDays` / `meta.spendWindow`),勿把 `stats` 区间合计 `spend` 当「每天消耗」。Google 消耗走 `account-spend-overview`,`startDate`/`endDate` 带东八区墙钟(`…T00:00:00+08:00` / `…T23:59:59+08:00`),与 `stats` 同口径。与 `balance` / `accounts-digest` 分工见 `references/core/agent-conventions.md` §八。
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
| `google-analysis --sections overview`(Google 网关 `OverviewSectionData`) | **实时**;请求起止为 **YYYY-MM-DD**(不附加时区) | ✅ 可查当天 | 当天/今日消耗、当天高消耗账号排行 |
|
|
28
28
|
| `google-analysis --sections campaign-types`(`types-summary`) | **实时** | ✅ 可查当天 | 当天系列类型分布 |
|
|
29
29
|
| `google-analysis` 其他 range 维度(`campaigns` / `keywords` / `devices` / `geographic` / `daily-metrics` / …) | 实时(受 Google Ads API 同步延迟影响);**一律 YYYY-MM-DD**(不附加时区;与 `stats` 的 UTC+8 不同) | 可查当天,但当天可能尚未结算 | 周期分析、报告 |
|
|
30
|
-
| `stats -m Google` / `balance-scan -m Google` / `accounts-digest -m Google` / `list-accounts -m Google` 合并消耗(TSO `account-spend-overview`,2026-05 起) | **后端自动分流**:窗口完全在历史 → `database`;含今天 → `googleCombined
|
|
30
|
+
| `stats -m Google` / `balance-scan -m Google` / `accounts-digest -m Google` / `list-accounts -m Google` 合并消耗(TSO `account-spend-overview`,2026-05 起) | **后端自动分流**:窗口完全在历史 → `database`;含今天 → `googleCombined`。**凡走本接口的请求起止一律东八区墙钟**(`…T00:00:00+08:00` ~ `…T23:59:59+08:00`) | ✅ 可查当天(含今天时切实时聚合) | 历史回溯、巡检、周报 KPI、余额续航估算 |
|
|
31
31
|
| `stats` / `balance-scan` / `accounts-digest` / `list-accounts` 的 **TikTok / Yandex / BingV2 / Kwai / MetaAd** 合并消耗(TSO `accountsoverview`;MetaAd OAuth 户走 `FacebookAds` 段) | **每日同步昨天** | ❌ 查今天会全为 0 | 历史回溯、巡检、余额续航估算(口径为"截至昨天") |
|
|
32
32
|
| `balance`(`GetMediaAccountInfo`) | 实时 | — | 仅当前余额,不反映消耗 |
|
|
33
33
|
|
|
@@ -22,7 +22,7 @@ siluzan-tso balance-scan -m Google [--threshold-days <n>] [--min-balance <n>] [-
|
|
|
22
22
|
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
23
23
|
| 账户 | `data.items[].mediaCustomerId`、`data.items[].name`、`data.items[].advertiserName` |
|
|
24
24
|
| 余额(主币种金额,与平台余额接口一致) | **`data.items[].balance`**(由 `remainingAccountBudget` 计算而来) |
|
|
25
|
-
| 近 7 日估算日均消耗 | **`data.items[].dailySpend`** = 近 7 日总消耗 / 7(窗口为 **[T-7, T-1]
|
|
25
|
+
| 近 7 日估算日均消耗 | **`data.items[].dailySpend`** = 近 7 日总消耗 / 7(窗口为 **[T-7, T-1]**,北京时间截至昨天的 7 个自然日,**不含当天**;Google 请求带 `+08:00`) |
|
|
26
26
|
| 按余额÷日均估算的续航天数 | **`data.items[].remainingDays`** = `balance / dailySpend`(消耗过低 < `minDailySpend` 时为 `null`) |
|
|
27
27
|
| 建议充值额(按 `meta.thresholds.targetDaysForTopup` 目标) | **`data.items[].recommendedTopup`** |
|
|
28
28
|
| 命中原因(阈值逻辑) | **`data.items[].hitReason`**:`low-days` \| `low-balance` \| `both` \| `none`(已检查但未触阈值)。`data.items` 含全部已检查账户;预警筛 `hitReason !== "none"`。`meta.hitCount` 为触阈值条数 |
|
|
@@ -180,46 +180,14 @@
|
|
|
180
180
|
}
|
|
181
181
|
</style>
|
|
182
182
|
<style>
|
|
183
|
-
|
|
184
|
-
.insight {
|
|
185
|
-
margin-top: 12px;
|
|
186
|
-
padding: 12px 14px;
|
|
187
|
-
border-radius: 12px;
|
|
188
|
-
background: #f7f3ea;
|
|
189
|
-
border: 1px solid rgba(28, 27, 25, 0.08);
|
|
190
|
-
}
|
|
191
|
-
.insight strong { display: block; margin-bottom: 6px; }
|
|
192
|
-
.insight ul { margin: 0; padding-left: 1.1em; }
|
|
193
|
-
.insight li { margin: 0.25em 0; }
|
|
194
|
-
.insight.suggestion { background: #eef6f1; border-color: rgba(31, 122, 78, 0.18); }
|
|
183
|
+
/* 补充:开发样例未覆盖的极少数辅助样式(其余全部复用样例的 .panel/.narrative/.issue-card/.todo-item/.badge) */
|
|
195
184
|
.empty-note {
|
|
196
185
|
color: var(--muted);
|
|
197
186
|
padding: 18px;
|
|
198
187
|
text-align: center;
|
|
199
188
|
font-size: 13px;
|
|
200
189
|
}
|
|
201
|
-
.
|
|
202
|
-
.todo-list li { margin: 0.35em 0; }
|
|
203
|
-
.panel table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
204
|
-
.panel th, .panel td {
|
|
205
|
-
padding: 8px 10px;
|
|
206
|
-
border-bottom: 1px solid rgba(28, 27, 25, 0.08);
|
|
207
|
-
text-align: left;
|
|
208
|
-
vertical-align: top;
|
|
209
|
-
}
|
|
210
|
-
.panel th { color: var(--muted); font-weight: 600; white-space: nowrap; }
|
|
211
|
-
.panel td.num, .panel th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
212
|
-
.fold summary {
|
|
213
|
-
cursor: pointer;
|
|
214
|
-
font-weight: 600;
|
|
215
|
-
padding: 8px 0;
|
|
216
|
-
list-style: none;
|
|
217
|
-
}
|
|
218
|
-
.fold summary::-webkit-details-marker { display: none; }
|
|
219
|
-
.nav a.active { background: rgba(252, 62, 57, 0.12); color: var(--yandex); }
|
|
220
|
-
.hero h1 { margin: 8px 0 12px; font-size: clamp(1.4rem, 2.5vw, 1.9rem); }
|
|
221
|
-
.issues { margin: 12px 0; padding: 12px 14px; border-left: 3px solid var(--yandex); background: #fff6f5; }
|
|
222
|
-
|
|
190
|
+
.panel > .table-wrap:first-child table { margin-top: 0; }
|
|
223
191
|
</style>
|
|
224
192
|
</head>
|
|
225
193
|
<body>
|
|
@@ -262,14 +230,24 @@
|
|
|
262
230
|
.replace(/"/g, """);
|
|
263
231
|
}
|
|
264
232
|
|
|
265
|
-
|
|
233
|
+
/**
|
|
234
|
+
* 统一的“有效数字”判定:null / undefined / "" 一律视为无数据(返回 undefined),
|
|
235
|
+
* 避免 `Number(null) === 0` 把「无转化时 CPA 为空」误显示成 "$0.00" / "0.00%"。
|
|
236
|
+
*/
|
|
237
|
+
function toNum(v) {
|
|
238
|
+
if (v === null || v === undefined || v === "") return undefined;
|
|
266
239
|
const n = Number(v);
|
|
267
|
-
return Number.isFinite(n) ?
|
|
240
|
+
return Number.isFinite(n) ? n : undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function int(v) {
|
|
244
|
+
const n = toNum(v);
|
|
245
|
+
return n === undefined ? "—" : Math.round(n).toLocaleString("en-US");
|
|
268
246
|
}
|
|
269
247
|
|
|
270
248
|
function money(v, currency) {
|
|
271
|
-
const n =
|
|
272
|
-
if (
|
|
249
|
+
const n = toNum(v);
|
|
250
|
+
if (n === undefined) return "—";
|
|
273
251
|
const c = currency || "USD";
|
|
274
252
|
if (c === "USD" || c === "RUB" || c === "EUR") {
|
|
275
253
|
try {
|
|
@@ -284,14 +262,14 @@
|
|
|
284
262
|
}
|
|
285
263
|
|
|
286
264
|
function pct(v) {
|
|
287
|
-
const n =
|
|
288
|
-
if (
|
|
265
|
+
const n = toNum(v);
|
|
266
|
+
if (n === undefined) return "—";
|
|
289
267
|
return (n * 100).toFixed(2) + "%";
|
|
290
268
|
}
|
|
291
269
|
|
|
292
270
|
function num2(v) {
|
|
293
|
-
const n =
|
|
294
|
-
return
|
|
271
|
+
const n = toNum(v);
|
|
272
|
+
return n === undefined ? "—" : n.toFixed(2);
|
|
295
273
|
}
|
|
296
274
|
|
|
297
275
|
function asArr(v) {
|
|
@@ -302,13 +280,17 @@
|
|
|
302
280
|
return data.meta?.currency || data.kpis?.currency || "USD";
|
|
303
281
|
}
|
|
304
282
|
|
|
283
|
+
/**
|
|
284
|
+
* 分析 + 建议区块——样式对齐开发样例的 `.narrative`(badge「叙述」/「建议」+ ul),
|
|
285
|
+
* 而不是本文件之前自造的 `.insight` 盒子。
|
|
286
|
+
*/
|
|
305
287
|
function analysisBlock(section) {
|
|
306
288
|
const analysis = asArr(section?.analysis).filter((x) => String(x || "").trim());
|
|
307
289
|
const suggestions = asArr(section?.suggestions).filter((x) => String(x || "").trim());
|
|
308
290
|
const parts = [];
|
|
309
291
|
if (analysis.length) {
|
|
310
292
|
parts.push(
|
|
311
|
-
'<div class="
|
|
293
|
+
'<div class="narrative"><h3><span class="badge b-info">叙述</span> 数据解读</h3><ul>' +
|
|
312
294
|
analysis.map((a) => "<li>" + escapeHtml(a) + "</li>").join("") +
|
|
313
295
|
"</ul></div>"
|
|
314
296
|
);
|
|
@@ -317,7 +299,7 @@
|
|
|
317
299
|
}
|
|
318
300
|
if (suggestions.length) {
|
|
319
301
|
parts.push(
|
|
320
|
-
'<div class="
|
|
302
|
+
'<div class="narrative"><h3><span class="badge b-warn">建议</span> 优化建议</h3><ul>' +
|
|
321
303
|
suggestions.map((s) => "<li>" + escapeHtml(s) + "</li>").join("") +
|
|
322
304
|
"</ul></div>"
|
|
323
305
|
);
|
|
@@ -330,15 +312,16 @@
|
|
|
330
312
|
'<div class="section-head"><div><h2>' +
|
|
331
313
|
escapeHtml(title) +
|
|
332
314
|
"</h2>" +
|
|
333
|
-
(desc ? '<p
|
|
315
|
+
(desc ? '<p>' + escapeHtml(desc) + "</p>" : "") +
|
|
334
316
|
"</div></div>"
|
|
335
317
|
);
|
|
336
318
|
}
|
|
337
319
|
|
|
320
|
+
/** 仅返回 `.table-wrap`(不含外层 `.panel`),供调用方与叙述区块共用同一个 `.panel` 容器,对齐开发样例结构。 */
|
|
338
321
|
function tableHtml(headers, rowsHtml) {
|
|
339
322
|
if (!rowsHtml) return '<div class="empty-note">' + EMPTY_NOTE + "</div>";
|
|
340
323
|
return (
|
|
341
|
-
'<div class="
|
|
324
|
+
'<div class="table-wrap"><table><thead><tr>' +
|
|
342
325
|
headers
|
|
343
326
|
.map(function (h) {
|
|
344
327
|
const cls = h.num ? ' class="num"' : "";
|
|
@@ -375,11 +358,14 @@
|
|
|
375
358
|
'</td><td class="num">' +
|
|
376
359
|
num2(r.conversions) +
|
|
377
360
|
'</td><td class="num">' +
|
|
361
|
+
pct(r.conversionRate) +
|
|
362
|
+
'</td><td class="num">' +
|
|
378
363
|
money(r.costPerConversion, currency) +
|
|
379
364
|
"</td>"
|
|
380
365
|
);
|
|
381
366
|
}
|
|
382
367
|
|
|
368
|
+
/** 对齐开发样例关键词/搜索词/地域/设备表格的列集合(含 cr% 转化率)。 */
|
|
383
369
|
const METRIC_HEADERS = [
|
|
384
370
|
{ label: "消耗", num: true },
|
|
385
371
|
{ label: "展示", num: true },
|
|
@@ -387,6 +373,7 @@
|
|
|
387
373
|
{ label: "CTR", num: true },
|
|
388
374
|
{ label: "CPC", num: true },
|
|
389
375
|
{ label: "转化", num: true },
|
|
376
|
+
{ label: "转化率", num: true },
|
|
390
377
|
{ label: "CPA", num: true },
|
|
391
378
|
];
|
|
392
379
|
|
|
@@ -443,6 +430,7 @@
|
|
|
443
430
|
? int(kpis.activeDays) + " 个有效投放日"
|
|
444
431
|
: "";
|
|
445
432
|
|
|
433
|
+
// 对齐开发样例:5 张 KPI 卡(余额/消耗/日均消耗/点击-CTR/转化-CPA),展示量并入「点击 / CTR」的 delta 文案。
|
|
446
434
|
const kpiCards = [
|
|
447
435
|
{
|
|
448
436
|
label: "账户余额" + (balAsOf ? "(截至 " + balAsOf + ")" : ""),
|
|
@@ -452,7 +440,7 @@
|
|
|
452
440
|
{
|
|
453
441
|
label: "本期消耗",
|
|
454
442
|
value: money(kpis.spend, currency),
|
|
455
|
-
delta:
|
|
443
|
+
delta: activeHint,
|
|
456
444
|
},
|
|
457
445
|
{
|
|
458
446
|
label: "日均消耗",
|
|
@@ -462,12 +450,12 @@
|
|
|
462
450
|
{
|
|
463
451
|
label: "点击 / CTR",
|
|
464
452
|
value: int(kpis.clicks),
|
|
465
|
-
delta:
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
453
|
+
delta:
|
|
454
|
+
"CTR " +
|
|
455
|
+
pct(kpis.ctr) +
|
|
456
|
+
" · CPC " +
|
|
457
|
+
money(kpis.averageCpc, currency) +
|
|
458
|
+
(kpis.impressions != null ? " · 展示 " + int(kpis.impressions) : ""),
|
|
471
459
|
},
|
|
472
460
|
{
|
|
473
461
|
label: "转化 / CPA",
|
|
@@ -506,6 +494,8 @@
|
|
|
506
494
|
'</td><td class="num">' +
|
|
507
495
|
money(r.spend, currency) +
|
|
508
496
|
'</td><td class="num">' +
|
|
497
|
+
int(r.impressions) +
|
|
498
|
+
'</td><td class="num">' +
|
|
509
499
|
int(r.clicks) +
|
|
510
500
|
'</td><td class="num">' +
|
|
511
501
|
pct(r.ctr) +
|
|
@@ -523,6 +513,7 @@
|
|
|
523
513
|
{ label: "区间" },
|
|
524
514
|
{ label: "天数", num: true },
|
|
525
515
|
{ label: "消耗", num: true },
|
|
516
|
+
{ label: "展示", num: true },
|
|
526
517
|
{ label: "点击", num: true },
|
|
527
518
|
{ label: "CTR", num: true },
|
|
528
519
|
{ label: "CPC", num: true },
|
|
@@ -557,7 +548,7 @@
|
|
|
557
548
|
})
|
|
558
549
|
.join("");
|
|
559
550
|
dailyFold =
|
|
560
|
-
'<details class="fold
|
|
551
|
+
'<details class="fold"><summary>每日消费数据概览(点击展开,共 ' +
|
|
561
552
|
daily.length +
|
|
562
553
|
" 天)</summary>" +
|
|
563
554
|
tableHtml(
|
|
@@ -614,57 +605,85 @@
|
|
|
614
605
|
'<div class="kpi-grid" style="margin-bottom:14px">' +
|
|
615
606
|
kpiCards +
|
|
616
607
|
"</div>" +
|
|
617
|
-
|
|
618
|
-
? '<h3 style="margin:18px 0 10px;font-size:15px">周报汇总</h3>' + weeklyTable
|
|
619
|
-
: "") +
|
|
620
|
-
dailyFold +
|
|
608
|
+
'<div class="panel">' +
|
|
621
609
|
(exec
|
|
622
|
-
? '<div
|
|
610
|
+
? '<div style="padding:14px 16px 0; font-weight:600; font-size:14px;">一句话数据概况</div><div style="padding:6px 16px 0">' +
|
|
623
611
|
exec +
|
|
624
612
|
"</div>"
|
|
625
613
|
: "") +
|
|
614
|
+
(weeklyTable
|
|
615
|
+
? '<div style="padding:14px 16px 0; font-weight:600; font-size:14px;">汇总数据概览(周)</div>' +
|
|
616
|
+
weeklyTable
|
|
617
|
+
: "") +
|
|
618
|
+
dailyFold +
|
|
626
619
|
analysisBlock(sec) +
|
|
620
|
+
nextNarrative(data) +
|
|
621
|
+
"</div>" +
|
|
627
622
|
"</div></section>"
|
|
628
623
|
);
|
|
629
624
|
}
|
|
630
625
|
|
|
631
|
-
|
|
626
|
+
/** 将 priority(P0/P1/P2 或 high/medium/low)归一为开发样例的徽标文案 + 颜色类。 */
|
|
627
|
+
function priorityBadge(pri, idx) {
|
|
628
|
+
const raw = String(pri || "").trim().toLowerCase();
|
|
629
|
+
if (raw === "high" || raw === "p0") return { label: "P0", cls: "b-danger" };
|
|
630
|
+
if (raw === "low" || raw === "p2") return { label: "P2", cls: "b-info" };
|
|
631
|
+
if (raw === "medium" || raw === "p1" || !raw) return { label: "P1", cls: "b-warn" };
|
|
632
|
+
return { label: String(pri), cls: "b-warn" };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* 「后续优化动作」——对齐开发样例:不是独立 `<section>`,而是嵌在「账户总览」
|
|
637
|
+
* 同一个 `.panel` 内、紧跟数据概述之后的第二个 `.narrative`(id="next" 供导航锚点定位)。
|
|
638
|
+
*/
|
|
639
|
+
function nextNarrative(data) {
|
|
632
640
|
const recs = asArr((data.narrative || {}).recommendations);
|
|
633
641
|
if (!recs.length) {
|
|
634
642
|
return (
|
|
635
|
-
'<
|
|
636
|
-
sectionHead("后续优化动作") +
|
|
643
|
+
'<div class="narrative" id="next"><h3><span class="badge b-warn">待办</span> 后续优化动作与展望</h3>' +
|
|
637
644
|
'<div class="empty-note">' +
|
|
638
645
|
EMPTY_NOTE +
|
|
639
|
-
"</div></div
|
|
646
|
+
"</div></div>"
|
|
640
647
|
);
|
|
641
648
|
}
|
|
649
|
+
// 对齐开发样例的 `.todo-item`(badge + 文案 + 锚点链接三栏网格),而非普通 <li> 列表。
|
|
642
650
|
const items = recs
|
|
643
651
|
.map(function (r, i) {
|
|
644
652
|
if (typeof r === "string") {
|
|
645
|
-
|
|
653
|
+
const b = priorityBadge(null, i);
|
|
654
|
+
return (
|
|
655
|
+
'<div class="todo-item"><span class="badge ' +
|
|
656
|
+
b.cls +
|
|
657
|
+
'">' +
|
|
658
|
+
b.label +
|
|
659
|
+
"</span><div>" +
|
|
660
|
+
escapeHtml(r) +
|
|
661
|
+
"</div><span></span></div>"
|
|
662
|
+
);
|
|
646
663
|
}
|
|
647
|
-
const
|
|
664
|
+
const b = priorityBadge(r.priority, i);
|
|
648
665
|
const text = r.text || r.content || "";
|
|
649
666
|
const anchor = r.anchor
|
|
650
|
-
? '
|
|
651
|
-
: "";
|
|
667
|
+
? '<a href="' + escapeHtml(r.anchor) + '">→</a>'
|
|
668
|
+
: "<span></span>";
|
|
652
669
|
return (
|
|
653
|
-
"
|
|
654
|
-
|
|
655
|
-
"
|
|
670
|
+
'<div class="todo-item"><span class="badge ' +
|
|
671
|
+
b.cls +
|
|
672
|
+
'">' +
|
|
673
|
+
b.label +
|
|
674
|
+
"</span><div>" +
|
|
656
675
|
escapeHtml(text) +
|
|
676
|
+
"</div>" +
|
|
657
677
|
anchor +
|
|
658
|
-
"</
|
|
678
|
+
"</div>"
|
|
659
679
|
);
|
|
660
680
|
})
|
|
661
681
|
.join("");
|
|
662
682
|
return (
|
|
663
|
-
'<
|
|
664
|
-
|
|
665
|
-
'<div class="panel" style="padding:14px"><ol class="todo-list">' +
|
|
683
|
+
'<div class="narrative" id="next"><h3><span class="badge b-warn">待办</span> 后续优化动作与展望</h3>' +
|
|
684
|
+
'<div class="todo-list">' +
|
|
666
685
|
items +
|
|
667
|
-
"</
|
|
686
|
+
"</div></div>"
|
|
668
687
|
);
|
|
669
688
|
}
|
|
670
689
|
|
|
@@ -672,7 +691,9 @@
|
|
|
672
691
|
const data = opts.data;
|
|
673
692
|
const currency = currencyOf(data);
|
|
674
693
|
const sec = ((data.narrative || {}).sections || {})[opts.narrativeKey] || {};
|
|
675
|
-
const
|
|
694
|
+
const allRows = asArr(data.tables?.[opts.tableKey]);
|
|
695
|
+
const limit = opts.limit || 20;
|
|
696
|
+
const rows = topBySpend(allRows, limit);
|
|
676
697
|
let body = "";
|
|
677
698
|
if (rows.length) {
|
|
678
699
|
const rowHtml = rows
|
|
@@ -696,13 +717,23 @@
|
|
|
696
717
|
} else {
|
|
697
718
|
body = '<div class="empty-note">' + EMPTY_NOTE + "</div>";
|
|
698
719
|
}
|
|
720
|
+
// 行数提示:明确区分「共 N 行仅显示 Top M」与「本维度确实无数据」,避免误判为报告漏了该维度。
|
|
721
|
+
const countHint =
|
|
722
|
+
allRows.length > rows.length
|
|
723
|
+
? "共 " + allRows.length + " 行,按消耗降序显示 Top " + rows.length + "。"
|
|
724
|
+
: allRows.length > 0
|
|
725
|
+
? "共 " + allRows.length + " 行。"
|
|
726
|
+
: "";
|
|
727
|
+
const desc = [opts.desc, countHint].filter(Boolean).join(" ");
|
|
699
728
|
return (
|
|
700
729
|
'<section id="' +
|
|
701
730
|
opts.id +
|
|
702
731
|
'"><div class="wrap">' +
|
|
703
|
-
sectionHead(opts.title,
|
|
732
|
+
sectionHead(opts.title, desc) +
|
|
733
|
+
'<div class="panel">' +
|
|
704
734
|
body +
|
|
705
735
|
analysisBlock(sec) +
|
|
736
|
+
"</div>" +
|
|
706
737
|
"</div></section>"
|
|
707
738
|
);
|
|
708
739
|
}
|
|
@@ -793,13 +824,17 @@
|
|
|
793
824
|
|
|
794
825
|
return (
|
|
795
826
|
'<section id="daily"><div class="wrap">' +
|
|
796
|
-
sectionHead("日报表", "
|
|
827
|
+
sectionHead("日报表", "按日消耗/展示/点击/转化明细") +
|
|
828
|
+
'<div class="panel">' +
|
|
797
829
|
dailyBody +
|
|
798
830
|
analysisBlock(sec) +
|
|
831
|
+
"</div>" +
|
|
799
832
|
"</div></section>" +
|
|
800
833
|
'<section id="weekly"><div class="wrap">' +
|
|
801
|
-
sectionHead("周报表", "
|
|
834
|
+
sectionHead("周报表", "由每日数据按 ISO 周本地聚合") +
|
|
835
|
+
'<div class="panel">' +
|
|
802
836
|
weeklyBody +
|
|
837
|
+
"</div>" +
|
|
803
838
|
"</div></section>"
|
|
804
839
|
);
|
|
805
840
|
}
|
|
@@ -809,15 +844,14 @@
|
|
|
809
844
|
const parts = [];
|
|
810
845
|
parts.push(renderHero(data));
|
|
811
846
|
parts.push(renderOverview(data));
|
|
812
|
-
parts.push(renderNext(data));
|
|
813
847
|
parts.push(
|
|
814
848
|
dimSection({
|
|
815
849
|
data: data,
|
|
816
850
|
id: "search",
|
|
817
851
|
narrativeKey: "search",
|
|
818
852
|
tableKey: "search",
|
|
819
|
-
title: "
|
|
820
|
-
desc: "
|
|
853
|
+
title: "广告类型 · Search",
|
|
854
|
+
desc: "搜索网络(Search)投放汇总",
|
|
821
855
|
nameLabel: "网络",
|
|
822
856
|
nameOf: function (r) {
|
|
823
857
|
return r.network || "SEARCH";
|
|
@@ -831,7 +865,7 @@
|
|
|
831
865
|
narrativeKey: "campaigns",
|
|
832
866
|
tableKey: "campaigns",
|
|
833
867
|
title: "Campaign",
|
|
834
|
-
desc: "
|
|
868
|
+
desc: "按消耗排序的系列明细",
|
|
835
869
|
nameLabel: "系列",
|
|
836
870
|
nameOf: function (r) {
|
|
837
871
|
return r.campaignName || r.campaignId || "—";
|
|
@@ -856,7 +890,7 @@
|
|
|
856
890
|
narrativeKey: "keywords",
|
|
857
891
|
tableKey: "keywords",
|
|
858
892
|
title: "关键词",
|
|
859
|
-
desc: "
|
|
893
|
+
desc: "按消耗排序的关键词明细",
|
|
860
894
|
nameLabel: "关键词",
|
|
861
895
|
nameOf: function (r) {
|
|
862
896
|
return r.keyword || "—";
|
|
@@ -881,7 +915,7 @@
|
|
|
881
915
|
narrativeKey: "searchTerms",
|
|
882
916
|
tableKey: "searchTerms",
|
|
883
917
|
title: "搜索词",
|
|
884
|
-
desc: "
|
|
918
|
+
desc: "按消耗排序的搜索词明细(仅近 180 天数据)",
|
|
885
919
|
nameLabel: "搜索词",
|
|
886
920
|
nameOf: function (r) {
|
|
887
921
|
return r.searchQuery || "—";
|
|
@@ -906,10 +940,16 @@
|
|
|
906
940
|
narrativeKey: "geo",
|
|
907
941
|
tableKey: "geo",
|
|
908
942
|
title: "地域",
|
|
909
|
-
desc: "
|
|
943
|
+
desc: "按消耗排序的地域明细",
|
|
910
944
|
nameLabel: "地域",
|
|
911
945
|
nameOf: function (r) {
|
|
912
|
-
return
|
|
946
|
+
return (
|
|
947
|
+
r.locationName ||
|
|
948
|
+
r.targetingLocationName ||
|
|
949
|
+
r.location ||
|
|
950
|
+
r.name ||
|
|
951
|
+
(r.locationId != null ? "地域 " + r.locationId : "—")
|
|
952
|
+
);
|
|
913
953
|
},
|
|
914
954
|
limit: 30,
|
|
915
955
|
})
|
|
@@ -921,7 +961,7 @@
|
|
|
921
961
|
narrativeKey: "devices",
|
|
922
962
|
tableKey: "devices",
|
|
923
963
|
title: "设备",
|
|
924
|
-
desc: "
|
|
964
|
+
desc: "按消耗排序的设备明细",
|
|
925
965
|
nameLabel: "设备",
|
|
926
966
|
nameOf: function (r) {
|
|
927
967
|
return r.device || "—";
|
|
@@ -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.45-beta.
|
|
12
|
+
$PKG_VERSION = '1.1.45-beta.3'
|
|
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.45-beta.
|
|
12
|
+
readonly PKG_VERSION="1.1.45-beta.3"
|
|
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"
|