siluzan-tso-cli 1.1.25-beta.1 → 1.1.25-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 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.25-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.25-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -4611,6 +4611,118 @@ var init_accounts_digest2 = __esm({
4611
4611
  }
4612
4612
  });
4613
4613
 
4614
+ // src/commands/google-analysis/wrap-snapshot-envelope.ts
4615
+ function coerceToRowArray(value) {
4616
+ if (Array.isArray(value)) return value;
4617
+ if (value === null || typeof value !== "object") return [];
4618
+ const o = value;
4619
+ const keys = Object.keys(o);
4620
+ if (keys.length === 0) return [];
4621
+ if (keys.every((k) => /^\d+$/.test(k))) {
4622
+ return keys.sort((a, b) => Number(a) - Number(b)).map((k) => o[k]);
4623
+ }
4624
+ return [];
4625
+ }
4626
+ function asRecord(value) {
4627
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
4628
+ return value;
4629
+ }
4630
+ function readGatewayList(bag) {
4631
+ const items = coerceToRowArray(bag.data);
4632
+ const meta = {};
4633
+ if (typeof bag.code === "number") meta.code = bag.code;
4634
+ if (typeof bag.message === "string") meta.message = bag.message;
4635
+ return { items, ...Object.keys(meta).length > 0 ? { meta } : {} };
4636
+ }
4637
+ function flattenMaterials(payload) {
4638
+ const bag = asRecord(payload);
4639
+ if (!bag) return [];
4640
+ const images = coerceToRowArray(bag.images).map(
4641
+ (row) => asRecord(row) ? { ...row, assetType: "image" } : row
4642
+ );
4643
+ const videos = coerceToRowArray(bag.videos).map(
4644
+ (row) => asRecord(row) ? { ...row, assetType: "video" } : row
4645
+ );
4646
+ return [...images, ...videos];
4647
+ }
4648
+ function wrapGoogleAnalysisSnapshotPayload(section, payload) {
4649
+ let items = [];
4650
+ let record = null;
4651
+ let meta;
4652
+ if (COMPOSITE_SECTIONS.has(section)) {
4653
+ items = flattenMaterials(payload);
4654
+ } else if (RECORD_ONLY_SECTIONS.has(section)) {
4655
+ record = asRecord(payload);
4656
+ } else if (GATEWAY_LIST_SECTIONS.has(section)) {
4657
+ const bag = asRecord(payload);
4658
+ if (bag) {
4659
+ const r = readGatewayList(bag);
4660
+ items = r.items;
4661
+ meta = r.meta;
4662
+ }
4663
+ } else if (ROOT_ARRAY_SECTIONS.has(section)) {
4664
+ items = coerceToRowArray(payload);
4665
+ } else {
4666
+ const rowKey = ROW_KEY_BY_SECTION[section];
4667
+ const bag = asRecord(payload);
4668
+ if (rowKey && bag) {
4669
+ items = coerceToRowArray(bag[rowKey]);
4670
+ } else {
4671
+ const arr = coerceToRowArray(payload);
4672
+ if (arr.length > 0) items = arr;
4673
+ else record = asRecord(payload);
4674
+ }
4675
+ }
4676
+ return {
4677
+ schemaVersion: GOOGLE_ANALYSIS_FILE_SCHEMA_VERSION,
4678
+ section,
4679
+ itemCount: items.length,
4680
+ items,
4681
+ record,
4682
+ ...meta !== void 0 ? { meta } : {}
4683
+ };
4684
+ }
4685
+ var GOOGLE_ANALYSIS_FILE_SCHEMA_VERSION, ROW_KEY_BY_SECTION, RECORD_ONLY_SECTIONS, COMPOSITE_SECTIONS, ROOT_ARRAY_SECTIONS, GATEWAY_LIST_SECTIONS;
4686
+ var init_wrap_snapshot_envelope = __esm({
4687
+ "src/commands/google-analysis/wrap-snapshot-envelope.ts"() {
4688
+ "use strict";
4689
+ GOOGLE_ANALYSIS_FILE_SCHEMA_VERSION = 3;
4690
+ ROW_KEY_BY_SECTION = {
4691
+ keywords: "keywords",
4692
+ campaigns: "campaigns",
4693
+ devices: "devices",
4694
+ geographic: "countries",
4695
+ "geo-matched": "countries",
4696
+ "campaign-geo": "countries",
4697
+ "campaign-geo-matched": "countries",
4698
+ "campaign-device": "devices",
4699
+ audience: "audience"
4700
+ };
4701
+ RECORD_ONLY_SECTIONS = /* @__PURE__ */ new Set([
4702
+ "overview",
4703
+ "resource-counts",
4704
+ "dimension-summary",
4705
+ "gold-account",
4706
+ "final-urls",
4707
+ "campaign-types",
4708
+ "ads-index"
4709
+ ]);
4710
+ COMPOSITE_SECTIONS = /* @__PURE__ */ new Set(["materials"]);
4711
+ ROOT_ARRAY_SECTIONS = /* @__PURE__ */ new Set([
4712
+ "campaign-hour",
4713
+ "extensions",
4714
+ "asset-images",
4715
+ "videos",
4716
+ "daily-metrics",
4717
+ "conversion-actions"
4718
+ ]);
4719
+ GATEWAY_LIST_SECTIONS = /* @__PURE__ */ new Set([
4720
+ "search-terms",
4721
+ "ads"
4722
+ ]);
4723
+ }
4724
+ });
4725
+
4614
4726
  // src/utils/snapshot/google-analysis.ts
4615
4727
  import * as fs7 from "fs/promises";
4616
4728
  import * as path11 from "path";
@@ -4618,7 +4730,7 @@ function googleAnalysisManifestFile(accountId) {
4618
4730
  return `${applyIdSuffix("manifest", accountId)}.json`;
4619
4731
  }
