siluzan-tso-cli 1.1.53-beta.2 → 1.1.53-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.53-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.53-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -2467,7 +2467,13 @@ function rawRequest(url, options) {
2467
2467
  }
2468
2468
  reject(err);
2469
2469
  });
2470
- if (options.body) req.write(options.body);
2470
+ if (options.body !== void 0) {
2471
+ const payload = typeof options.body === "string" ? Buffer.from(options.body) : options.body;
2472
+ if (!headers["Content-Length"] && !headers["content-length"]) {
2473
+ req.setHeader("Content-Length", payload.length);
2474
+ }
2475
+ req.write(payload);
2476
+ }
2471
2477
  req.end();
2472
2478
  });
2473
2479
  }
@@ -2562,6 +2568,20 @@ function formatHttpError(status, rawBody, opts) {
2562
2568
  function buildAuthHeaders(config) {
2563
2569
  return buildSiluzanAuthHeaders(config);
2564
2570
  }
2571
+ async function parseApiResponse(status, text2, verbose) {
2572
+ if (status < 200 || status >= 300) {
2573
+ throw new Error(formatHttpError(status, text2, { verbose }));
2574
+ }
2575
+ if (!text2.trim()) return null;
2576
+ try {
2577
+ return parseJsonWithStringIds(text2);
2578
+ } catch {
2579
+ if (verbose) {
2580
+ console.error(` \u26A0\uFE0F \u54CD\u5E94\u975E JSON\uFF0C\u539F\u59CB\u5185\u5BB9\uFF1A${redactSensitive(text2).slice(0, 200)}`);
2581
+ }
2582
+ return text2;
2583
+ }
2584
+ }
2565
2585
  async function apiFetch(url, config, options = {}, verbose = false) {
2566
2586
  const method = options.method ?? "GET";
2567
2587
  const reqHeaders = {
@@ -2580,19 +2600,7 @@ async function apiFetch(url, config, options = {}, verbose = false) {
2580
2600
  headers: reqHeaders,
2581
2601
  body
2582
2602
  });
2583
- const text2 = res.text;
2584
- if (res.status < 200 || res.status >= 300) {
2585
- throw new Error(formatHttpError(res.status, text2, { verbose }));
2586
- }
2587
- if (!text2.trim()) return null;
2588
- try {
2589
- return parseJsonWithStringIds(text2);
2590
- } catch {
2591
- if (verbose) {
2592
- console.error(` \u26A0\uFE0F \u54CD\u5E94\u975E JSON\uFF0C\u539F\u59CB\u5185\u5BB9\uFF1A${redactSensitive(text2).slice(0, 200)}`);
2593
- }
2594
- return text2;
2595
- }
2603
+ return parseApiResponse(res.status, res.text, verbose);
2596
2604
  }
2597
2605
  async function apiFetchWithHeaders(url, config, options = {}, verbose = false) {
2598
2606
  const method = options.method ?? "GET";
@@ -134432,16 +134440,23 @@ function parseInquiryRecords(raw) {
134432
134440
  for (const item of list) {
134433
134441
  if (!item || typeof item !== "object") continue;
134434
134442
  const row = item;
134435
- const countryZh = pick(row, ["countryZh", "country", "\u56FD\u5BB6", "\u56FD\u5BB6(\u4E2D\u6587)"]);
134436
- const date = pick(row, ["date", "\u65F6\u95F4", "time"]);
134443
+ const countryZh = pick(row, [
134444
+ "\u76EE\u6807\u56FD\u5BB6/\u5730\u533A",
134445
+ "countryZh",
134446
+ "country",
134447
+ "\u56FD\u5BB6",
134448
+ "\u56FD\u5BB6(\u4E2D\u6587)"
134449
+ ]);
134450
+ const date = pick(row, ["\u8BE2\u76D8\u65E5\u671F", "date", "\u65F6\u95F4", "time"]);
134437
134451
  const month = normalizeMonth(pick(row, ["month", "\u6708\u4EFD", "ym"]), date || void 0);
134438
134452
  if (!countryZh && !month) continue;
134439
134453
  out.push({
134440
134454
  month,
134441
134455
  countryZh,
134442
134456
  date: date || void 0,
134443
- product: pick(row, ["product", "\u4EA7\u54C1"]) || void 0,
134444
- source: pick(row, ["source", "\u5BA2\u6237\u6765\u6E90", "\u6765\u6E90"]) || void 0
134457
+ product: pick(row, ["\u4EA7\u54C1/\u54C1\u7C7B", "product", "\u4EA7\u54C1"]) || void 0,
134458
+ source: pick(row, ["\u8BE2\u76D8\u6765\u6E90\u5173\u952E\u8BCD", "source", "\u5BA2\u6237\u6765\u6E90", "\u6765\u6E90"]) || void 0,
134459
+ quality: pick(row, ["\u8BE2\u76D8\u8D28\u91CF\u5206\u7EA7", "qualityGrade", "quality"]) || void 0
134445
134460
  });
134446
134461
  }
134447
134462
  return out;
@@ -134464,20 +134479,36 @@ function countByCountry(rows) {
134464
134479
  }
134465
134480
 
134466
134481
  // src/commands/google-quarterly/plan-defaults.ts
134482
+ var PLAN_MONTH_SHARES = [0.3, 0.35, 0.35];
134483
+ function splitSuggested(spendLow) {
134484
+ const a = round22(spendLow * PLAN_MONTH_SHARES[0]);
134485
+ const b = round22(spendLow * PLAN_MONTH_SHARES[1]);
134486
+ const c = round22(spendLow - a - b);
134487
+ return [a, b, c];
134488
+ }
134467
134489
  function buildSpendPlanDefaults(currentSpend) {
134468
134490
  if (!(currentSpend > 0)) {
134469
134491
  return {
134470
134492
  currentSpend: 0,
134493
+ spendFloor: null,
134471
134494
  spendLow: null,
134472
134495
  spendHigh: null,
134473
- note: "\u672C\u671F\u65E0\u82B1\u8D39\uFF0C\u65E0\u6CD5\u7ED9\u9ED8\u8BA4\u9884\u7B97"
134496
+ monthShares: PLAN_MONTH_SHARES,
134497
+ monthSuggested: null,
134498
+ note: "\u672C\u671F\u65E0\u82B1\u8D39\uFF0C\u65E0\u6CD5\u7ED9\u9ED8\u8BA4\u9884\u7B97\uFF1B\u7981\u6B62\u624B\u7F16\u91D1\u989D"
134474
134499
  };
134475
134500
  }
134501
+ const floor = round22(currentSpend);
134502
+ const low = round22(currentSpend * 1.3);
134503
+ const high = round22(currentSpend * 1.5);
134476
134504
  return {
134477
- currentSpend: round22(currentSpend),
134478
- spendLow: round22(currentSpend * 1.3),
134479
- spendHigh: round22(currentSpend * 1.5),
134480
- note: "\u9ED8\u8BA4\u76EE\u6807\uFF1A\u82B1\u8D39\u8F83\u672C\u671F\u63D0\u5347 30%\uFF5E50%"
134505
+ currentSpend: floor,
134506
+ spendFloor: floor,
134507
+ spendLow: low,
134508
+ spendHigh: high,
134509
+ monthShares: PLAN_MONTH_SHARES,
134510
+ monthSuggested: splitSuggested(low),
134511
+ note: "\u4E09\u6863\u57FA\u4E8E\u672C\u671F\u82B1\u8D39\u80FD\u529B\uFF0C\u4E0D\u662F\u5BA2\u6237\u5DF2\u6279\u9884\u7B97\uFF1B\u5EFA\u8BAE\u6863\u6708\u5EA6\u9ED8\u8BA4 30%/35%/35%\uFF1BCPA / \u8BE2\u76D8\u4E0D\u5916\u63A8"
134481
134512
  };
134482
134513
  }
134483
134514
 
@@ -134490,14 +134521,19 @@ var continentMap = null;
134490
134521
  function continentsPath() {
134491
134522
  const dir = path27.dirname(fileURLToPath6(import.meta.url));
134492
134523
  const candidates = [
134524
+ // dist/index.js → dist/skill/references/...
134525
+ path27.join(dir, "skill", "references", "analytics", "geo-continents.json"),
134493
134526
  path27.join(dir, "skill", "siluzan-ads", "references", "analytics", "geo-continents.json"),
134527
+ // dist/index.js → tso-cli/assets/...
134528
+ path27.join(dir, "..", "assets", "siluzan-ads", "references", "analytics", "geo-continents.json"),
134529
+ // src/commands/google-quarterly/region.ts → tso-cli/assets/...
134494
134530
  path27.join(dir, "..", "..", "assets", "siluzan-ads", "references", "analytics", "geo-continents.json"),
134495
134531
  path27.join(dir, "..", "..", "..", "assets", "siluzan-ads", "references", "analytics", "geo-continents.json")
134496
134532
  ];
134497
134533
  for (const p of candidates) {
134498
134534
  if (fs22.existsSync(p)) return p;
134499
134535
  }
134500
- return candidates[1];
134536
+ return candidates[0];
134501
134537
  }
134502
134538
  function loadContinentMap() {
134503
134539
  if (continentMap) return continentMap;
@@ -134538,6 +134574,12 @@ function countByRegion(countryCounts, continents = loadContinentMap()) {
134538
134574
  }
134539
134575
 
134540
134576
  // src/commands/google-quarterly/special-ads.ts
134577
+ var EXPLORE_TYPE_META = [
134578
+ { id: "video", label: "\u89C6\u9891 / YouTube", role: "\u54C1\u724C\u58F0\u91CF", title: "\u89C6\u9891 / YouTube \u54C1\u724C\u58F0\u91CF", chip: "\u54C1\u724C\u58F0\u91CF" },
134579
+ { id: "pmax", label: "\u6548\u679C\u6700\u5927\u5316", role: "\u8F6C\u5316\u6548\u7387", title: "\u6548\u679C\u6700\u5927\u5316", chip: "\u8DE8\u6E20\u9053\u6548\u679C" },
134580
+ { id: "demand", label: "\u9700\u6C42\u5F00\u53D1 / \u53D1\u73B0", role: "\u5185\u5BB9\u79CD\u8349", title: "\u9700\u6C42\u5F00\u53D1 / \u53D1\u73B0\u5E7F\u544A", chip: "\u5185\u5BB9\u79CD\u8349" },
134581
+ { id: "display", label: "\u5C55\u793A", role: "\u518D\u8425\u9500", title: "\u5C55\u793A\u5E7F\u544A", chip: "\u8986\u76D6\xB7\u518D\u8425\u9500" }
134582
+ ];
134541
134583
  function hasTraffic(row) {
134542
134584
  return rowSpend(row) > 0 || num2(row.impressions) > 0 || num2(row.clicks) > 0;
134543
134585
  }
@@ -134588,6 +134630,27 @@ function fromCampaign(row) {
134588
134630
  conversions: num2(row.conversions)
134589
134631
  };
134590
134632
  }
134633
+ function sortBySpend(items3) {
134634
+ return [...items3].sort((a, b) => b.spend - a.spend);
134635
+ }
134636
+ function metricsFromRows(items3) {
134637
+ let spend = 0;
134638
+ let impressions = 0;
134639
+ let clicks = 0;
134640
+ let conversions = 0;
134641
+ for (const row of items3) {
134642
+ spend += row.spend;
134643
+ impressions += row.impressions;
134644
+ clicks += row.clicks;
134645
+ conversions += row.conversions;
134646
+ }
134647
+ return withRates({ spend, impressions, clicks, conversions });
134648
+ }
134649
+ function campaignsInBucket(campaigns, bucket) {
134650
+ return sortBySpend(
134651
+ campaigns.filter((row) => channelBucket(row.channelTypeV2) === bucket && hasTraffic(row)).map(fromCampaign)
134652
+ );
134653
+ }
134591
134654
  function pickYoutubeAdSubscriptions(actions) {
134592
134655
  const hits = actions.filter((row) => {
134593
134656
  const name2 = pickStr2(row, ["name", "Name", "conversionAction"]).toLowerCase();
@@ -134608,32 +134671,47 @@ function pickYoutubeAdSubscriptions(actions) {
134608
134671
  note: "\u53E3\u5F84=Google Ads \u8F6C\u5316\u52A8\u4F5C\uFF08YouTube \u8BA2\u9605\uFF09\uFF0C\u4E0D\u662F\u9891\u9053\u7C89\u6570"
134609
134672
  };
134610
134673
  }
134611
- function pickSpecialAds(videos, campaigns) {
134612
- const videoHits = videos.filter(hasTraffic);
134613
- if (videoHits.length > 0) {
134674
+ function videoRowsFromReport(videos) {
134675
+ return sortBySpend(videos.filter(hasTraffic).map(fromVideo));
134676
+ }
134677
+ function buildExploreAds(videos, campaigns, accountSpend) {
134678
+ const videoFromReport = videoRowsFromReport(videos);
134679
+ const videoItems = videoFromReport.length > 0 ? videoFromReport : campaignsInBucket(campaigns, "\u89C6\u9891");
134680
+ const byId = {
134681
+ video: videoItems,
134682
+ pmax: campaignsInBucket(campaigns, "\u6548\u679C\u6700\u5927\u5316"),
134683
+ demand: campaignsInBucket(campaigns, "\u53D1\u73B0"),
134684
+ display: campaignsInBucket(campaigns, "\u5C55\u793A")
134685
+ };
134686
+ const types = EXPLORE_TYPE_META.map((meta) => {
134687
+ const items3 = byId[meta.id];
134614
134688
  return {
134615
- source: "youtube",
134616
- label: "YouTube / \u89C6\u9891\u5E7F\u544A",
134617
- items: videoHits.map(fromVideo).sort((a, b) => b.spend - a.spend)
134689
+ ...meta,
134690
+ spend: items3.reduce((sum, row) => sum + row.spend, 0),
134691
+ metrics: metricsFromRows(items3),
134692
+ items: items3
134618
134693
  };
134694
+ });
134695
+ return {
134696
+ types,
134697
+ accountSpend,
134698
+ hasAny: types.some((t) => t.items.length > 0)
134699
+ };
134700
+ }
134701
+ function pickSpecialAds(videos, campaigns) {
134702
+ const videoHits = videoRowsFromReport(videos);
134703
+ if (videoHits.length > 0) {
134704
+ return { source: "youtube", label: "YouTube / \u89C6\u9891\u5E7F\u544A", items: videoHits };
134619
134705
  }
134620
- const pmax = campaigns.filter(
134621
- (row) => String(row.channelTypeV2 ?? "").toUpperCase() === "PERFORMANCE_MAX" && hasTraffic(row)
134622
- );
134706
+ const pmax = campaignsInBucket(campaigns, "\u6548\u679C\u6700\u5927\u5316");
134623
134707
  if (pmax.length > 0) {
134624
- return {
134625
- source: "pmax",
134626
- label: "\u6548\u679C\u6700\u5927\u5316",
134627
- items: pmax.map(fromCampaign).sort((a, b) => b.spend - a.spend)
134628
- };
134708
+ return { source: "pmax", label: "\u6548\u679C\u6700\u5927\u5316", items: pmax };
134629
134709
  }
134630
- const other = campaigns.filter((row) => !isSearchChannel(row.channelTypeV2) && hasTraffic(row));
134710
+ const other = sortBySpend(
134711
+ campaigns.filter((row) => !isSearchChannel(row.channelTypeV2) && hasTraffic(row)).map(fromCampaign)
134712
+ );
134631
134713
  if (other.length > 0) {
134632
- return {
134633
- source: "other",
134634
- label: "\u975E\u641C\u7D22\u5E7F\u544A",
134635
- items: other.map(fromCampaign).sort((a, b) => b.spend - a.spend)
134636
- };
134714
+ return { source: "other", label: "\u975E\u641C\u7D22\u5E7F\u544A", items: other };
134637
134715
  }
134638
134716
  return { source: "none", label: "", items: [] };
134639
134717
  }
@@ -134728,7 +134806,8 @@ function buildQuarterlyFacts(input) {
134728
134806
  });
134729
134807
  const videos = input.currentSnap.videos ?? [];
134730
134808
  const specialAds = pickSpecialAds(videos, input.currentSnap.campaigns);
134731
- const videoRows = specialAds.source === "youtube" ? specialAds.items : [];
134809
+ const exploreAds = buildExploreAds(videos, input.currentSnap.campaigns, currentMetrics.spend);
134810
+ const videoRows = videoRowsFromReport(videos);
134732
134811
  const videoImpr = videoRows.reduce((s, r) => s + r.impressions, 0);
134733
134812
  const videoSpend = videoRows.reduce((s, r) => s + r.spend, 0);
134734
134813
  const videoClicks = videoRows.reduce((s, r) => s + r.clicks, 0);
@@ -134809,6 +134888,7 @@ function buildQuarterlyFacts(input) {
134809
134888
  countries,
134810
134889
  usStates,
134811
134890
  specialAds,
134891
+ exploreAds,
134812
134892
  videosSummary: {
134813
134893
  impressions: videoImpr,
134814
134894
  spend: videoSpend,
@@ -134869,6 +134949,25 @@ function roundFacts(facts) {
134869
134949
  ...facts.videosSummary,
134870
134950
  spend: round22(facts.videosSummary.spend),
134871
134951
  averageCpv: facts.videosSummary.averageCpv == null ? null : round22(facts.videosSummary.averageCpv)
134952
+ },
134953
+ exploreAds: {
134954
+ ...facts.exploreAds,
134955
+ accountSpend: round22(facts.exploreAds.accountSpend),
134956
+ types: facts.exploreAds.types.map((t) => ({
134957
+ ...t,
134958
+ spend: round22(t.spend),
134959
+ metrics: {
134960
+ ...t.metrics,
134961
+ spend: round22(t.metrics.spend),
134962
+ cpc: round22(t.metrics.cpc),
134963
+ cpa: round22(t.metrics.cpa)
134964
+ },
134965
+ items: t.items.map((row) => ({
134966
+ ...row,
134967
+ spend: round22(row.spend),
134968
+ averageCpv: row.averageCpv == null ? row.averageCpv : round22(row.averageCpv)
134969
+ }))
134970
+ }))
134872
134971
  }
134873
134972
  };
134874
134973
  }
@@ -135054,6 +135153,7 @@ async function runGoogleQuarterlyCollect(opts) {
135054
135153
  ],
135055
135154
  inquiryNote: facts.meta.inquiryNote,
135056
135155
  specialAds: facts.specialAds.source,
135156
+ exploreAds: facts.exploreAds.types.filter((t) => t.items.length > 0).map((t) => t.id),
135057
135157
  planDefaults: facts.planDefaults
135058
135158
  }
135059
135159
  };
@@ -135066,6 +135166,7 @@ async function runGoogleQuarterlyCollect(opts) {
135066
135166
  accountId,
135067
135167
  current,
135068
135168
  specialAds: facts.specialAds.source,
135169
+ exploreAds: facts.exploreAds.types.filter((t) => t.items.length > 0).map((t) => t.id),
135069
135170
  inquirySource: facts.meta.inquirySource,
135070
135171
  usGeoSource: facts.meta.usGeoSource,
135071
135172
  drillCountry: facts.meta.drillCountry,
@@ -12,6 +12,7 @@
12
12
  | 命令参数 / 字段 | 路由表「必读文档」 |
13
13
  | 纪律与对用户怎么说 | `references/core/agent-conventions.md` |
14
14
  | 报告结构纲要 | `report-templates/*.md`(勿读 `*.html`) |
15
+ | P10 下期规划怎么写 | `report-templates/google-quarterly-plan.md` |
15
16
 
16
17
  ## 启动顺序
17
18
 
@@ -121,7 +121,7 @@ siluzan-tso -h
121
121
  | ------------------ | ------ | -------- |
122
122
  | Google 广告账户诊断 / 健康检查(**含账户 ID**) | **P1** | `references/analytics/account-analytics.md` + `report-templates/google-ads-diagnosis.md`(可选对照 `report-templates/google-account-diagnosis-report.md`) |
123
123
  | Google 账户周期 **数据汇总**(只要花费/转化/8 维/Excel;**无**规划/客户版/大族) | **P4** | `report-templates/google-period-report.md` + `references/analytics/account-analytics.md`;Excel 加 `google-period-report-excel.md` |
124
- | Google/谷歌 **月度报告** / **月总结与规划** / **季度报告** / **Q3 汇总** / **Q4 规划** / **季度汇报** / **投放季报** / 客户版 / 大族模板 / 下月规划(效率+区域询盘+国家明细) | **P10** | `report-templates/google-quarterly-report.md`(询盘载体须先转 JSON 再 `--inquiries`,禁止把 xlsx 丢给 collect;`collect`+`render`;花费默认 +30%~+50%;**不做**行业数据) |
124
+ | Google/谷歌 **月度报告** / **月总结与规划** / **季度报告** / **Q3 汇总** / **Q4 规划** / **季度汇报** / **投放季报** / 客户版 / 大族模板 / 下月规划(效率+区域询盘+国家明细) | **P10** | `report-templates/google-quarterly-report.md` + **`report-templates/google-quarterly-plan.md`**(**未带询盘表须先问并等待**,禁止不问就 collect;有表须先转 JSON 再 `--inquiries`,禁止把 xlsx 丢给 collect;`collect`+`render`;规划九章;花费三档基于本期、不是已批预算;**不做**行业数据) |
125
125
  | Meta/Facebook 周期或诊断 | **P4-FB** | `report-templates/meta-period-report.md` + `assets/meta-period-report-rules.md` + `references/analytics/facebook-analysis-guide.md` |
126
126
  | TikTok / Bing 周期报告 | **P4** | `references/analytics/account-analytics.md` + 对应 `report-templates/*-period-report.md` |
127
127
  | Yandex 账户分析 / 周期 / 月报 | **P4** | `report-templates/yandex-period-report.md` + `references/analytics/account-analytics.md`;Excel 加 `yandex-period-report-excel.md` |
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.53-beta.2",
4
- "publishedAt": 1789552686722
3
+ "version": "1.1.53-beta.4",
4
+ "publishedAt": 1789721782096
5
5
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "siluzan-tso/google-quarterly-report.schema.json",
4
4
  "title": "GoogleQuarterlyReport",
5
- "description": "Google 季度经营报告。Agent 只写 narrative;kpis/monthly/specialAds/planDefaults/usStates(消耗最高国家的一级行政区)由 collect 覆盖。",
5
+ "description": "Google 季度经营报告。Agent 只写 narrative;kpis/monthly/exploreAds/specialAds/planDefaults/usStates(消耗最高国家的一级行政区)由 collect 覆盖。",
6
6
  "type": "object",
7
7
  "required": ["narrative"],
8
8
  "properties": {
@@ -29,7 +29,7 @@
29
29
  "summary": { "$ref": "#/$defs/analysisBlock" }
30
30
  }
31
31
  },
32
- "plan": { "$ref": "#/$defs/analysisBlock" }
32
+ "plan": { "$ref": "#/$defs/planBlock" }
33
33
  }
34
34
  }
35
35
  },
@@ -41,6 +41,22 @@
41
41
  "goals": { "type": "array", "items": { "type": "string" } },
42
42
  "text": { "type": "string" }
43
43
  }
