siluzan-tso-cli 1.1.43-beta.1 → 1.1.43-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.43-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.43-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -5544,6 +5544,32 @@ var init_daily_metrics_range = __esm({
5544
5544
  }
5545
5545
  });
5546
5546
 
5547
+ // src/utils/snapshot/async-lock.ts
5548
+ async function withAsyncLock(key, fn) {
5549
+ const prev = chains.get(key) ?? Promise.resolve();
5550
+ let release;
5551
+ const gate = new Promise((resolve21) => {
5552
+ release = resolve21;
5553
+ });
5554
+ chains.set(
5555
+ key,
5556
+ prev.catch(() => void 0).then(() => gate)
5557
+ );
5558
+ await prev.catch(() => void 0);
5559
+ try {
5560
+ return await fn();
5561
+ } finally {
5562
+ release();
5563
+ }
5564
+ }
5565
+ var chains;
5566
+ var init_async_lock = __esm({
5567
+ "src/utils/snapshot/async-lock.ts"() {
5568
+ "use strict";
5569
+ chains = /* @__PURE__ */ new Map();
5570
+ }
5571
+ });
5572
+
5547
5573
  // src/commands/google-analysis/wrap-snapshot-envelope.ts
5548
5574
  function coerceToRowArray(value) {
5549
5575
  if (Array.isArray(value)) return value;
@@ -5766,26 +5792,29 @@ async function writeGoogleAnalysisSnapshot(params) {
5766
5792
  const outlineBody = formatOutlineFileBody(fileName, normalizedWrapped, outlineExtra);
5767
5793
  await fs7.writeFile(path11.join(absDir, outlineFileName), `${outlineBody}
5768
5794
  `, "utf8");
5769
- const existing = await readManifestIfExists(absDir, accountSlug || void 0);
5770
5795
  const newArtifact = {
5771
5796
  section: params.section,
5772
5797
  file: relativeFile,
5773
5798
  writtenAt,
5774
5799
  ...params.endpointHint !== void 0 ? { endpointHint: params.endpointHint } : {}
5775
5800
  };
5776
- const merged = mergeGoogleAnalysisManifest(existing, {
5777
- accountId: params.accountId,
5778
- dateRange: params.dateRange,
5779
- cliVersion: params.cliVersion,
5780
- newArtifact
5781
- });
5782
5801
  const manifestFile = googleAnalysisManifestFile(accountSlug || void 0);
5783
- await fs7.writeFile(
5784
- path11.join(absDir, manifestFile),
5785
- `${JSON.stringify(stringifyIdsDeep(merged), null, 2)}
5802
+ const manifestPath = path11.join(absDir, manifestFile);
5803
+ await withAsyncLock(manifestPath, async () => {
5804
+ const existing = await readManifestIfExists(absDir, accountSlug || void 0);
5805
+ const merged = mergeGoogleAnalysisManifest(existing, {
5806
+ accountId: params.accountId,
5807
+ dateRange: params.dateRange,
5808
+ cliVersion: params.cliVersion,
5809
+ newArtifact
5810
+ });
5811
+ await fs7.writeFile(
5812
+ manifestPath,
5813
+ `${JSON.stringify(stringifyIdsDeep(merged), null, 2)}
5786
5814
  `,
5787
- "utf8"
5788
- );
5815
+ "utf8"
5816
+ );
5817
+ });
5789
5818
  return {
5790
5819
  kind: "siluzan-tso-google-analysis-snapshot",
5791
5820
  schemaVersion: SCHEMA_VERSION2,
@@ -5803,6 +5832,7 @@ var init_google_analysis = __esm({
5803
5832
  "use strict";
5804
5833
  init_cli_json();
5805
5834
  init_wrap_snapshot_envelope();
5835
+ init_async_lock();
5806
5836
  init_dir();
5807
5837
  LEGACY_MANIFEST_FILE = "manifest.json";
5808
5838
  CLI_PACKAGE = "siluzan-tso-cli";
@@ -102288,6 +102318,56 @@ async function loadSectionFile(snapshotDir, file) {
102288
102318
  return null;
102289
102319
  }
102290
102320
  }
102321
+ function dateTagFromRange(dateRange) {
102322
+ if (!dateRange || dateRange.mode !== "range") return void 0;
102323
+ return `${dateRange.start.replace(/-/g, "")}-${dateRange.end.replace(/-/g, "")}`;
102324
+ }
102325
+ function dateTagFromMeta(meta) {
102326
+ const start = typeof meta?.startDate === "string" ? meta.startDate : "";
102327
+ const end = typeof meta?.endDate === "string" ? meta.endDate : "";
102328
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(start) || !/^\d{4}-\d{2}-\d{2}$/.test(end)) return void 0;
102329
+ return `${start.replace(/-/g, "")}-${end.replace(/-/g, "")}`;
102330
+ }
102331
+ function pickSectionSnapshotFile(files, section, accountId, preferredDateTag) {
102332
+ const escapedSection = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
102333
+ const escapedId = accountId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
102334
+ const re = new RegExp(`^${escapedSection}-${escapedId}(?:_(\\d{8}-\\d{8}))?\\.json$`);
102335
+ const matches = files.filter((f) => re.test(f));
102336
+ if (matches.length === 0) return null;
102337
+ if (preferredDateTag) {
102338
+ const preferred = matches.find((f) => f.endsWith(`_${preferredDateTag}.json`));
102339
+ if (preferred) return preferred;
102340
+ }
102341
+ return [...matches].sort().at(-1) ?? null;
102342
+ }
102343
+ async function recoverMissingReportSections(sectionMap, absDir, accountId, preferredDateTag) {
102344
+ if (!accountId) return;
102345
+ let entries;
102346
+ try {
102347
+ entries = await fs20.readdir(absDir);
102348
+ } catch {
102349
+ return;
102350
+ }
102351
+ for (const section of REPORT_MERGE_SECTIONS) {
102352
+ if (sectionMap.has(section)) continue;
102353
+ const file = pickSectionSnapshotFile(entries, section, accountId, preferredDateTag);
102354
+ if (!file) continue;
102355
+ const data = await loadSectionFile(absDir, file);
102356
+ if (data) sectionMap.set(section, data);
102357
+ }
102358
+ }
102359
+ function periodMetricsFromRow(row) {
102360
+ if (!row) return null;
102361
+ return {
102362
+ cost: num2(row.spend),
102363
+ impressions: num2(row.impressions),
102364
+ clicks: num2(row.clicks),
102365
+ conversions: num2(row.conversions),
102366
+ ctr: num2(row.ctr),
102367
+ averageCpc: num2(row.averageCpc),
102368
+ costPerConversion: num2(row.costPerConversion)
102369
+ };
102370
+ }
102291
102371
  function items(envelope) {
102292
102372
  if (!envelope || !Array.isArray(envelope.items)) return [];
102293
102373
  return envelope.items.filter((row) => asRecord11(row) !== null);
@@ -102342,16 +102422,18 @@ async function mergeGoogleAnalysisSnapshotIntoReport(payload, snapshotDir) {
102342
102422
  if (!manifest) {
102343
102423
  throw new Error(`\u672A\u5728 ${absDir} \u627E\u5230 manifest*.json\uFF0C\u8BF7\u5148\u6267\u884C google-analysis --json-out`);
102344
102424
  }
102425
+ const accountId = manifest.googleAnalysis?.accountId ?? metaAccountId;
102426
+ const dateRange = manifest.googleAnalysis?.dateRange;
102427
+ const preferredDateTag = dateTagFromRange(dateRange) ?? dateTagFromMeta(asRecord11(payload.meta) ?? void 0);
102345
102428
  const sectionMap = /* @__PURE__ */ new Map();
102346
102429
  for (const art of manifest.artifacts ?? []) {
102347
102430
  const data = await loadSectionFile(absDir, art.file);
102348
102431
  if (data) sectionMap.set(art.section, data);
102349
102432
  }
102433
+ await recoverMissingReportSections(sectionMap, absDir, accountId, preferredDateTag);
102350
102434
  const overviewRecord = record(sectionMap.get("overview"));
102351
102435
  const currentPeriod = asRecord11(overviewRecord?.currentPeriod);
102352
102436
  const previousPeriod = asRecord11(overviewRecord?.previousPeriod);
102353
- const accountId = manifest.googleAnalysis?.accountId ?? metaAccountId;
102354
- const dateRange = manifest.googleAnalysis?.dateRange;
102355
102437
  const meta = { ...payload.meta ?? {} };
102356
102438
  if (!meta.accountId && accountId) meta.accountId = accountId;
102357
102439
  if (!meta.currency) {
@@ -102363,16 +102445,9 @@ async function mergeGoogleAnalysisSnapshotIntoReport(payload, snapshotDir) {
102363
102445
  if (!meta.generatedAt) meta.generatedAt = (/* @__PURE__ */ new Date()).toISOString();
102364
102446
  const campaignRows2 = items(sectionMap.get("campaigns"));
102365
102447
  const campaignKpis = sumCampaignRowsToPeriodKpis(campaignRows2);
102448
+ const dimensionSummaryRecord = record(sectionMap.get("dimension-summary"));
102366
102449
  const kpis = { ...payload.kpis ?? {} };
102367
- const currentKpiSource = campaignKpis ?? (currentPeriod ? {
102368
- cost: num2(currentPeriod.spend),
102369
- impressions: num2(currentPeriod.impressions),
102370
- clicks: num2(currentPeriod.clicks),
102371
- conversions: num2(currentPeriod.conversions),
102372
- ctr: num2(currentPeriod.ctr),
102373
- averageCpc: num2(currentPeriod.averageCpc),
102374
- costPerConversion: num2(currentPeriod.costPerConversion)
102375
- } : null);
102450
+ const currentKpiSource = campaignKpis ?? periodMetricsFromRow(currentPeriod) ?? periodMetricsFromRow(dimensionSummaryRecord);
102376
102451
  if (currentKpiSource) {
102377
102452
  if (currentKpiSource.cost != null) kpis.cost = currentKpiSource.cost;
102378
102453
  if (currentKpiSource.impressions != null) kpis.impressions = currentKpiSource.impressions;
@@ -102411,22 +102486,21 @@ async function mergeGoogleAnalysisSnapshotIntoReport(payload, snapshotDir) {
102411
102486
  }
102412
102487
  }
102413
102488
  const tables = { ...payload.tables ?? {} };
102414
- {
102415
- const summary = record(sectionMap.get("dimension-summary"));
102416
- if (summary) {
102417
- tables.dimensionSummary = {
102418
- cost: num2(summary.spend),
102419
- impressions: num2(summary.impressions),
102420
- clicks: num2(summary.clicks),
102421
- conversions: num2(summary.conversions),
102422
- ctr: num2(summary.ctr),
102423
- averageCpc: num2(summary.averageCpc),
102424
- costPerConversion: num2(summary.costPerConversion),
102425
- searchImpressionShare: num2(summary.searchImpressionShare),
102426
- searchBudgetLostImpressionShare: num2(summary.searchBudgetLostImpressionShare),
102427
- searchRankLostImpressionShare: num2(summary.searchRankLostImpressionShare)
102428
- };
102429
- }
102489
+ if (dimensionSummaryRecord) {
102490
+ tables.dimensionSummary = {
102491
+ cost: num2(dimensionSummaryRecord.spend),
102492
+ impressions: num2(dimensionSummaryRecord.impressions),
102493
+ clicks: num2(dimensionSummaryRecord.clicks),
102494
+ conversions: num2(dimensionSummaryRecord.conversions),
102495
+ ctr: num2(dimensionSummaryRecord.ctr),
102496
+ averageCpc: num2(dimensionSummaryRecord.averageCpc),
102497
+ costPerConversion: num2(dimensionSummaryRecord.costPerConversion),
102498
+ searchImpressionShare: num2(dimensionSummaryRecord.searchImpressionShare),
102499
+ searchBudgetLostImpressionShare: num2(
102500
+ dimensionSummaryRecord.searchBudgetLostImpressionShare
102501
+ ),
102502
+ searchRankLostImpressionShare: num2(dimensionSummaryRecord.searchRankLostImpressionShare)
102503
+ };
102430
102504
  }
102431
102505
  if (campaignRows2.length > 0) {
102432
102506
  tables.campaigns = campaignRows2.map((r) => ({
@@ -102464,10 +102538,20 @@ async function mergeGoogleAnalysisSnapshotIntoReport(payload, snapshotDir) {
102464
102538
  tables
102465
102539
  };
102466
102540
  }
102541
+ var REPORT_MERGE_SECTIONS;
102467
102542
  var init_merge_snapshot = __esm({
102468
102543
  "src/commands/google-analysis/merge-snapshot.ts"() {
102469
102544
  "use strict";
102470
102545
  init_google_analysis();
102546
+ REPORT_MERGE_SECTIONS = [
102547
+ "overview",
102548
+ "daily-metrics",
102549
+ "dimension-summary",
102550
+ "campaigns",
102551
+ "devices",
102552
+ "geographic",
102553
+ "keywords"
102554
+ ];
102471
102555
  }
102472
102556
  });
102473
102557
 
@@ -102511,6 +102595,19 @@ function validateGooglePeriodReportContent(data) {
102511
102595
  "\u22651 \u6BB5\u6267\u884C\u6458\u8981\uFF0C\u6982\u8FF0\u672C\u671F\u6D88\u8017/\u8F6C\u5316/CTR/CPA \u8D70\u52BF"
102512
102596
  );
102513
102597
  }
102598
+ const kpis = asRecord12(data.kpis);
102599
+ const hasCoreKpi = ["cost", "impressions", "clicks"].some(
102600
+ (key) => Number.isFinite(Number(kpis?.[key]))
102601
+ );
102602
+ if (!hasCoreKpi) {
102603
+ pushMissing(
102604
+ missing,
102605
+ "kpis-core-metrics",
102606
+ "\u6267\u884C\u6458\u8981 KPI",
102607
+ "kpis.cost / kpis.impressions / kpis.clicks",
102608
+ "\u81F3\u5C11\u4E00\u9879\u987B\u4E3A\u6709\u6548\u6570\u5B57\uFF1B\u8BF7\u52A0 --snapshot-dir \u4ECE google-analysis \u5FEB\u7167\u5408\u5E76\uFF08\u52FF\u624B\u586B HTML\uFF09"
102609
+ );
102610
+ }
102514
102611
  const sectionAnalysisCounts = {};
102515
102612
  const sectionSuggestionCounts = {};
102516
102613
  for (const key of SECTION_KEYS) {
@@ -102571,7 +102668,7 @@ function formatGooglePeriodReportErrors(result) {
102571
102668
  ...lines,
102572
102669
  "",
102573
102670
  "\u8BF7\u8865\u5168 narrative.sections.<dimension>.{analysis,suggestions} \u4E0E narrative.recommendations \u540E\u91CD\u65B0\u6267\u884C render\u3002",
102574
- "\u6570\u503C\u7C7B\u5B57\u6BB5\uFF08kpis/charts/tables\uFF09\u53EF\u7701\u7565\uFF0C\u52A0 --snapshot-dir \u7531 CLI \u4ECE google-analysis \u5FEB\u7167\u81EA\u52A8\u5408\u5E76\uFF1BAgent \u53EA\u9700\u64B0\u5199\u53D9\u4E8B\u6587\u5B57\uFF0C\u7981\u6B62\u624B\u5199 HTML\u3002"
102671
+ "kpis/charts/tables \u987B\u7531 --snapshot-dir \u4ECE google-analysis \u5FEB\u7167\u81EA\u52A8\u5408\u5E76\uFF08Agent \u53EA\u5199 narrative\uFF09\uFF1B\u5408\u5E76\u540E kpis \u4ECD\u4E3A\u7A7A\u65F6\u68C0\u67E5\u5FEB\u7167\u76EE\u5F55 manifest \u662F\u5426\u542B overview/campaigns/dimension-summary\u3002\u7981\u6B62\u624B\u5199 HTML\u3002"
102575
102672
  ].join("\n");
102576
102673
  }
102577
102674
  var SECTION_KEYS, SECTION_LABELS;
@@ -108342,6 +108439,7 @@ init_cli_table();
108342
108439
  init_cli_json_snapshot();
108343
108440
 
108344
108441
  // src/utils/snapshot/report-analysis.ts
108442
+ init_async_lock();
108345
108443
  init_google_analysis();
108346
108444
  init_cli_json();
108347
108445
  import * as fs8 from "fs/promises";
@@ -108415,25 +108513,24 @@ async function writeReportAnalysisSnapshot(params) {
108415
108513
  `,
108416
108514
  "utf8"
108417
108515
  );
108418
- const existing = await readReportManifestIfExists(absDir, accountSlug || void 0);
108419
108516
  const newArtifact = {
108420
108517
  section: params.section,
108421
108518
  file: fileName,
108422
108519
  writtenAt
108423
108520
  };
108424
- const merged = mergeReportAnalysisManifest(existing, {
108425
- accountId: params.accountId,
108426
- dateRange: params.dateRange,
108427
- cliVersion: params.cliVersion,
108428
- newArtifact
108429
- });
108430
108521
  const manifestFile = reportManifestFile(accountSlug || void 0);
108431
- await fs8.writeFile(
108432
- path12.join(absDir, manifestFile),
108433
- `${JSON.stringify(merged, null, 2)}
108434
- `,
108435
- "utf8"
108436
- );
108522
+ const manifestPath = path12.join(absDir, manifestFile);
108523
+ await withAsyncLock(manifestPath, async () => {
108524
+ const existing = await readReportManifestIfExists(absDir, accountSlug || void 0);
108525
+ const merged = mergeReportAnalysisManifest(existing, {
108526
+ accountId: params.accountId,
108527
+ dateRange: params.dateRange,
108528
+ cliVersion: params.cliVersion,
108529
+ newArtifact
108530
+ });
108531
+ await fs8.writeFile(manifestPath, `${JSON.stringify(merged, null, 2)}
108532
+ `, "utf8");
108533
+ });
108437
108534
  return {
108438
108535
  kind: "siluzan-tso-report-analysis-snapshot",
108439
108536
  schemaVersion: SCHEMA_VERSION3,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.43-beta.1",
4
- "publishedAt": 1785983688138
3
+ "version": "1.1.43-beta.2",
4
+ "publishedAt": 1785994362313
5
5
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "siluzan-tso/google-period-report.schema.json",
4
4
  "title": "GooglePeriodReport",
5
- "description": "Google Ads 周期分析报告 JSON(对齐 google-period-report.md 8 默认维度;供 google-analysis render 注入 HTML)。Agent 只需撰写 narrative 字段,kpis/charts/tables 可省略由 --snapshot-dir 从 google-analysis 落盘快照自动合并。",
5
+ "description": "Google Ads 周期分析报告 JSON(对齐 google-period-report.md 8 默认维度;供 google-analysis render 注入 HTML)。Agent 只需撰写 narrativekpis/charts/tables --snapshot-dir 从 google-analysis 快照合并。render 校验要求合并后 kpis.cost/impressions/clicks 至少一项为有效数字,否则拒绝出 HTML(避免执行摘要卡片全「—」)。",
6
6
  "type": "object",
7
7
  "required": ["meta", "narrative"],
8
8
  "properties": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "kpis": {
23
23
  "type": "object",
24
- "description": "账户级 KPI(执行摘要 KPI 网格);可省略,由 --snapshot-dir overview 合并",
24
+ "description": "账户级 KPI(执行摘要 KPI 网格);Agent 可省略,但 render 时须经 --snapshot-dir 合并出 cost/impressions/clicks",
25
25
  "properties": {
26
26
  "cost": { "type": "number" },
27
27
  "impressions": { "type": "number" },
@@ -15,7 +15,7 @@
15
15
  | ---------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
16
  | **1. 拉数** | Agent 调 CLI | `google-analysis -a <id> --start <s> --end <e> --json-out ./snap-google --sections <见下方默认维度对应 CLI>` |
17
17
  | **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`),完成筛选、聚合、排序与洞察 |
18
- | **3. 写 JSON** | Agent | 按 `assets/google-period-report.schema.json` 撰写 `google-period-report.json`:仅 `meta` + `narrative`(8 维度叙事)为必填,`kpis`/`charts`/`tables` 可省略由 CLI 自动合并 |
18
+ | **3. 写 JSON** | Agent | 按 `assets/google-period-report.schema.json` 撰写 `google-period-report.json`:只写 `meta` + `narrative`(8 维度叙事);`kpis`/`charts`/`tables` 留给步骤 4 `--snapshot-dir` 合并(合并后缺 KPI 会拒绝出 HTML) |
19
19
  | **4. 渲染 HTML** | CLI | `google-analysis render` — **校验 narrative 8 维度必含字段**,缺项报错不生成 HTML;**禁止** Agent 手写/拼接 HTML |
20
20
 
21
21
  ```bash
@@ -33,7 +33,7 @@ siluzan-tso google-analysis render \
33
33
  `google-period-report.json` 顶层结构:
34
34
 
35
35
  - `meta`:`accountId`(必填)/ `accountName` / `currency` / `startDate` / `endDate`;可省略由 `--snapshot-dir` 从 `overview` + manifest 自动合并。
36
- - `kpis`:账户级 KPI(消耗/展示/点击/转化/CTR/CPC/CPA,含可选 `previousPeriod` 环比);可省略。本期 KPI 由 CLI 优先对 `campaigns` 全系列累加并重算比率(规避 overview/stats 相对 Google API 延迟);无 campaigns 快照时回退 `overview.currentPeriod`。环比仍来自 `overview.previousPeriod`。
36
+ - `kpis`:账户级 KPI(消耗/展示/点击/转化/CTR/CPC/CPA,含可选 `previousPeriod` 环比);Agent 可省略,但 **render 合并后** `cost`/`impressions`/`clicks` 至少一项须为有效数字,否则拒绝出 HTML(避免执行摘要卡片全「—」)。本期 KPI 由 CLI 优先对 `campaigns` 全系列累加并重算比率;无 campaigns 时回退 `overview.currentPeriod`,再回退 `dimension-summary`。环比仍来自 `overview.previousPeriod`。
37
37
  - `charts.dailyTrend`:每日趋势(`dates`/`cost`/`conversions`/`cpa`);可省略由 `daily-metrics` 自动合并。
38
38
  - `tables.{dimensionSummary,campaigns,devices,geographic,keywords}`:对应 §3~§7 表格数据;可省略由同名 `google-analysis` section 自动合并。
39
39
  - `narrative`(**Agent 必填,唯一由 Agent 撰写的内容**):
@@ -53,7 +53,7 @@ siluzan-tso google-analysis render \
53
53
  模板源码:`report-templates/google-period-report.html`(结构与样式,ECharts 渲染逻辑已内联)。
54
54
 
55
55
  `render` 会向输出目录写入 HTML,并注入 `window.__GOOGLE_PERIOD_REPORT__`。
56
- 传 `--snapshot-dir` 时 CLI 自动从快照合并 KPI、每日趋势图表、周期汇总/系列/设备/地域/关键词表格:**数值一律以 CLI 快照覆盖**(`meta` 仅补空)。本期 KPI 优先 `campaigns` 累加;Agent 只写 `narrative`,勿自填 `kpis`/`tables`。
56
+ 传 `--snapshot-dir` 时 CLI 自动从快照合并 KPI、每日趋势图表、周期汇总/系列/设备/地域/关键词表格:**数值一律以 CLI 快照覆盖**(`meta` 仅补空)。本期 KPI 优先 `campaigns` 累加。若 manifest 缺条目但磁盘上仍有对应 `*.json`,CLI 会按文件名自愈补齐。Agent 只写 `narrative`,勿自填 `kpis`/`tables`。
57
57
 
58
58
  ---
59
59
 
@@ -15,7 +15,7 @@
15
15
  | ---------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
16
  | **1. 拉数** | Agent 调 CLI | `google-analysis -a <id> --start <s> --end <e> --json-out ./snap-google --sections <见下方默认维度对应 CLI>` |
17
17
  | **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`),完成筛选、聚合、排序与洞察 |
18
- | **3. 写 JSON** | Agent | 按 `assets/google-period-report.schema.json` 撰写 `google-period-report.json`:仅 `meta` + `narrative`(8 维度叙事)为必填,`kpis`/`charts`/`tables` 可省略由 CLI 自动合并 |
18
+ | **3. 写 JSON** | Agent | 按 `assets/google-period-report.schema.json` 撰写 `google-period-report.json`:只写 `meta` + `narrative`(8 维度叙事);`kpis`/`charts`/`tables` 留给步骤 4 `--snapshot-dir` 合并(合并后缺 KPI 会拒绝出 HTML) |
19
19
  | **4. 渲染 HTML** | CLI | `google-analysis render` — **校验 narrative 8 维度必含字段**,缺项报错不生成 HTML;**禁止** Agent 手写/拼接 HTML |
20
20
 
21
21
  ```bash
@@ -33,7 +33,7 @@ siluzan-tso google-analysis render \
33
33
  `google-period-report.json` 顶层结构:
34
34
 
35
35
  - `meta`:`accountId`(必填)/ `accountName` / `currency` / `startDate` / `endDate`;可省略由 `--snapshot-dir` 从 `overview` + manifest 自动合并。
36
- - `kpis`:账户级 KPI(消耗/展示/点击/转化/CTR/CPC/CPA,含可选 `previousPeriod` 环比);可省略。本期 KPI 由 CLI 优先对 `campaigns` 全系列累加并重算比率(规避 overview/stats 相对 Google API 延迟);无 campaigns 快照时回退 `overview.currentPeriod`。环比仍来自 `overview.previousPeriod`。
36
+ - `kpis`:账户级 KPI(消耗/展示/点击/转化/CTR/CPC/CPA,含可选 `previousPeriod` 环比);Agent 可省略,但 **render 合并后** `cost`/`impressions`/`clicks` 至少一项须为有效数字,否则拒绝出 HTML(避免执行摘要卡片全「—」)。本期 KPI 由 CLI 优先对 `campaigns` 全系列累加并重算比率;无 campaigns 时回退 `overview.currentPeriod`,再回退 `dimension-summary`。环比仍来自 `overview.previousPeriod`。
37
37
  - `charts.dailyTrend`:每日趋势(`dates`/`cost`/`conversions`/`cpa`);可省略由 `daily-metrics` 自动合并。
38
38
  - `tables.{dimensionSummary,campaigns,devices,geographic,keywords}`:对应 §3~§7 表格数据;可省略由同名 `google-analysis` section 自动合并。
39
39
  - `narrative`(**Agent 必填,唯一由 Agent 撰写的内容**):
@@ -53,7 +53,7 @@ siluzan-tso google-analysis render \
53
53
  模板源码:`report-templates/google-period-report.html`(结构与样式,ECharts 渲染逻辑已内联)。
54
54
 
55
55
  `render` 会向输出目录写入 HTML,并注入 `window.__GOOGLE_PERIOD_REPORT__`。
56
- 传 `--snapshot-dir` 时 CLI 自动从快照合并 KPI、每日趋势图表、周期汇总/系列/设备/地域/关键词表格:**数值一律以 CLI 快照覆盖**(`meta` 仅补空)。本期 KPI 优先 `campaigns` 累加;Agent 只写 `narrative`,勿自填 `kpis`/`tables`。
56
+ 传 `--snapshot-dir` 时 CLI 自动从快照合并 KPI、每日趋势图表、周期汇总/系列/设备/地域/关键词表格:**数值一律以 CLI 快照覆盖**(`meta` 仅补空)。本期 KPI 优先 `campaigns` 累加。若 manifest 缺条目但磁盘上仍有对应 `*.json`,CLI 会按文件名自愈补齐。Agent 只写 `narrative`,勿自填 `kpis`/`tables`。
57
57
 
58
58
  ---
59
59
 
@@ -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.43-beta.1'
12
+ $PKG_VERSION = '1.1.43-beta.2'
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.43-beta.1"
12
+ readonly PKG_VERSION="1.1.43-beta.2"
13
13
  readonly CLI_BIN="siluzan-tso"
14
14
  readonly SKILL_LABEL="Siluzan TSO"
15
15
  readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.43-beta.1",
3
+ "version": "1.1.43-beta.2",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",