siluzan-tso-cli 1.1.57-beta.1 → 1.1.57-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.
Files changed (29) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +126 -20
  3. package/dist/skill/SKILL.md +2 -1
  4. package/dist/skill/_meta.json +2 -2
  5. package/dist/skill/references/core/agent-conventions.md +3 -1
  6. package/dist/skill/references/core/playbooks.md +2 -1
  7. package/dist/skill/references/report-templates/bing-period-report.md +1 -1
  8. package/dist/skill/references/report-templates/google-period-report.md +1 -1
  9. package/dist/skill/references/report-templates/google-quarterly-plan.md +2 -0
  10. package/dist/skill/references/report-templates/google-quarterly-report.md +2 -0
  11. package/dist/skill/references/report-templates/meta-period-report.md +1 -1
  12. package/dist/skill/references/report-templates/tiktok-period-report.md +1 -1
  13. package/dist/skill/references/report-templates/yandex-period-report.md +1 -1
  14. package/dist/skill/report-templates/bing-period-report.html +14 -2
  15. package/dist/skill/report-templates/bing-period-report.md +1 -1
  16. package/dist/skill/report-templates/google-period-report.html +14 -2
  17. package/dist/skill/report-templates/google-period-report.md +1 -1
  18. package/dist/skill/report-templates/google-quarterly-plan.md +2 -0
  19. package/dist/skill/report-templates/google-quarterly-report.html +20 -10
  20. package/dist/skill/report-templates/google-quarterly-report.md +2 -0
  21. package/dist/skill/report-templates/meta-period-report.html +6 -2
  22. package/dist/skill/report-templates/meta-period-report.md +1 -1
  23. package/dist/skill/report-templates/tiktok-period-report.html +14 -2
  24. package/dist/skill/report-templates/tiktok-period-report.md +1 -1
  25. package/dist/skill/report-templates/yandex-period-report.html +15 -1
  26. package/dist/skill/report-templates/yandex-period-report.md +1 -1
  27. package/dist/skill/scripts/install.ps1 +1 -1
  28. package/dist/skill/scripts/install.sh +1 -1
  29. package/package.json +1 -1
package/README.md CHANGED
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.57-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.57-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -7241,6 +7241,80 @@ var init_geo_en2zh = __esm({
7241
7241
  }
7242
7242
  });
7243
7243
 
7244
+ // src/commands/report-period.ts
7245
+ function classifyReportPeriod(current, previous) {
7246
+ const start = tryParse(current?.start);
7247
+ const end = tryParse(current?.end);
7248
+ if (!start || !end) return customPeriod();
7249
+ if (isFullCalendarQuarter(start, end)) {
7250
+ const tag = quarterTagFromMonth(start.m);
7251
+ const prev = tryParse(previous?.start);
7252
+ const prevEnd = tryParse(previous?.end);
7253
+ return {
7254
+ current: tag,
7255
+ previous: prev && prevEnd && isFullCalendarQuarter(prev, prevEnd) ? quarterTagFromMonth(prev.m) : "\u4E0A\u671F",
7256
+ next: tag === "Q4" ? "Q1" : `Q${Number(tag.slice(1)) + 1}`,
7257
+ unit: "\u5B63\u5EA6"
7258
+ };
7259
+ }
7260
+ if (start.d === 1 && start.y === end.y && start.m === end.m) {
7261
+ const prev = tryParse(previous?.start);
7262
+ const prevEnd = tryParse(previous?.end);
7263
+ const prevSameMonth = prev && prevEnd && prev.y === prevEnd.y && prev.m === prevEnd.m;
7264
+ const nextMonth = start.m === 12 ? 1 : start.m + 1;
7265
+ return {
7266
+ current: `${start.m}\u6708`,
7267
+ previous: prevSameMonth ? `${prev.m}\u6708` : "\u4E0A\u671F",
7268
+ next: `${nextMonth}\u6708`,
7269
+ unit: "\u6708\u5EA6"
7270
+ };
7271
+ }
7272
+ return customPeriod();
7273
+ }
7274
+ function rangeFromMeta(meta) {
7275
+ if (!meta || typeof meta !== "object") return null;
7276
+ const record2 = meta;
7277
+ if (typeof record2.startDate !== "string" || typeof record2.endDate !== "string") return null;
7278
+ return { start: record2.startDate, end: record2.endDate };
7279
+ }
7280
+ function attachPeriodChrome(payload) {
7281
+ return { ...payload, periodChrome: classifyReportPeriod(rangeFromMeta(payload.meta)) };
7282
+ }
7283
+ function periodWord(unit, neutral) {
7284
+ return unit || neutral;
7285
+ }
7286
+ function customPeriod() {
7287
+ return { current: "\u672C\u671F", previous: "\u4E0A\u671F", next: "\u4E0B\u671F", unit: "" };
7288
+ }
7289
+ function tryParse(iso) {
7290
+ if (!iso) return null;
7291
+ const match = DATE_RE2.exec(iso.trim());
7292
+ if (!match) return null;
7293
+ const y = Number(match[1]);
7294
+ const m = Number(match[2]);
7295
+ const d = Number(match[3]);
7296
+ if (m < 1 || m > 12 || d < 1 || d > 31) return null;
7297
+ return { y, m, d };
7298
+ }
7299
+ function lastDayOfMonth(y, m) {
7300
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
7301
+ }
7302
+ function isFullCalendarQuarter(start, end) {
7303
+ if (start.y !== end.y || start.d !== 1) return false;
7304
+ if (![1, 4, 7, 10].includes(start.m)) return false;
7305
+ return end.m === start.m + 2 && end.d === lastDayOfMonth(end.y, end.m);
7306
+ }
7307
+ function quarterTagFromMonth(month) {
7308
+ return `Q${Math.ceil(month / 3)}`;
7309
+ }
7310
+ var DATE_RE2;
7311
+ var init_report_period = __esm({
7312
+ "src/commands/report-period.ts"() {
7313
+ "use strict";
7314
+ DATE_RE2 = /^(\d{4})-(\d{2})-(\d{2})/;
7315
+ }
7316
+ });
7317
+
7244
7318
  // src/commands/google-analysis/sections.ts
7245
7319
  var SECTIONS;