44
+ },
45
+ "planBlock": {
46
+ "type": "object",
47
+ "description": "下期规划九章。analysis 必填;其余八章写规划时都应有。规则见 report-templates/google-quarterly-plan.md。",
48
+ "properties": {
49
+ "analysis": { "type": "array", "minItems": 1, "items": { "type": "string" } },
50
+ "goals": { "$ref": "#/$defs/analysisBlock" },
51
+ "budget": { "$ref": "#/$defs/analysisBlock" },
52
+ "structure": { "$ref": "#/$defs/analysisBlock" },
53
+ "keywords": { "$ref": "#/$defs/analysisBlock" },
54
+ "audience": { "$ref": "#/$defs/analysisBlock" },
55
+ "creative": { "$ref": "#/$defs/analysisBlock" },
56
+ "roadmap": { "$ref": "#/$defs/analysisBlock" },
57
+ "risks": { "$ref": "#/$defs/analysisBlock" },
58
+ "text": { "type": "string" }
59
+ }
44
60
  }
45
61
  }
46
62
  }
@@ -108,6 +108,7 @@
108
108
  | ---------------------------------------------------------------- | ------------------------------ |
109
109
  | `google-period-report.md` | Google 账户周期/月度报告(P4) |
110
110
  | `google-quarterly-report.md` | Google 季度经营报告(P10) |
