siluzan-tso-cli 1.1.45-beta.4 → 1.1.45

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
@@ -43,7 +43,7 @@ HTML 报告模板外部资源:**ECharts** 统一使用 `https://staticpn.siluz
43
43
  在**用户的目标项目根目录**执行(根据用户使用的助手选择 `--ai`):
44
44
 
45
45
  ```bash
46
- npm install -g siluzan-tso-cli@beta
46
+ npm install -g siluzan-tso-cli
47
47
  siluzan-tso init --ai cursor # 写入 Cursor(默认)
48
48
  siluzan-tso init --ai cursor,claude # 同时写入多个平台
49
49
  siluzan-tso init --ai all # 写入所有支持的平台
@@ -51,7 +51,6 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.45-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
54
 
56
55
  | 助手 | 建议 `--ai` |
57
56
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -4026,7 +4026,7 @@ var DEFAULT_API_BASE;
4026
4026
  var init_defaults = __esm({
4027
4027
  "src/config/defaults.ts"() {
4028
4028
  "use strict";
4029
- DEFAULT_API_BASE = "https://tso-api-ci.siluzan.com";
4029
+ DEFAULT_API_BASE = "https://tso-api.siluzan.com";
4030
4030
  }
4031
4031
  });
4032
4032
 
@@ -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;
@@ -134364,7 +134240,7 @@ function pickMetrics2(row, extras = []) {
134364
134240
  }