7246
7320
  var init_sections = __esm({
@@ -104656,7 +104730,7 @@ function asRecord23(value) {
104656
104730
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
104657
104731
  }
104658
104732
  function injectReportData2(html, payload) {
104659
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
104733
+ const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
104660
104734
  if (!html.includes(INJECT_MARKER)) {
104661
104735
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 google-period-report.html");
104662
104736
  }
@@ -104699,8 +104773,9 @@ async function runGoogleAnalysisRender(opts) {
104699
104773
  await fsPromises2.mkdir(outDir, { recursive: true });
104700
104774
  await fsPromises2.writeFile(outPath, injectReportData2(html, data), "utf8");
104701
104775
  const { stats } = contentCheck;
104776
+ const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u5468\u671F");
104702
104777
  console.log(`
104703
- \u2705 Google Ads \u5468\u671F\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
104778
+ \u2705 Google Ads ${cover}\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
104704
104779
  `);
104705
104780
  console.log(
104706
104781
  ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u6267\u884C\u6458\u8981 ${stats.executiveSummaryCount} \u6BB5\uFF5C\u4F18\u5316\u5EFA\u8BAE ${stats.recommendationCount} \u6761\uFF5C8 \u4E2A\u6570\u636E\u7EF4\u5206\u6790+\u5EFA\u8BAE\u5747\u5DF2\u9F50\u5168
@@ -104716,6 +104791,7 @@ var init_render_report = __esm({
104716
104791
  "use strict";
104717
104792
  init_merge_snapshot();
104718
104793
  init_report_content();
104794
+ init_report_period();
104719
104795
  TEMPLATE_BASENAME = "google-period-report.html";
104720
104796
  INJECT_MARKER = "window.__GOOGLE_PERIOD_REPORT__ = window.__GOOGLE_PERIOD_REPORT__ || null;";
104721
104797
  }
@@ -137077,9 +137153,10 @@ async function fetchYoutubeChannel(opts) {
137077
137153
  }
137078
137154
 
137079
137155
  // src/commands/google-quarterly/windows.ts
137080
- var DATE_RE2 = /^(\d{4})-(\d{2})-(\d{2})$/;
137156
+ init_report_period();
137157
+ var DATE_RE3 = /^(\d{4})-(\d{2})-(\d{2})$/;
137081
137158
  function parseIsoDate(iso) {
137082
- const match = DATE_RE2.exec(iso.trim());
137159
+ const match = DATE_RE3.exec(iso.trim());
137083
137160
  if (!match) throw new Error(`\u65E5\u671F\u987B\u4E3A YYYY-MM-DD\uFF1A${iso}`);
137084
137161
  const y = Number(match[1]);
137085
137162
  const m = Number(match[2]);
@@ -137093,7 +137170,7 @@ function pad(n) {
137093
137170
  function isoDate(y, m, d) {
137094
137171
  return `${y}-${pad(m)}-${pad(d)}`;
137095
137172
  }
137096
- function lastDayOfMonth(y, m) {
137173
+ function lastDayOfMonth2(y, m) {
137097
137174
  return new Date(Date.UTC(y, m, 0)).getUTCDate();
137098
137175
  }
137099
137176
  function listMonths(start, end) {
@@ -137104,7 +137181,7 @@ function listMonths(start, end) {
137104
137181
  let m = s.m;
137105
137182
  while (y < e.y || y === e.y && m <= e.m) {
137106
137183
  const monthStart = y === s.y && m === s.m ? start : isoDate(y, m, 1);
137107
- const monthEnd = y === e.y && m === e.m ? end : isoDate(y, m, lastDayOfMonth(y, m));
137184
+ const monthEnd = y === e.y && m === e.m ? end : isoDate(y, m, lastDayOfMonth2(y, m));
137108
137185
  months.push({
137109
137186
  start: monthStart,
137110
137187
  end: monthEnd,
@@ -137139,14 +137216,14 @@ function previousCalendarQuarter(start, end) {
137139
137216
  const e = parseIsoDate(end);
137140
137217
  const quarterStartMonths = [1, 4, 7, 10];
137141
137218
  if (s.d !== 1 || !quarterStartMonths.includes(s.m)) return null;
137142
- if (e.m !== s.m + 2 || e.d !== lastDayOfMonth(e.y, e.m)) return null;
137219
+ if (e.m !== s.m + 2 || e.d !== lastDayOfMonth2(e.y, e.m)) return null;
137143
137220
  const prevEndMonth = s.m - 1;
137144
137221
  const year = prevEndMonth < 1 ? s.y - 1 : s.y;
137145
137222
  const endM = prevEndMonth < 1 ? 12 : prevEndMonth;
137146
137223
  const startM = endM - 2;
137147
137224
  return {
137148
137225
  start: isoDate(year, startM, 1),
137149
- end: isoDate(year, endM, lastDayOfMonth(year, endM))
137226
+ end: isoDate(year, endM, lastDayOfMonth2(year, endM))
137150
137227
  };
137151
137228
  }
137152
137229
  function yearOverYearRange(start, end) {
@@ -137159,13 +137236,23 @@ function yearOverYearRange(start, end) {
137159
137236
  }
137160
137237
  function shiftYear(p, delta) {
137161
137238
  const y = p.y + delta;
137162
- const last = lastDayOfMonth(y, p.m);
137239
+ const last = lastDayOfMonth2(y, p.m);
137163
137240
  return isoDate(y, p.m, Math.min(p.d, last));
137164
137241
  }
137165
137242
  function utcToIso(ms) {
137166
137243
  const d = new Date(ms);
137167
137244
  return isoDate(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
137168
137245
  }
137246
+ function reportPeriodChrome(current, previous) {
137247
+ const kind = classifyReportPeriod(current, previous);
137248
+ if (kind.unit === "\u5B63\u5EA6") {
137249
+ return { ...kind, reportTitle: "Google \u5B63\u5EA6\u7ECF\u8425\u62A5\u544A", coverEn: "QUARTERLY REPORT" };
137250
+ }
137251
+ if (kind.unit === "\u6708\u5EA6") {
137252
+ return { ...kind, reportTitle: "Google \u6708\u5EA6\u7ECF\u8425\u62A5\u544A", coverEn: "MONTHLY REPORT" };
137253
+ }
137254
+ return { ...kind, reportTitle: "Google \u7ECF\u8425\u62A5\u544A", coverEn: "PERFORMANCE REPORT" };
137255
+ }
137169
137256
  function lastCompleteQuarter(now = /* @__PURE__ */ new Date()) {
137170
137257
  const parts = new Intl.DateTimeFormat("en-CA", {
137171
137258
  timeZone: "Asia/Shanghai",
@@ -137180,7 +137267,7 @@ function lastCompleteQuarter(now = /* @__PURE__ */ new Date()) {
137180
137267
  const year = m <= 3 ? y - 1 : y;
137181
137268
  return {
137182
137269
  start: isoDate(year, qStartMonth, 1),
137183
- end: isoDate(year, qEndMonth, lastDayOfMonth(year, qEndMonth))
137270
+ end: isoDate(year, qEndMonth, lastDayOfMonth2(year, qEndMonth))
137184
137271
  };
137185
137272
  }
137186
137273
 
@@ -138053,17 +138140,28 @@ async function runGoogleQuarterlyRender(opts) {
138053
138140
  }
138054
138141
  const outPath = path31.resolve(opts.out ?? path31.join(path31.dirname(dataPath), GOOGLE_QUARTERLY_HTML_BASENAME));
138055
138142
  await fs26.mkdir(path31.dirname(outPath), { recursive: true });
138056
- const outHtml = injectQuarterlyReport(html, merged);
138143
+ const periodChrome = reportPeriodChrome(readMetaRange(merged, "current"), readMetaRange(merged, "previous"));
138144
+ const outHtml = injectQuarterlyReport(html, { ...merged, periodChrome });
138057
138145
  if (!outHtml.includes("homeCDN/echarts.min.js") || /new Chart\s*\(/.test(outHtml)) {
138058
138146
  console.error("\n\u274C \u6A21\u677F\u5FC5\u987B\u4F7F\u7528 ECharts\uFF0C\u7981\u6B62 Chart.js\u3002\n");
138059
138147
  process.exit(1);
138060
138148
  }
138061
138149
  await fs26.writeFile(outPath, outHtml, "utf8");
138062
138150
  console.log(`
138063
- \u2705 Google \u5B63\u5EA6\u7ECF\u8425\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
138151
+ \u2705 ${periodChrome.reportTitle}\u5DF2\u751F\u6210\uFF1A${outPath}
138064
138152
  `);
138065
138153
  console.log("\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u5373\u53EF\u67E5\u770B\uFF08ECharts\uFF1Astaticpn.siluzan.com/homeCDN/echarts.min.js\uFF09\u3002\n");
138066
138154
  }
138155
+ function readMetaRange(payload, key) {
138156
+ const meta = payload.meta;
138157
+ if (!meta || typeof meta !== "object") return null;
138158
+ const block = meta[key];
138159
+ if (!block || typeof block !== "object") return null;
138160
+ const start = block.start;
138161
+ const end = block.end;
138162
+ if (typeof start !== "string" || typeof end !== "string") return null;
138163
+ return { start, end };
138164
+ }
138067
138165
 
138068
138166
  // src/commands/google-quarterly/register.ts
138069
138167
  function registerGoogleQuarterlyCommands(program2) {
@@ -139011,6 +139109,7 @@ function formatMetaPeriodReportErrors(result) {
139011
139109
  }
139012
139110
 
139013
139111
  // src/commands/facebook-analysis/render-report.ts
139112
+ init_report_period();
139014
139113
  var TEMPLATE_BASENAMES2 = {
139015
139114
  html: "meta-period-report.html"
139016
139115
  };
@@ -139060,7 +139159,7 @@ function asRecord27(value) {
139060
139159
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
139061
139160
  }
139062
139161
  function injectReportData3(html, payload) {
139063
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
139162
+ const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
139064
139163
  if (!html.includes(INJECT_MARKER2)) {
139065
139164
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 meta-period-report.html");
139066
139165
  }
@@ -139123,8 +139222,9 @@ async function runFacebookAnalysisRender(opts) {
139123
139222
  written.push(outPath);
139124
139223
  }
139125
139224
  const { stats } = contentCheck;
139225
+ const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u5468\u671F");
139126
139226
  console.log(`
139127
- \u2705 Meta/Facebook \u5468\u671F\u5206\u6790\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${written.join(" \u3001 ")}
139227
+ \u2705 Meta/Facebook ${cover}\u5206\u6790\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${written.join(" \u3001 ")}
139128
139228
  `);
139129
139229
  console.log(
139130
139230
  ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u5EFA\u8BAE ${stats.recommendationCount}\uFF5C\u56DB\u95EE ${stats.fourQuestionsCount}\uFF5C\u65E5 ${stats.dailyRows}\uFF5C\u56FD\u5BB6 ${stats.countryRows}\uFF5C\u7CFB\u5217 ${stats.campaignRows}\uFF5C\u53D7\u4F17 ${stats.audienceRows}
@@ -141619,6 +141719,7 @@ function formatBingPeriodReportErrors(result) {
141619
141719
  }
141620
141720
 
141621
141721
  // src/commands/bing-analysis/render-report.ts
141722
+ init_report_period();
141622
141723
  var TEMPLATE_BASENAME3 = "bing-period-report.html";
141623
141724
  var INJECT_MARKER4 = "window.__BING_PERIOD_REPORT__ = window.__BING_PERIOD_REPORT__ || null;";
141624
141725
  function resolveSkillTemplatePath6(basename13) {
@@ -141651,7 +141752,7 @@ function asRecord34(value) {
141651
141752
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
141652
141753
  }
141653
141754
  function injectReportData5(html, payload) {
141654
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
141755
+ const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
141655
141756
  if (!html.includes(INJECT_MARKER4)) {
141656
141757
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 bing-period-report.html");
141657
141758
  }
@@ -141694,8 +141795,9 @@ async function runBingAnalysisRender(opts) {
141694
141795
  await fsPromises5.mkdir(outDir, { recursive: true });
141695
141796
  await fsPromises5.writeFile(outPath, injectReportData5(html, data), "utf8");
141696
141797
  const { stats } = contentCheck;
141798
+ const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u8D26\u6237");
141697
141799
  console.log(`
141698
- \u2705 Bing \u5E7F\u544A\u8D26\u6237\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
141800
+ \u2705 Bing \u5E7F\u544A${cover}\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
141699
141801
  `);
141700
141802
  console.log(
141701
141803
  ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u6267\u884C\u6458\u8981 ${stats.executiveSummaryCount} \u6BB5\uFF5C\u4F18\u5316\u5EFA\u8BAE ${stats.recommendationCount} \u6761\uFF5C9 \u4E2A\u5206\u6790\u5C0F\u8282\u5747\u5DF2\u9F50\u5168
@@ -142516,6 +142618,7 @@ function formatTikTokPeriodReportErrors(result) {
142516
142618
  }
142517
142619
 
142518
142620
  // src/commands/tiktok-analysis/render-report.ts
142621
+ init_report_period();
142519
142622
  var TEMPLATE_BASENAME4 = "tiktok-period-report.html";
142520
142623
  var INJECT_MARKER5 = "window.__TIKTOK_PERIOD_REPORT__ = window.__TIKTOK_PERIOD_REPORT__ || null;";
142521
142624
  function resolveSkillTemplatePath7(basename13) {
@@ -142548,7 +142651,7 @@ function asRecord37(value) {
142548
142651
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
142549
142652
  }
142550
142653
  function injectReportData6(html, payload) {
142551
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
142654
+ const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
142552
142655
  if (!html.includes(INJECT_MARKER5)) {
142553
142656
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 tiktok-period-report.html");
142554
142657
  }
@@ -142591,8 +142694,9 @@ async function runTikTokAnalysisRender(opts) {
142591
142694
  await fsPromises6.mkdir(outDir, { recursive: true });
142592
142695
  await fsPromises6.writeFile(outPath, injectReportData6(html, data), "utf8");
142593
142696
  const { stats } = contentCheck;
142697
+ const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u8D26\u6237");
142594
142698
  console.log(`
142595
- \u2705 TikTok \u5E7F\u544A\u4E3B\u8D26\u6237\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
142699
+ \u2705 TikTok \u5E7F\u544A\u4E3B${cover}\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
142596
142700
  `);
142597
142701
  console.log(
142598
142702
  ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u6267\u884C\u6458\u8981 ${stats.executiveSummaryCount} \u6BB5\uFF5C\u4F18\u5316\u5EFA\u8BAE ${stats.recommendationCount} \u6761\uFF5C\u5206\u6790\u5C0F\u8282\u5DF2\u6309\u6709\u6570\u636E\u7684\u7EF4\u5EA6\u6821\u9A8C
@@ -144290,6 +144394,7 @@ function formatYandexPeriodReportErrors(result) {
144290
144394
  }
144291
144395
 
144292
144396
  // src/commands/yandex-analysis/render-report.ts
144397
+ init_report_period();
144293
144398
  var TEMPLATE_BASENAME5 = "yandex-period-report.html";
144294
144399
  var INJECT_MARKER6 = "window.__YANDEX_PERIOD_REPORT__ = window.__YANDEX_PERIOD_REPORT__ || null;";
144295
144400
  function resolveSkillTemplatePath8(basename13) {
@@ -144335,7 +144440,7 @@ function refreshWeeklyFromDaily(data) {
144335
144440
  };
144336
144441
  }
144337
144442
  function injectReportData7(html, payload) {
144338
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
144443
+ const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
144339
144444
  if (!html.includes(INJECT_MARKER6)) {
144340
144445
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 yandex-period-report.html");
144341
144446
  }
@@ -144377,8 +144482,9 @@ async function runYandexAnalysisRender(opts) {
144377
144482
  await fsPromises7.mkdir(outDir, { recursive: true });
144378
144483
  await fsPromises7.writeFile(outPath, injectReportData7(html, data), "utf8");
144379
144484
  const { stats } = contentCheck;
144485
+ const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u8D26\u6237");
144380
144486
  console.log(`
144381
- \u2705 Yandex \u5E7F\u544A\u8D26\u6237\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
144487
+ \u2705 Yandex \u5E7F\u544A${cover}\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
144382
144488
  `);
144383
144489
  console.log(
144384
144490
  ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u4F18\u5316\u5EFA\u8BAE ${stats.recommendationCount} \u6761\uFF5C8 \u4E2A\u5206\u6790\u7AE0\u8282\u5747\u5DF2\u9F50\u5168
@@ -46,6 +46,7 @@ siluzan-tso -h
46
46
  - **Google 广告类型边界**:CLI/Skill **仅支持** Google **搜索广告(Search)** 与 **Performance Max(PMax)** 的方案、创建与精细管理;**不支持展示广告(Display)**(含自适应展示广告 RDA、独立 Display 系列)。用户要做展示广告时须明确告知不支持,可改推 Search / PMax;**禁止**用 `campaign-create` / `ad-create` 伪装创建 Display。
47
47
  - **删系列 / 测试系列须先问(硬性)**:「尽快创建 / 按方案搭建」不含删除授权。创建失败(政策违规、名称占用、半成品)时**禁止**自行建测试系列逐地域排查、**禁止**自行 `ad campaign-delete`(含 Agent 自建系列);先报告原因与选项,用户同意后才带 `--i-confirm --commit`。也**禁止**为让创建通过而整类删减方案内容(否词/排除地域/投放国家):按 `batch get` 的 `failedOperations[]` 只改报错项。见 `google-ads-write.md`「创建失败时禁止的做法」。
48
48
  - **创建完成后交付(硬性)**:`ad batch diff` 的 stdout 含 `BEGIN_USER_DELIVERY_MARKDOWN`…`END_USER_DELIVERY_MARKDOWN`——**立刻**把中间全文原样发给用户(可先于补建);禁止只摘要、禁止只说「创建成功/详情已交付/未发现缺失」、禁止等全部补建结束再交付。
49
+ - **报告 render 成功即交付(硬性)**:`google-quarterly render`、`google-analysis render`、`google-ads-diagnosis render`、`website-diagnosis render`、`market-analysis render`,以及 Bing / TikTok / Yandex / Meta 的 `render`,stdout 给出 HTML 路径后**立刻**把该文件交给用户。P10 与各媒体周期报告封面的「季度 / 月度」已按日期写好(其余仍是「周期」或「账户」)。**禁止**再打开、grep 或修改这份 HTML,**禁止**为此再 render。用户没要 PDF 就不要 `html_to_pdf`;要了也只转一次,**禁止**再用 pdftotext、PyMuPDF、截图或视觉模型读 PDF/页面。细则见 `agent-conventions.md` §七。
49
50
  - `list-accounts` 无余额/消耗;列全部用 `--page-size 999`。查单户必须 `-k`;`itemCount < total` 时**禁止**判定账户未绑定。
50
51
  - 多户余额用 `balance-scan`(P2),多户消耗 / **转化成本·CPA 巡检**用 `accounts-digest`(P3,`--max-cpa`;**没有** `cpa-risk-scan`)。禁止外层 for-loop 逐户拉数。
51
52
  - `stats` 的 `spend` = **区间合计**,不是日消耗。
@@ -122,7 +123,7 @@ siluzan-tso -h
122
123
  | ------------------ | ------ | -------- |
123
124
  | Google 广告账户诊断 / 健康检查(**含账户 ID**) | **P1** | `references/analytics/account-analytics.md` + `report-templates/google-ads-diagnosis.md`(可选对照 `report-templates/google-account-diagnosis-report.md`) |
124
125
  | Google 账户周期 **数据汇总**(只要花费/转化/9 章/Excel;**无**规划/客户版/大族) | **P4** | `report-templates/google-period-report.md` + `references/analytics/account-analytics.md`;Excel 加 `google-period-report-excel.md` |
125
- | Google/谷歌 **月度报告** / **月总结与规划** / **季度报告** / **Q3 汇总** / **Q4 规划** / **季度汇报** / **投放季报** / 客户版 / 大族模板 / 下月规划(效率+区域询盘+国家明细) | **P10** | `report-templates/google-quarterly-report.md` + **`report-templates/google-quarterly-plan.md`**(**未带询盘表须先问并等待**,禁止不问就 collect;有表须先转 JSON 再 `--inquiries`,禁止把 xlsx 丢给 collect;`collect`+`render`;规划九章;花费三档基于本期、不是已批预算;**不做**行业数据) |
126
+ | Google/谷歌 **月度报告** / **月总结与规划** / **季度报告** / **Q3 汇总** / **Q4 规划** / **季度汇报** / **投放季报** / 客户版 / 大族模板 / 下月规划(效率+区域询盘+国家明细) | **P10** | `report-templates/google-quarterly-report.md` + **`report-templates/google-quarterly-plan.md`**(**未带询盘表须先问并等待**,禁止不问就 collect;有表须先转 JSON 再 `--inquiries`,禁止把 xlsx 丢给 collect;`collect`+`render`;规划九章;花费三档基于本期、不是已批预算;**不做**行业数据;**render 成功立刻交付 HTML**,禁止改 HTML / 转 PDF / 截图验收) |
126
127
  | Meta/Facebook 周期或诊断 | **P4-FB** | `report-templates/meta-period-report.md` + `assets/meta-period-report-rules.md` + `references/analytics/facebook-analysis-guide.md` |
127
128
  | TikTok / Bing 周期报告 | **P4** | `references/analytics/account-analytics.md` + 对应 `report-templates/*-period-report.md` |
128
129
  | 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.57-beta.1",
4
- "publishedAt": 1792724333584
3
+ "version": "1.1.57-beta.2",
4
+ "publishedAt": 1792735232928
5
5
  }
@@ -221,7 +221,9 @@
221
221
 
222
222
  ## 七、交付前自检(报告 / Excel / 含金额话术)
223
223
 
224
- > 在产物文件已写入磁盘**之后、发给用户之前**执行;不靠外部校验脚本,由 Agent **亲自 Read 最终产物文件**(HTML / Markdown 等;二进制 xlsx 无法 Read 时在对话贴自检表逐条勾选,依据为生成脚本的 stdout 摘要)。审阅阶段只看最终产物 + 已掌握的账户元数据,**不**回头 Read 落盘业务 JSON。
224
+ > CLI `render` 写出的 HTML(P1、P4、P4-FB、P8、P9、P10,以及 Bing / TikTok / Yandex / Meta 周期或诊断):命令成功并打印路径就是自检通过。**立刻交付该 HTML**。P10 与各媒体周期报告的「季度 / 月度」已按日期写进封面(对不上的周期报告仍写「周期」或「账户」),不要 Read、grep 或修改成品,不要为此再 render。用户没要 PDF 就不要转;要了也只转一次,不要用 pdftotext、PyMuPDF、截图或视觉模型读页面。下面 A/B/C 只用于 Agent 自己写的 Excel / Markdown。
225
+
226
+ > 在 Agent 自写产物已写入磁盘**之后、发给用户之前**执行;不靠外部校验脚本,由 Agent **亲自 Read 最终产物文件**(Markdown 等;二进制 xlsx 无法 Read 时在对话贴自检表逐条勾选,依据为生成脚本的 stdout 摘要)。审阅阶段只看最终产物 + 已掌握的账户元数据,**不**回头 Read 落盘业务 JSON。
225
227
 
226
228
  **A · 币种**:首行含 `统计区间:…(货币:CNY|USD)`;全文符号与 `currencyCode` 一致(CNY=¥、USD=$,未混用);与当次 `list-accounts -k` 结果相同;多账户分币种分表、无跨币种「总计」行。
227
229
 
@@ -82,7 +82,7 @@
82
82
  4. **门禁**:对每个 section Read `<section>-<accountId>_*.outline.txt` 后再写脚本。
83
83
  5. 脚本读盘写产物:默认 **HTML**(`report-template*.html` 版式参考);用户要 **Excel** → Agent 脚本写 xlsx(**禁止**宿主第三方 xlsx Skill;**无** CLI excel 子命令)。
84
84
  6. 默认 HTML 报告须含:账户概览、投放趋势、Top 关键词/系列/地区/受众分布、优化建议;用户定制 Excel 以用户 Sheet 清单为准。
85
- - **产物**:按 conventions §七 自检;`accountId` 须与用户给的 `mediaCustomerId` 一致。
85
+ - **产物**:按 conventions §七 自检;`accountId` 须与用户给的 `mediaCustomerId` 一致。HTML 封面按区间写成「季度」或「月度」,对不上则仍是「周期」。不要打开 HTML 改称呼。
86
86
 
87
87
  > 用户用「OKKI 周报」固定话术 → 改走 **P6**,不按默认 9 章追问。
88
88
  > 只要账户级按日 Excel(无多维章节)→ **P4-DAILY**。
@@ -207,4 +207,5 @@
207
207
  5. 读 `google-quarterly-collect.json`,只写 `narrative` 到 `google-quarterly.json`(禁止改 kpis / monthly / planDefaults 数字)。
208
208
  6. 下期规划按 `google-quarterly-plan.md` 写九章。预算默认三档(维持本期 / +30% / +50%)+ 建议档月度 30/35/35,标「基于历史数据」,不是已批预算;CPA/询盘不外推;禁止自动调价/暂停指令。
209
209
  7. `google-quarterly render --data ./snap-p10/google-quarterly.json --out ./snap-p10/google-quarterly-report.html`。
210
+ 8. **render 成功即停并交付**该 HTML。封面称呼已按区间写好:完整自然季是「季度」,从 1 号起的同一个月(含未过完的本月)是「月度」,其余是「本期」。**禁止**打开或修改 HTML,**禁止**再把「季度 / 月度」改一遍,**禁止**再 render,**禁止**未要求就转 PDF、截图或用视觉模型验收。
210
211
  - **产物**:HTML 路径;新探索(视频 / PMax / 发现 / 展示)全无量则模板自动隐藏该章。
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 统计区间:`{startDate}` ~ `{endDate}`(**不可包含今天或昨天**,见下文)
4
4
  > 账户:`{mediaCustomerId}`(`{mediaCustomerName}`)
5
- > **默认交付物**:一份可打开的、带 ECharts 图表的 **HTML 文件**(`bing-period-report.html`)。
5
+ > **默认交付物**:一份可打开的、带 ECharts 图表的 **HTML 文件**(`bing-period-report.html`)。封面按日期写成「季度」或「月度」,对不上仍是「账户分析」。不要打开 HTML 改称呼。
6
6
  > 用户**未指定格式**时一律走下方 **标准四步流程**;**禁止** Agent 手写/拼接 HTML(语法错误会导致报告无法显示)。
7
7
  > **用户明确要求 Excel / xlsx** 时:拉数步骤不变,改为 Agent 脚本写 `.xlsx`;**不要**调用 `bing-analysis render`;禁止假设存在内置 excel 子命令。
8
8
 
@@ -52,7 +52,7 @@ siluzan-tso google-analysis render \
52
52
 
53
53
  模板源码:`report-templates/google-period-report.html`(结构与样式,ECharts 渲染逻辑已内联)。
54
54
 
55
- `render` 会向输出目录写入 HTML,并注入 `window.__GOOGLE_PERIOD_REPORT__`。
55
+ `render` 会向输出目录写入 HTML,并注入 `window.__GOOGLE_PERIOD_REPORT__`。封面按 `meta.startDate/endDate` 写成「季度分析」或「月度分析」;不是完整自然季、也不是从 1 号起的同一个月时,仍写「周期分析」。不要打开 HTML 改这几个字。
56
56
  传 `--snapshot-dir` 时 CLI 自动从快照合并 KPI、每日趋势图表、周期汇总/系列/设备/地域/受众/关键词表格:**数值一律以 CLI 快照覆盖**(`meta` 仅补空)。本期 KPI 优先 `campaigns` 累加。若 manifest 缺条目但磁盘上仍有对应 `*.json`,CLI 会按文件名自愈补齐。Agent 只写 `narrative`,勿自填 `kpis`/`tables`。
57
57
 
58
58
  ---
@@ -5,6 +5,8 @@
5
5
  > 数据只消费 `google-quarterly-collect.json`(及用户明确给的业务约束)。**禁止**再算一遍账户诊断,**禁止**编造预算 / CPA / 询盘 / 市场数。
6
6
  >
7
7
  > 编号 Q01–Q30 是规划条,**不是** playbook P10。
8
+ >
9
+ > 正文称呼跟区间走,和封面同一套:完整自然季写「季度」;从 1 号起的同一个月写「月度 / 本月」,路线图写「本月 / 次月 / 再次月」。封面由 `render` 生成,写完不要再改 HTML。
8
10
 
9
11
  ## 范围
10
12
 
@@ -63,6 +63,8 @@ siluzan-tso google-quarterly render \
63
63
  --out ./snap-p10/google-quarterly-report.html
64
64
  ```
65
65
 
66
+ `render` 打印 HTML 路径后**立刻交付该文件并结束**。封面和章节称呼由日期决定,不用改 HTML:完整自然季 →「Q3 季度…」;从 1 号起、起止在同一个月(含未过完的本月,如 10-01~10-22)→「10月 月度…」;跨月但不是完整自然季 →「本期」。**禁止**打开、grep 或改写 HTML,**禁止**为改「季度 / 月度」再跑一遍 render。用户没要 PDF 不要转换;要了也只转一次,不要读 PDF 正文、不要截图、不要用视觉模型验收。
67
+
66
68
  省略 `--start/--end` 时 collect 用上一完整自然季(上海日历)。
67
69
 
68
70
  可选:`--keyword-seeds "a,b"` 走 Keyword Planner(标注数据源,禁止写成行业指数)。**不做**频道订阅总数、订阅来源饼、视频类型(要 YouTube Analytics / 人工分类)。YouTube 广告能拿到的曝光 / TrueView / 完播 / CPV / 频道 ID /(若已关联)广告带来订阅,collect 写入 `exploreAds.types[video]` + `videosSummary`;PMax / 发现 / 展示并行写入 `exploreAds`。render 用 tab 切换,不编造 ABCD / 素材组 / 受众切分。
@@ -3,7 +3,7 @@
3
3
  > 统计区间:`{startDate}` ~ `{endDate}`
4
4
  > 账户:`{mediaCustomerId}`(`{mediaCustomerName}`)
5
5
 
6
- > **默认交付物**:一份可打开的、带 ECharts 图表的 **HTML 文件**(`meta-period-report.html`)。
6
+ > **默认交付物**:一份可打开的、带 ECharts 图表的 **HTML 文件**(`meta-period-report.html`)。封面按日期写成「季度复盘」或「月度复盘」,对不上仍是「周期复盘」。不要打开 HTML 改称呼。
7
7
  > 用户**未指定格式**时一律走下方 **标准四步流程**;**禁止**仅 Markdown 摘要或纯 JSON 充当终稿。
8
8
  > 用户要 **表格 / Excel / xlsx** 时:步骤 1~3 相同,步骤 4 改 `--format xlsx`(见文末)。
9
9
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  数据块:总览、系列/组/广告、视频素材、受众(含性别/年龄/兴趣合并)(**以 `--json-out` 实际字段为准**)。
7
7
 
8
- **默认交付:HTML(`tiktok-analysis render` 注入 Agent JSON 生成;禁止手写/拼接 HTML)**。用户指定 Excel 时 Agent 脚本写 xlsx。
8
+ **默认交付:HTML(`tiktok-analysis render` 注入 Agent JSON 生成;禁止手写/拼接 HTML)**。用户指定 Excel 时 Agent 脚本写 xlsx。封面按日期写成「季度」或「月度」,对不上仍是「账户分析」。不要打开 HTML 改称呼。
9
9
 
10
10
  ---
11
11
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 统计区间:`{startDate}` ~ `{endDate}`(**不能晚于今天**;省略默认近 7 天含今天)
4
4
  > 账户:`{mediaCustomerId}`(如 `porg-xxx`,与 `list-accounts -m Yandex` 一致)
5
- > **默认交付物**:一份可打开的 **HTML 文件**(`yandex-period-report.html`,样式对齐开发样例)。
5
+ > **默认交付物**:一份可打开的 **HTML 文件**(`yandex-period-report.html`,样式对齐开发样例)。封面按日期写成「季度」或「月度」,对不上仍是「账户分析」。不要打开 HTML 改称呼。
6
6
  > 用户**未指定格式**时一律走下方 **标准四步流程**;**禁止** Agent 手写/拼接 HTML。
7
7
  > **用户明确要求 Excel / xlsx** 时:拉数步骤不变,改为 Agent 脚本写 `.xlsx`;**不要**调用 `yandex-analysis render`;禁止假设存在内置 excel 子命令。见 `yandex-period-report-excel.md`。
8
8
 
@@ -609,6 +609,12 @@
609
609
  (function () {
610
610
  "use strict";
611
611
 
612
+ /** 只读 render 注入的 periodChrome,规则与 report-period.ts 相同。 */
613
+ function periodWord(data, neutral) {
614
+ const unit = data && data.periodChrome && data.periodChrome.unit;
615
+ return unit === "季度" || unit === "月度" ? unit : neutral;
616
+ }
617
+
612
618
  const PALETTE = [
613
619
  "#0ea5e9",
614
620
  "#6366f1",
@@ -772,7 +778,7 @@
772
778
  .join("");
773
779
  return `
774
780
  <div class="hero">
775
- <div class="eyebrow">BING ADS · 账户分析报告</div>
781
+ <div class="eyebrow">BING ADS · ${periodWord(data, "账户")}分析报告</div>
776
782
  <h1>${escapeHtml(meta.accountName || "Bing 广告账户")}</h1>
777
783
  <div class="meta-chips">
778
784
  <span class="meta-chip">📅 <b>${escapeHtml(period)}</b></span>
@@ -1159,7 +1165,7 @@ ${titleNotice}
1159
1165
 
1160
1166
  function renderFooter(data) {
1161
1167
  const generatedAt = data.meta?.generatedAt || new Date().toISOString();
1162
- return `<div class="footer"><span class="brand">siluzan-tso</span> · Bing 广告账户分析报告 · ${escapeHtml(generatedAt)} · 数据来源 bing-analysis</div>`;
1168
+ return `<div class="footer"><span class="brand">siluzan-tso</span> · Bing 广告${periodWord(data, "账户")}分析报告 · ${escapeHtml(generatedAt)} · 数据来源 bing-analysis</div>`;
1163
1169
  }
1164
1170
 
1165
1171
  function styleAxis(extra) {
@@ -1326,6 +1332,12 @@ ${titleNotice}
1326
1332
  '<div class="card">未找到报告数据(window.__BING_PERIOD_REPORT__ 为空),请通过 `bing-analysis render` 生成本文件。</div>';
1327
1333
  return;
1328
1334
  }
1335
+ const cover = periodWord(data, "账户");
1336
+ document.title =
1337
+ (data.meta && data.meta.accountName ? data.meta.accountName + " · " : "") +
1338
+ "Bing 广告" +
1339
+ cover +
1340
+ "分析报告";
1329
1341
  root.innerHTML = [
1330
1342
  renderHero(data),
1331
1343
  renderExecutiveSummary(data),
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 统计区间:`{startDate}` ~ `{endDate}`(**不可包含今天或昨天**,见下文)
4
4
  > 账户:`{mediaCustomerId}`(`{mediaCustomerName}`)
5
- > **默认交付物**:一份可打开的、带 ECharts 图表的 **HTML 文件**(`bing-period-report.html`)。
5
+ > **默认交付物**:一份可打开的、带 ECharts 图表的 **HTML 文件**(`bing-period-report.html`)。封面按日期写成「季度」或「月度」,对不上仍是「账户分析」。不要打开 HTML 改称呼。
6
6
  > 用户**未指定格式**时一律走下方 **标准四步流程**;**禁止** Agent 手写/拼接 HTML(语法错误会导致报告无法显示)。
7
7
  > **用户明确要求 Excel / xlsx** 时:拉数步骤不变,改为 Agent 脚本写 `.xlsx`;**不要**调用 `bing-analysis render`;禁止假设存在内置 excel 子命令。
8
8
 
@@ -656,6 +656,12 @@
656
656
  (function () {
657
657
  "use strict";
658
658
 
659
+ /** 只读 render 注入的 periodChrome,规则与 report-period.ts 相同。 */
660
+ function periodWord(data, neutral) {
661
+ const unit = data && data.periodChrome && data.periodChrome.unit;
662
+ return unit === "季度" || unit === "月度" ? unit : neutral;
663
+ }
664
+
659
665
  const PALETTE = [
660
666
  "#3b82f6",
661
667
  "#8b5cf6",
@@ -785,7 +791,7 @@
785
791
  .join("");
786
792
  return `
787
793
  <div class="hero">
788
- <div class="eyebrow">GOOGLE ADS · 周期分析报告</div>
794
+ <div class="eyebrow">GOOGLE ADS · ${periodWord(data, "周期")}分析报告</div>
789
795
  <h1>${escapeHtml(meta.accountName || "Google Ads 账户")}</h1>
790
796
  <div class="meta-chips">
791
797
  <span class="meta-chip">📅 <b>${escapeHtml(period)}</b></span>
@@ -1117,7 +1123,7 @@
1117
1123
 
1118
1124
  function renderFooter(data) {
1119
1125
  const generatedAt = data.meta?.generatedAt || new Date().toISOString();
1120
- return `<div class="footer"><span class="brand">siluzan-tso</span> · Google Ads 周期分析报告 · ${escapeHtml(generatedAt)} · 数据来源 google-analysis</div>`;
1126
+ return `<div class="footer"><span class="brand">siluzan-tso</span> · Google Ads ${periodWord(data, "周期")}分析报告 · ${escapeHtml(generatedAt)} · 数据来源 google-analysis</div>`;
1121
1127
  }
1122
1128
 
1123
1129
  function styleAxis(extra) {
@@ -1375,6 +1381,12 @@
1375
1381
  '<div class="card">未找到报告数据(window.__GOOGLE_PERIOD_REPORT__ 为空),请通过 `google-analysis render` 生成本文件。</div>';
1376
1382
  return;
1377
1383
  }
1384
+ const cover = periodWord(data, "周期");
1385
+ document.title =
1386
+ (data.meta && data.meta.accountName ? data.meta.accountName + " · " : "") +
1387
+ "Google Ads " +
1388
+ cover +
1389
+ "分析报告";
1378
1390
  root.innerHTML = [
1379
1391
  renderHero(data),
1380
1392
  renderExecutiveSummary(data),
@@ -52,7 +52,7 @@ siluzan-tso google-analysis render \
52
52
 
53
53
  模板源码:`report-templates/google-period-report.html`(结构与样式,ECharts 渲染逻辑已内联)。
54
54
 
55
- `render` 会向输出目录写入 HTML,并注入 `window.__GOOGLE_PERIOD_REPORT__`。
55
+ `render` 会向输出目录写入 HTML,并注入 `window.__GOOGLE_PERIOD_REPORT__`。封面按 `meta.startDate/endDate` 写成「季度分析」或「月度分析」;不是完整自然季、也不是从 1 号起的同一个月时,仍写「周期分析」。不要打开 HTML 改这几个字。
56
56
  传 `--snapshot-dir` 时 CLI 自动从快照合并 KPI、每日趋势图表、周期汇总/系列/设备/地域/受众/关键词表格:**数值一律以 CLI 快照覆盖**(`meta` 仅补空)。本期 KPI 优先 `campaigns` 累加。若 manifest 缺条目但磁盘上仍有对应 `*.json`,CLI 会按文件名自愈补齐。Agent 只写 `narrative`,勿自填 `kpis`/`tables`。
57
57
 
58
58
  ---
@@ -5,6 +5,8 @@
5
5
  > 数据只消费 `google-quarterly-collect.json`(及用户明确给的业务约束)。**禁止**再算一遍账户诊断,**禁止**编造预算 / CPA / 询盘 / 市场数。
6
6
  >
7
7
  > 编号 Q01–Q30 是规划条,**不是** playbook P10。
8
+ >
9
+ > 正文称呼跟区间走,和封面同一套:完整自然季写「季度」;从 1 号起的同一个月写「月度 / 本月」,路线图写「本月 / 次月 / 再次月」。封面由 `render` 生成,写完不要再改 HTML。
8
10
 
9
11
  ## 范围
10
12
 
@@ -593,14 +593,24 @@
593
593
  const adminLabel = meta.drillAdminLabel || "一级行政区";
594
594
  const drillTitle = drillCountry ? drillCountry + "下钻" : "消耗最高国家下钻";
595
595
  const showDrill = meta.usGeoSource === "subdivisions" && (data.usStates || []).length > 0;
596
- document.title = name + " · Google 季度经营报告";
596
+ const chrome = data.periodChrome || {
597
+ current: quarterTag(meta.current) || "本期",
598
+ previous: quarterTag(meta.previous) || "上期",
599
+ next: nextQuarterTag(meta.current),
600
+ unit: "季度",
601
+ reportTitle: "Google 季度经营报告",
602
+ coverEn: "QUARTERLY REPORT",
603
+ };
604
+ const curQ = chrome.current || "本期";
605
+ const prevQ = chrome.previous || "上期";
606
+ const nextQ = chrome.next || "下期";
607
+ const unit = chrome.unit || "";
608
+ const periodHead = unit ? curQ + " " + unit : curQ;
609
+ document.title = name + " · " + (chrome.reportTitle || "Google 经营报告");
597
610
  document.getElementById("nav-brand").innerHTML = esc(name) + ' · <span>Google Ads</span>';
598
611
  document.getElementById("nav-explore").classList.toggle("hidden", hideSpecial);
599
612
 
600
613
  const exec = lines(narrative.executiveSummary);
601
- const curQ = quarterTag(meta.current) || "本期";
602
- const prevQ = quarterTag(meta.previous) || "上期";
603
- const nextQ = nextQuarterTag(meta.current);
604
614
  const prev = kpis.previous || {};
605
615
  const vsYoy = kpis.vsYoy || {};
606
616
  const months = data.monthly || [];
@@ -631,9 +641,9 @@
631
641
  const root = document.getElementById("report-root");
632
642
  root.innerHTML = `
633
643
  <header class="cover" id="cover">
634
- <div class="cover-tag">GOOGLE ADS · QUARTERLY REPORT</div>
644
+ <div class="cover-tag">GOOGLE ADS · ${esc(chrome.coverEn || "QUARTERLY REPORT")}</div>
635
645
  <h1>${esc(name)}</h1>
636
- <p class="subtitle">${esc(curQ)} 季度数据汇总 &amp; ${esc(nextQ)} 规划</p>
646
+ <p class="subtitle">${esc(periodHead)}数据汇总 &amp; ${esc(nextQ)} 规划</p>
637
647
  <div class="cover-meta">
638
648
  <span>账户:<strong>${esc(meta.accountId || "—")}</strong></span>
639
649
  <span>渠道:<strong>${esc(channelsLabel)}</strong></span>
@@ -677,7 +687,7 @@
677
687
  </section>
678
688
  <section class="chapter" id="s1">
679
689
  <p class="num">PART 01</p>
680
- <h2>${esc(curQ)} 季度数据表现</h2>
690
+ <h2>${esc(periodHead)}数据表现</h2>
681
691
  <p>先看效率是否变好,再看预算花在哪,最后看询盘从哪些国家来。</p>
682
692
  </section>
683
693
  <section class="page" id="efficiency">
@@ -1594,14 +1604,14 @@
1594
1604
  function planChapters(plan) {
1595
1605
  if (!plan || typeof plan !== "object") return analysisBlock(plan, "规划");
1596
1606
  const chapters = [
1597
- ["analysis", "季度投放结论"],
1598
- ["goals", "季度目标与 KPI"],
1607
+ ["analysis", "投放结论"],
1608
+ ["goals", "目标与 KPI"],
1599
1609
  ["budget", "预算与市场规划"],
1600
1610
  ["structure", "广告账户结构"],
1601
1611
  ["keywords", "关键词与搜索主题"],
1602
1612
  ["audience", "受众规划"],
1603
1613
  ["creative", "素材与落地页"],
1604
- ["roadmap", "季度执行路线图"],
1614
+ ["roadmap", "执行路线图"],
1605
1615
  ["risks", "风险与待确认"],
1606
1616
  ];
1607
1617
  const parts = chapters.map(([key, title]) => {
@@ -63,6 +63,8 @@ siluzan-tso google-quarterly render \
63
63
  --out ./snap-p10/google-quarterly-report.html
64
64
  ```
65
65
 
66
+ `render` 打印 HTML 路径后**立刻交付该文件并结束**。封面和章节称呼由日期决定,不用改 HTML:完整自然季 →「Q3 季度…」;从 1 号起、起止在同一个月(含未过完的本月,如 10-01~10-22)→「10月 月度…」;跨月但不是完整自然季 →「本期」。**禁止**打开、grep 或改写 HTML,**禁止**为改「季度 / 月度」再跑一遍 render。用户没要 PDF 不要转换;要了也只转一次,不要读 PDF 正文、不要截图、不要用视觉模型验收。
67
+
66
68
  省略 `--start/--end` 时 collect 用上一完整自然季(上海日历)。
67
69
 
68
70
  可选:`--keyword-seeds "a,b"` 走 Keyword Planner(标注数据源,禁止写成行业指数)。**不做**频道订阅总数、订阅来源饼、视频类型(要 YouTube Analytics / 人工分类)。YouTube 广告能拿到的曝光 / TrueView / 完播 / CPV / 频道 ID /(若已关联)广告带来订阅,collect 写入 `exploreAds.types[video]` + `videosSummary`;PMax / 发现 / 展示并行写入 `exploreAds`。render 用 tab 切换,不编造 ABCD / 素材组 / 受众切分。
@@ -126,7 +126,7 @@ table.data th,table.data td{white-space:nowrap;font-size:.8rem}
126
126
  </nav>
127
127
  <div class="wrap">
128
128
  <header class="hero">
129
- <div class="hero-badge">Facebook Ads · 周期复盘</div>
129
+ <div class="hero-badge" id="heroBadge">Facebook Ads · 周期复盘</div>
130
130
  <h1 id="heroTitle">Meta / Facebook 广告投放分析报告</h1>
131
131
  <p class="sub" id="heroSub"></p>
132
132
  </header>
@@ -269,7 +269,11 @@ window.__META_PERIOD_REPORT__ = window.__META_PERIOD_REPORT__ || null;
269
269
  var brand = meta.accountName || meta.brandName || "Facebook 广告账户";
270
270
  var period = meta.periodLabel || ((meta.startDate || "") + " ~ " + (meta.endDate || ""));
271
271
  var campaign = meta.campaignName || "";
272
- document.title = brand + " Facebook 广告报告" + (campaign ? " | " + campaign : "");
272
+ var coverUnit = data.periodChrome && data.periodChrome.unit;
273
+ var coverWord = coverUnit === "季度" || coverUnit === "月度" ? coverUnit : "周期";
274
+ document.title = brand + " · Facebook " + coverWord + "分析报告" + (campaign ? " | " + campaign : "");
275
+ var heroBadge = document.getElementById("heroBadge");
276
+ if (heroBadge) heroBadge.textContent = "Facebook Ads · " + coverWord + "复盘";
273
277
  document.getElementById("heroTitle").innerHTML = esc(brand) + " · Facebook 广告投放<br/>" + esc(period) + "分析报告";
274
278
  document.getElementById("heroSub").textContent =
275
279
  (campaign ? "广告系列:" + campaign + " | " : "") +
@@ -3,7 +3,7 @@
3
3
  > 统计区间:`{startDate}` ~ `{endDate}`
4
4
  > 账户:`{mediaCustomerId}`(`{mediaCustomerName}`)
5
5
 
6
- > **默认交付物**:一份可打开的、带 ECharts 图表的 **HTML 文件**(`meta-period-report.html`)。
6
+ > **默认交付物**:一份可打开的、带 ECharts 图表的 **HTML 文件**(`meta-period-report.html`)。封面按日期写成「季度复盘」或「月度复盘」,对不上仍是「周期复盘」。不要打开 HTML 改称呼。
7
7
  > 用户**未指定格式**时一律走下方 **标准四步流程**;**禁止**仅 Markdown 摘要或纯 JSON 充当终稿。
8
8
  > 用户要 **表格 / Excel / xlsx** 时:步骤 1~3 相同,步骤 4 改 `--format xlsx`(见文末)。
9
9
 
@@ -639,6 +639,12 @@
639
639
  (function () {
640
640
  "use strict";
641
641
 
642
+ /** 只读 render 注入的 periodChrome,规则与 report-period.ts 相同。 */
643
+ function periodWord(data, neutral) {
644
+ const unit = data && data.periodChrome && data.periodChrome.unit;
645
+ return unit === "季度" || unit === "月度" ? unit : neutral;
646
+ }
647
+
642
648
  const PALETTE = [
643
649
  "#fe2c55",
644
650
  "#25f4ee",
@@ -765,7 +771,7 @@
765
771
  .join("");
766
772
  return `
767
773
  <div class="hero">
768
- <div class="eyebrow">TIKTOK ADS · 账户分析报告</div>
774
+ <div class="eyebrow">TIKTOK ADS · ${periodWord(data, "账户")}分析报告</div>
769
775
  <h1>${escapeHtml(meta.accountName || "TikTok 广告主账户")}</h1>
770
776
  <div class="meta-chips">
771
777
  <span class="meta-chip">📅 <b>${escapeHtml(period)}</b></span>
@@ -1031,7 +1037,7 @@
1031
1037
 
1032
1038
  function renderFooter(data) {
1033
1039
  const generatedAt = data.meta?.generatedAt || new Date().toISOString();
1034
- return `<div class="footer"><span class="brand">siluzan-tso</span> · TikTok 广告主账户分析报告 · ${escapeHtml(generatedAt)} · 数据来源 tiktok-analysis</div>`;
1040
+ return `<div class="footer"><span class="brand">siluzan-tso</span> · TikTok 广告主${periodWord(data, "账户")}分析报告 · ${escapeHtml(generatedAt)} · 数据来源 tiktok-analysis</div>`;
1035
1041
  }
1036
1042
 
1037
1043
  function styleAxis(extra) {
@@ -1122,6 +1128,12 @@
1122
1128
  '<div class="card">未找到报告数据(window.__TIKTOK_PERIOD_REPORT__ 为空),请通过 `tiktok-analysis render` 生成本文件。</div>';
1123
1129
  return;
1124
1130
  }
1131
+ const cover = periodWord(data, "账户");
1132
+ document.title =
1133
+ (data.meta && data.meta.accountName ? data.meta.accountName + " · " : "") +
1134
+ "TikTok 广告主" +
1135
+ cover +
1136
+ "分析报告";
1125
1137
  root.innerHTML = [
1126
1138
  renderHero(data),
1127
1139
  renderExecutiveSummary(data),
@@ -5,7 +5,7 @@
5
5
 
6
6
  数据块:总览、系列/组/广告、视频素材、受众(含性别/年龄/兴趣合并)(**以 `--json-out` 实际字段为准**)。
7
7
 
8
- **默认交付:HTML(`tiktok-analysis render` 注入 Agent JSON 生成;禁止手写/拼接 HTML)**。用户指定 Excel 时 Agent 脚本写 xlsx。
8
+ **默认交付:HTML(`tiktok-analysis render` 注入 Agent JSON 生成;禁止手写/拼接 HTML)**。用户指定 Excel 时 Agent 脚本写 xlsx。封面按日期写成「季度」或「月度」,对不上仍是「账户分析」。不要打开 HTML 改称呼。
9
9
 
10
10
  ---
11
11
 
@@ -445,6 +445,12 @@
445
445
  (function () {
446
446
  "use strict";
447
447
 
448
+ /** 只读 render 注入的 periodChrome,规则与 report-period.ts 相同。 */
449
+ function periodWord(data, neutral) {
450
+ const unit = data && data.periodChrome && data.periodChrome.unit;
451
+ return unit === "季度" || unit === "月度" ? unit : neutral;
452
+ }
453
+
448
454
  const EMPTY_NOTE = "本节暂无数据";
449
455
 
450
456
  function escapeHtml(s) {
@@ -703,7 +709,9 @@
703
709
  " 数据报告";
704
710
  return (
705
711
  '<div class="wrap hero">' +
706
- '<div class="eyebrow">YANDEX DIRECT · 账户分析报告</div>' +
712
+ '<div class="eyebrow">YANDEX DIRECT · ' +
713
+ periodWord(data, "账户") +
714
+ "分析报告</div>" +
707
715
  "<h1>" +
708
716
  escapeHtml(title) +
709
717
  "</h1>" +
@@ -1470,6 +1478,12 @@
1470
1478
  '<div class="wrap"><div class="panel empty-note">未找到报告数据(window.__YANDEX_PERIOD_REPORT__ 为空),请通过 `yandex-analysis render` 生成本文件。</div></div>';
1471
1479
  return;
1472
1480
  }
1481
+ const cover = periodWord(data, "账户");
1482
+ document.title =
1483
+ (data.meta && data.meta.accountName ? data.meta.accountName + " · " : "") +
1484
+ "Yandex 广告" +
1485
+ cover +
1486
+ "分析报告";
1473
1487
  root.innerHTML = renderAll(data);
1474
1488
  bindNav();
1475
1489
  });
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 统计区间:`{startDate}` ~ `{endDate}`(**不能晚于今天**;省略默认近 7 天含今天)
4
4
  > 账户:`{mediaCustomerId}`(如 `porg-xxx`,与 `list-accounts -m Yandex` 一致)
5
- > **默认交付物**:一份可打开的 **HTML 文件**(`yandex-period-report.html`,样式对齐开发样例)。
5
+ > **默认交付物**:一份可打开的 **HTML 文件**(`yandex-period-report.html`,样式对齐开发样例)。封面按日期写成「季度」或「月度」,对不上仍是「账户分析」。不要打开 HTML 改称呼。
6
6
  > 用户**未指定格式**时一律走下方 **标准四步流程**;**禁止** Agent 手写/拼接 HTML。
7
7
  > **用户明确要求 Excel / xlsx** 时:拉数步骤不变,改为 Agent 脚本写 `.xlsx`;**不要**调用 `yandex-analysis render`;禁止假设存在内置 excel 子命令。见 `yandex-period-report-excel.md`。
8
8
 
@@ -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.57-beta.1'
12
+ $PKG_VERSION = '1.1.57-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.57-beta.1"
12
+ readonly PKG_VERSION="1.1.57-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.57-beta.1",
3
+ "version": "1.1.57-beta.2",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",