111
+ | `google-quarterly-plan.md` | P10 下期规划规则(九章) |
111
112
  | `google-period-report-excel.md` | P4 用户要 Excel |
112
113
  | `stats-daily-excel.md` | P4-DAILY 按日 Excel |
113
114
  | `meta-period-report.md` / `meta-period-report-excel.md` | Meta 周期(P4-FB) |
@@ -535,6 +535,7 @@ siluzan-tso yandex-analysis insights -a <porg-xxx> --json-out ./snap-yandex \
535
535
  | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
536
536
  | `google-period-report.md` | Google 周期汇总报告章节 |
537
537
  | `google-quarterly-report.md` | Google 季度经营报告(`google-quarterly collect`+`render`,P10) |
538
+ | `google-quarterly-plan.md` | P10 下期规划规则(写 `narrative.plan` 前必读) |
538
539
  | `google-account-diagnosis-report.md` | Google 诊断报告纲要 |
539
540
  | `google-ads-diagnosis.md` | Google Ads 诊断报告完整骨架 |
540
541
  | `meta-period-report.md` | Meta(Facebook)周期报告 |
@@ -180,6 +180,8 @@
180
180
 
181
181
  涉及「投放数据 / 消耗 / 报告 / 周报 / 月报 / 优化建议」且用户未给明确起止日期时**必须反问**(示例:A) 最近完整自然周 B) 本月 1 号到昨天 C) 自定义 YYYY-MM-DD)。给出范围后,报告首行标注 `统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`。