4620
4732
  function buildOutlineHints(section) {
4621
- const hints = [];
4733
+ const hints = [...GOOGLE_ANALYSIS_ENVELOPE_OUTLINE_HINTS];
4622
4734
  if (RATE_BEARING_SECTIONS.has(section)) {
4623
4735
  hints.push(...GOOGLE_ANALYSIS_RATE_NORMALIZED_OUTLINE_HINTS);
4624
4736
  }
@@ -4656,7 +4768,8 @@ async function readManifestIfExists(absDir, accountId) {
4656
4768
  try {
4657
4769
  const raw = await fs7.readFile(manifestPath, "utf8");
4658
4770
  const parsed = JSON.parse(raw);
4659
- if (parsed?.schemaVersion !== SCHEMA_VERSION2 || !Array.isArray(parsed.artifacts)) {
4771
+ const sv = parsed?.schemaVersion;
4772
+ if (sv !== 2 && sv !== SCHEMA_VERSION2 || !Array.isArray(parsed.artifacts)) {
4660
4773
  continue;
4661
4774
  }
4662
4775
  return parsed;
@@ -4696,12 +4809,16 @@ async function writeGoogleAnalysisSnapshot(params) {
4696
4809
  );
4697
4810
  const relativeFile = fileName;
4698
4811
  const writtenAt = (/* @__PURE__ */ new Date()).toISOString();
4699
- const body = JSON.stringify(params.payload, null, 2);
4812
+ const wrapped = wrapGoogleAnalysisSnapshotPayload(
4813
+ params.section,
4814
+ params.payload
4815
+ );
4816
+ const body = JSON.stringify(wrapped, null, 2);
4700
4817
  await fs7.writeFile(path11.join(absDir, fileName), `${body}
4701
4818
  `, "utf8");
4702
4819
  const outlineFileName = snapshotOutlineFileName(fileName);
4703
4820
  const outlineExtra = buildOutlineHints(params.section);
4704
- const outlineBody = formatOutlineFileBody(fileName, params.payload, outlineExtra);
4821
+ const outlineBody = formatOutlineFileBody(fileName, wrapped, outlineExtra);
4705
4822
  await fs7.writeFile(path11.join(absDir, outlineFileName), `${outlineBody}
4706
4823
  `, "utf8");
4707
4824
  const existing = await readManifestIfExists(absDir, accountSlug || void 0);
@@ -4735,15 +4852,16 @@ async function writeGoogleAnalysisSnapshot(params) {
4735
4852
  agentHint: OUTLINE_AGENT_HINT
4736
4853
  };
4737
4854
  }
4738
- var LEGACY_MANIFEST_FILE, CLI_PACKAGE2, SCHEMA_VERSION2, DEFAULT_FIELD_GUIDE2, GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS, GOOGLE_ANALYSIS_CAMPAIGNS_COMPETITIVE_METRICS_HINTS, RATE_BEARING_SECTIONS, GOOGLE_ANALYSIS_RATE_NORMALIZED_OUTLINE_HINTS, GOOGLE_ANALYSIS_ZH_FIELD_HINTS_BY_SECTION;
4855
+ var LEGACY_MANIFEST_FILE, CLI_PACKAGE2, SCHEMA_VERSION2, DEFAULT_FIELD_GUIDE2, GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS, GOOGLE_ANALYSIS_CAMPAIGNS_COMPETITIVE_METRICS_HINTS, RATE_BEARING_SECTIONS, GOOGLE_ANALYSIS_RATE_NORMALIZED_OUTLINE_HINTS, GOOGLE_ANALYSIS_ENVELOPE_OUTLINE_HINTS, GOOGLE_ANALYSIS_ZH_FIELD_HINTS_BY_SECTION;
4739
4856
  var init_google_analysis = __esm({
4740
4857
  "src/utils/snapshot/google-analysis.ts"() {
4741
4858
  "use strict";
4742
4859
  init_cli_json();
4860
+ init_wrap_snapshot_envelope();
4743
4861
  init_dir();
4744
4862
  LEGACY_MANIFEST_FILE = "manifest.json";
4745
4863
  CLI_PACKAGE2 = "siluzan-tso-cli";
4746
- SCHEMA_VERSION2 = 2;
4864
+ SCHEMA_VERSION2 = 3;
4747
4865
  DEFAULT_FIELD_GUIDE2 = {
4748
4866
  markdownRefs: ["references/accounts/currency.md", "references/analytics/account-analytics.md"],
4749
4867
  tsTypesModule: "tso-cli/src/types/google-analysis-api.ts"
@@ -4782,6 +4900,10 @@ var init_google_analysis = __esm({
4782
4900
  "// \u6982\u7387\u5B57\u6BB5\uFF1A`ctr` / `conversionRate` \u81EA manifest schemaVersion 2\uFF082026-05\uFF09\u8D77\u5DF2\u7531 CLI \u7EDF\u4E00\u5F52\u4E00\u4E3A **0~1 \u5C0F\u6570**\uFF08\u5982 `0.0964` = 9.64%\uFF09\u3002",
4783
4901
  "// \u5199 Excel 0~1 \u5C0F\u6570\u5217\uFF1A\u76F4\u63A5\u5199\u5165\uFF1B\u5199\u300Cx%\u300D\u6587\u6848\uFF1A`(v * 100).toFixed(2) + '%'`\uFF1B**\u7981\u6B62**\u518D \xF7100\u3002`interactionRate` \u4ECD\u662F\u5B57\u7B26\u4E32 0~1 \u5C0F\u6570\uFF0C`parseFloat` \u540E\u4F7F\u7528\u3002"
4784
4902
  ];
4903
+ GOOGLE_ANALYSIS_ENVELOPE_OUTLINE_HINTS = [
4904
+ "// \u7EDF\u4E00\u8BBF\u95EE\u5C42\uFF08manifest / \u6587\u4EF6\u5185\u5747\u4E3A `schemaVersion: 3`\uFF09\uFF1A\u5224\u522B\u53EA\u6709\u4E00\u6761\u2014\u2014`record` \u975E null \u5373**\u6C47\u603B\u7EF4\u5EA6**\uFF08overview/resource-counts/gold-account/ads-index/dimension-summary/final-urls/campaign-types\uFF09\uFF0C\u8BFB `record`\uFF1B\u5426\u5219\u8BFB `items[]`\uFF08\u6240\u6709\u5217\u8868\u7EF4\u5EA6\uFF0C\u542B materials\uFF0C\u884C\u5DF2\u7EDF\u4E00\u642C\u5165\uFF0C`itemCount` = `items.length`\uFF09\u3002",
4905
+ "// \u7EF4\u5EA6\u4E13\u5C5E\u539F\u952E\u540D\uFF08keywords/campaigns/countries/data \u7B49\uFF09\u5DF2**\u79FB\u9664**\uFF0C\u4E0D\u8981\u518D\u6309\u7EF4\u5EA6\u731C\u952E\u540D\uFF1B`search-terms`/`ads` \u7684\u7F51\u5173 `code`/`message` \u5728 `meta`\uFF08\u4E0E\u6570\u636E\u65E0\u5173\uFF09\uFF1Bmaterials \u884C\u5E26 `assetType: 'image'|'video'`\u3002"
4906
+ ];
4785
4907
  GOOGLE_ANALYSIS_ZH_FIELD_HINTS_BY_SECTION = {
4786
4908
  keywords: [
4787
4909
  "// \u4E2D\u6587\u8BD1\u540D\uFF1A`keywordMatchTypeZh`\uFF08\u7531 `keywordMatchType` \u7ECF match-type-en2zh.json \u7FFB\u8BD1\uFF0C\u8986\u76D6 BROAD/PHRASE/EXACT \u4E0E Broad/Phrase/Exact \u5927\u5C0F\u5199\uFF1B\u672A\u547D\u4E2D\u65F6\u8BE5\u5B57\u6BB5\u7F3A\u7701\uFF09\u3002"
@@ -102574,6 +102696,7 @@ var init_google_analysis2 = __esm({
102574
102696
  init_normalize_rates();
102575
102697
  init_normalize_impression_shares();
102576
102698
  init_translate_fields();
102699
+ init_wrap_snapshot_envelope();
102577
102700
  init_register_cli();
102578
102701
  }
102579
102702
  });
@@ -112931,7 +113054,7 @@ init_cli_table();
112931
113054
  // src/commands/ad/campaign-batch-diff.ts
112932
113055
  init_auth();
112933
113056
  init_cli_json_snapshot();
112934
- function asRecord(v) {
113057
+ function asRecord2(v) {
112935
113058
  return v && typeof v === "object" && !Array.isArray(v) ? v : null;
112936
113059
  }
112937
113060
  function pickString(...vals) {
@@ -112985,7 +113108,7 @@ function liveAdHasHeadline(ad, headline) {
112985
113108
  function formatExtensionPlanned(ext) {
112986
113109
  if (!ext) return "\u2014";
112987
113110
  const type = pickString(ext["typeV2"], ext["AssetFieldType"], ext["type"]);
112988
- const props = asRecord(ext["Properties"]);
113111
+ const props = asRecord2(ext["Properties"]);
112989
113112
  const phone = props ? pickString(props["PhoneNumber"], props["phoneNumber"]) : "";
112990
113113
  const code = props ? pickString(props["ContryCode"], props["CountryCode"]) : "";
112991
113114
  const parts = [type ? `\u7C7B\u578B: ${type}` : "", code || phone ? `\u7535\u8BDD: ${code}${phone}` : ""].filter(Boolean);
@@ -112996,13 +113119,13 @@ function listPlannedKeywords(campaign) {
112996
113119
  if (!Array.isArray(groups)) return [];
112997
113120
  const out = [];
112998
113121
  for (let gi = 0; gi < groups.length; gi++) {
112999
- const g = asRecord(groups[gi]);
113122
+ const g = asRecord2(groups[gi]);
113000
113123
  if (!g) continue;
113001
113124
  const groupName = pickString(g["Name"], g["name"]) || `AdGroupsForBatchJob[${gi}]`;
113002
113125
  const blocks = g["KeywordsForBatchJob"];
113003
113126
  if (!Array.isArray(blocks)) continue;
113004
113127
  for (let bi = 0; bi < blocks.length; bi++) {
113005
- const block = asRecord(blocks[bi]);
113128
+ const block = asRecord2(blocks[bi]);
113006
113129
  if (!block) continue;
113007
113130
  const matchTypeV2 = pickString(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113008
113131
  const texts = block["KeywordText"];
@@ -113026,7 +113149,7 @@ function listPlannedNegativeKeywords(campaign) {
113026
113149
  if (!Array.isArray(blocks)) return [];
113027
113150
  const out = [];
113028
113151
  for (let bi = 0; bi < blocks.length; bi++) {
113029
- const block = asRecord(blocks[bi]);
113152
+ const block = asRecord2(blocks[bi]);
113030
113153
  if (!block) continue;
113031
113154
  const matchTypeV2 = pickString(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113032
113155
  const texts = block["KeywordText"];
@@ -113066,7 +113189,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113066
113189
  const plannedGroupCount = Array.isArray(groups) ? groups.length : 0;
113067
113190
  if (Array.isArray(groups)) {
113068
113191
  for (let gi = 0; gi < groups.length; gi++) {
113069
- const g = asRecord(groups[gi]);
113192
+ const g = asRecord2(groups[gi]);
113070
113193
  if (!g) continue;
113071
113194
  const groupName = pickString(g["Name"], g["name"]) || `AdGroupsForBatchJob[${gi}]`;
113072
113195
  const groupPath = `campaign.AdGroupsForBatchJob[${gi}]`;
@@ -113097,7 +113220,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113097
113220
  const blocks = g["KeywordsForBatchJob"];
113098
113221
  if (Array.isArray(blocks)) {
113099
113222
  for (let bi = 0; bi < blocks.length; bi++) {
113100
- const block = asRecord(blocks[bi]);
113223
+ const block = asRecord2(blocks[bi]);
113101
113224
  if (!block) continue;
113102
113225
  const matchTypeV2 = pickString(block["MatchTypeV2"], block["matchTypeV2"]) || "BROAD";
113103
113226
  const texts = block["KeywordText"];
@@ -113126,7 +113249,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113126
113249
  const ads = g["AdsForBatchJob"];
113127
113250
  if (Array.isArray(ads)) {
113128
113251
  for (let ai = 0; ai < ads.length; ai++) {
113129
- const ad = asRecord(ads[ai]);
113252
+ const ad = asRecord2(ads[ai]);
113130
113253
  if (!ad) continue;
113131
113254
  const path24 = `${groupPath}.AdsForBatchJob[${ai}]`;
113132
113255
  const primary = pickString(ad["headlinePart1"], ad["AdTitle"]);
@@ -113182,7 +113305,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113182
113305
  ).length;
113183
113306
  if (plannedExt > liveExt && Array.isArray(extBlocks)) {
113184
113307
  for (let ei = liveExt; ei < extBlocks.length; ei++) {
113185
- const ext = asRecord(extBlocks[ei]);
113308
+ const ext = asRecord2(extBlocks[ei]);
113186
113309
  missing.push({
113187
113310
  layer: "extension",
113188
113311
  path: `campaign.ExtensionsForBatchJob[${ei}]`,
@@ -113210,7 +113333,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
113210
113333
  plannedKeywords: plannedKeywords.length,
113211
113334
  liveKeywords: liveKwCount,
113212
113335
  plannedAds: Array.isArray(groups) ? groups.reduce((n, g) => {
113213
- const gr = asRecord(g);
113336
+ const gr = asRecord2(g);
113214
113337
  const ads = gr?.["AdsForBatchJob"];
113215
113338
  return n + (Array.isArray(ads) ? ads.length : 0);
113216
113339
  }, 0) : 0,
@@ -113284,7 +113407,7 @@ async function fetchLiveSnapshotForCampaign(config, accountId, campaignId, campa
113284
113407
  };
113285
113408
  }
113286
113409
  function resolveCampaignIdFromBatch(record) {
113287
- const campaign = asRecord(record["campaign"]);
113410
+ const campaign = asRecord2(record["campaign"]);
113288
113411
  return pickString(campaign?.["Id"], campaign?.["id"], record["campaignId"]);
113289
113412
  }
113290
113413
  function shellArgPath(p) {
@@ -121390,7 +121513,7 @@ function readJsonFile(filePath) {
121390
121513
  }
121391
121514
  });
121392
121515
  }
121393
- function asRecord2(value) {
121516
+ function asRecord3(value) {
121394
121517
  if (value && typeof value === "object" && !Array.isArray(value)) {
121395
121518
  return value;
121396
121519
  }
@@ -121416,10 +121539,10 @@ function injectReportData(html, payload) {
121416
121539
  async function runWebsiteDiagnosisRender(opts) {
121417
121540
  const dataPath = path17.resolve(opts.dataFile);
121418
121541
  const dataRaw = await readJsonFile(dataPath);
121419
- let data = asRecord2(dataRaw);
121542
+ let data = asRecord3(dataRaw);
121420
121543
  if (opts.collectFile) {
121421
121544
  const collectRaw = await readJsonFile(path17.resolve(opts.collectFile));
121422
- data = mergeCollectLighthouse(data, asRecord2(collectRaw));
121545
+ data = mergeCollectLighthouse(data, asRecord3(collectRaw));
121423
121546
  }
121424
121547
  const templatePath = websiteDiagnosisReportTemplatePath();
121425
121548
  let html;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.25-beta.1",
4
- "publishedAt": 1780455474013
3
+ "version": "1.1.25-beta.3",
4
+ "publishedAt": 1780474555353
5
5
  }
@@ -257,7 +257,7 @@ siluzan-tso account-history --start 2026-03-01 --end 2026-03-31 --json-out ./sna
257
257
  | 状态 | 含义 | 下一步操作 |
258
258
  | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
259
259
  | `Pending` | 审核中 | 等待,可反复运行此命令轮询;审核周期因媒体而异 |
260
- | `Approved` | 审核通过 | 运行 `list-accounts -m <媒体>` 确认账户已出现;引导用户充值激活(`config show` 取 `webUrl`,打开 `{webUrl}/v3/foreign_trade/tso/recharge`) |
260
+ | `Approved` | 审核通过 | 运行 `list-accounts -m <媒体>` 确认账户已出现;引导用户充值激活(`config show` 取 `webUrl`,按 `finance.md` 打开对应媒体充值页;例如 Google 为 `{webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=Google`;Kwai、Yandex 当前没有对应充值界面) |
261
261
  | `Rejected` | 被拒 | 查看 `--json-out` 落盘中的 `reason` 字段了解拒绝原因;修改资料后重新提交;若原因不明,引导用户联系丝路赞客服 |
262
262
 
263
263
  ---
@@ -34,7 +34,7 @@
34
34
  1. **`list-accounts`**(推荐第一步):`items[].ma.currencyCode`;表格有 **「币种」** 列。
35
35
  2. **`balance` / `balance-scan` / `accounts-digest`**:`items[].currencyCode` 或行内已格式化的金额(含代码)。
36
36
  3. **`stats`**:`items[].currencyCode`(Google 含今天窗口时可能无币种,见 `accounts/accounts.md` 时效性说明)。
37
- 4. **`google-analysis --sections overview`**:`overview-*.json` 根级或 `account.currencyCode`。
37
+ 4. **`google-analysis --sections overview`**:`overview-*.json` `record.currencyCode`(汇总维度,`schemaVersion 3` 起整块在 `record`)。
38
38
  5. **`ad campaigns` / `ad groups`**:JSON 内 `currencyCode`(与账户主币种一致)。
39
39
  6. **发票/充值**:`invoice billable` 的 `currencyCode`;人民币订单与美金订单开票类型不同,见 `accounts/finance.md`。
40
40
 
@@ -105,36 +105,86 @@ siluzan-tso config show
105
105
 
106
106
  ### 功能总览
107
107
 
108
- | 功能 | 引导用户打开的网页路径 | CLI 支持 |
109
- | ---------------------------- | ------------------------------------ | --------------------------------------- |
110
- | 现金充值(单笔) | `/recharge/pay` | ❌ 引导网页 |
111
- | 现金充值(批量) | `/recharge/pay_batch` | ❌ 引导网页 |
112
- | 月结充值 | `/recharge/accountBillingQuota` | ❌ 引导网页 |
113
- | 丝路赞钱包(充值/提现/明细) | `/recharge/siluzanWallet` | ❌ 引导网页 |
114
- | 媒体转账记录 | `/recharge/accountTransfer` | ✅ `transfer` 命令 |
115
- | 开票记录 | `/recharge/invoiceList` | ✅ `invoice list` |
116
- | 开票申请列表 | `/recharge/invoicingApplicationList` | ✅ `invoice billable` / `invoice apply` |
108
+ | 功能 | 引导用户打开的网页路径 | CLI 支持 |
109
+ | ---------------------------- | ----------------------------------------------------------------------- | --------------------------------------- |
110
+ | 传统充值/现金充值(单笔,仅 Google/TikTok/Meta/Microsoft) | `/v3/foreign_trade/tso/recharge/pay?mediaType=<mediaType>` | ❌ 引导网页 |
111
+ | 现金充值(批量,仅 Google/TikTok) | `/v3/foreign_trade/tso/recharge/pay_batch?mediaType=<Google|TikTok>` | ❌ 引导网页 |
112
+ | 月结充值(仅 Google/TikTok/Meta/Microsoft) | `/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=<mediaType>` | ❌ 引导网页 |
113
+ | 丝路赞钱包(充值/提现/明细) | `/v3/foreign_trade/tso/recharge/siluzanWallet` | ❌ 引导网页 |
114
+ | 媒体转账记录 | `/v3/foreign_trade/tso/recharge/accountTransfer` | ✅ `transfer` 命令 |
115
+ | 开票记录 | `/v3/foreign_trade/tso/recharge/invoiceList` | ✅ `invoice list` |
116
+ | 开票申请列表 | `/v3/foreign_trade/tso/recharge/invoicingApplicationList` | ✅ `invoice billable` / `invoice apply` |
117
+
118
+ ### 充值页媒体参数
119
+
120
+ 当用户指明媒体平台时,优先给出对应媒体的传统充值/现金充值直达页:
121
+
122
+ | 媒体 | `mediaType` 参数 | 传统充值/现金充值链接 |
123
+ | ---- | ---------------- | ------------ |
124
+ | Google | `Google` | `{webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=Google` |
125
+ | TikTok | `TikTok` | `{webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=TikTok` |
126
+ | Meta / Facebook | `MetaAd` | `{webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=MetaAd` |
127
+ | Microsoft / Bing | `MicrosoftAd` | `{webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=MicrosoftAd` |
128
+
129
+ > Yandex、Kwai 当前没有传统充值/现金充值界面;用户询问这些媒体充值时,不要拼接充值页链接,应说明当前网页不提供对应充值入口,并建议联系丝路赞客服或业务负责人确认处理方式。
130
+
131
+ 批量充值页面当前仅支持 Google 和 TikTok:
132
+
133
+ | 媒体 | 批量充值链接 |
134
+ | ---- | ------------ |
135
+ | Google | `{webUrl}/v3/foreign_trade/tso/recharge/pay_batch?mediaType=Google` |
136
+ | TikTok | `{webUrl}/v3/foreign_trade/tso/recharge/pay_batch?mediaType=TikTok` |
137
+
138
+ > Meta、Microsoft/Bing、Yandex、Kwai 当前不要引导到批量充值页。
139
+
140
+ 月结充值页面当前仅支持 Google、TikTok、Meta 和 Microsoft/Bing:
141
+
142
+ | 媒体 | 月结充值链接 |
143
+ | ---- | ------------ |
144
+ | Google | `{webUrl}/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=Google` |
145
+ | TikTok | `{webUrl}/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=TikTok` |
146
+ | Meta / Facebook | `{webUrl}/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=MetaAd` |
147
+ | Microsoft / Bing | `{webUrl}/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=MicrosoftAd` |
148
+
149
+ > Yandex、Kwai 当前不要引导到月结充值页。
117
150
 
118
151
  ### 引导用户的标准话术
119
152
 
120
- 当用户需要充值/查看钱包时,先取 `webUrl`,再给出完整链接:
153
+ 当用户需要充值/查看钱包时,先取 `webUrl`,再给出完整链接。用户已指定媒体时,优先给对应媒体的单笔充值链接;未指定媒体时,先询问媒体或给出下列通用入口:
121
154
 
122
155
  ```
123
156
  需要进行充值,请访问丝路赞平台对应页面完成操作:
124
157
 
125
- - 现金充值(单笔):{webUrl}/recharge/pay
126
- - 现金充值(批量):{webUrl}/recharge/pay_batch
127
- - 月结充值: {webUrl}/recharge/accountBillingQuota
128
- - 丝路赞钱包: {webUrl}/recharge/siluzanWallet
158
+ - Google 充值: {webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=Google
159
+ - TikTok 充值: {webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=TikTok
160
+ - Meta 充值: {webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=MetaAd
161
+ - Microsoft/Bing 充值:{webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=MicrosoftAd
162
+ - Google 批量充值: {webUrl}/v3/foreign_trade/tso/recharge/pay_batch?mediaType=Google
163
+ - TikTok 批量充值: {webUrl}/v3/foreign_trade/tso/recharge/pay_batch?mediaType=TikTok
164
+ - Google 月结充值: {webUrl}/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=Google
165
+ - TikTok 月结充值: {webUrl}/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=TikTok
166
+ - Meta 月结充值: {webUrl}/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=MetaAd
167
+ - Microsoft/Bing 月结充值:{webUrl}/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=MicrosoftAd
168
+ - 丝路赞钱包: {webUrl}/v3/foreign_trade/tso/recharge/siluzanWallet
169
+
170
+ Yandex、Kwai 当前没有传统充值/现金充值和月结充值界面,需联系丝路赞客服或业务负责人确认处理方式。
171
+ 批量充值当前仅支持 Google 和 TikTok;Meta、Microsoft/Bing、Yandex、Kwai 不要引导到批量充值页。
129
172
  ```
130
173
 
131
174
  **示例:**
132
175
 
133
176
  ```
134
- - 现金充值(单笔):https://www-ci.siluzan.com/recharge/pay
135
- - 现金充值(批量):https://www-ci.siluzan.com/recharge/pay_batch
136
- - 月结充值: https://www-ci.siluzan.com/recharge/accountBillingQuota
137
- - 丝路赞钱包: https://www-ci.siluzan.com/recharge/siluzanWallet
177
+ - Google 充值:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google
178
+ - TikTok 充值:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=TikTok
179
+ - Meta 充值:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=MetaAd
180
+ - Microsoft/Bing 充值:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=MicrosoftAd
181
+ - Google 批量充值:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay_batch?mediaType=Google
182
+ - TikTok 批量充值:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay_batch?mediaType=TikTok
183
+ - Google 月结充值:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=Google
184
+ - TikTok 月结充值:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=TikTok
185
+ - Meta 月结充值:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=MetaAd
186
+ - Microsoft/Bing 月结充值:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/accountBillingQuota?mediaType=MicrosoftAd
187
+ - 丝路赞钱包:https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/siluzanWallet
138
188
  ```
139
189
 
140
190
  ---
@@ -282,7 +282,7 @@ siluzan-tso list-accounts -m MetaAd
282
282
  | 状态 | 含义 | 下一步 |
283
283
  | ---- | ---- | ------ |
284
284
  | 审核中 | 等待媒体审核 | 继续 `account-history` 轮询 |
285
- | 已通过 | 账户可用 | `list-accounts` 确认 + 网页充值 |
285
+ | 已通过 | 账户可用 | `list-accounts` 确认 + `finance.md` 打开对应媒体充值页(传统充值/月结充值仅 Google/TikTok/Meta/Microsoft 有页面;Kwai、Yandex 当前没有对应充值界面) |
286
286
  | 已拒绝 | 资料问题 | 查看拒绝原因,修正后重新提交 |
287
287
 
288
288
  完整参数:`siluzan-tso open-account <subcommand> -h`
@@ -60,7 +60,7 @@ siluzan-tso open-account google-wizard
60
60
 
61
61
  ```bash
62
62
  siluzan-tso account-history -m Google
63
- # 审核通过后:config show → {webUrl}/v3/foreign_trade/tso/recharge
63
+ # 审核通过后:config show → {webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=Google
64
64
  ```
65
65
 
66
66
  ---
@@ -140,7 +140,7 @@ siluzan-tso google-analysis -a <id> --exclude materials,gold-account --json-out
140
140
  | `keywords` | 关键词;默认 `costGreater=0`(仅有消耗)、`limit=0`(不封顶)、`orderByCost=true`;可用 `--limit` 限制 Top N |
141
141
  | `search-terms` | 搜索词;默认 `limit=0`(不封顶)、`orderByCost=true`;落盘前过滤 `spend>0`;含 `queryTargetingStatus` / `queryTargetingStatusZh`(已添加/已排除/都没有);可用 `--limit` 限制 Top N |
142
142
  | `campaigns` | 广告系列 |
143
- | `campaign-hour` | 系列按小时(根为 JSON 数组) |
143
+ | `campaign-hour` | 系列按小时(行在 `items[]`) |
144
144
  | `ads` | 广告;与 `ad list` 同源 |
145
145
  | `extensions` | 附加信息;可选 `--level` |
146
146
  | `devices` | 设备分布(账户级 `DeviceSectionData`) |
@@ -152,7 +152,7 @@ siluzan-tso google-analysis -a <id> --exclude materials,gold-account --json-out
152
152
  | `audience` | 受众;可选 `--audience-type` |
153
153
  | `asset-images` | 图片素材 |
154
154
  | `videos` | 视频 |
155
- | `materials` | 合并图片+视频 `{ images, videos }` |
155
+ | `materials` | 图片+视频拍平进 `items[]`,每行带 `assetType: 'image' \| 'video'` |
156
156
  | `resource-counts` | 结构统计 |
157
157
  | `conversion-actions` | 转化动作 |
158
158
  | `daily-metrics` | 按日指标(主平台 `GET …/report/media-account/google/account-daily-reports`,`--json-out` 落盘为按日数组)。**广告诊断报告**中:金额/CPA **保留 2 位小数**,转化/点击/展示为整数;须配趋势**分析**段落(见 `google-ads-diagnosis.md`) |
@@ -162,6 +162,33 @@ siluzan-tso google-analysis -a <id> --exclude materials,gold-account --json-out
162
162
  | `dimension-summary` | 账户汇总 |
163
163
  | `campaign-types` | 系列类型(实时,可查当天;不传 `--start`/`--end`) |
164
164
 
165
+ ### 落盘 JSON
166
+
167
+ | 字段 | 含义 |
168
+ | ---- | ---- |
169
+ | `schemaVersion` | 文件内固定 `3` |
170
+ | `section` | 维度名(与文件名 stem 一致;仅标识,读数据用不到) |
171
+ | `itemCount` | `items.length` |
172
+ | `items` | **所有列表维度的行数据**统一入口(含 `materials`);汇总维度为 `[]` |
173
+ | `record` | **汇总维度**的整块对象;列表维度为 `null` |
174
+ | `meta` | 仅 `search-terms` / `ads`:网关 `code`/`message`(与数据无关,可忽略) |
175
+
176
+ **唯一判别规则**:`record` 非 `null` → 读 `record`(汇总维度);否则 → 读 `items[]`(列表维度)。
177
+
178
+ | 桶 | 维度 | 读法 |
179
+ | -- | ---- | ---- |
180
+ | **列表** | `keywords`、`search-terms`、`campaigns`、`campaign-hour`、`ads`、`extensions`、`devices`、`geographic`、`geo-matched`、`campaign-geo`、`campaign-geo-matched`、`campaign-device`、`audience`、`asset-images`、`videos`、`materials`、`daily-metrics`、`conversion-actions` | `d.items[]` |
181
+ | **汇总** | `overview`、`resource-counts`、`dimension-summary`、`gold-account`、`ads-index`、`final-urls`、`campaign-types` | `d.record` |
182
+
183
+ 脚本示例(Node)—— 所有维度同一套读法:
184
+
185
+ ```javascript
186
+ const d = require("./snap/campaigns-9526903813_20260401-20260430.json");
187
+ const data = d.record ?? d.items; // record 非空=汇总维度;否则=列表行
188
+ ```
189
+
190
+ 旧快照(manifest `schemaVersion` 1/2,无 `items`/`record`)须按 `*.outline.txt` 最后一行类型读取。
191
+
165
192
  ### stdout 摘要
166
193
 
167
194
  ```json
@@ -196,24 +223,11 @@ siluzan-tso google-analysis -a <id> --exclude materials,gold-account --json-out
196
223
  | 平均点击成本 | `averageCpc` |
197
224
  | 转化成本 | `costPerConversion` |
198
225
 
199
- ### 中文化字段(En→Zh 字典补充)
200
-
201
- CLI 在落盘前为以下维度自动补「中文译名字段」(**原英文字段保留**,便于排错;字典未命中时该字段缺省):
202
-
203
- | 维度 | 源字段(英文) | 新增字段(中文) | 字典源 |
204
- | -------------- | ------------------ | -------------------- | --------------------------------------------------------------- |
205
- | `keywords` | `keywordMatchType` | `keywordMatchTypeZh` | `match-type-en2zh.json`(覆盖 `BROAD/PHRASE/EXACT` 大小写写法) |
206
- | `search-terms` | `matchType` | `matchTypeZh` | `match-type-en2zh.json` |
207
- | `search-terms` | `queryTargetingStatus` | `queryTargetingStatusZh` | 网关 `Added`→已添加、`Excluded`→已排除、`None`→都没有(写 Excel「已添加/已排除」列用此字段) |
208
- | `campaign-geo` | `countryOrRegion` | `countryOrRegionZh` | `geo-en2zh.json`(覆盖 105 个国家/地区) |
209
- | `geo-matched` | `countryOrRegion` | `countryOrRegionZh` | `geo-en2zh.json` |
210
- | `campaign-geo-matched` | `countryOrRegion` | `countryOrRegionZh` | `geo-en2zh.json` |
211
-
212
- 写 Excel / 报表脚本可直接读 `keywordMatchTypeZh` / `matchTypeZh` / `countryOrRegionZh` 输出中文,**不要**自己维护字典或在线翻译。判断覆盖率用 `if (row.xxxZh)` 即可。
213
-
214
226
  ### CampaignSectionData 关键字段
215
227
 
216
- `campaigns[]` 每行额外包含:`conversionsValue`、`conversionsValuePerCost`(`spend ≤ 0` 时为 0)、`campaignTargetCpaYuan`、`maximizeConversionsTargetCpaYuan`、`manualCpcEnhancedCpcEnabled`、`percentCpcEnhancedCpcEnabled`。所有金额字段(`*Yuan` 后缀)已统一为元,可直接展示,无需换算。
228
+ > `campaigns` 维度的系列行在 `items[]`(`schemaVersion 3` 起统一信封;本节下文的 `campaigns[]` 即指 `items[]`)。
229
+
230
+ `items[]` 每行额外包含:`conversionsValue`、`conversionsValuePerCost`(`spend ≤ 0` 时为 0)、`campaignTargetCpaYuan`、`maximizeConversionsTargetCpaYuan`、`manualCpcEnhancedCpcEnabled`、`percentCpcEnhancedCpcEnabled`。所有金额字段(`*Yuan` 后缀)已统一为元,可直接展示,无需换算。
217
231
 
218
232
  日预算字段为 `budgetAmountYuan`(元),脚本可直接 `row.budgetAmountYuan`;网关原始 `budgetAmount`(分)已不再落盘。
219
233
 
@@ -239,11 +253,11 @@ CLI 在落盘前为以下维度自动补「中文译名字段」(**原英文
239
253
 
240
254
  #### 行顶 legacy 份额(与 `competitiveMetrics` 同名 3 项)
241
255
 
242
- `campaigns[]` / `keywords[]` 行顶仍保留 `searchImpressionShare`、`searchBudgetLostImpressionShare`、`searchRankLostImpressionShare`。CLI 落盘前已统一为 **0~1 小数**(与 `competitiveMetrics` 同名项一致;有嵌套时以 `competitiveMetrics` 为准)。Top/AbsoluteTop/Content/ClickShare 等扩展项仅存在于 `competitiveMetrics`。展示为百分比:`(v * 100).toFixed(2) + '%'`。
256
+ `campaigns` / `keywords` 维度的 `items[]` 行顶仍保留 `searchImpressionShare`、`searchBudgetLostImpressionShare`、`searchRankLostImpressionShare`。CLI 落盘前已统一为 **0~1 小数**(与 `competitiveMetrics` 同名项一致;有嵌套时以 `competitiveMetrics` 为准)。Top/AbsoluteTop/Content/ClickShare 等扩展项仅存在于 `competitiveMetrics`。展示为百分比:`(v * 100).toFixed(2) + '%'`。
243
257
 
244
258
  ### campaign-hour 字段
245
259
 
246
- `campaignId`、`campaignName`、`date`、`hour`、`spend`、`impressions`、`clicks`、`conversions`。根为 JSON 数组(非 `{ campaigns: … }` 包装)。
260
+ `items[]` 每行:`campaignId`、`campaignName`、`date`、`hour`、`spend`、`impressions`、`clicks`、`conversions`(`schemaVersion 3` 起行统一在 `items[]`,不再是裸数组根)。
247
261
 
248
262
  ---
249
263
 
@@ -70,7 +70,7 @@ siluzan-tso account-history -m Google
70
70
  siluzan-tso list-accounts -m Google
71
71
 
72
72
  # 第四步:充值激活(必须网页完成)
73
- # siluzan-tso config show 取 webUrl,打开:{webUrl}/v3/foreign_trade/tso/recharge
73
+ # siluzan-tso config show 取 webUrl,打开:{webUrl}/v3/foreign_trade/tso/recharge/pay?mediaType=Google
74
74
  # 美元账户最低约 100 USD,人民币账户约 700 CNY
75
75
  ```
76
76
 
@@ -197,7 +197,7 @@ siluzan-tso account-history -m Google
197
197
  | 状态 | 含义 | 下一步 |
198
198
  | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
199
199
  | `Pending` | 审核中 | 等待,可反复轮询 |
200
- | `Approved` | 审核通过 | `list-accounts` 确认账户出现;引导用户充值激活(见各媒体第四步) |
200
+ | `Approved` | 审核通过 | `list-accounts` 确认账户出现;按 `accounts/finance.md` 引导用户打开对应媒体充值页(传统充值/月结充值仅 Google/TikTok/Meta/Microsoft 有页面;Kwai、Yandex 当前没有对应充值界面) |
201
201
  | `Rejected` | 被拒 | 查看拒绝原因(`account-history --json-out ./snap` 的 `reason` 字段);修改资料后重新提交;如无法确定原因,引导用户联系丝路赞客服 |
202
202
 
203
203
  ---
@@ -71,7 +71,7 @@ mkdir -p ./snap-monitor && siluzan-tso google-analysis -a <mediaCustomerId> --se
71
71
  mkdir -p ./snap-monitor && siluzan-tso google-analysis -a <mediaCustomerId> --sections final-urls --json-out ./snap-monitor
72
72
  ```
73
73
 
74
- 读 **`./snap-monitor/final-urls.json`**。根为 **对象**:**键名**为网关返回的资源标识(以当次 JSON 为准),**值为字符串数组**(每个元素是一条最终到达网址)。CLI **不**代发 HTTP 请求判断 4xx/5xx;死链判定须在宿主对 URL 执行 HEAD/GET(注意频率与 robots/合规)。
74
+ 读 **`./snap-monitor/final-urls.json`**。`final-urls` 为**汇总维度**,整块对象在 **`record`**(`schemaVersion 3` 起;`items` 为 `[]`):`record` 的**键名**为网关返回的资源标识(以当次 JSON 为准),**值为字符串数组**(每个元素是一条最终到达网址)。CLI **不**代发 HTTP 请求判断 4xx/5xx;死链判定须在宿主对 URL 执行 HEAD/GET(注意频率与 robots/合规)。
75
75
 
76
76
  **按创意行拉数**(可与拒审、启停共用一轮数据):
77
77
 
@@ -18,7 +18,7 @@
18
18
  mkdir -p ./snap-scale && siluzan-tso google-analysis -a <mediaCustomerId> --sections campaigns --start <YYYY-MM-DD> --end <YYYY-MM-DD> --json-out ./snap-scale
19
19
  ```
20
20
 
21
- 读 **`./snap-scale/campaigns-<accountId>.json`**(具体路径见 stdout 摘要的 `writtenFiles[0]` 或 `manifest-<accountId>.json` 的 `artifacts`)。根对象常见 **`campaigns[]`**,单行关注(键名以当次落盘 JSON 为准):
21
+ 读 **`./snap-scale/campaigns-<accountId>.json`**(具体路径见 stdout 摘要的 `writtenFiles[0]` 或 `manifest-<accountId>.json` 的 `artifacts`)。系列行在 **`items[]`**(`schemaVersion 3` 统一信封),单行关注(键名以当次落盘 JSON 为准):
22
22
 
23
23
  - **`conversionsValuePerCost`**(与 Google「转化价值/费用」语义一致,作 ROAS 代理)
24
24
  - **`searchBudgetLostImpressionShare`**、**`searchRankLostImpressionShare`**、**`searchImpressionShare`**
@@ -40,7 +40,7 @@ siluzan-tso ad campaigns -a <mediaCustomerId> --start <YYYY-MM-DD> --end <YYYY-M
40
40
  mkdir -p ./snap-scale && siluzan-tso google-analysis -a <mediaCustomerId> --sections campaign-hour --start <YYYY-MM-DD> --end <YYYY-MM-DD> --json-out ./snap-scale
41
41
  ```
42
42
 
43
- 读 **`./snap-scale/campaign-hour.json`**。根为**数组**:**`campaignId`**、**`date`**、**`hour`**、**`spend`**。宿主可做「近若干小时花费 vs 预期」的辅助条件。
43
+ 读 **`./snap-scale/campaign-hour.json`**。行在 **`items[]`**:**`campaignId`**、**`date`**、**`hour`**、**`spend`**。宿主可做「近若干小时花费 vs 预期」的辅助条件。
44
44
 
45
45
  ### 4. 条件示例(仅示意)
46
46
 
@@ -125,7 +125,7 @@ siluzan-tso ad adgroup-edit -a <mediaCustomerId> --id <adGroupId> --max-cpc <主
125
125
 
126
126
  见 **`references/google-ads/google-ads.md`**「广告组编辑」。
127
127
 
128
- 写前**必须**先 **`ad groups --json-out ./snap` / `ad campaigns --json-out ./snap`** 取当前值,**读取主币种金额**:组侧读 `maxCPCAmountYuan` / `targetCpaAmountYuan`(元);系列列表侧 `ad campaigns` 的 `budget` 也是元(与写参 `--budget` 一致);`google-analysis campaigns-*.json` 的 `budgetAmountYuan` 同。在宿主内按主币种算新值(如下调 12%:`newYuan = round(oldYuan * 0.88 * 100) / 100`),再以主币种金额作为 `--target-cpa` / `--max-cpc` / `--budget` 传回。
128
+ 写前**必须**先 **`ad groups --json-out ./snap` / `ad campaigns --json-out ./snap`** 取当前值,**读取主币种金额**:组侧读 `maxCPCAmountYuan` / `targetCpaAmountYuan`(元);系列列表侧 `ad campaigns` 的 `budget` 也是元(与写参 `--budget` 一致);`google-analysis campaigns-*.json` 的 `items[]` 行 `budgetAmountYuan` 同。在宿主内按主币种算新值(如下调 12%:`newYuan = round(oldYuan * 0.88 * 100) / 100`),再以主币种金额作为 `--target-cpa` / `--max-cpc` / `--budget` 传回。
129
129
  **严禁** 自己再做 `÷100` / `÷1_000_000` 换算——`*Yuan` 字段已经是元,再换算就是错的。
130
130
 
131
131
  ### 写后复核
@@ -89,7 +89,7 @@ for month in <S月, S+1月, E月>:
89
89
  **说明**:
90
90
 
91
91
  - **禁止**用 `daily-metrics` 填 Sheet 4 上区:该维度走 TSO 主平台 `account-daily-reports`,与 Google 前台 / Web 分析(`CampaignSectionData`)**不同数据源**,产品已确认存在不可接受偏差。
92
- - **Sheet 4 上区(账户月汇总)**:读 `./<YYYY-MM>/campaigns-*.json`,对 `campaigns[]` **全系列求和**得到当月账户级点击/展示/费用/转化,再算 CTR/CPC/CPA/CVR(公式见下文 Sheet 4);与 Google「广告系列」页同区间加总口径一致。
92
+ - **Sheet 4 上区(账户月汇总)**:读 `./<YYYY-MM>/campaigns-*.json`,对 `items[]`(`schemaVersion 3` 行统一在 `items`)**全系列求和**得到当月账户级点击/展示/费用/转化,再算 CTR/CPC/CPA/CVR(公式见下文 Sheet 4);与 Google「广告系列」页同区间加总口径一致。
93
93
  - `./snap-inquiry/campaigns-<accountId>_<S>-<E>.json`(步骤 2,整段 S~E):仅用于 **Sheet 5** 系列明细;**不得**用它按月拆行填 Sheet 4。
94
94
  - `geographic`:账户级国家聚合;Sheet 6 与 Sheet 4 **下区**(重点国切片)按月目录读取。
95
95
  - `keywords` / `search-terms`:Sheet 7 / 8;默认拉取**全部有消耗**行(`keywords` 网关 `costGreater=0`,`limit=0` 不封顶);需 TOP N 时加 `--limit <n>`。
@@ -186,7 +186,7 @@ Sheet 5 整段系列明细读:`./snap-inquiry/campaigns-<accountId>_<S>-<E>.js
186
186
  ### 数值格式(强约束)
187
187
 
188
188
  - **金额、CPL / CPA / CPC / 平均费用**:保留 **2 位小数**,写明货币代码(如 `¥123.45 CNY`)。
189
- - **点击率 / 转化率 / 互动率**(`ctr` / `conversionRate` / `interactionRate`):CLI 已归一为 **0~1 小数**(schemaVersion: 2),写入 Excel 时**直接写 0~1**(与运营样表对齐,Excel 单元格用百分比格式自动渲染)。话术 `x%` 用 `(v * 100).toFixed(2) + "%"`。
189
+ - **点击率 / 转化率 / 互动率**(`ctr` / `conversionRate` / `interactionRate`):CLI 已归一为 **0~1 小数**(schemaVersion 2),写入 Excel 时**直接写 0~1**(与运营样表对齐,Excel 单元格用百分比格式自动渲染)。话术 `x%` 用 `(v * 100).toFixed(2) + "%"`。
190
190
  - **空值**:`interactions === 0` 时「平均费用」「互动率」填 `—`,**禁止**除零。
191
191
 
192
192
  ### 分析输出区位置(**全局硬约束**)
@@ -317,14 +317,14 @@ Sheet 5 整段系列明细读:`./snap-inquiry/campaigns-<accountId>_<S>-<E>.js
317
317
  | 项 | 约定 |
318
318
  | --- | --- |
319
319
  | 数据源 | `./snap-inquiry/<YYYY-MM>/campaigns-<accountId>_<月1号>-<月末>.json`(每月单独拉 `campaigns`;见「落盘防覆盖」) |
320
- | 聚合范围 | 该月内 **全部** `campaigns[]` 行(含已暂停但在区间内有消耗的系列;**禁止**只取 Top N 或手工筛系列) |
320
+ | 聚合范围 | 该月内 **全部** `items[]` 行(`schemaVersion 3` 行统一在 `items`;含已暂停但在区间内有消耗的系列;**禁止**只取 Top N 或手工筛系列) |
321
321
  | 数据口径 | `google-analysis campaigns` 与账户月表一致;账户月表 = 各系列在当月起止日内指标之和 |
322
322
  | 禁止 | `daily-metrics` / `stats` / `overview` 填上区投放列(询盘列仍来自 `inquiries.json`) |
323
323
 
324
324
  **脚本聚合(每月一份 `campaigns-*.json`)**:
325
325
 
326
326
  ```text
327
- rows = payload.campaigns ?? []
327
+ rows = payload.items ?? []
328
328
  spend = Σ rows[].spend
329
329
  clicks = Σ rows[].clicks
330
330
  impressions = Σ rows[].impressions
@@ -339,7 +339,7 @@ CPL = 询盘个数 > 0 ? spend / 询盘个数 : —
339
339
 
340
340
  `时间` 列写 `YYYY-MM` 或 `YYYY年M月`(与样表一致即可)。金额列用 `list-accounts` 的 `currencyCode`。
341
341
 
342
- **可选校验(不写入 Excel,仅日志)**:同月再拉 `overview`,对比 `currentPeriod.spend` 与系列 `spend` 之和;偏差 >1% 时在 Agent 交付说明中脚注差异,**仍以系列加总为准**。
342
+ **可选校验(不写入 Excel,仅日志)**:同月再拉 `overview`,对比 `record.currentPeriod.spend`(汇总维度在 `record`)与系列 `spend` 之和;偏差 >1% 时在 Agent 交付说明中脚注差异,**仍以系列加总为准**。
343
343
 
344
344
  **上区**(B→L,跳过 A 列保持与运营样表一致)3 行月汇总:
345
345
 
@@ -347,7 +347,7 @@ CPL = 询盘个数 > 0 ? spend / 询盘个数 : —
347
347
  | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
348
348
  | R1 | (A 列空)`汇总` |
349
349
  | R2 | `时间` \| `点击次数` \| `展示次数` \| `点击率` \| `平均每次点击费用` \| `费用` \| `转化次数` \| `每次转化费用` \| `转化率` \| `询盘个数` \| `CPL` |
350
- | R3..R5 | 倒序写 3 个月(**E 月在上**,S 月在下):脚本读 `./<YYYY-MM>/campaigns-*.json` 按上表对 `campaigns[]` 全量求和;`询盘个数` = `inquiries.json` 按月 count;`CPL` = `费用 / 询盘个数`(询盘 0 填 `—`) |
350
+ | R3..R5 | 倒序写 3 个月(**E 月在上**,S 月在下):脚本读 `./<YYYY-MM>/campaigns-*.json` 按上表对 `items[]` 全量求和;`询盘个数` = `inquiries.json` 按月 count;`CPL` = `费用 / 询盘个数`(询盘 0 填 `—`) |
351
351
 
352
352
  **下区**(B→G)3 月 × **重点国家 / 非重点国** 切片(共 6 行):
353
353
 
@@ -381,7 +381,7 @@ CPL = 询盘个数 > 0 ? spend / 询盘个数 : —
381
381
  | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
382
382
  | R1 | `广告系列表现明细 · 账户 <mediaCustomerId(带连字符)> · <数据开始月>-<数据结束月>` |
383
383
  | R2(A→M,13 列) | `系列名称` \| `系列ID` \| `策略` \| `开始日期` \| `消耗 (¥)` \| `展示` \| `点击` \| `CTR` \| `平均CPC` \| `转化` \| `CPA (¥)` \| `转化率` \| `消耗占比` |
384
- | R3… | 读 `campaigns-*.json`:`campaignName` / `campaignId` / `biddingStrategyTypeV2`(中文化:`MAXIMIZE_CONVERSIONS` → `Max转化`、`TARGET_SPEND` → `目标支出`、`MANUAL_CPC` → `手动CPC` 等;未命中保留原文)/ `startDate` / `spend` / `impressions` / `clicks` / `ctr` / `averageCpc` / `conversions` / `costPerConversion` / `conversionRate` / **`消耗占比` = `spend` / `sum(spend)`**(脚本算,写 0~1 小数) |
384
+ | R3… | 读 `campaigns-*.json` 的 `items[]`:`campaignName` / `campaignId` / `biddingStrategyTypeV2`(中文化:`MAXIMIZE_CONVERSIONS` → `Max转化`、`TARGET_SPEND` → `目标支出`、`MANUAL_CPC` → `手动CPC` 等;未命中保留原文)/ `startDate` / `spend` / `impressions` / `clicks` / `ctr` / `averageCpc` / `conversions` / `costPerConversion` / `conversionRate` / **`消耗占比` = `spend` / `sum(spend)`**(脚本算,写 0~1 小数) |
385
385
  | 末行 | `合计`:A 列写 `合计`;金额、展示、点击、转化 求和;CTR/CVR/CPA 按合计反推(**禁止**对各行比率求平均) |
386
386
 
387
387
  **下区**:
@@ -427,7 +427,7 @@ CPL = 询盘个数 > 0 ? spend / 询盘个数 : —
427
427
  | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
428
428
  | R1 | `关键词表现 · 账户 <id> · <分析月份>` |
429
429
  | R2(A→J,10 列) | `关键词` \| `匹配方式` \| `系列` \| `消耗 (¥)` \| `展示` \| `点击` \| `CTR` \| `平均CPC` \| `转化` \| `CPA (¥)` |
430
- | R3… | 读 `keywords-*.json`(`keywords[]`):`keyword` / `keywordMatchTypeZh`(中文匹配方式:`Broad` / `Phrase` / `Exact`)/ `campaignName` / `spend` / `impressions` / `clicks` / `ctr` / `averageCpc` / `conversions` / `costPerConversion` |
430
+ | R3… | 读 `keywords-*.json` 的 `items[]`:`keyword` / `keywordMatchTypeZh`(中文匹配方式:`Broad` / `Phrase` / `Exact`)/ `campaignName` / `spend` / `impressions` / `clicks` / `ctr` / `averageCpc` / `conversions` / `costPerConversion` |
431
431
 
432
432
  按 `转化` 降序、`消耗` 降序双排序。
433
433
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  | 维度 | CLI | 备注 |
30
30
  | ------------- | ----------------------------------------------- | ------------------------------------------------------------ |
31
- | 系列按小时 | `google-analysis --sections campaign-hour` | `campaign-hour`;根为 JSON 数组,含 `date`/`hour`/消耗与效果 |
31
+ | 系列按小时 | `google-analysis --sections campaign-hour` | 行在 `items[]`,含 `date`/`hour`/消耗与效果 |
32
32
  | 受众分布 | `google-analysis --sections audience` | 可分 `SystemDefined` / `UserDefined` |
33
33
  | 搜索词报告 | `google-analysis --sections search-terms` | 高消耗搜索词;`queryTargetingStatusZh` 列(已添加/已排除/都没有) |
34
34
  | 广告创意表现 | `google-analysis --sections ads` | 广告标题/类型/到达网址 |
@@ -79,7 +79,7 @@ siluzan-tso google-analysis -a <mediaCustomerId> --start <S> --end <E> --json-ou
79
79
 
80
80
  ### 点击率 / 转化率 / 互动率
81
81
 
82
- > **2026-05 起 CLI 已统一归一**(manifest `schemaVersion: 2`):所有 `<section>-*.json` 中的 `ctr` / `conversionRate` 一律为 **0~1 小数**(如 `0.10` = 10.00%)。详见 `references/analytics/account-analytics.md`「指标字段对照」。
82
+ > **2026-05 起 CLI 已统一归一**(manifest `schemaVersion 2`):所有 `<section>-*.json` 中的 `ctr` / `conversionRate` 一律为 **0~1 小数**(如 `0.10` = 10.00%)。`schemaVersion 3` 起行数据统一在 `items[]`、汇总在 `record`。详见 `references/analytics/account-analytics.md`「指标字段对照」「落盘 JSON 统一信封」。
83
83
 
84
84
  | 场景 | 处理 |
85
85
  | ------------------------------------------------- | -------------------------------------------------------------------------- |
@@ -116,7 +116,7 @@ siluzan-tso google-analysis -a <mediaCustomerId> --start <S> --end <E> --json-ou
116
116
  | R1 | `广告系列报告`(如运营改名,可改文案但 Sheet 名仍为 `账户报告`) |
117
117
  | R2 | 统计区间 |
118
118
  | R3(A→K,11 列) | `广告系列` \| `预算` \| `费用` \| `展示次数` \| `点击次数` \| `点击率` \| `平均每次点击费用` \| `所有转化次数` \| `转化次数` \| `每次转化费用` \| `转化率` |
119
- | R4… | 来自 `campaigns-*.json`:`campaignName`、`budgetAmountYuan`(元)、`spend`、`impressions`、`clicks`、`ctr`(已归一直接写入「点击率」)、`averageCpc`、`allConversions`→「所有转化次数」、`conversions`→「转化次数」、`costPerConversion`、`conversionRate`(已归一直接写入「转化率」);缺 `allConversions` 时与「转化次数」同值或填 `0` / `—`,并在脚注说明 |
119
+ | R4… | 来自 `campaigns-*.json` 的 `items[]`(`schemaVersion 3` 行统一在 `items`):`campaignName`、`budgetAmountYuan`(元)、`spend`、`impressions`、`clicks`、`ctr`(已归一直接写入「点击率」)、`averageCpc`、`allConversions`→「所有转化次数」、`conversions`→「转化次数」、`costPerConversion`、`conversionRate`(已归一直接写入「转化率」);缺 `allConversions` 时与「转化次数」同值或填 `0` / `—`,并在脚注说明 |
120
120
  | 末行「合计」 | **广告系列**列填 `总计`;**预算**列填 `--`;**展示 / 点击 / 所有转化次数 / 转化次数 / 费用** 做列求和;**点击率** = 合计点击 ÷ 合计展示;**转化率** = 合计转化 ÷ 合计点击(**禁止**用合计转化 ÷ 合计展示);**每次转化费用** = 合计费用 ÷ 合计转化(转化为 0 时填 `—`);禁止对各行比率取算术平均 |
121
121
 
122
122
  表下**留白若干行**后写 **「数据复盘」**:
@@ -183,7 +183,7 @@ siluzan-tso google-analysis -a <mediaCustomerId> --start <S> --end <E> --json-ou
183
183
  | R1 | `地理位置报告` |
184
184
  | R2 | 统计区间 |
185
185
  | R3(A→M,13 列) | `地理位置` \| `广告系列` \| `展示次数` \| `互动次数` \| `互动率` \| `费用` \| `平均费用` \| `点击次数` \| `点击率` \| `所有转化次数` \| `转化次数` \| `每次转化费用` \| `转化率` |
186
- | R4… | `campaign-geo-matched-*.json`(`countries[]`):`countryOrRegion`→地理位置;`campaignName`→「广告系列」;`allConversions`→「所有转化次数」;`conversions`→「转化次数」;**互动次数**→`interactions`;**互动率**→`interactionRate`(字符串须解析)或 `interactions/impressions`,`interactions` 为 0 时填 `—`;**平均费用 必须** = `spend / interactions`,`interactions` 为 0 / null / undefined 时填 `—`;`ctr` / `conversionRate` 直接写入;其余列按 outline 映射 |
186
+ | R4… | `campaign-geo-matched-*.json` 的 `items[]`(`schemaVersion 3` 行统一在 `items`):`countryOrRegion`→地理位置;`campaignName`→「广告系列」;`allConversions`→「所有转化次数」;`conversions`→「转化次数」;**互动次数**→`interactions`;**互动率**→`interactionRate`(字符串须解析)或 `interactions/impressions`,`interactions` 为 0 时填 `—`;**平均费用 必须** = `spend / interactions`,`interactions` 为 0 / null / undefined 时填 `—`;`ctr` / `conversionRate` 直接写入;其余列按 outline 映射 |
187
187
 
188
188
  ---
189
189
 
@@ -207,7 +207,7 @@ siluzan-tso google-analysis -a <mediaCustomerId> --start <S> --end <E> --json-ou
207
207
  ```
208
208
 
209
209
  - **第 1 条「平均每天」**:`区间总消耗 / 区间日历天数`(含起止日),脚本计算,禁止手填。
210
- - **第 3 条**:CLI 返回的若是账户整体 CTR,写「整体点击率」而非「搜索点击率」;可区分时再写「搜索点击率」。`overview-*.json`(`schemaVersion: 2`)的 `ctr` 已是 0~1 小数(如 `0.10`),话术 `x%` 用 `(ctr * 100).toFixed(2) + "%"`,**禁止**直接打印数值得到 "0.10%"。
210
+ - **第 3 条**:CLI 返回的若是账户整体 CTR,写「整体点击率」而非「搜索点击率」;可区分时再写「搜索点击率」。`overview-*.json`(汇总维度,`schemaVersion 3` 起整块在 `record`)的 `record.ctr` 已是 0~1 小数(如 `0.10`),话术 `x%` 用 `(ctr * 100).toFixed(2) + "%"`,**禁止**直接打印数值得到 "0.10%"。
211
211
  - **所有金额、百分比** 一律 **2 位小数**。
212
212
 
213
213
  ---
@@ -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.25-beta.1'
12
+ $PKG_VERSION = '1.1.25-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.25-beta.1"
12
+ readonly PKG_VERSION="1.1.25-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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.25-beta.1",
3
+ "version": "1.1.25-beta.3",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",