134365
134241
  async function loadSectionPayload3(snapshotDir, file) {
134366
134242
  try {
134367
- const raw = await fs33.readFile(path44.join(snapshotDir, file), "utf8");
134243
+ const raw = await fs33.readFile(path43.join(snapshotDir, file), "utf8");
134368
134244
  return JSON.parse(raw);
134369
134245
  } catch {
134370
134246
  return null;
@@ -134384,7 +134260,7 @@ async function readManifest6(snapshotDir, accountId) {
134384
134260
  }
134385
134261
  for (const name2 of candidates) {
134386
134262
  try {
134387
- const raw = await fs33.readFile(path44.join(snapshotDir, name2), "utf8");
134263
+ const raw = await fs33.readFile(path43.join(snapshotDir, name2), "utf8");
134388
134264
  const parsed = JSON.parse(raw);
134389
134265
  if (Array.isArray(parsed.artifacts) && parsed.artifacts.length > 0) return parsed;
134390
134266
  } catch {
@@ -134392,8 +134268,12 @@ async function readManifest6(snapshotDir, accountId) {
134392
134268
  }
134393
134269
  return null;
134394
134270
  }
134271
+ function normalizeYandexDailyDate(value) {
134272
+ const m = /(\d{4}-\d{2}-\d{2})/.exec(String(value ?? "").trim());
134273
+ return m?.[1] ?? "";
134274
+ }
134395
134275
  function parseYmd2(value) {
134396
- const s = String(value ?? "").trim().slice(0, 10);
134276
+ const s = normalizeYandexDailyDate(value);
134397
134277
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
134398
134278
  if (!m) return null;
134399
134279
  const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
@@ -134410,26 +134290,28 @@ function isoWeekKey(d) {
134410
134290
  const dow = (day.getDay() + 6) % 7;
134411
134291
  const weekStart = new Date(day);
134412
134292
  weekStart.setDate(weekStart.getDate() - dow);
134293
+ const weekEnd = new Date(weekStart);
134294
+ weekEnd.setDate(weekEnd.getDate() + 6);
134413
134295
  const thursday = new Date(weekStart);
134414
134296
  thursday.setDate(thursday.getDate() + 3);
134415
134297
  const yearStart = new Date(thursday.getFullYear(), 0, 1);
134416
134298
  const week = Math.floor((thursday.getTime() - yearStart.getTime()) / 864e5 / 7) + 1;
134417
134299
  const label = `W${String(week).padStart(2, "0")}`;
134418
- return { key: `${thursday.getFullYear()}-${label}`, label, weekStart };
134300
+ return { key: `${thursday.getFullYear()}-${label}`, label, weekStart, weekEnd };
134419
134301
  }
134420
134302
  function aggregateDailyToWeekly(dailyRows) {
134421
134303
  const map = /* @__PURE__ */ new Map();
134422
134304
  for (const row of dailyRows) {
134423
134305
  const d = parseYmd2(row.date);
134424
134306
  if (!d) continue;
134425
- const { key, label, weekStart } = isoWeekKey(d);
134307
+ const { key, label, weekStart, weekEnd } = isoWeekKey(d);
134426
134308
  let acc = map.get(key);
134427
134309
  if (!acc) {
134428
134310
  acc = {
134429
134311
  label,
134430
134312
  weekStart,
134431
- minDate: d,
134432
- maxDate: d,
134313
+ weekEnd,
134314
+ days: 0,
134433
134315
  spend: 0,
134434
134316
  impressions: 0,
134435
134317
  clicks: 0,
@@ -134437,20 +134319,21 @@ function aggregateDailyToWeekly(dailyRows) {
134437
134319
  };
134438
134320
  map.set(key, acc);
134439
134321
  }
134440
- if (d < acc.minDate) acc.minDate = d;
134441
- if (d > acc.maxDate) acc.maxDate = d;
134322
+ acc.days += 1;
134442
134323
  acc.spend += num10(row.spend) ?? 0;
134443
134324
  acc.impressions += num10(row.impressions) ?? 0;
134444
134325
  acc.clicks += num10(row.clicks) ?? 0;
134445
134326
  acc.conversions += num10(row.conversions) ?? 0;
134446
134327
  }
134447
134328
  return [...map.values()].sort((a, b) => a.weekStart.getTime() - b.weekStart.getTime()).map((acc) => {
134448
- const days = Math.floor((acc.maxDate.getTime() - acc.minDate.getTime()) / 864e5) + 1;
134329
+ const startDate = formatYmd2(acc.weekStart);
134330
+ const endDate = formatYmd2(acc.weekEnd);
134449
134331
  return {
134450
134332
  weekLabel: acc.label,
134451
- startDate: formatYmd2(acc.minDate),
134452
- endDate: formatYmd2(acc.maxDate),
134453
- days,
134333
+ isoWeek: acc.label,
134334
+ startDate,
134335
+ endDate,
134336
+ days: acc.days,
134454
134337
  spend: round25(acc.spend),
134455
134338
  impressions: Math.round(acc.impressions),
134456
134339
  clicks: Math.round(acc.clicks),
@@ -134461,10 +134344,25 @@ function aggregateDailyToWeekly(dailyRows) {
134461
134344
  };
134462
134345
  });
134463
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
+ }
134464
134363
  function mapDailyRows(payload) {
134465
134364
  return asItems(payload).map((r) => {
134466
- const dateRaw = r.date;
134467
- const date = typeof dateRaw === "string" ? dateRaw.slice(0, 10) : dateRaw != null ? String(dateRaw).slice(0, 10) : "";
134365
+ const date = normalizeYandexDailyDate(r.date);
134468
134366
  return pickMetrics2({ ...r, date }, ["date"]);
134469
134367
  });
134470
134368
  }
@@ -134518,7 +134416,7 @@ function mapDeviceRows2(payload) {
134518
134416
  return asItems(payload).map((r) => pickMetrics2(r, ["device", "network"]));
134519
134417
  }
134520
134418
  async function mergeYandexAnalysisSnapshotIntoReport(payload, snapshotDir) {
134521
- const absDir = path44.resolve(snapshotDir);
134419
+ const absDir = path43.resolve(snapshotDir);
134522
134420
  const metaAccountId = typeof asRecord26(payload.meta)?.accountId === "string" ? asRecord26(payload.meta)?.accountId : void 0;
134523
134421
  const manifest = await readManifest6(snapshotDir, metaAccountId);
134524
134422
  if (!manifest) {
@@ -134595,6 +134493,132 @@ async function mergeYandexAnalysisSnapshotIntoReport(payload, snapshotDir) {
134595
134493
  return { ...payload, meta, kpis, tables };
134596
134494
  }
134597
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
+
134598
134622
  // src/commands/yandex-analysis/report-content.ts
134599
134623
  var SECTION_KEYS4 = [
134600
134624
  "overview",
@@ -134735,6 +134759,19 @@ function validateYandexPeriodReportContent(data) {
134735
134759
  );
134736
134760
  }
134737
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
+ }
134738
134775
  const rowInsights = asRecord27(narrative.rowInsights) ?? {};
134739
134776
  const rowInsightCoverage = {};
134740
134777
  for (const spec of ROW_INSIGHT_SPECS) {
@@ -134822,6 +134859,19 @@ function asRecord28(value) {
134822
134859
  }
134823
134860
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
134824
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
+ }
134825
134875
  function injectReportData7(html, payload) {
134826
134876
  const json = JSON.stringify(payload).replace(/</g, "\\u003c");
134827
134877
  if (!html.includes(INJECT_MARKER6)) {
@@ -134835,6 +134885,7 @@ async function runYandexAnalysisRender(opts) {
134835
134885
  if (opts.snapshotDir?.trim()) {
134836
134886
  data = await mergeYandexAnalysisSnapshotIntoReport(data, path45.resolve(opts.snapshotDir));
134837
134887
  }
134888
+ data = refreshWeeklyFromDaily(data);
134838
134889
  const contentCheck = validateYandexPeriodReportContent(data);
134839
134890
  if (!contentCheck.ok) {
134840
134891
  console.error(`
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.45-beta.4",
4
- "publishedAt": 1786689638774
3
+ "version": "1.1.45",
4
+ "publishedAt": 1786695850282
5
5
  }
@@ -167,7 +167,7 @@ siluzan-tso account-history --start 2026-03-01 --end 2026-03-31 --json-out ./sna
167
167
  | 状态 | 含义 | 下一步操作 |
168
168
  | ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
169
169
  | `Pending` | 审核中 | 等待,可反复运行此命令轮询;审核周期因媒体而异 |
170
- | `Approved` | 审核通过 | 运行 `list-accounts -m <媒体>` 确认账户已出现;引导用户充值激活(`config show` 取 `webUrl`,按 `finance.md` 打开对应媒体充值页;例如 Google 为 `https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google`;Kwai、Yandex 当前没有对应充值界面) |
170
+ | `Approved` | 审核通过 | 运行 `list-accounts -m <媒体>` 确认账户已出现;引导用户充值激活(`config show` 取 `webUrl`,按 `finance.md` 打开对应媒体充值页;例如 Google 为 `https://www.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google`;Kwai、Yandex 当前没有对应充值界面) |
171
171
  | `Rejected` | 被拒 | 查看 `--json-out` 落盘中的 `reason` 字段了解拒绝原因;修改资料后重新提交;若原因不明,引导用户联系丝路赞客服 |
172
172
 
173
173
  ---
@@ -488,7 +488,7 @@ siluzan-tso account email-deauth -c <mediaCustomerId> --invitation-id <id> --res
488
488
 
489
489
  | 功能 | 媒体 | 网页路径 |
490
490
  | --------------------------------------- | ------ | ------------------------------------------------- |
491
- | **账户激活**(邀请他人激活 / 充值激活) | Google | `https://www-ci.siluzan.com/v3/foreign_trade/tso/manageAccounts` |
491
+ | **账户激活**(邀请他人激活 / 充值激活) | Google | `https://www.siluzan.com/v3/foreign_trade/tso/manageAccounts` |
492
492
 
493
493
  **Agent 建议话术**:
494
494
 
@@ -497,5 +497,5 @@ siluzan-tso account email-deauth -c <mediaCustomerId> --invitation-id <id> --res
497
497
  siluzan-tso config show # 查看 webUrl 字段
498
498
 
499
499
  # 账户激活(Google)→ 引导至账户管理页
500
- # https://www-ci.siluzan.com/v3/foreign_trade/tso/manageAccounts
500
+ # https://www.siluzan.com/v3/foreign_trade/tso/manageAccounts
501
501
  ```
@@ -126,7 +126,7 @@ siluzan-tso config show
126
126
 
127
127
  ### 充值页链接(按媒体 × 类型)
128
128
 
129
- 链接模式:`https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/<page>?mediaType=<mediaType>`;丝路赞钱包:`https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/siluzanWallet`(无媒体参数)。
129
+ 链接模式:`https://www.siluzan.com/v3/foreign_trade/tso/recharge/<page>?mediaType=<mediaType>`;丝路赞钱包:`https://www.siluzan.com/v3/foreign_trade/tso/recharge/siluzanWallet`(无媒体参数)。
130
130
 
131
131
  | 充值类型 | `<page>` | 支持媒体(`mediaType` 参数) |
132
132
  | ------------------------- | --------------------- | ------------------------------------------- |
@@ -1,6 +1,6 @@
1
1
  # 各媒体开户
2
2
 
3
- > 网页链接:`https://www-ci.siluzan.com/v3/foreign_trade/tso/accountOpeningHistory?tso=%2Fv3umijs%2Ftso%2FaccountOpeningHistory`
3
+ > 网页链接:`https://www.siluzan.com/v3/foreign_trade/tso/accountOpeningHistory?tso=%2Fv3umijs%2Ftso%2FaccountOpeningHistory`
4
4
  > 多命令串联见 `references/core/workflows.md` § 流程一。
5
5
 
6
6
  ## Contents
@@ -65,7 +65,7 @@ siluzan-tso open-account google-wizard
65
65
 
66
66
  ```bash
67
67
  siluzan-tso account-history -m Google
68
- # 审核通过后:config show → https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google
68
+ # 审核通过后:config show → https://www.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google
69
69
  ```
70
70
 
71
71
  ---
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  # rag:知识库检索(TSO 广告投放辅助)
2
2
 
3
- 知识库管理页面在 https://www-ci.siluzan.com/knowledge-base/
3
+ 知识库管理页面在 https://www.siluzan.com/knowledge-base/
4
4
 
5
5
  为 **广告投放、账户分析、拓词、诊断报告** 等 TSO 业务提供**企业已入库**的产品、行业、客户背景事实依据。
6
6
 
@@ -190,9 +190,9 @@ siluzan-tso report push receive-emails -m Google [--json-out ./snap]
190
190
 
191
191
  | 媒体 | 报告类型 | URL 模板 |
192
192
  | ------ | ---------------- | ----------------------------------------------------------- |
193
- | Google | 日报(Daily) | `https://www-ci.siluzan.com/media-report/publish/{entityId}?culture=zh-CN` |
194
- | Google | 小时报(Hourly) | `https://www-ci.siluzan.com/media-report/hour/{entityId}?culture=zh-CN` |
195
- | TikTok | 日报 | `https://www-ci.siluzan.com/media-report/publish/{entityId}?culture=zh-CN` |
193
+ | Google | 日报(Daily) | `https://www.siluzan.com/media-report/publish/{entityId}?culture=zh-CN` |
194
+ | Google | 小时报(Hourly) | `https://www.siluzan.com/media-report/hour/{entityId}?culture=zh-CN` |
195
+ | TikTok | 日报 | `https://www.siluzan.com/media-report/publish/{entityId}?culture=zh-CN` |
196
196
 
197
197
  `entityId` 来自 `siluzan-tso report list --json-out ./snap` 中每条记录的 `entityId` 字段。
198
198
 
@@ -205,8 +205,8 @@ siluzan-tso report list -m Google --json-out ./snap
205
205
 
206
206
  # 第二步:查看 webUrl
207
207
  siluzan-tso config show
208
- # webUrl: https://www-ci.siluzan.com
208
+ # webUrl: https://www.siluzan.com
209
209
 
210
210
  # 第三步:拼接链接(Google 日报)
211
- # https://www-ci.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
211
+ # https://www.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
212
212
  ```
@@ -10,7 +10,7 @@
10
10
  ## 安装 CLI
11
11
 
12
12
  ```bash
13
- npm install -g siluzan-tso-cli@beta
13
+ npm install -g siluzan-tso-cli
14
14
  ```
15
15
 
16
16
  ---
@@ -64,7 +64,7 @@ siluzan-tso config set --api-key <Key> # 或 config 直接写入
64
64
  siluzan-tso config set --token <Token> # 备用:设置 JWT Token
65
65
  ```
66
66
 
67
- API Key 获取入口:`https://www-ci.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
67
+ API Key 获取入口:`https://www.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
68
68
 
69
69
  ```bash
70
70
  # 第 1 步:让用户报出手机号后,立刻发码(命令立即返回,不会等待输入)
@@ -127,9 +127,9 @@ siluzan-tso config show
127
127
 
128
128
  ```
129
129
  构建环境 : production
130
- apiBaseUrl : https://tso-api-ci.siluzan.com
131
- googleApiUrl : https://googleapi-ci.mysiluzan.com
132
- webUrl : https://www-ci.siluzan.com
130
+ apiBaseUrl : https://tso-api.siluzan.com
131
+ googleApiUrl : https://googleapi.mysiluzan.com
132
+ webUrl : https://www.siluzan.com
133
133
  apiKey : abcd****1234
134
134
  ```
135
135
 
@@ -6,7 +6,7 @@
6
6
  siluzan-tso config show # 取 webUrl
7
7
  ```
8
8
 
9
- 首页地址:`https://www-ci.siluzan.com/v3/foreign_trade/tso/home`
9
+ 首页地址:`https://www.siluzan.com/v3/foreign_trade/tso/home`
10
10
 
11
11
  ---
12
12
 
@@ -25,7 +25,7 @@ siluzan-tso config show # 取 webUrl
25
25
 
26
26
  ## 推荐话术
27
27
 
28
- 1. **「和首页一样的总览」** → 打开 `https://www-ci.siluzan.com/v3/foreign_trade/tso/home`。
28
+ 1. **「和首页一样的总览」** → 打开 `https://www.siluzan.com/v3/foreign_trade/tso/home`。
29
29
  2. **「某个 Google 账户昨天花了多少」** → `list-accounts -m Google` + `stats -m Google -a <id>`。
30
30
  3. **「有待充值账户」** → 说明聚合数据在首页;CLI 可 `list-accounts` + `balance` 逐户排查,或引导充值页。
31
31
 
@@ -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
 
@@ -90,6 +90,7 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
90
90
  | ------------------------------------ | ---------------------------------------------------- |
91
91
  | 把 `ctr` 当已是百分数再 ÷100 | 落盘为 0~1;Excel 用百分比格式或 `*100` 后加 `%` |
92
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
 
@@ -6,12 +6,12 @@
6
6
  > 用户**未指定格式**时一律走下方 **标准四步流程**;**禁止** Agent 手写/拼接 HTML。
7
7
  > **用户明确要求 Excel / xlsx** 时:拉数步骤不变,改为 Agent 脚本写 `.xlsx`;**不要**调用 `yandex-analysis render`;禁止假设存在内置 excel 子命令。见 `yandex-period-report-excel.md`。
8
8
 
9
- 数据块(YandexAPI `/yandex-analysis` 8 维 + 本地周聚合):总览、按日、Search 网络、系列、关键词、搜索词、地域、设备;周报由 `daily` 本地聚合(后端无独立 weekly)。
9
+ 数据块(YandexAPI `/yandex-analysis` 8 维 + 本地周聚合):总览、按日、Search 网络、系列、关键词、搜索词、地域、设备;周报由 CLI 写入 `daily.weekly`(后端无独立 weekly)。**禁止**自行按 `date` 聚周。
10
10
 
11
11
  | CLI `--sections` | 落盘形状(CI 实测) | 报告 `tables.*` / KPI |
12
12
  | ---------------- | -------------------------------------------------------- | ---------------------------------------------------------- |
13
13
  | `overview` | 对象:`account` / `currency` / `balance` / `totals` … | → `meta` + `kpis`(余额、日均、续航天数) |
14
- | `daily` | `{ account, startDate, endDate, items: DayRow[] }` | → `tables.daily[]`;并聚合 `tables.weekly[]` |
14
+ | `daily` | `{ account, startDate, endDate, items: DayRow[], weekly: WeekRow[] }` | → `tables.daily[]` + `tables.weekly[]`(勿重算) |
15
15
  | `search` | `{ …, items: SearchRow[] }` | → `tables.search[]` |
16
16
  | `campaigns` | `{ …, items: CampaignRow[] }`(默认 limit=100) | → `tables.campaigns[]` |
17
17
  | `keywords` | `{ …, items: KeywordRow[] }` | → `tables.keywords[]` |
@@ -28,7 +28,7 @@
28
28
  | 步骤 | 执行者 | 动作 |
29
29
  | ---------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
30
30
  | **1. 拉数** | Agent 调 CLI | `yandex-analysis -a <porg-xxx> --start <s> --end <e> --json-out ./snap-yandex`(全 8 维或 `--sections` 指定子集) |
31
- | **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`),对照 outline 完成聚合与洞察 |
31
+ | **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`)。周趋势读 `daily-*.json` 的 `weekly[]`,**禁止**自己按 `date` 聚周。 |
32
32
  | **3. 写 JSON** | Agent | 撰写 `yandex-period-report.json`:仅 `meta.accountId` + `narrative`(8 个分析章节)为必填;`kpis`/`tables` 可省略由 `--snapshot-dir` 合并 |
33
33
  | **4. 渲染 HTML** | CLI | `yandex-analysis render` — **校验 narrative 8 个分析章节必含字段**,缺项报错不生成 HTML;**禁止** Agent 手写/拼接 HTML |
34
34
 
@@ -49,7 +49,7 @@ siluzan-tso yandex-analysis render \
49
49
  - `kpis`:`spend` / `impressions` / `clicks` / `conversions` / `ctr` / `averageCpc` / `costPerConversion` / `conversionRate` / `roi` / `balance` / `avgDailySpend` / `activeDays` / `runwayDays` / `balanceAsOf`;可省略。
50
50
  - `tables`(可省略,由 `--snapshot-dir` 映射):
51
51
  - `daily[]`:`{date, spend, impressions, clicks, ctr, averageCpc, conversions, …}`
52
- - `weekly[]`:`{weekLabel, startDate, endDate, days, spend, impressions, clicks, ctr, averageCpc, conversions, costPerConversion}`(CLI 由 daily 聚合)
52
+ - `weekly[]`:`{weekLabel, isoWeek, startDate, endDate, days, spend, …}`(**只读** CLI 已算好的周;`startDate`/`endDate` 为该 ISO 周周一~周日,`days` 为有数据的天数。禁止自填)
53
53
  - `search[]`:`{network, spend, …}`
54
54
  - `campaigns[]`:`{campaignId, campaignName, status, state, campaignType, budget, strategy, …metrics}`
55
55
  - `keywords[]`:`{keyword, campaignName, criteriaType, network, …metrics}`
@@ -60,15 +60,14 @@ siluzan-tso yandex-analysis render \
60
60
  - `executiveSummary[]`:≥1 段执行摘要
61
61
  - `sections.{overview,search,campaigns,keywords,searchTerms,geo,devices,daily}`:每个维度各 `{analysis[](≥1,含数字依据), suggestions[](≥1)}`
62
62
  - `recommendations[]`:≥3 条跨维度优化建议(可用 `{priority,text,anchor}`,anchor 对齐样例锚点如 `#keyword` / `#query`)
63
- - `rowInsights`:有关键词表格行时,**HTML 将展示的 Top N 行必须有中文翻译**;`render` 缺项会拒绝出 HTML。问题/建议可空。见下方「逐行洞察」
63
+ - `rowInsights`:有关键词表格行时,**HTML 将展示的 Top N 行必须有中文翻译**;`render` 缺项会拒绝出 HTML。见下方「逐行洞察」。Campaign / 关键词 / 搜索词 / 地域 / 设备列表**不展示**「问题」「建议」列,行级判断写入各维 `sections.*.analysis/suggestions` 即可。
64
64
 
65
65
  #### 逐行洞察 `narrative.rowInsights`
66
66
 
67
- 开发样例的部分表格列(关键词"中文翻译"、各维度"问题/建议")不是原始接口字段,而是 **Agent 基于已拉取数据做的翻译/判断**:
67
+ 表格主观列目前只有关键词「中文翻译」,不是原始接口字段,由 Agent 基于已拉取原文翻译:
68
68
 
69
69
  - **必填(`render` 校验)**:`tables.keywords` 有行时,须为 HTML 将展示的每一行(按消耗降序,最多 40 行)提供匹配的 `rowInsights`,且有非空 `translation`。缺一项即拒绝生成 HTML。
70
- - **可空**:各维 `issue` / `suggestion`(空表示该行无问题,模板显示「—」)。
71
- - 搜索词「分类」Yandex 不提供,也无法由现有指标算出,报告不展示该列。
70
+ - Campaign「学习期」、搜索词「分类」、各维行级「问题/建议」均不展示。
72
71
 
73
72
  示例:
74
73
 
@@ -77,25 +76,13 @@ siluzan-tso yandex-analysis render \
77
76
  "rowInsights": {
78
77
  // 按 keyword 原文匹配(如同词跨系列,可加 campaignName 消歧)
79
78
  "keywords": [
80
- { "match": { "keyword": "металлопрокатное оборудование" }, "translation": "金属轧制设备", "issue": "", "suggestion": "可加大预算" }
81
- ],
82
- // 按 searchQuery 原文匹配
83
- "searchTerms": [
84
- { "match": { "searchQuery": "прокатный стан своими руками" }, "issue": "DIY 意图消耗", "suggestion": "添加否定短语 своими руками" }
85
- ],
86
- // 按 campaignName 匹配
87
- "campaigns": [
88
- { "match": { "campaignName": "Search - 自动定向" }, "issue": "自动定向 CTR 弱", "suggestion": "评估降价或暂停" }
89
- ],
90
- // 按 locationName(城市名)匹配
91
- "geo": [{ "match": { "locationName": "Moscow" }, "issue": "", "suggestion": "" }],
92
- // 按 device 匹配
93
- "devices": [{ "match": { "device": "tablet" }, "issue": "样本不足", "suggestion": "暂不调整设备系数" }]
79
+ { "match": { "keyword": "металлопрокатное оборудование" }, "translation": "金属轧制设备" }
80
+ ]
94
81
  }
95
82
  }
96
83
  ```
97
84
 
98
- - `translation` 是 Agent **对已知原文的翻译**,允许生成;不得编造原始接口没有、也无法由现有指标算出的硬事实。Campaign「学习期」、搜索词「分类」无数据源,报告不展示这两列。
85
+ - `translation` 是 Agent **对已知原文的翻译**,允许生成;不得编造原始接口没有、也无法由现有指标算出的硬事实。
99
86
  - 每条洞察按原文匹配一行;HTML 将展示的关键词行若缺中文翻译,`render` 会拒绝出 HTML。
100
87
 
101
88
  ### 快照合并范围(`--snapshot-dir`)
@@ -104,9 +91,17 @@ siluzan-tso yandex-analysis render \
104
91
 
105
92
  1. `meta`(仅补空字段)
106
93
  2. **`kpis` / `tables.*` 一律以 CLI 快照覆盖**(有对应 section 时)
107
- 3. `tables.weekly` 由 `daily.items` 本地 ISO 周聚合
94
+ 3. `tables.weekly` 由 CLI 从 `daily.items` ISO 周重算并覆盖(与落盘 `daily.weekly` 同一套函数)
108
95
 
109
- Agent 只需撰写 `narrative`;**禁止手写 HTML**,也**禁止**自填 `kpis`/`tables` 数值。
96
+ Agent 只需撰写 `narrative`;**禁止手写 HTML**,也**禁止**自填 `kpis`/`tables` 数值,**禁止**自行聚周。
97
+
98
+ ### 周趋势(禁止再出现 NaN)
99
+
100
+ 网关 `daily.items[].date` 可能是 ISO 时间戳(`2026-07-01T00:00:00+00:00`)。CLI 落盘时已截成 `YYYY-MM-DD`,并写好 `weekly[]`。
101
+
102
+ - 写「周趋势」叙事时:**脚本读 `weekly[]`**(`weekLabel` / `startDate` / `endDate` / `spend` / `conversions` / `days`)。
103
+ - **禁止** `new Date(date + 'T00:00:00Z')`:日期已是完整时间戳时再拼接会变成 Invalid Date,年份/月份变成 `NaN`,报告出现 `NaN-NaN-NaN`。
104
+ - **禁止**自己算 ISO 周、自己拆周。周末无投放时 `days` 会小于 7,属正常。
110
105
 
111
106
  ---
112
107
 
@@ -130,7 +125,8 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
130
125
 
131
126
  - 写脚本前先读各 `yandex-*-<id>.outline.txt`(或 stdout 摘要里的 outline),再脚本读 `.json`。
132
127
  - `search` / `campaigns` / `keywords` / `geo` / `devices` 在 Direct 侧过滤 `AdNetworkType=SEARCH`。
133
- - Campaign「学习期」、搜索词「分类」无接口字段、也无法由现有指标算出,报告不展示这两列。关键词中文翻译可由 Agent 通过 `narrative.rowInsights` 提供(见上方「逐行洞察」)。
128
+ - 周趋势读 `daily-*.json` `weekly[]`,禁止自行聚周、禁止给 `date` 再拼 `T00:00:00Z`。
129
+ - Campaign「学习期」、搜索词「分类」、各维行级「问题/建议」均不展示。关键词中文翻译可由 Agent 通过 `narrative.rowInsights` 提供(见上方「逐行洞察」)。
134
130
 
135
131
  ---
136
132
 
@@ -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
 
@@ -90,6 +90,7 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
90
90
  | ------------------------------------ | ---------------------------------------------------- |
91
91
  | 把 `ctr` 当已是百分数再 ÷100 | 落盘为 0~1;Excel 用百分比格式或 `*100` 后加 `%` |
92
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
 
@@ -380,9 +380,26 @@
380
380
  return String(v ?? "").trim().toLowerCase();
381
381
  }
382
382
 
383
+ /** 周区间文案:含 NaN 时回退,避免脚本误聚周把 Invalid Date 写进表格。 */
384
+ function weekRangeText(r) {
385
+ const start = String((r && r.startDate) || "").trim();
386
+ const end = String((r && r.endDate) || "").trim();
387
+ const joined = start && end ? start + " ~ " + end : start || end;
388
+ if (joined && !/nan/i.test(joined)) return joined;
389
+ const label = String((r && r.weekLabel) || "").trim();
390
+ if (label && !/nan/i.test(label)) return label;
391
+ return "—";
392
+ }
393
+
394
+ function weekLabelText(r) {
395
+ const label = String((r && (r.isoWeek || r.weekLabel)) || "").trim();
396
+ if (label && !/nan/i.test(label)) return label;
397
+ return "—";
398
+ }
399
+
383
400
  /**
384
- * 从 Agent 可选提供的 `narrative.rowInsights.<dim>[]` 里按主字段(如关键词原文/搜索词原文)
385
- * 找匹配行的洞察(翻译/分类/问题/建议)。找不到返回 null——渲染层一律显示「—」,不编造内容。
401
+ * 从 Agent 提供的 `narrative.rowInsights.<dim>[]` 按主字段匹配行洞察(目前仅关键词中文翻译)。
402
+ * 找不到返回 null——渲染层显示「—」,不编造内容。
386
403
  * 每条洞察支持 `{match:{...}, ...}` 或扁平写法 `{keyword, campaignName, ...}`。
387
404
  */
388
405
  function findInsight(list, primaryField, primaryValue, row) {
@@ -541,9 +558,9 @@
541
558
  .map(function (r) {
542
559
  return (
543
560
  "<tr><td>" +
544
- escapeHtml(r.weekLabel || "") +
561
+ escapeHtml(weekLabelText(r)) +
545
562
  "</td><td>" +
546
- escapeHtml((r.startDate || "") + " ~ " + (r.endDate || "")) +
563
+ escapeHtml(weekRangeText(r)) +
547
564
  '</td><td class="num">' +
548
565
  int(r.days) +
549
566
  '</td><td class="num">' +
@@ -852,9 +869,9 @@
852
869
  .map(function (r) {
853
870
  return (
854
871
  "<tr><td>" +
855
- escapeHtml(r.weekLabel || "") +
872
+ escapeHtml(weekLabelText(r)) +
856
873
  "</td><td>" +
857
- escapeHtml((r.startDate || "") + " ~ " + (r.endDate || "")) +
874
+ escapeHtml(weekRangeText(r)) +
858
875
  '</td><td class="num">' +
859
876
  int(r.days) +
860
877
  '</td><td class="num">' +
@@ -883,7 +900,7 @@
883
900
  "</div>" +
884
901
  "</div></section>" +
885
902
  '<section id="weekly"><div class="wrap">' +
886
- sectionHead("周报表", "由每日数据按 ISO 周本地聚合") +
903
+ sectionHead("周报表", " CLI 从 daily 按 ISO 周聚合(禁止脚本重算)") +
887
904
  '<div class="panel">' +
888
905
  weeklyBody +
889
906
  "</div>" +
@@ -963,16 +980,8 @@
963
980
  { label: "Conv", num: true },
964
981
  { label: "CPA", num: true },
965
982
  { label: "状态" },
966
- { label: "问题" },
967
- { label: "建议" },
968
983
  ],
969
- rowCells: function (r, currency, data) {
970
- const insight = findInsight(
971
- rowInsightsOf(data, "campaigns"),
972
- "campaignName",
973
- r.campaignName,
974
- r
975
- );
984
+ rowCells: function (r, currency) {
976
985
  return (
977
986
  "<td>" +
978
987
  escapeHtml(r.campaignName || r.campaignId || "—") +
@@ -996,10 +1005,6 @@
996
1005
  money(r.costPerConversion, currency) +
997
1006
  "</td><td>" +
998
1007
  campaignStatusBadge(r) +
999
- "</td><td>" +
1000
- escapeHtml((insight && insight.issue) || "—") +
1001
- "</td><td>" +
1002
- escapeHtml((insight && insight.suggestion) || "—") +
1003
1008
  "</td>"
1004
1009
  );
1005
1010
  },
@@ -1015,7 +1020,7 @@
1015
1020
  title: "关键词",
1016
1021
  desc: "按消耗排序的关键词明细",
1017
1022
  footnote:
1018
- "「中文翻译」「问题/建议」为 Agent 撰写报告时提供(narrative.rowInsights.keywords),未提供的行显示「—」,不做机械占位。",
1023
+ "「中文翻译」为 Agent 撰写报告时提供(narrative.rowInsights.keywords),未提供的行显示「—」,不做机械占位。",
1019
1024
  headers: [
1020
1025
  { label: "Campaign" },
1021
1026
  { label: "Group" },
@@ -1029,8 +1034,6 @@
1029
1034
  { label: "CPA", num: true },
1030
1035
  { label: "CTR", num: true },
1031
1036
  { label: "CPC", num: true },
1032
- { label: "问题" },
1033
- { label: "建议" },
1034
1037
  ],
1035
1038
  rowCells: function (r, currency, data) {
1036
1039
  const insight = findInsight(rowInsightsOf(data, "keywords"), "keyword", r.keyword, r);
@@ -1059,10 +1062,6 @@
1059
1062
  pct(derivedCtr(r)) +
1060
1063
  '</td><td class="num">' +
1061
1064
  money(derivedCpc(r), currency) +
1062
- "</td><td>" +
1063
- escapeHtml((insight && insight.issue) || "—") +
1064
- "</td><td>" +
1065
- escapeHtml((insight && insight.suggestion) || "—") +
1066
1065
  "</td>"
1067
1066
  );
1068
1067
  },
@@ -1077,8 +1076,6 @@
1077
1076
  tableKey: "searchTerms",
1078
1077
  title: "搜索词",
1079
1078
  desc: "按消耗排序的搜索词明细(仅近 180 天数据)",
1080
- footnote:
1081
- "「问题/建议」为 Agent 撰写报告时提供(narrative.rowInsights.searchTerms),未提供的行显示「—」,不做机械占位。",
1082
1079
  headers: [
1083
1080
  { label: "Search Query" },
1084
1081
  { label: "匹配关键词" },
@@ -1090,16 +1087,8 @@
1090
1087
  { label: "Conv", num: true },
1091
1088
  { label: "cr%", num: true },
1092
1089
  { label: "CPA", num: true },
1093
- { label: "问题" },
1094
- { label: "建议" },
1095
1090
  ],
1096
- rowCells: function (r, currency, data) {
1097
- const insight = findInsight(
1098
- rowInsightsOf(data, "searchTerms"),
1099
- "searchQuery",
1100
- r.searchQuery,
1101
- r
1102
- );
1091
+ rowCells: function (r, currency) {
1103
1092
  return (
1104
1093
  "<td>" +
1105
1094
  escapeHtml(r.searchQuery || "—") +
@@ -1121,10 +1110,6 @@
1121
1110
  pct(derivedCr(r)) +
1122
1111
  '</td><td class="num">' +
1123
1112
  money(derivedCpa(r), currency) +
1124
- "</td><td>" +
1125
- escapeHtml((insight && insight.issue) || "—") +
1126
- "</td><td>" +
1127
- escapeHtml((insight && insight.suggestion) || "—") +
1128
1113
  "</td>"
1129
1114
  );
1130
1115
  },
@@ -1151,8 +1136,6 @@
1151
1136
  { label: "Conv", num: true },
1152
1137
  { label: "cr%", num: true },
1153
1138
  { label: "CPA", num: true },
1154
- { label: "问题" },
1155
- { label: "建议" },
1156
1139
  ],
1157
1140
  rowCells: function (r, currency, data) {
1158
1141
  const cityName =
@@ -1161,7 +1144,6 @@
1161
1144
  r.location ||
1162
1145
  r.name ||
1163
1146
  (r.locationId != null ? "地域 " + r.locationId : "—");
1164
- const insight = findInsight(rowInsightsOf(data, "geo"), "locationName", cityName, r);
1165
1147
  return (
1166
1148
  "<td>" +
1167
1149
  escapeHtml((data.meta && data.meta.region) || "—") +
@@ -1183,10 +1165,6 @@
1183
1165
  pct(derivedCr(r)) +
1184
1166
  '</td><td class="num">' +
1185
1167
  money(derivedCpa(r), currency) +
1186
- "</td><td>" +
1187
- escapeHtml((insight && insight.issue) || "—") +
1188
- "</td><td>" +
1189
- escapeHtml((insight && insight.suggestion) || "—") +
1190
1168
  "</td>"
1191
1169
  );
1192
1170
  },
@@ -1211,11 +1189,8 @@
1211
1189
  { label: "Conv", num: true },
1212
1190
  { label: "cr%", num: true },
1213
1191
  { label: "CPA", num: true },
1214
- { label: "问题" },
1215
- { label: "建议" },
1216
1192
  ],
1217
- rowCells: function (r, currency, data) {
1218
- const insight = findInsight(rowInsightsOf(data, "devices"), "device", r.device, r);
1193
+ rowCells: function (r, currency) {
1219
1194
  return (
1220
1195
  "<td>" +
1221
1196
  escapeHtml(r.device || "—") +
@@ -1235,10 +1210,6 @@
1235
1210
  pct(derivedCr(r)) +
1236
1211
  '</td><td class="num">' +
1237
1212
  money(derivedCpa(r), currency) +
1238
- "</td><td>" +
1239
- escapeHtml((insight && insight.issue) || "—") +
1240
- "</td><td>" +
1241
- escapeHtml((insight && insight.suggestion) || "—") +
1242
1213
  "</td>"
1243
1214
  );
1244
1215
  },
@@ -6,12 +6,12 @@
6
6
  > 用户**未指定格式**时一律走下方 **标准四步流程**;**禁止** Agent 手写/拼接 HTML。
7
7
  > **用户明确要求 Excel / xlsx** 时:拉数步骤不变,改为 Agent 脚本写 `.xlsx`;**不要**调用 `yandex-analysis render`;禁止假设存在内置 excel 子命令。见 `yandex-period-report-excel.md`。
8
8
 
9
- 数据块(YandexAPI `/yandex-analysis` 8 维 + 本地周聚合):总览、按日、Search 网络、系列、关键词、搜索词、地域、设备;周报由 `daily` 本地聚合(后端无独立 weekly)。
9
+ 数据块(YandexAPI `/yandex-analysis` 8 维 + 本地周聚合):总览、按日、Search 网络、系列、关键词、搜索词、地域、设备;周报由 CLI 写入 `daily.weekly`(后端无独立 weekly)。**禁止**自行按 `date` 聚周。
10
10
 
11
11
  | CLI `--sections` | 落盘形状(CI 实测) | 报告 `tables.*` / KPI |
12
12
  | ---------------- | -------------------------------------------------------- | ---------------------------------------------------------- |
13
13
  | `overview` | 对象:`account` / `currency` / `balance` / `totals` … | → `meta` + `kpis`(余额、日均、续航天数) |
14
- | `daily` | `{ account, startDate, endDate, items: DayRow[] }` | → `tables.daily[]`;并聚合 `tables.weekly[]` |
14
+ | `daily` | `{ account, startDate, endDate, items: DayRow[], weekly: WeekRow[] }` | → `tables.daily[]` + `tables.weekly[]`(勿重算) |
15
15
  | `search` | `{ …, items: SearchRow[] }` | → `tables.search[]` |
16
16
  | `campaigns` | `{ …, items: CampaignRow[] }`(默认 limit=100) | → `tables.campaigns[]` |
17
17
  | `keywords` | `{ …, items: KeywordRow[] }` | → `tables.keywords[]` |
@@ -28,7 +28,7 @@
28
28
  | 步骤 | 执行者 | 动作 |
29
29
  | ---------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
30
30
  | **1. 拉数** | Agent 调 CLI | `yandex-analysis -a <porg-xxx> --start <s> --end <e> --json-out ./snap-yandex`(全 8 维或 `--sections` 指定子集) |
31
- | **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`),对照 outline 完成聚合与洞察 |
31
+ | **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`)。周趋势读 `daily-*.json` 的 `weekly[]`,**禁止**自己按 `date` 聚周。 |
32
32
  | **3. 写 JSON** | Agent | 撰写 `yandex-period-report.json`:仅 `meta.accountId` + `narrative`(8 个分析章节)为必填;`kpis`/`tables` 可省略由 `--snapshot-dir` 合并 |
33
33
  | **4. 渲染 HTML** | CLI | `yandex-analysis render` — **校验 narrative 8 个分析章节必含字段**,缺项报错不生成 HTML;**禁止** Agent 手写/拼接 HTML |
34
34
 
@@ -49,7 +49,7 @@ siluzan-tso yandex-analysis render \
49
49
  - `kpis`:`spend` / `impressions` / `clicks` / `conversions` / `ctr` / `averageCpc` / `costPerConversion` / `conversionRate` / `roi` / `balance` / `avgDailySpend` / `activeDays` / `runwayDays` / `balanceAsOf`;可省略。
50
50
  - `tables`(可省略,由 `--snapshot-dir` 映射):
51
51
  - `daily[]`:`{date, spend, impressions, clicks, ctr, averageCpc, conversions, …}`
52
- - `weekly[]`:`{weekLabel, startDate, endDate, days, spend, impressions, clicks, ctr, averageCpc, conversions, costPerConversion}`(CLI 由 daily 聚合)
52
+ - `weekly[]`:`{weekLabel, isoWeek, startDate, endDate, days, spend, …}`(**只读** CLI 已算好的周;`startDate`/`endDate` 为该 ISO 周周一~周日,`days` 为有数据的天数。禁止自填)
53
53
  - `search[]`:`{network, spend, …}`
54
54
  - `campaigns[]`:`{campaignId, campaignName, status, state, campaignType, budget, strategy, …metrics}`
55
55
  - `keywords[]`:`{keyword, campaignName, criteriaType, network, …metrics}`
@@ -60,15 +60,14 @@ siluzan-tso yandex-analysis render \
60
60
  - `executiveSummary[]`:≥1 段执行摘要
61
61
  - `sections.{overview,search,campaigns,keywords,searchTerms,geo,devices,daily}`:每个维度各 `{analysis[](≥1,含数字依据), suggestions[](≥1)}`
62
62
  - `recommendations[]`:≥3 条跨维度优化建议(可用 `{priority,text,anchor}`,anchor 对齐样例锚点如 `#keyword` / `#query`)
63
- - `rowInsights`:有关键词表格行时,**HTML 将展示的 Top N 行必须有中文翻译**;`render` 缺项会拒绝出 HTML。问题/建议可空。见下方「逐行洞察」
63
+ - `rowInsights`:有关键词表格行时,**HTML 将展示的 Top N 行必须有中文翻译**;`render` 缺项会拒绝出 HTML。见下方「逐行洞察」。Campaign / 关键词 / 搜索词 / 地域 / 设备列表**不展示**「问题」「建议」列,行级判断写入各维 `sections.*.analysis/suggestions` 即可。
64
64
 
65
65
  #### 逐行洞察 `narrative.rowInsights`
66
66
 
67
- 开发样例的部分表格列(关键词"中文翻译"、各维度"问题/建议")不是原始接口字段,而是 **Agent 基于已拉取数据做的翻译/判断**:
67
+ 表格主观列目前只有关键词「中文翻译」,不是原始接口字段,由 Agent 基于已拉取原文翻译:
68
68
 
69
69
  - **必填(`render` 校验)**:`tables.keywords` 有行时,须为 HTML 将展示的每一行(按消耗降序,最多 40 行)提供匹配的 `rowInsights`,且有非空 `translation`。缺一项即拒绝生成 HTML。
70
- - **可空**:各维 `issue` / `suggestion`(空表示该行无问题,模板显示「—」)。
71
- - 搜索词「分类」Yandex 不提供,也无法由现有指标算出,报告不展示该列。
70
+ - Campaign「学习期」、搜索词「分类」、各维行级「问题/建议」均不展示。
72
71
 
73
72
  示例:
74
73
 
@@ -77,25 +76,13 @@ siluzan-tso yandex-analysis render \
77
76
  "rowInsights": {
78
77
  // 按 keyword 原文匹配(如同词跨系列,可加 campaignName 消歧)
79
78
  "keywords": [
80
- { "match": { "keyword": "металлопрокатное оборудование" }, "translation": "金属轧制设备", "issue": "", "suggestion": "可加大预算" }
81
- ],
82
- // 按 searchQuery 原文匹配
83
- "searchTerms": [
84
- { "match": { "searchQuery": "прокатный стан своими руками" }, "issue": "DIY 意图消耗", "suggestion": "添加否定短语 своими руками" }
85
- ],
86
- // 按 campaignName 匹配
87
- "campaigns": [
88
- { "match": { "campaignName": "Search - 自动定向" }, "issue": "自动定向 CTR 弱", "suggestion": "评估降价或暂停" }
89
- ],
90
- // 按 locationName(城市名)匹配
91
- "geo": [{ "match": { "locationName": "Moscow" }, "issue": "", "suggestion": "" }],
92
- // 按 device 匹配
93
- "devices": [{ "match": { "device": "tablet" }, "issue": "样本不足", "suggestion": "暂不调整设备系数" }]
79
+ { "match": { "keyword": "металлопрокатное оборудование" }, "translation": "金属轧制设备" }
80
+ ]
94
81
  }
95
82
  }
96
83
  ```
97
84
 
98
- - `translation` 是 Agent **对已知原文的翻译**,允许生成;不得编造原始接口没有、也无法由现有指标算出的硬事实。Campaign「学习期」、搜索词「分类」无数据源,报告不展示这两列。
85
+ - `translation` 是 Agent **对已知原文的翻译**,允许生成;不得编造原始接口没有、也无法由现有指标算出的硬事实。
99
86
  - 每条洞察按原文匹配一行;HTML 将展示的关键词行若缺中文翻译,`render` 会拒绝出 HTML。
100
87
 
101
88
  ### 快照合并范围(`--snapshot-dir`)
@@ -104,9 +91,17 @@ siluzan-tso yandex-analysis render \
104
91
 
105
92
  1. `meta`(仅补空字段)
106
93
  2. **`kpis` / `tables.*` 一律以 CLI 快照覆盖**(有对应 section 时)
107
- 3. `tables.weekly` 由 `daily.items` 本地 ISO 周聚合
94
+ 3. `tables.weekly` 由 CLI 从 `daily.items` ISO 周重算并覆盖(与落盘 `daily.weekly` 同一套函数)
108
95
 
109
- Agent 只需撰写 `narrative`;**禁止手写 HTML**,也**禁止**自填 `kpis`/`tables` 数值。
96
+ Agent 只需撰写 `narrative`;**禁止手写 HTML**,也**禁止**自填 `kpis`/`tables` 数值,**禁止**自行聚周。
97
+
98
+ ### 周趋势(禁止再出现 NaN)
99
+
100
+ 网关 `daily.items[].date` 可能是 ISO 时间戳(`2026-07-01T00:00:00+00:00`)。CLI 落盘时已截成 `YYYY-MM-DD`,并写好 `weekly[]`。
101
+
102
+ - 写「周趋势」叙事时:**脚本读 `weekly[]`**(`weekLabel` / `startDate` / `endDate` / `spend` / `conversions` / `days`)。
103
+ - **禁止** `new Date(date + 'T00:00:00Z')`:日期已是完整时间戳时再拼接会变成 Invalid Date,年份/月份变成 `NaN`,报告出现 `NaN-NaN-NaN`。
104
+ - **禁止**自己算 ISO 周、自己拆周。周末无投放时 `days` 会小于 7,属正常。
110
105
 
111
106
  ---
112
107
 
@@ -130,7 +125,8 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
130
125
 
131
126
  - 写脚本前先读各 `yandex-*-<id>.outline.txt`(或 stdout 摘要里的 outline),再脚本读 `.json`。
132
127
  - `search` / `campaigns` / `keywords` / `geo` / `devices` 在 Direct 侧过滤 `AdNetworkType=SEARCH`。
133
- - Campaign「学习期」、搜索词「分类」无接口字段、也无法由现有指标算出,报告不展示这两列。关键词中文翻译可由 Agent 通过 `narrative.rowInsights` 提供(见上方「逐行洞察」)。
128
+ - 周趋势读 `daily-*.json` `weekly[]`,禁止自行聚周、禁止给 `date` 再拼 `T00:00:00Z`。
129
+ - Campaign「学习期」、搜索词「分类」、各维行级「问题/建议」均不展示。关键词中文翻译可由 Agent 通过 `narrative.rowInsights` 提供(见上方「逐行洞察」)。
134
130
 
135
131
  ---
136
132
 
@@ -9,11 +9,11 @@ $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.45-beta.4'
12
+ $PKG_VERSION = '1.1.45'
13
13
  $CLI_BIN = 'siluzan-tso'
14
14
  $SKILL_LABEL = 'Siluzan TSO'
15
- $INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
16
- $WEB_BASE = 'https://www-ci.siluzan.com'
15
+ $INSTALL_CMD = 'npm install -g siluzan-tso-cli'
16
+ $WEB_BASE = 'https://www.siluzan.com'
17
17
 
18
18
  # -- Constants ----------------------------------------------------------------
19
19
  $NODE_MAJOR_MIN = 18
@@ -9,11 +9,11 @@ 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.45-beta.4"
12
+ readonly PKG_VERSION="1.1.45"
13
13
  readonly CLI_BIN="siluzan-tso"
14
14
  readonly SKILL_LABEL="Siluzan TSO"
15
- readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
16
- readonly WEB_BASE="https://www-ci.siluzan.com"
15
+ readonly INSTALL_CMD="npm install -g siluzan-tso-cli"
16
+ readonly WEB_BASE="https://www.siluzan.com"
17
17
 
18
18
  # -- Constants ----------------------------------------------------------------
19
19
  readonly NODE_MAJOR_MIN=18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.45-beta.4",
3
+ "version": "1.1.45",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",