182
182
 
183
+ **P10 询盘表**:季度/月度经营报告若用户**没附**询盘表,必须先问有没有、等回复;未回答前禁止 `google-quarterly collect` / render。用户说「没有」再继续。话术见 `report-templates/google-quarterly-report.md`「询盘入场」。
184
+
183
185
  **例外**(不反问):
184
186
 
185
187
  - `list-accounts` 列全部 / 数个数:一次 `list-accounts -m <媒体> --page-size 999 --json-out <dir>`,脚本读落盘 `total` / `items[]`;**禁止**默认 page-size 20 再翻页(详见 `references/accounts/accounts-list.md` § Agent 意图速查)。
@@ -323,9 +323,9 @@
323
323
  | P1 / P4 / P10 | 账户 ID | 「请提供 Google 广告账户 ID(mediaCustomerId)。」 |
324
324
  | P1 / P4 | 时间 | 按 `agent-conventions.md` §五 反问区间 |
325
325
  | P10 | 时间 | 季度话术可省略(上一完整季);月度报告 / 下月规划用上一完整月或用户指定月 |
326
- | P10 | 询盘附件 | 先转 `inquiries.json` `--inquiries`;**禁止**把 xlsx/csv 原文件传给 collect |
326
+ | P10 | 询盘附件 | **未带表先问「有没有本期询盘表」并等待**;有则先看一眼表头再转 `inquiries.json` `--inquiries`;用户说没有再 collect。**禁止**不问就出报告;**禁止**把 xlsx/csv 原文件传给 collect |
327
327
  | P6 | 非 OKKI 话术 | 若用户实际要普通周期报告 → 改 P4 并说明 |
