siluzan-tso-cli 1.1.45-beta.3 → 1.1.45-beta.5

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.45-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.45-beta.5),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -134013,7 +134013,7 @@ var YANDEX_SECTIONS = [
134013
134013
  },
134014
134014
  {
134015
134015
  name: "daily",
134016
- description: "\u6309\u65E5\u7EE9\u6548\uFF08\u53EF\u4F9B\u8C03\u7528\u65B9\u81EA\u884C\u805A\u5468\uFF09",
134016
+ description: "\u6309\u65E5\u7EE9\u6548\uFF1B\u843D\u76D8\u542B weekly[]\uFF08ISO \u5468\uFF09\uFF0C\u7981\u6B62\u81EA\u884C\u805A\u5468",
134017
134017
  path: "daily"
134018
134018
  },
134019
134019
  {
@@ -134061,7 +134061,7 @@ var YANDEX_SECTION_ALIASES = {
134061
134061
 
134062
134062
  // src/commands/yandex-analysis/run-batch.ts
134063
134063
  init_auth();
134064
- import * as path43 from "path";
134064
+ import * as path44 from "path";
134065
134065
  import { performance as performance8 } from "perf_hooks";
134066
134066
  init_version();
134067
134067
 
@@ -134193,133 +134193,9 @@ function endpointHintForYandexSection(def) {
134193
134193
  return `GET \u2026/yandex-analysis/${def.path}?account=&start=&end=`;
134194
134194
  }
134195
134195
 
134196
- // src/commands/yandex-analysis/resolve-sections.ts
134197
- function normalizeSectionToken4(raw) {
134198
- const t = raw.trim();
134199
- if (!t) return null;
134200
- if (YANDEX_SECTION_NAMES.includes(t)) {
134201
- return t;
134202
- }
134203
- return YANDEX_SECTION_ALIASES[t] ?? null;
134204
- }
134205
- function resolveSectionList5(sections, exclude) {
134206
- const splitClean = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
134207
- const include = splitClean(sections);
134208
- const dropRaw = splitClean(exclude);
134209
- const unknown = [...include, ...dropRaw].filter((n) => normalizeSectionToken4(n) === null);
134210
- if (unknown.length > 0) {
134211
- console.error(
134212
- `
134213
- \u274C \u672A\u77E5\u7684 section \u540D\u79F0\uFF1A${unknown.join(", ")}
134214
- \u53EF\u9009\uFF1A${YANDEX_SECTION_NAMES.join(", ")}
134215
- \u522B\u540D\uFF1A${Object.keys(YANDEX_SECTION_ALIASES).join(", ")}
134216
- `
134217
- );
134218
- process.exit(1);
134219
- }
134220
- const includeCanonical = new Set(include.map((n) => normalizeSectionToken4(n)).filter(Boolean));
134221
- const dropCanonical = new Set(dropRaw.map((n) => normalizeSectionToken4(n)).filter(Boolean));
134222
- const base = include.length > 0 ? YANDEX_SECTIONS.filter((s) => includeCanonical.has(s.name)) : [...YANDEX_SECTIONS];
134223
- return base.filter((s) => !dropCanonical.has(s.name));
134224
- }
134225
-
134226
- // src/commands/yandex-analysis/run-batch.ts
134227
- async function runWithConcurrency6(tasks, concurrency) {
134228
- const results = new Array(tasks.length);
134229
- let cursor = 0;
134230
- const workers = new Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(async () => {
134231
- while (true) {
134232
- const i = cursor++;
134233
- if (i >= tasks.length) return;
134234
- results[i] = await tasks[i]();
134235
- }
134236
- });
134237
- await Promise.all(workers);
134238
- return results;
134239
- }
134240
- async function runAllYandexSections(opts) {
134241
- const id = assertYandexAccountId(opts.account);
134242
- const config = loadConfig(opts.token);
134243
- if (!opts.jsonOut?.trim()) {
134244
- console.error("\n\u274C \u5FC5\u987B\u4F20 --json-out <\u76EE\u5F55>\uFF08\u5404\u7EF4\u5EA6\u843D\u76D8\u540E\u7531 report-manifest \u7D22\u5F15\uFF09\u3002\n");
134245
- process.exit(1);
134246
- }
134247
- const targets = resolveSectionList5(opts.sections, opts.exclude);
134248
- if (targets.length === 0) {
134249
- console.error("\n\u274C \u6CA1\u6709\u8981\u6267\u884C\u7684 section\uFF08\u8BF7\u68C0\u67E5 --sections / --exclude\uFF09\u3002\n");
134250
- process.exit(1);
134251
- }
134252
- const { startDate, endDate } = resolveYandexDateRange(opts.start, opts.end);
134253
- const concurrency = Math.max(
134254
- 1,
134255
- Math.min(typeof opts.concurrency === "number" ? opts.concurrency : 5, 16)
134256
- );
134257
- const cliVersion = getCurrentVersion2();
134258
- const overallT0 = performance8.now();
134259
- const tasks = targets.map((def) => async () => {
134260
- const t0 = performance8.now();
134261
- const hint = endpointHintForYandexSection(def);
134262
- try {
134263
- const payload = await fetchYandexSectionPayload(
134264
- def,
134265
- { ...opts, start: startDate, end: endDate },
134266
- config,
134267
- id
134268
- );
134269
- const summary2 = await writeReportAnalysisSnapshot({
134270
- snapshotDir: opts.jsonOut,
134271
- section: def.name,
134272
- accountId: id,
134273
- dateRange: { start: startDate, end: endDate },
134274
- payload,
134275
- cliVersion
134276
- });
134277
- return {
134278
- section: def.name,
134279
- ok: true,
134280
- elapsedMs: performance8.now() - t0,
134281
- file: summary2.writtenFiles[0],
134282
- endpointHint: hint
134283
- };
134284
- } catch (err) {
134285
- return {
134286
- section: def.name,
134287
- ok: false,
134288
- elapsedMs: performance8.now() - t0,
134289
- error: err instanceof Error ? err.message : String(err),
134290
- endpointHint: hint
134291
- };
134292
- }
134293
- });
134294
- const results = await runWithConcurrency6(tasks, concurrency);
134295
- const succeeded = results.filter((r) => r.ok).length;
134296
- const failed = results.length - succeeded;
134297
- const absoluteSnapshotDir = path43.resolve(opts.jsonOut);
134298
- const summary = {
134299
- kind: "siluzan-tso-yandex-analysis-snapshot-batch",
134300
- absoluteSnapshotDir,
134301
- manifestFile: reportManifestFile(id),
134302
- accountId: id,
134303
- totalSections: results.length,
134304
- succeeded,
134305
- failed,
134306
- concurrency,
134307
- elapsedMs: performance8.now() - overallT0,
134308
- results
134309
- };
134310
- console.log(JSON.stringify(summary));
134311
- if (failed > 0) process.exit(2);
134312
- }
134313
-
134314
- // src/commands/yandex-analysis/render-report.ts
134315
- import fs34 from "fs";
134316
- import fsPromises7 from "fs/promises";
134317
- import path45 from "path";
134318
- import { fileURLToPath as fileURLToPath11 } from "url";
134319
-
134320
134196
  // src/commands/yandex-analysis/merge-snapshot.ts
134321
134197
  import * as fs33 from "fs/promises";
134322
- import * as path44 from "path";
134198
+ import * as path43 from "path";
134323
134199
  function asRecord26(value) {
134324
134200
  if (value && typeof value === "object" && !Array.isArray(value)) {
134325
134201
  return value;
@@ -134342,15 +134218,19 @@ function asItems(payload) {
134342
134218
  return items2.filter((r) => asRecord26(r) !== null);
134343
134219
  }
134344
134220
  function pickMetrics2(row, extras = []) {
134221
+ const spend = num10(row.spend) ?? 0;
134222
+ const impressions = num10(row.impressions) ?? 0;
134223
+ const clicks = num10(row.clicks) ?? 0;
134224
+ const conversions = num10(row.conversions) ?? 0;
134345
134225
  const out = {
134346
- spend: num10(row.spend) ?? 0,
134347
- impressions: num10(row.impressions) ?? 0,
134348
- clicks: num10(row.clicks) ?? 0,
134349
- ctr: num10(row.ctr),
134350
- averageCpc: num10(row.averageCpc),
134351
- conversions: num10(row.conversions) ?? 0,
134352
- conversionRate: num10(row.conversionRate),
134353
- costPerConversion: num10(row.costPerConversion),
134226
+ spend,
134227
+ impressions,
134228
+ clicks,
134229
+ conversions,
134230
+ ctr: num10(row.ctr) ?? (impressions > 0 ? clicks / impressions : void 0),
134231
+ averageCpc: num10(row.averageCpc) ?? (clicks > 0 ? round25(spend / clicks) : void 0),
134232
+ conversionRate: num10(row.conversionRate) ?? (clicks > 0 ? conversions / clicks : void 0),
134233
+ costPerConversion: num10(row.costPerConversion) ?? (conversions > 0 ? round25(spend / conversions) : void 0),
134354
134234
  roi: num10(row.roi)
134355
134235
  };
134356
134236
  for (const f of extras) {
@@ -134360,7 +134240,7 @@ function pickMetrics2(row, extras = []) {
134360
134240
  }
134361
134241
  async function loadSectionPayload3(snapshotDir, file) {
134362
134242
  try {
134363
- const raw = await fs33.readFile(path44.join(snapshotDir, file), "utf8");
134243
+ const raw = await fs33.readFile(path43.join(snapshotDir, file), "utf8");
134364
134244
  return JSON.parse(raw);
134365
134245
  } catch {
134366
134246
  return null;
@@ -134380,7 +134260,7 @@ async function readManifest6(snapshotDir, accountId) {
134380
134260
  }
134381
134261
  for (const name2 of candidates) {
134382
134262
  try {
134383
- const raw = await fs33.readFile(path44.join(snapshotDir, name2), "utf8");
134263
+ const raw = await fs33.readFile(path43.join(snapshotDir, name2), "utf8");
134384
134264
  const parsed = JSON.parse(raw);
134385
134265
  if (Array.isArray(parsed.artifacts) && parsed.artifacts.length > 0) return parsed;
134386
134266
  } catch {
@@ -134388,8 +134268,12 @@ async function readManifest6(snapshotDir, accountId) {
134388
134268
  }
134389
134269
  return null;
134390
134270
  }
134271
+ function normalizeYandexDailyDate(value) {
134272
+ const m = /(\d{4}-\d{2}-\d{2})/.exec(String(value ?? "").trim());
134273
+ return m?.[1] ?? "";
134274
+ }
134391
134275
  function parseYmd2(value) {
134392
- const s = String(value ?? "").trim().slice(0, 10);
134276
+ const s = normalizeYandexDailyDate(value);
134393
134277
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
134394
134278
  if (!m) return null;
134395
134279
  const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
@@ -134406,26 +134290,28 @@ function isoWeekKey(d) {
134406
134290
  const dow = (day.getDay() + 6) % 7;
134407
134291
  const weekStart = new Date(day);
134408
134292
  weekStart.setDate(weekStart.getDate() - dow);
134293
+ const weekEnd = new Date(weekStart);
134294
+ weekEnd.setDate(weekEnd.getDate() + 6);
134409
134295
  const thursday = new Date(weekStart);
134410
134296
  thursday.setDate(thursday.getDate() + 3);
134411
134297
  const yearStart = new Date(thursday.getFullYear(), 0, 1);
134412
134298
  const week = Math.floor((thursday.getTime() - yearStart.getTime()) / 864e5 / 7) + 1;
134413
134299
  const label = `W${String(week).padStart(2, "0")}`;
134414
- return { key: `${thursday.getFullYear()}-${label}`, label, weekStart };
134300
+ return { key: `${thursday.getFullYear()}-${label}`, label, weekStart, weekEnd };
134415
134301
  }
134416
134302
  function aggregateDailyToWeekly(dailyRows) {
134417
134303
  const map = /* @__PURE__ */ new Map();
134418
134304
  for (const row of dailyRows) {
134419
134305
  const d = parseYmd2(row.date);
134420
134306
  if (!d) continue;
134421
- const { key, label, weekStart } = isoWeekKey(d);
134307
+ const { key, label, weekStart, weekEnd } = isoWeekKey(d);
134422
134308
  let acc = map.get(key);
134423
134309
  if (!acc) {
134424
134310
  acc = {
134425
134311
  label,
134426
134312
  weekStart,
134427
- minDate: d,
134428
- maxDate: d,
134313
+ weekEnd,
134314
+ days: 0,
134429
134315
  spend: 0,
134430
134316
  impressions: 0,
134431
134317
  clicks: 0,
@@ -134433,20 +134319,21 @@ function aggregateDailyToWeekly(dailyRows) {
134433
134319
  };
134434
134320
  map.set(key, acc);
134435
134321
  }
134436
- if (d < acc.minDate) acc.minDate = d;
134437
- if (d > acc.maxDate) acc.maxDate = d;
134322
+ acc.days += 1;
134438
134323
  acc.spend += num10(row.spend) ?? 0;
134439
134324
  acc.impressions += num10(row.impressions) ?? 0;
134440
134325
  acc.clicks += num10(row.clicks) ?? 0;
134441
134326
  acc.conversions += num10(row.conversions) ?? 0;
134442
134327
  }
134443
134328
  return [...map.values()].sort((a, b) => a.weekStart.getTime() - b.weekStart.getTime()).map((acc) => {
134444
- const days = Math.floor((acc.maxDate.getTime() - acc.minDate.getTime()) / 864e5) + 1;
134329
+ const startDate = formatYmd2(acc.weekStart);
134330
+ const endDate = formatYmd2(acc.weekEnd);
134445
134331
  return {
134446
134332
  weekLabel: acc.label,
134447
- startDate: formatYmd2(acc.minDate),
134448
- endDate: formatYmd2(acc.maxDate),
134449
- days,
134333
+ isoWeek: acc.label,
134334
+ startDate,
134335
+ endDate,
134336
+ days: acc.days,
134450
134337
  spend: round25(acc.spend),
134451
134338
  impressions: Math.round(acc.impressions),
134452
134339
  clicks: Math.round(acc.clicks),
@@ -134457,10 +134344,25 @@ function aggregateDailyToWeekly(dailyRows) {
134457
134344
  };
134458
134345
  });
134459
134346
  }
134347
+ function prepareYandexDailyPayload(payload) {
134348
+ const obj = asRecord26(payload) ?? {};
134349
+ const items2 = asItems(obj).map((r) => {
134350
+ const date = normalizeYandexDailyDate(r.date);
134351
+ return date ? { ...r, date } : r;
134352
+ });
134353
+ return { ...obj, items: items2, weekly: aggregateDailyToWeekly(items2) };
134354
+ }
134355
+ var YANDEX_DAILY_OUTLINE_HINTS = [
134356
+ "// `items[].date` \u5DF2\u89C4\u8303\u4E3A YYYY-MM-DD\u3002\u63A5\u53E3\u53EF\u80FD\u8FD4\u56DE ISO \u65F6\u95F4\u6233\uFF08\u5982 2026-07-01T00:00:00+00:00\uFF09\uFF0CCLI \u5DF2\u622A\u65AD\uFF1B\u7981\u6B62\u518D\u62FC\u63A5 T00:00:00Z\uFF0C\u5426\u5219 new Date() \u4F1A Invalid Date\uFF0C\u5468\u6807\u7B7E\u53D8\u6210 NaN\u3002",
134357
+ "// \u5468\u8D8B\u52BF\u8BFB\u672C\u6587\u4EF6 `weekly[]`\uFF08ISO \u5468\u4E00~\u5468\u65E5\uFF1B`days` \u4E3A\u6709\u6570\u636E\u7684\u5929\u6570\uFF09\u3002\u7981\u6B62\u81EA\u884C\u6309 date \u805A\u5468\u3002"
134358
+ ];
134359
+ function buildYandexOutlineHints(section) {
134360
+ if (section === "daily") return YANDEX_DAILY_OUTLINE_HINTS;
134361
+ return void 0;
134362
+ }
134460
134363
  function mapDailyRows(payload) {
134461
134364
  return asItems(payload).map((r) => {
134462
- const dateRaw = r.date;
134463
- const date = typeof dateRaw === "string" ? dateRaw.slice(0, 10) : dateRaw != null ? String(dateRaw).slice(0, 10) : "";
134365
+ const date = normalizeYandexDailyDate(r.date);
134464
134366
  return pickMetrics2({ ...r, date }, ["date"]);
134465
134367
  });
134466
134368
  }
@@ -134514,7 +134416,7 @@ function mapDeviceRows2(payload) {
134514
134416
  return asItems(payload).map((r) => pickMetrics2(r, ["device", "network"]));
134515
134417
  }
134516
134418
  async function mergeYandexAnalysisSnapshotIntoReport(payload, snapshotDir) {
134517
- const absDir = path44.resolve(snapshotDir);
134419
+ const absDir = path43.resolve(snapshotDir);
134518
134420
  const metaAccountId = typeof asRecord26(payload.meta)?.accountId === "string" ? asRecord26(payload.meta)?.accountId : void 0;
134519
134421
  const manifest = await readManifest6(snapshotDir, metaAccountId);
134520
134422
  if (!manifest) {
@@ -134591,6 +134493,132 @@ async function mergeYandexAnalysisSnapshotIntoReport(payload, snapshotDir) {
134591
134493
  return { ...payload, meta, kpis, tables };
134592
134494
  }
134593
134495
 
134496
+ // src/commands/yandex-analysis/resolve-sections.ts
134497
+ function normalizeSectionToken4(raw) {
134498
+ const t = raw.trim();
134499
+ if (!t) return null;
134500
+ if (YANDEX_SECTION_NAMES.includes(t)) {
134501
+ return t;
134502
+ }
134503
+ return YANDEX_SECTION_ALIASES[t] ?? null;
134504
+ }
134505
+ function resolveSectionList5(sections, exclude) {
134506
+ const splitClean = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
134507
+ const include = splitClean(sections);
134508
+ const dropRaw = splitClean(exclude);
134509
+ const unknown = [...include, ...dropRaw].filter((n) => normalizeSectionToken4(n) === null);
134510
+ if (unknown.length > 0) {
134511
+ console.error(
134512
+ `
134513
+ \u274C \u672A\u77E5\u7684 section \u540D\u79F0\uFF1A${unknown.join(", ")}
134514
+ \u53EF\u9009\uFF1A${YANDEX_SECTION_NAMES.join(", ")}
134515
+ \u522B\u540D\uFF1A${Object.keys(YANDEX_SECTION_ALIASES).join(", ")}
134516
+ `
134517
+ );
134518
+ process.exit(1);
134519
+ }
134520
+ const includeCanonical = new Set(include.map((n) => normalizeSectionToken4(n)).filter(Boolean));
134521
+ const dropCanonical = new Set(dropRaw.map((n) => normalizeSectionToken4(n)).filter(Boolean));
134522
+ const base = include.length > 0 ? YANDEX_SECTIONS.filter((s) => includeCanonical.has(s.name)) : [...YANDEX_SECTIONS];
134523
+ return base.filter((s) => !dropCanonical.has(s.name));
134524
+ }
134525
+
134526
+ // src/commands/yandex-analysis/run-batch.ts
134527
+ async function runWithConcurrency6(tasks, concurrency) {
134528
+ const results = new Array(tasks.length);
134529
+ let cursor = 0;
134530
+ const workers = new Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(async () => {
134531
+ while (true) {
134532
+ const i = cursor++;
134533
+ if (i >= tasks.length) return;
134534
+ results[i] = await tasks[i]();
134535
+ }
134536
+ });
134537
+ await Promise.all(workers);
134538
+ return results;
134539
+ }
134540
+ async function runAllYandexSections(opts) {
134541
+ const id = assertYandexAccountId(opts.account);
134542
+ const config = loadConfig(opts.token);
134543
+ if (!opts.jsonOut?.trim()) {
134544
+ console.error("\n\u274C \u5FC5\u987B\u4F20 --json-out <\u76EE\u5F55>\uFF08\u5404\u7EF4\u5EA6\u843D\u76D8\u540E\u7531 report-manifest \u7D22\u5F15\uFF09\u3002\n");
134545
+ process.exit(1);
134546
+ }
134547
+ const targets = resolveSectionList5(opts.sections, opts.exclude);
134548
+ if (targets.length === 0) {
134549
+ console.error("\n\u274C \u6CA1\u6709\u8981\u6267\u884C\u7684 section\uFF08\u8BF7\u68C0\u67E5 --sections / --exclude\uFF09\u3002\n");
134550
+ process.exit(1);
134551
+ }
134552
+ const { startDate, endDate } = resolveYandexDateRange(opts.start, opts.end);
134553
+ const concurrency = Math.max(
134554
+ 1,
134555
+ Math.min(typeof opts.concurrency === "number" ? opts.concurrency : 5, 16)
134556
+ );
134557
+ const cliVersion = getCurrentVersion2();
134558
+ const overallT0 = performance8.now();
134559
+ const tasks = targets.map((def) => async () => {
134560
+ const t0 = performance8.now();
134561
+ const hint = endpointHintForYandexSection(def);
134562
+ try {
134563
+ const raw = await fetchYandexSectionPayload(
134564
+ def,
134565
+ { ...opts, start: startDate, end: endDate },
134566
+ config,
134567
+ id
134568
+ );
134569
+ const payload = def.name === "daily" ? prepareYandexDailyPayload(raw) : raw;
134570
+ const summary2 = await writeReportAnalysisSnapshot({
134571
+ snapshotDir: opts.jsonOut,
134572
+ section: def.name,
134573
+ accountId: id,
134574
+ dateRange: { start: startDate, end: endDate },
134575
+ payload,
134576
+ cliVersion,
134577
+ outlineExtraHints: buildYandexOutlineHints(def.name)
134578
+ });
134579
+ return {
134580
+ section: def.name,
134581
+ ok: true,
134582
+ elapsedMs: performance8.now() - t0,
134583
+ file: summary2.writtenFiles[0],
134584
+ endpointHint: hint
134585
+ };
134586
+ } catch (err) {
134587
+ return {
134588
+ section: def.name,
134589
+ ok: false,
134590
+ elapsedMs: performance8.now() - t0,
134591
+ error: err instanceof Error ? err.message : String(err),
134592
+ endpointHint: hint
134593
+ };
134594
+ }
134595
+ });
134596
+ const results = await runWithConcurrency6(tasks, concurrency);
134597
+ const succeeded = results.filter((r) => r.ok).length;
134598
+ const failed = results.length - succeeded;
134599
+ const absoluteSnapshotDir = path44.resolve(opts.jsonOut);
134600
+ const summary = {
134601
+ kind: "siluzan-tso-yandex-analysis-snapshot-batch",
134602
+ absoluteSnapshotDir,
134603
+ manifestFile: reportManifestFile(id),
134604
+ accountId: id,
134605
+ totalSections: results.length,
134606
+ succeeded,
134607
+ failed,
134608
+ concurrency,
134609
+ elapsedMs: performance8.now() - overallT0,
134610
+ results
134611
+ };
134612
+ console.log(JSON.stringify(summary));
134613
+ if (failed > 0) process.exit(2);
134614
+ }
134615
+
134616
+ // src/commands/yandex-analysis/render-report.ts
134617
+ import fs34 from "fs";
134618
+ import fsPromises7 from "fs/promises";
134619
+ import path45 from "path";
134620
+ import { fileURLToPath as fileURLToPath11 } from "url";
134621
+
134594
134622
  // src/commands/yandex-analysis/report-content.ts
134595
134623
  var SECTION_KEYS4 = [
134596
134624
  "overview",
@@ -134627,6 +134655,39 @@ function asRecord27(value) {
134627
134655
  function pushMissing6(missing, id, chapter, dimension, hint) {
134628
134656
  missing.push({ id, chapter, dimension, hint });
134629
134657
  }
134658
+ function normKey(value) {
134659
+ return String(value ?? "").trim().toLowerCase();
134660
+ }
134661
+ function insightMatchRecord(item) {
134662
+ const rec = asRecord27(item);
134663
+ if (!rec) return {};
134664
+ return asRecord27(rec.match) ?? rec;
134665
+ }
134666
+ function findRowInsight(list, primaryField, primaryValue, row) {
134667
+ const target = normKey(primaryValue);
134668
+ if (!target) return null;
134669
+ const candidates = list.filter((it) => normKey(insightMatchRecord(it)[primaryField]) === target);
134670
+ if (candidates.length <= 1) return asRecord27(candidates[0]);
134671
+ const campaign = normKey(row.campaignName);
134672
+ const narrowed = candidates.find(
134673
+ (it) => normKey(insightMatchRecord(it).campaignName) === campaign
134674
+ );
134675
+ return asRecord27(narrowed ?? candidates[0]);
134676
+ }
134677
+ function topRowsBySpend(rows, limit) {
134678
+ return rows.map((r) => asRecord27(r)).filter((r) => r != null).sort((a, b) => (Number(b.spend) || 0) - (Number(a.spend) || 0)).slice(0, limit);
134679
+ }
134680
+ var ROW_INSIGHT_SPECS = [
134681
+ {
134682
+ tableKey: "keywords",
134683
+ insightKey: "keywords",
134684
+ matchField: "keyword",
134685
+ requiredField: "translation",
134686
+ limit: 40,
134687
+ chapter: "\u2463\u5173\u952E\u8BCD",
134688
+ label: "\u4E2D\u6587\u7FFB\u8BD1"
134689
+ }
134690
+ ];
134630
134691
  function validateYandexPeriodReportContent(data) {
134631
134692
  const missing = [];
134632
134693
  const meta = asRecord27(data.meta) ?? {};
@@ -134697,6 +134758,51 @@ function validateYandexPeriodReportContent(data) {
134697
134758
  "\u81F3\u5C11\u4E00\u9879\u6709\u6548\u6570\u5B57\uFF08\u8BF7\u4F20 --snapshot-dir \u7531 CLI \u4ECE overview \u5408\u5E76\uFF09"
134698
134759
  );
134699
134760
  }
134761
+ const tables = asRecord27(data.tables) ?? {};
134762
+ const weeklyRows = asArray7(tables.weekly).map((r) => asRecord27(r)).filter((r) => r != null);
134763
+ for (const [i, row] of weeklyRows.entries()) {
134764
+ const label = `${row.weekLabel ?? ""} ${row.startDate ?? ""} ${row.endDate ?? ""}`;
134765
+ if (/nan/i.test(label) || !normalizeYandexDailyDate(row.startDate) || !normalizeYandexDailyDate(row.endDate)) {
134766
+ pushMissing6(
134767
+ missing,
134768
+ `tables-weekly-${i}`,
134769
+ "\u2467\u65E5\u671F/\u5468\u62A5",
134770
+ `tables.weekly[${i}]`,
134771
+ "\u5468\u6807\u7B7E/\u8D77\u6B62\u65E5\u671F\u65E0\u6548\uFF08\u542B NaN \u6216\u975E YYYY-MM-DD\uFF09\u3002\u8BF7\u4F20 --snapshot-dir\uFF0C\u7531 CLI \u4ECE daily \u805A\u5408 weekly\uFF0C\u7981\u6B62\u81EA\u884C\u6309 date \u62FC T00:00:00Z \u805A\u5468\u3002"
134772
+ );
134773
+ }
134774
+ }
134775
+ const rowInsights = asRecord27(narrative.rowInsights) ?? {};
134776
+ const rowInsightCoverage = {};
134777
+ for (const spec of ROW_INSIGHT_SPECS) {
134778
+ const shown = topRowsBySpend(asArray7(tables[spec.tableKey]), spec.limit).filter(
134779
+ (row) => textLen7(row[spec.matchField])
134780
+ );
134781
+ const insights = asArray7(rowInsights[spec.insightKey]);
134782
+ let covered = 0;
134783
+ const uncovered = [];
134784
+ for (const row of shown) {
134785
+ const insight = findRowInsight(insights, spec.matchField, row[spec.matchField], row);
134786
+ if (insight && textLen7(insight[spec.requiredField]) > 0) {
134787
+ covered += 1;
134788
+ } else {
134789
+ uncovered.push(String(row[spec.matchField]));
134790
+ }
134791
+ }
134792
+ rowInsightCoverage[spec.insightKey] = { shown: shown.length, covered };
134793
+ if (shown.length === 0) continue;
134794
+ if (covered < shown.length) {
134795
+ const sample = uncovered.slice(0, 3).join("\u3001");
134796
+ const more = uncovered.length > 3 ? ` \u7B49 ${uncovered.length} \u884C` : "";
134797
+ pushMissing6(
134798
+ missing,
134799
+ `row-insights-${spec.insightKey}`,
134800
+ spec.chapter,
134801
+ `narrative.rowInsights.${spec.insightKey}.${spec.requiredField}`,
134802
+ `HTML \u5C06\u5C55\u793A ${shown.length} \u884C\uFF0C\u6BCF\u884C\u987B\u6709${spec.label}\uFF08\u5F53\u524D ${covered}/${shown.length}\uFF09\u3002\u672A\u8986\u76D6\u793A\u4F8B\uFF1A${sample}${more}`
134803
+ );
134804
+ }
134805
+ }
134700
134806
  return {
134701
134807
  ok: missing.length === 0,
134702
134808
  missing,
@@ -134704,7 +134810,8 @@ function validateYandexPeriodReportContent(data) {
134704
134810
  executiveSummaryCount: executiveSummary.length,
134705
134811
  recommendationCount: recommendations.length,
134706
134812
  sectionAnalysisCounts,
134707
- sectionSuggestionCounts
134813
+ sectionSuggestionCounts,
134814
+ rowInsightCoverage
134708
134815
  }
134709
134816
  };
134710
134817
  }
@@ -134715,7 +134822,7 @@ function formatYandexPeriodReportErrors(result) {
134715
134822
  `\u62A5\u544A JSON \u7F3A\u5C11 ${result.missing.length} \u9879\u5FC5\u542B\u5185\u5BB9\uFF08\u5BF9\u9F50 yandex-period-report.md\uFF09\uFF1A`,
134716
134823
  ...lines,
134717
134824
  "",
134718
- "\u8BF7\u8865\u5168 narrative.sections.<dimension>.{analysis,suggestions} \u4E0E narrative.recommendations \u540E\u91CD\u65B0\u6267\u884C render\u3002",
134825
+ "\u8BF7\u8865\u5168 narrative.sections.<dimension>.{analysis,suggestions}\u3001narrative.recommendations\uFF0C\u4EE5\u53CA\u8868\u683C\u884C\u5BF9\u5E94\u7684 narrative.rowInsights\uFF08\u5173\u952E\u8BCD translation\uFF09\u540E\u91CD\u65B0\u6267\u884C render\u3002",
134719
134826
  "\u6570\u503C\u7C7B\u5B57\u6BB5\uFF08kpis/tables\uFF09\u987B\u7531 --snapshot-dir \u4ECE\u843D\u76D8 JSON \u5408\u5E76\uFF1BAgent \u53EA\u64B0\u5199\u53D9\u4E8B\uFF0C\u7981\u6B62\u624B\u5199 HTML\u3002"
134720
134827
  ].join("\n");
134721
134828
  }
@@ -134752,6 +134859,19 @@ function asRecord28(value) {
134752
134859
  }
134753
134860
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
134754
134861
  }
134862
+ function refreshWeeklyFromDaily(data) {
134863
+ const tables = data.tables;
134864
+ if (!tables || typeof tables !== "object" || Array.isArray(tables)) return data;
134865
+ const daily = Array.isArray(tables.daily) ? tables.daily : [];
134866
+ if (daily.length === 0) return data;
134867
+ return {
134868
+ ...data,
134869
+ tables: {
134870
+ ...tables,
134871
+ weekly: aggregateDailyToWeekly(daily)
134872
+ }
134873
+ };
134874
+ }
134755
134875
  function injectReportData7(html, payload) {
134756
134876
  const json = JSON.stringify(payload).replace(/</g, "\\u003c");
134757
134877
  if (!html.includes(INJECT_MARKER6)) {
@@ -134765,6 +134885,7 @@ async function runYandexAnalysisRender(opts) {
134765
134885
  if (opts.snapshotDir?.trim()) {
134766
134886
  data = await mergeYandexAnalysisSnapshotIntoReport(data, path45.resolve(opts.snapshotDir));
134767
134887
  }
134888
+ data = refreshWeeklyFromDaily(data);
134768
134889
  const contentCheck = validateYandexPeriodReportContent(data);
134769
134890
  if (!contentCheck.ok) {
134770
134891
  console.error(`
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.45-beta.3",
4
- "publishedAt": 1786677354676
3
+ "version": "1.1.45-beta.5",
4
+ "publishedAt": 1786693588914
5
5
  }
@@ -405,7 +405,7 @@ siluzan-tso yandex-analysis render \
405
405
  --out ./yandex-period-report.html
406
406
  ```
407
407
 
408
- `search` / `campaigns` / `keywords` / `geo` / `devices` 在 Direct 侧过滤 `AdNetworkType=SEARCH`。无 Metrica / 写操作。报告模板:`report-templates/yandex-period-report.md`(8 个分析章节必写总结+建议;`tables.weekly` 由 daily 聚合)。
408
+ `search` / `campaigns` / `keywords` / `geo` / `devices` 在 Direct 侧过滤 `AdNetworkType=SEARCH`。无 Metrica / 写操作。报告模板:`report-templates/yandex-period-report.md`(8 个分析章节必写总结+建议;周趋势读落盘 `daily.weekly[]`,**禁止**自行按 `date` 聚周)。
409
409
 
410
410
  ---
411
411
 
@@ -35,7 +35,7 @@
35
35
  | --------------- | ------------------------- | ---------------------------- |
36
36
  | 账户总览 | `overview` | 对象:`totals` / `balance` … |
37
37
  | 日报表 | `daily` | `items[]` |
38
- | 周报表 | (无) | 脚本按 ISO 周聚合 `daily` |
38
+ | 周报表 | `daily` `weekly[]` | **读落盘 weekly,禁止自行聚周** |
39
39
  | Search 网络 | `search` | `items[]` |
40
40
  | 广告系列 | `campaigns` | `items[]` |
41
41
  | 关键词 | `keywords` | `items[]` |
@@ -78,7 +78,7 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
78
78
  | 地域 | `locationName` / `targetingLocationName` |
79
79
  | 设备 | `device` |
80
80
  | 日报 | `date` |
81
- | 周报 | `weekLabel`、`startDate`、`endDate`、`days`(脚本生成) |
81
+ | 周报 | `daily.weekly[]` 的 `weekLabel`、`startDate`、`endDate`、`days`(CLI 已算) |
82
82
 
83
83
  **账户总览 Sheet**:KPI 区(余额、本期消耗、日均、点击、CTR、CPC、转化)+ **全部叙事**(执行摘要、各维总结/建议、≥3 条跨维优化建议)写在本 Sheet;列表 Sheet 仅数据表。
84
84
 
@@ -89,7 +89,8 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
89
89
  | ❌ 禁止 | ✅ 正确 |
90
90
  | ------------------------------------ | ---------------------------------------------------- |
91
91
  | 把 `ctr` 当已是百分数再 ÷100 | 落盘为 0~1;Excel 用百分比格式或 `*100` 后加 `%` |
92
- | 编造学习期 / 翻译字段 | API 无则叙事说明「数据不可用」 |
92
+ | 编造学习期、搜索词分类等无接口硬事实 | 无字段且算不出则不写该列;关键词翻译走 rowInsights |
93
+ | 自行按 `daily.date` 聚周(易出 `NaN-NaN-NaN`) | 读 `daily.weekly[]`;`date` 已是 YYYY-MM-DD,禁止再拼 `T00:00:00Z` |
93
94
  | `campaignId` 写成数字 | `String(id)` + 文本格式 |
94
95
  | search-terms 拉超过 180 天 | 缩短区间或排除该维 |
95
96