328
- | P7 | 无询盘文件 | 贴字段清单,**禁止编造询盘**;若用户只要账户级零转化 → 改 **P3** |
328
+ | P7 | 无询盘文件 | 贴字段清单,**禁止编造询盘**;用表前先看一眼表头;若用户只要账户级零转化 → 改 **P3** |
329
329
  | P3(§零·D) | 时间区间 | 按 `agent-conventions.md` §五 反问;巡检默认近 7 天须写明区间 |
330
330
  | W3 | 预算/地域 | 先列待确认项,再进入 campaign-plan 流水线 |
331
331
 
@@ -154,7 +154,7 @@
154
154
  - **时间窗口强约束**:**严格 3 个月** = 分析月份 + 向前 2 个完整自然月,**禁止**扩展到 7 个月。
155
155
  - **步骤**:
156
156
  1. (可选)Read `subagent-orchestration.md` § P7。
157
- 2. 询盘资料入场:用户附文件 → 解析落盘 `./snap-inquiry/inquiries.json`;仅给账户 ID → 反问账户 + 分析月份并贴字段清单,**禁止编造询盘数据**。
157
+ 2. 询盘资料入场:用户附文件 → 先看一眼表头再落盘 `./snap-inquiry/inquiries.json`(列名接近即可);仅给账户 ID → 反问账户 + 分析月份并贴字段清单,**禁止编造询盘数据**。
158
158
  3. CLI 拉数(同一目录):`list-accounts`、`google-analysis --sections campaigns,keywords,search-terms,campaign-geo`,并按月拉 `m1/m2/m3` 的 `campaigns,geographic`。
159
159
  4. 脚本聚合计算 8 Sheet 数据;国家→大洲映射读 `geo-continents.json`,**禁止**硬编码国家名。
160
160
  - **产物**:必产 8 Sheet xlsx(版式见模板),**只能**由 Agent 脚本生成,**禁止**假设 `… excel` 子命令存在。
@@ -195,14 +195,14 @@
195
195
  ## P10 · Google 季度经营报告
196
196
 
197
197
  - **触发**(命中越多越稳):Google/谷歌广告 **月度报告**、**月总结与规划**、**季度报告**、**Q3 汇总**、**Q4 规划**、**季度汇报**、**投放季报**、客户版/对外、大族模板、下月规划;或要「效率 + 区域询盘 + 国家明细 + 规划」;或「结论→效率→结构→新探索→总结→Q4」。**不是** P1 诊断、**不是** P4「只要花费转化」的周期表。用户提「行业参考」仍走 P10,**禁止编造行业数**(本批无行业章)。
198
- - **必读**:`report-templates/google-quarterly-report.md`。
198
+ - **必读**:`report-templates/google-quarterly-report.md`;写 `narrative.plan` 前再 Read `report-templates/google-quarterly-plan.md`。
199
199
  - **默认产物**:**HTML**(`google-quarterly render`)。
200
200
  - **步骤**:
201
201
  1. 确认账户与区间:季度话术省略日期 → 上一完整自然季;**月度报告 / 下月规划** → 上一完整自然月(或用户指定月)作 `--start/--end`。
202
- 2. `list-accounts -m Google -k <mediaCustomerId> --json-out ./snap-p10`(取户名、币种)。
203
- 3. **询盘载体 JSON(CLI 不读 xlsx)**:用户给了 xlsx / csv / 截图 / 粘贴表 → Agent 抽行落盘 `./snap-p10/inquiries.json`(字段见 `google-quarterly-report.md`「询盘入场」)再 `--inquiries`;**禁止**把原始文件传给 collect。无载体可 `--inquiry-conversion <名>`;再没有则询盘位留空,**禁止编造**。
204
- 4. `google-quarterly collect -a <id> --start <S> --end <D> --json-out ./snap-p10 --account-name "<名>"`(可选 `--keyword-seeds`)。YouTube 广告层用 Ads 视频报表,不拉频道订阅总数 / 来源饼 / 视频类型。
202
+ 2. **询盘未入场则先问再停**(话术见 `google-quarterly-report.md`「询盘入场」)。没有表格且用户未说「没有」时,**禁止** collect / narrative / render。用户发来表 → 抽行落盘 `./snap-p10/inquiries.json` 再 `--inquiries`(**禁止**把 xlsx 原文件传给 collect);用户说没有 → 询盘位留空(可选 `--inquiry-conversion`,须用户点名),**禁止编造**。
203
+ 3. `list-accounts -m Google -k <mediaCustomerId> --json-out ./snap-p10`(取户名、币种)。可与等询盘回复并行,但 collect 必须等询盘问完。
204
+ 4. `google-quarterly collect -a <id> --start <S> --end <D> --json-out ./snap-p10 --account-name "<名>"`(可选 `--keyword-seeds`)。新探索四类并行(视频 / PMax / 发现 / 展示);视频用 Ads 视频报表,不拉频道订阅总数 / 来源饼 / 视频类型。
205
205
  5. 读 `google-quarterly-collect.json`,只写 `narrative` 到 `google-quarterly.json`(禁止改 kpis / monthly / planDefaults 数字)。
206
- 6. 下期规划默认花费 +30%~+50%(用 `planDefaults.spendLow`~`spendHigh`);CPA/询盘目标不自动外推。
206
+ 6. 下期规划按 `google-quarterly-plan.md` 写九章。预算默认三档(维持本期 / +30% / +50%)+ 建议档月度 30/35/35,标「基于历史数据」,不是已批预算;CPA/询盘不外推;禁止自动调价/暂停指令。
207
207
  7. `google-quarterly render --data ./snap-p10/google-quarterly.json --out ./snap-p10/google-quarterly-report.html`。
208
- - **产物**:HTML 路径;专项广告无量则模板自动隐藏该章。
208
+ - **产物**:HTML 路径;新探索(视频 / PMax / 发现 / 展示)全无量则模板自动隐藏该章。
@@ -22,7 +22,8 @@
22
22
  | 文件 | 适用场景 |
23
23
  | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
24
  | `google-period-report.md` | **Google 周期分析报告(默认)**:终稿=HTML,四步流程 → `google-analysis render` 出 HTML |
25
- | `google-quarterly-report.md` / `.html` | **Google 季度经营报告(P10)**:`google-quarterly collect`+`render`;花费默认 +30%~+50%;无行业章 |
25
+ | `google-quarterly-report.md` / `.html` | **Google 季度经营报告(P10)**:`google-quarterly collect`+`render`;花费三档基于本期;无行业章 |
26
+ | `google-quarterly-plan.md` | **P10 下期规划规则**:九章 + Q01–Q30;写 `narrative.plan` 前必读 |
26
27
  | `google-period-report-excel.md` | **仅用户要 Excel 时**(P4 定制 Sheet):先 `*.outline.txt` 后脚本写 xlsx;账户须 `-k` 核验,无 CLI excel 子命令 |
27
28
  | `stats-daily-excel.md` | **P4-DAILY**:账户级按日 Excel(`stats --by-day`) |
28
29
  | `google-account-diagnosis-report.md` | Google 账户深度诊断(健康度/转化/结构等) |
@@ -44,9 +44,9 @@
44
44
  2. 同时贴出**询盘字段清单 + 1 行示例**,让用户用任意方式补齐再回传:
45
45
 
46
46
  ```
47
- 请把近 3 个月({S} ~ {E})的询盘记录回贴或上传(Excel / CSV / 截图 / 文本表格均可),字段如下:
48
- 月份 | 序号(可空) | 国家(中文) | 时间(YYYY-MM-DD Excel 序列) | 产品 | 客户来源 | IP(可空)
49
- 示例:4月份 | 1 | 印度 | 2026-04-12 | 滴灌带 | WhatsApp(www.extruder.cn)
47
+ 请把近 3 个月({S} ~ {E})的询盘记录回贴或上传(Excel / CSV / 截图 / 文本表格均可),字段如下(* 为必填):
48
+ 序号 | 询盘日期* | 询盘来源关键词 | 目标国家/地区* | 产品/品类* | 客户名称/公司 | 联系方式 | 询盘内容摘要 | 询盘质量分级* | 跟进状态 | 是否成交 | 成交金额(USD)
49
+ 示例:1 | 2026-04-12 | WhatsApp | 印度 | 滴灌带 | | | | 合格 | | |
50
50
  ```
51
51
 
52
52
  3. 拿到资料后由宿主 Agent 解析、落盘 `inquiries.json`,再进入「拉数与写 Excel」节。
@@ -58,8 +58,8 @@
58
58
  1. **账户**:`mediaCustomerId` 来自 `list-accounts -m Google -k <id>`,**禁止猜 ID**。
59
59
  2. **分析月份**:默认**上一个完整自然月**(例:今天 5/13 → 默认 4 月)。用户未明确时**反问**(A) 上月 B) 本月 1 号到昨天 C) 自定义 YYYY-MM)。
60
60
  3. **时间窗口(硬约束)**:**严格 3 个月** = 分析月份 + 向前 2 个完整自然月。例:分析月 = 2026-04 → `--start 2026-02-01 --end 2026-04-30`。**所有月度表只展示 3 行**(不延伸到 7 个月)。
61
- 4. **询盘字段最小集**:`月份 / 国家(中文) / 时间 / 产品 / 客户来源`;可选 `序号 / IP`。
62
- 5. **日期规范化**:`2026.4.1` / `2026/4/1` 等先规范成 `YYYY-MM-DD` 再传 CLI
61
+ 4. **用询盘数据前先看一眼表头**。常见列见上;* 表示通常要有对应含义的列(`询盘日期`≈时间/date,`目标国家/地区`≈国家,`产品/品类`≈产品,`询盘质量分级`有则用)。列名接近即可,不必逐格校验。
62
+ 5. **日期规范化**:`2026.4.1` / `2026/4/1` 等先规范成 `YYYY-MM-DD` 再传 CLI。`月份` 可由询盘日期推导。
63
63
 
64
64
  ---
65
65
 
@@ -155,23 +155,30 @@ Sheet 5 整段系列明细读:`./snap-inquiry/campaigns-<accountId>_<S>-<E>.js
155
155
  ```json
156
156
  [
157
157
  {
158
- "month": "4月份",
159
158
  "seq": 1,
159
+ "date": "2026-04-12",
160
+ "sourceKeyword": "WhatsApp",
160
161
  "country": "印度",
161
- "time": "2026-04-12",
162
162
  "product": "滴灌带",
163
- "source": "WhatsApp(www.extruder.cn)",
164
- "ip": null
163
+ "company": null,
164
+ "contact": null,
165
+ "summary": null,
166
+ "quality": "合格",
167
+ "followUp": null,
168
+ "won": null,
169
+ "amountUsd": null
165
170
  }
166
171
  ]
167
172
  ```
168
173
 
169
- - `month`:保留运营原文(`1月份` / `4月份` …)。
170
- - `country`:**中文名**,与 `countryOrRegionZh` 字典对齐(脚本会自动 lookup 大洲)。Agent 解析时遇到英文/缩写应先翻译为中文,未知国家保留原文并在脚本里标 `未知大洲`。
171
- - `time`:`YYYY-MM-DD`;Excel 日期序列号也接受,由解析方转换。
172
- - `product`:自由文本(`滴灌带` / `打包带` / `片材` / `滴头` / `模具` / `贴条` / `涂布水带` …),脚本以此作 Sheet 3 产品维度透视分桶 key。
173
- - `source`:自由文本(`WhatsApp` / `tawk.to` / `info邮箱` / `网站留言` / `微信(xxxx)` / `阿里巴巴` / `made-in-china` / `Youtube` / `TikTok` …),脚本以此作 Sheet 3 客户来源透视分桶 key。
174
- - `ip`:可空。
174
+ 落盘前先看一眼表头,按含义对上即可(`时间`=`询盘日期`,`国家(中文)`=`目标国家/地区`,`产品`=`产品/品类`)。
175
+
176
+ - `date`:询盘日期,`YYYY-MM-DD`。
177
+ - `country`:目标国家/地区,中文名,与 `countryOrRegionZh` 字典对齐。
178
+ - `product`:产品/品类,Sheet 3 产品分桶。
179
+ - `sourceKeyword` / `source`:来源,Sheet 3 来源分桶(可空)。
180
+ - `quality` / 其余可选列:有则写入,没有就空着。
181
+ - `month`:可由 `date` 推导。**禁止编造**缺失值。
175
182
 
176
183
  ---
177
184