siluzan-tso-cli 1.1.34-beta.4 → 1.1.35-beta.1

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.34-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.35-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -5754,6 +5754,150 @@ var init_sections = __esm({
5754
5754
  }
5755
5755
  });
5756
5756
 
5757
+ // src/utils/http-retry.ts
5758
+ function classifyError(err) {
5759
+ const status = parseHttpStatusFromError(err);
5760
+ if (status === 401) return { class: "abort", status };
5761
+ if (status === void 0) {
5762
+ return { class: "retryable" };
5763
+ }
5764
+ if (status === 408 || status === 429) return { class: "retryable", status };
5765
+ if (status >= 500 && status <= 599) return { class: "retryable", status };
5766
+ if (status >= 400 && status <= 499) return { class: "permanent", status };
5767
+ return { class: "permanent", status };
5768
+ }
5769
+ function computeBackoffMs(attempt, policy) {
5770
+ const exp = policy.baseMs * 2 ** Math.max(0, attempt - 1);
5771
+ const capped = Math.min(exp, policy.maxBackoffMs);
5772
+ const jitter = Math.floor(Math.random() * policy.baseMs);
5773
+ return capped + jitter;
5774
+ }
5775
+ function sleep3(ms, signal) {
5776
+ return new Promise((resolve15, reject) => {
5777
+ if (signal?.aborted) {
5778
+ reject(new Error("retry sleep aborted"));
5779
+ return;
5780
+ }
5781
+ const timer = setTimeout(() => {
5782
+ signal?.removeEventListener("abort", onAbort);
5783
+ resolve15();
5784
+ }, ms);
5785
+ const onAbort = () => {
5786
+ clearTimeout(timer);
5787
+ reject(new Error("retry sleep aborted"));
5788
+ };
5789
+ signal?.addEventListener("abort", onAbort, { once: true });
5790
+ });
5791
+ }
5792
+ async function withTaskTimeout(factory, timeoutMs, label) {
5793
+ if (!timeoutMs || timeoutMs <= 0) return factory();
5794
+ let timer;
5795
+ try {
5796
+ return await Promise.race([
5797
+ factory(),
5798
+ new Promise((_, reject) => {
5799
+ timer = setTimeout(() => {
5800
+ reject(new Error(`\u8BF7\u6C42\u8D85\u65F6\uFF08${(timeoutMs / 1e3).toFixed(0)} \u79D2\uFF09\uFF1A${label}`));
5801
+ }, timeoutMs);
5802
+ })
5803
+ ]);
5804
+ } finally {
5805
+ if (timer) clearTimeout(timer);
5806
+ }
5807
+ }
5808
+ async function fetchWithRetry(factory, options) {
5809
+ const policy = { ...DEFAULT_RETRY_POLICY, ...options.policy ?? {} };
5810
+ let lastErr;
5811
+ let lastStatus;
5812
+ for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
5813
+ if (options.signal?.aborted) {
5814
+ throw new AbortRunError({
5815
+ message: `\u4EFB\u52A1\u88AB\u5916\u90E8\u4FE1\u53F7\u4E2D\u6B62\uFF1A${options.label}`
5816
+ });
5817
+ }
5818
+ try {
5819
+ return await withTaskTimeout(factory, policy.taskTimeoutMs, options.label);
5820
+ } catch (raw) {
5821
+ const err = raw instanceof Error ? raw : new Error(String(raw));
5822
+ const cls = classifyError(err);
5823
+ lastErr = err;
5824
+ lastStatus = cls.status;
5825
+ if (cls.class === "abort") {
5826
+ throw new AbortRunError({
5827
+ message: `Token \u5931\u6548\u6216\u81F4\u547D\u9519\u8BEF\uFF08${options.label}\uFF09\uFF1A${err.message}`,
5828
+ status: cls.status,
5829
+ cause: err
5830
+ });
5831
+ }
5832
+ if (cls.class === "permanent" || attempt >= policy.maxAttempts) {
5833
+ throw new HttpRetryError({
5834
+ message: cls.class === "permanent" ? `\u6C38\u4E45\u5931\u8D25\uFF08${options.label}\uFF09\uFF1A${err.message}` : `\u91CD\u8BD5 ${attempt} \u6B21\u4ECD\u5931\u8D25\uFF08${options.label}\uFF09\uFF1A${err.message}`,
5835
+ class: cls.class === "permanent" ? "permanent" : "retryable",
5836
+ status: cls.status,
5837
+ attempts: attempt,
5838
+ cause: err
5839
+ });
5840
+ }
5841
+ const backoffMs = computeBackoffMs(attempt, policy);
5842
+ options.onRetry?.({ attempt, backoffMs, status: cls.status, error: err });
5843
+ try {
5844
+ await sleep3(backoffMs, options.signal);
5845
+ } catch {
5846
+ throw new AbortRunError({
5847
+ message: `\u91CD\u8BD5\u7B49\u5F85\u88AB\u4E2D\u6B62\uFF1A${options.label}`,
5848
+ cause: err
5849
+ });
5850
+ }
5851
+ }
5852
+ }
5853
+ throw new HttpRetryError({
5854
+ message: `\u672A\u77E5\u91CD\u8BD5\u5931\u8D25\uFF08${options.label}\uFF09\uFF1A${lastErr?.message ?? "no error"}`,
5855
+ class: "retryable",
5856
+ status: lastStatus,
5857
+ attempts: policy.maxAttempts,
5858
+ cause: lastErr
5859
+ });
5860
+ }
5861
+ var HttpRetryError, AbortRunError, DEFAULT_RETRY_POLICY;
5862
+ var init_http_retry = __esm({
5863
+ "src/utils/http-retry.ts"() {
5864
+ "use strict";
5865
+ init_dist();
5866
+ HttpRetryError = class extends Error {
5867
+ class;
5868
+ status;
5869
+ attempts;
5870
+ cause;
5871
+ constructor(opts) {
5872
+ super(opts.message);
5873
+ this.name = "HttpRetryError";
5874
+ this.class = opts.class;
5875
+ this.status = opts.status;
5876
+ this.attempts = opts.attempts;
5877
+ this.cause = opts.cause;
5878
+ }
5879
+ };
5880
+ AbortRunError = class extends HttpRetryError {
5881
+ constructor(opts) {
5882
+ super({
5883
+ message: opts.message,
5884
+ class: "abort",
5885
+ status: opts.status,
5886
+ attempts: 1,
5887
+ cause: opts.cause
5888
+ });
5889
+ this.name = "AbortRunError";
5890
+ }
5891
+ };
5892
+ DEFAULT_RETRY_POLICY = {
5893
+ maxAttempts: 3,
5894
+ baseMs: 1e3,
5895
+ maxBackoffMs: 3e4,
5896
+ taskTimeoutMs: 6e4
5897
+ };
5898
+ }
5899
+ });
5900
+
5757
5901
  // src/commands/google-analysis/normalize-impression-shares.ts
5758
5902
  function normalizeShareValue(value, competitiveMetrics, field) {
5759
5903
  const cmVal = competitiveMetrics?.[field];
@@ -101399,6 +101543,28 @@ var init_translate_fields = __esm({
101399
101543
  }
101400
101544
  });
101401
101545
 
101546
+ // src/commands/google-analysis/daily-metrics-range.ts
101547
+ function pad2(n) {
101548
+ return String(n).padStart(2, "0");
101549
+ }
101550
+ function formatChinaOffsetDateTime(d) {
101551
+ const sh = new Date(d.getTime() + CHINA_OFFSET_MS);
101552
+ return `${sh.getUTCFullYear()}-${pad2(sh.getUTCMonth() + 1)}-${pad2(sh.getUTCDate())}T${pad2(sh.getUTCHours())}:${pad2(sh.getUTCMinutes())}:${pad2(sh.getUTCSeconds())}+08:00`;
101553
+ }
101554
+ function resolveDailyMetricsQueryTimes(startDate, endDate, now = /* @__PURE__ */ new Date()) {
101555
+ const startInstant = `${startDate}T00:00:00+08:00`;
101556
+ const endOfDay = /* @__PURE__ */ new Date(`${endDate}T23:59:59+08:00`);
101557
+ const endInstant = endOfDay.getTime() > now.getTime() ? formatChinaOffsetDateTime(now) : formatChinaOffsetDateTime(endOfDay);
101558
+ return { startInstant, endInstant };
101559
+ }
101560
+ var CHINA_OFFSET_MS;
101561
+ var init_daily_metrics_range = __esm({
101562
+ "src/commands/google-analysis/daily-metrics-range.ts"() {
101563
+ "use strict";
101564
+ CHINA_OFFSET_MS = 8 * 36e5;
101565
+ }
101566
+ });
101567
+
101402
101568
  // src/commands/google-analysis/fetch.ts
101403
101569
  function defaultDateRange3() {
101404
101570
  const end = /* @__PURE__ */ new Date();
@@ -101432,9 +101598,19 @@ function buildSearchParams(def, start, end, extras) {
101432
101598
  const s = params.toString();
101433
101599
  return s ? `?${s}` : "";
101434
101600
  }
101435
- async function fetchJson(config, pathWithQuery, verbose) {
101601
+ async function fetchGoogleAnalysisHttp(label, factory, http2) {
101602
+ return fetchWithRetry(factory, {
101603
+ label,
101604
+ policy: { ...DEFAULT_RETRY_POLICY, ...http2?.retry },
101605
+ signal: http2?.signal
101606
+ });
101607
+ }
101608
+ async function fetchJson(config, pathWithQuery, verbose, label = pathWithQuery, http2) {
101436
101609
  const url = `${config.googleApiUrl}${pathWithQuery}`;
101437
- return apiFetch2(url, config, {}, verbose);
101610
+ return fetchGoogleAnalysisHttp(label, () => apiFetch2(url, config, {}, verbose), http2);
101611
+ }
101612
+ async function fetchMainPlatformJson(url, config, verbose, label, http2) {
101613
+ return fetchGoogleAnalysisHttp(label, () => apiFetch2(url, config, {}, verbose), http2);
101438
101614
  }
101439
101615
  function deviceBidModifierLookupKey(campaignId, adGroupId, deviceType) {
101440
101616
  return `${campaignId ?? ""}\0${adGroupId ?? ""}\0${deviceType ?? ""}`;
@@ -101482,54 +101658,55 @@ function rowsFromAccountDailyReportsEnvelope(raw, mediaCustomerId) {
101482
101658
  const list = block?.data;
101483
101659
  return Array.isArray(list) ? list : [];
101484
101660
  }
101485
- async function fetchGoogleAnalysisSectionJson(config, fullPath, verbose, name2) {
101661
+ async function fetchGoogleAnalysisSectionJson(config, fullPath, verbose, name2, label, http2) {
101662
+ const fetchSection = () => fetchJson(config, fullPath, verbose, label, http2);
101486
101663
  switch (name2) {
101487
101664
  case "overview":
101488
- return fetchJson(config, fullPath, verbose);
101665
+ return fetchSection();
101489
101666
  case "keywords":
101490
- return fetchJson(config, fullPath, verbose);
101667
+ return fetchSection();
101491
101668
  case "search-terms":
101492
- return fetchJson(config, fullPath, verbose);
101669
+ return fetchSection();
101493
101670
  case "campaigns":
101494
- return fetchJson(config, fullPath, verbose);
101671
+ return fetchSection();
101495
101672
  case "campaign-hour":
101496
- return fetchJson(config, fullPath, verbose);
101673
+ return fetchSection();
101497
101674
  case "ads":
101498
- return fetchJson(config, fullPath, verbose);
101675
+ return fetchSection();
101499
101676
  case "extensions":
101500
- return fetchJson(config, fullPath, verbose);
101677
+ return fetchSection();
101501
101678
  case "devices":
101502
- return fetchJson(config, fullPath, verbose);
101679
+ return fetchSection();
101503
101680
  case "geographic":
101504
- return fetchJson(config, fullPath, verbose);
101681
+ return fetchSection();
101505
101682
  case "geo-matched":
101506
- return fetchJson(config, fullPath, verbose);
101683
+ return fetchSection();
101507
101684
  case "campaign-geo":
101508
- return fetchJson(config, fullPath, verbose);
101685
+ return fetchSection();
101509
101686
  case "campaign-geo-matched":
101510
- return fetchJson(config, fullPath, verbose);
101687
+ return fetchSection();
101511
101688
  case "campaign-device":
101512
- return fetchJson(config, fullPath, verbose);
101689
+ return fetchSection();
101513
101690
  case "audience":
101514
- return fetchJson(config, fullPath, verbose);
101691
+ return fetchSection();
101515
101692
  case "asset-images":
101516
- return fetchJson(config, fullPath, verbose);
101693
+ return fetchSection();
101517
101694
  case "videos":
101518
- return fetchJson(config, fullPath, verbose);
101695
+ return fetchSection();
101519
101696
  case "resource-counts":
101520
- return fetchJson(config, fullPath, verbose);
101697
+ return fetchSection();
101521
101698
  case "conversion-actions":
101522
- return fetchJson(config, fullPath, verbose);
101699
+ return fetchSection();
101523
101700
  case "gold-account":
101524
- return fetchJson(config, fullPath, verbose);
101701
+ return fetchSection();
101525
101702
  case "ads-index":
101526
- return fetchJson(config, fullPath, verbose);
101703
+ return fetchSection();
101527
101704
  case "final-urls":
101528
- return fetchJson(config, fullPath, verbose);
101705
+ return fetchSection();
101529
101706
  case "dimension-summary":
101530
- return fetchJson(config, fullPath, verbose);
101707
+ return fetchSection();
101531
101708
  case "campaign-types":
101532
- return fetchJson(config, fullPath, verbose);
101709
+ return fetchSection();
101533
101710
  default:
101534
101711
  return assertNever(name2, "google-analysis");
101535
101712
  }
@@ -101544,7 +101721,7 @@ function manifestDateRange(def, start, end) {
101544
101721
  const { startDate, endDate } = resolveDateRange2(start, end);
101545
101722
  return { mode: "range", start: startDate, end: endDate };
101546
101723
  }
101547
- async function fetchSectionPayload(def, opts, config, id) {
101724
+ async function fetchSectionPayload(def, opts, config, id, http2) {
101548
101725
  const extras = {};
101549
101726
  if (def.keywordOptions) {
101550
101727
  if (typeof opts.limit === "number" && opts.limit > 0) {
@@ -101571,9 +101748,10 @@ async function fetchSectionPayload(def, opts, config, id) {
101571
101748
  const q = `?${new URLSearchParams({ startDate, endDate }).toString()}`;
101572
101749
  const videoPath = `/reporting/media-account/${id}/Videos${q}`;
101573
101750
  const imagePath = `/reporting/media-account/${id}/CampaignAssetView${q}`;
101751
+ const verbose = !!opts.verbose;
101574
101752
  const [images, videos] = await Promise.all([
101575
- fetchJson(config, imagePath, !!opts.verbose),
101576
- fetchJson(config, videoPath, !!opts.verbose)
101753
+ fetchJson(config, imagePath, verbose, `materials/images/${id}`, http2),
101754
+ fetchJson(config, videoPath, verbose, `materials/videos/${id}`, http2)
101577
101755
  ]);
101578
101756
  const merged = { images, videos };
101579
101757
  return annotateZhFields(
@@ -101587,12 +101765,21 @@ async function fetchSectionPayload(def, opts, config, id) {
101587
101765
  if (def.name === "campaign-device") {
101588
101766
  const sectionPath2 = def.path(id);
101589
101767
  const query2 = buildSearchParams(def, opts.start, opts.end, extras);
101768
+ const verbose = !!opts.verbose;
101590
101769
  const [report, modifiers] = await Promise.all([
101591
- fetchJson(config, `${sectionPath2}${query2}`, !!opts.verbose),
101770
+ fetchJson(
101771
+ config,
101772
+ `${sectionPath2}${query2}`,
101773
+ verbose,
101774
+ `campaign-device/${id}`,
101775
+ http2
101776
+ ),
101592
101777
  fetchJson(
101593
101778
  config,
101594
101779
  `/campaignmanagement/${id}/BidModifiers/Devices`,
101595
- !!opts.verbose
101780
+ verbose,
101781
+ `campaign-device/bid-modifiers/${id}`,
101782
+ http2
101596
101783
  )
101597
101784
  ]);
101598
101785
  const data2 = mergeDeviceBidModifiersIntoReport(report, modifiers);
@@ -101606,13 +101793,20 @@ async function fetchSectionPayload(def, opts, config, id) {
101606
101793
  }
101607
101794
  if (def.name === "daily-metrics") {
101608
101795
  const { startDate, endDate } = resolveDateRange2(opts.start, opts.end);
101796
+ const { startInstant, endInstant } = resolveDailyMetricsQueryTimes(startDate, endDate);
101609
101797
  const params = new URLSearchParams({
101610
101798
  mediaCustomerIds: id,
101611
- startDate: `${startDate}T00:00:00+08:00`,
101612
- endDate: `${endDate}T23:59:59+08:00`
101799
+ startDate: startInstant,
101800
+ endDate: endInstant
101613
101801
  });
101614
101802
  const url = `${config.apiBaseUrl}/report/media-account/google/account-daily-reports?${params.toString()}`;
101615
- const raw = await apiFetch2(url, config, {}, !!opts.verbose);
101803
+ const raw = await fetchMainPlatformJson(
101804
+ url,
101805
+ config,
101806
+ !!opts.verbose,
101807
+ `daily-metrics/${id}`,
101808
+ http2
101809
+ );
101616
101810
  const rows = rowsFromAccountDailyReportsEnvelope(raw, id);
101617
101811
  return annotateZhFields(
101618
101812
  normalizeMoneyScales(
@@ -101628,7 +101822,9 @@ async function fetchSectionPayload(def, opts, config, id) {
101628
101822
  config,
101629
101823
  `${sectionPath}${query}`,
101630
101824
  !!opts.verbose,
101631
- def.name
101825
+ def.name,
101826
+ `${def.name}/${id}`,
101827
+ http2
101632
101828
  );
101633
101829
  let payload = stripLegacyGoogleFieldsIfV2Present(data);
101634
101830
  payload = normalizeRateScales(payload, def.name);
@@ -101665,7 +101861,8 @@ async function fetchOneGoogleSection(opts) {
101665
101861
  conversionsGreater: opts.conversionsGreater
101666
101862
  },
101667
101863
  opts.config,
101668
- opts.accountId
101864
+ opts.accountId,
101865
+ { retry: opts.retry, signal: opts.signal }
101669
101866
  );
101670
101867
  return {
101671
101868
  payload,
@@ -101679,156 +101876,14 @@ var init_fetch = __esm({
101679
101876
  "use strict";
101680
101877
  init_auth();
101681
101878
  init_strip_legacy_google_fields();
101879
+ init_http_retry();
101682
101880
  init_sections();
101683
101881
  init_normalize_impression_shares();
101684
101882
  init_normalize_rates();
101685
101883
  init_normalize_money();
101686
101884
  init_normalize_overview();
101687
101885
  init_translate_fields();
101688
- }
101689
- });
101690
-
101691
- // src/utils/http-retry.ts
101692
- function classifyError(err) {
101693
- const status = parseHttpStatusFromError(err);
101694
- if (status === 401) return { class: "abort", status };
101695
- if (status === void 0) {
101696
- return { class: "retryable" };
101697
- }
101698
- if (status === 408 || status === 429) return { class: "retryable", status };
101699
- if (status >= 500 && status <= 599) return { class: "retryable", status };
101700
- if (status >= 400 && status <= 499) return { class: "permanent", status };
101701
- return { class: "permanent", status };
101702
- }
101703
- function computeBackoffMs(attempt, policy) {
101704
- const exp = policy.baseMs * 2 ** Math.max(0, attempt - 1);
101705
- const capped = Math.min(exp, policy.maxBackoffMs);
101706
- const jitter = Math.floor(Math.random() * policy.baseMs);
101707
- return capped + jitter;
101708
- }
101709
- function sleep3(ms, signal) {
101710
- return new Promise((resolve15, reject) => {
101711
- if (signal?.aborted) {
101712
- reject(new Error("retry sleep aborted"));
101713
- return;
101714
- }
101715
- const timer = setTimeout(() => {
101716
- signal?.removeEventListener("abort", onAbort);
101717
- resolve15();
101718
- }, ms);
101719
- const onAbort = () => {
101720
- clearTimeout(timer);
101721
- reject(new Error("retry sleep aborted"));
101722
- };
101723
- signal?.addEventListener("abort", onAbort, { once: true });
101724
- });
101725
- }
101726
- async function withTaskTimeout(factory, timeoutMs, label) {
101727
- if (!timeoutMs || timeoutMs <= 0) return factory();
101728
- let timer;
101729
- try {
101730
- return await Promise.race([
101731
- factory(),
101732
- new Promise((_, reject) => {
101733
- timer = setTimeout(() => {
101734
- reject(new Error(`\u8BF7\u6C42\u8D85\u65F6\uFF08${(timeoutMs / 1e3).toFixed(0)} \u79D2\uFF09\uFF1A${label}`));
101735
- }, timeoutMs);
101736
- })
101737
- ]);
101738
- } finally {
101739
- if (timer) clearTimeout(timer);
101740
- }
101741
- }
101742
- async function fetchWithRetry(factory, options) {
101743
- const policy = { ...DEFAULT_RETRY_POLICY, ...options.policy ?? {} };
101744
- let lastErr;
101745
- let lastStatus;
101746
- for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
101747
- if (options.signal?.aborted) {
101748
- throw new AbortRunError({
101749
- message: `\u4EFB\u52A1\u88AB\u5916\u90E8\u4FE1\u53F7\u4E2D\u6B62\uFF1A${options.label}`
101750
- });
101751
- }
101752
- try {
101753
- return await withTaskTimeout(factory, policy.taskTimeoutMs, options.label);
101754
- } catch (raw) {
101755
- const err = raw instanceof Error ? raw : new Error(String(raw));
101756
- const cls = classifyError(err);
101757
- lastErr = err;
101758
- lastStatus = cls.status;
101759
- if (cls.class === "abort") {
101760
- throw new AbortRunError({
101761
- message: `Token \u5931\u6548\u6216\u81F4\u547D\u9519\u8BEF\uFF08${options.label}\uFF09\uFF1A${err.message}`,
101762
- status: cls.status,
101763
- cause: err
101764
- });
101765
- }
101766
- if (cls.class === "permanent" || attempt >= policy.maxAttempts) {
101767
- throw new HttpRetryError({
101768
- message: cls.class === "permanent" ? `\u6C38\u4E45\u5931\u8D25\uFF08${options.label}\uFF09\uFF1A${err.message}` : `\u91CD\u8BD5 ${attempt} \u6B21\u4ECD\u5931\u8D25\uFF08${options.label}\uFF09\uFF1A${err.message}`,
101769
- class: cls.class === "permanent" ? "permanent" : "retryable",
101770
- status: cls.status,
101771
- attempts: attempt,
101772
- cause: err
101773
- });
101774
- }
101775
- const backoffMs = computeBackoffMs(attempt, policy);
101776
- options.onRetry?.({ attempt, backoffMs, status: cls.status, error: err });
101777
- try {
101778
- await sleep3(backoffMs, options.signal);
101779
- } catch {
101780
- throw new AbortRunError({
101781
- message: `\u91CD\u8BD5\u7B49\u5F85\u88AB\u4E2D\u6B62\uFF1A${options.label}`,
101782
- cause: err
101783
- });
101784
- }
101785
- }
101786
- }
101787
- throw new HttpRetryError({
101788
- message: `\u672A\u77E5\u91CD\u8BD5\u5931\u8D25\uFF08${options.label}\uFF09\uFF1A${lastErr?.message ?? "no error"}`,
101789
- class: "retryable",
101790
- status: lastStatus,
101791
- attempts: policy.maxAttempts,
101792
- cause: lastErr
101793
- });
101794
- }
101795
- var HttpRetryError, AbortRunError, DEFAULT_RETRY_POLICY;
101796
- var init_http_retry = __esm({
101797
- "src/utils/http-retry.ts"() {
101798
- "use strict";
101799
- init_dist();
101800
- HttpRetryError = class extends Error {
101801
- class;
101802
- status;
101803
- attempts;
101804
- cause;
101805
- constructor(opts) {
101806
- super(opts.message);
101807
- this.name = "HttpRetryError";
101808
- this.class = opts.class;
101809
- this.status = opts.status;
101810
- this.attempts = opts.attempts;
101811
- this.cause = opts.cause;
101812
- }
101813
- };
101814
- AbortRunError = class extends HttpRetryError {
101815
- constructor(opts) {
101816
- super({
101817
- message: opts.message,
101818
- class: "abort",
101819
- status: opts.status,
101820
- attempts: 1,
101821
- cause: opts.cause
101822
- });
101823
- this.name = "AbortRunError";
101824
- }
101825
- };
101826
- DEFAULT_RETRY_POLICY = {
101827
- maxAttempts: 3,
101828
- baseMs: 1e3,
101829
- maxBackoffMs: 3e4,
101830
- taskTimeoutMs: 6e4
101831
- };
101886
+ init_daily_metrics_range();
101832
101887
  }
101833
101888
  });
101834
101889
 
@@ -102260,24 +102315,19 @@ async function runBatch(opts) {
102260
102315
  await recordTaskTransition(opts.paths, inner, task, "running", { attempts: 0 });
102261
102316
  const t0 = performance2.now();
102262
102317
  try {
102263
- const result = await fetchWithRetry(
102264
- () => fetcher({
102265
- config: opts.config,
102266
- accountId: task.accountId,
102267
- section: task.section,
102268
- start: opts.start,
102269
- end: opts.end,
102270
- limit: opts.keywordLimit,
102271
- costGreater: opts.campaignGeoCostGreater,
102272
- clickGreater: opts.campaignGeoClickGreater,
102273
- conversionsGreater: opts.campaignGeoConversionsGreater
102274
- }),
102275
- {
102276
- label: `${task.section}/${task.accountId}`,
102277
- policy: opts.retry,
102278
- signal: aborterToken.signal
102279
- }
102280
- );
102318
+ const result = await fetcher({
102319
+ config: opts.config,
102320
+ accountId: task.accountId,
102321
+ section: task.section,
102322
+ start: opts.start,
102323
+ end: opts.end,
102324
+ limit: opts.keywordLimit,
102325
+ costGreater: opts.campaignGeoCostGreater,
102326
+ clickGreater: opts.campaignGeoClickGreater,
102327
+ conversionsGreater: opts.campaignGeoConversionsGreater,
102328
+ retry: opts.retry,
102329
+ signal: aborterToken.signal
102330
+ });
102281
102331
  await writer({
102282
102332
  snapshotDir: path25.join(opts.paths.resultsDir, sanitizeAccountSegment(task.accountId)),
102283
102333
  section: task.section,
@@ -103046,6 +103096,51 @@ async function runWithConcurrency(tasks, concurrency) {
103046
103096
  await Promise.all(workers);
103047
103097
  return results;
103048
103098
  }
103099
+ function sectionFetchErrorMessage(err) {
103100
+ return err instanceof Error ? err.message : String(err);
103101
+ }
103102
+ async function fetchAndWriteSection(def, opts, config, id, cliVersion) {
103103
+ const t0 = performance4.now();
103104
+ try {
103105
+ const { payload } = await fetchOneGoogleSection({
103106
+ config,
103107
+ accountId: id,
103108
+ section: def.name,
103109
+ start: opts.start,
103110
+ end: opts.end,
103111
+ verbose: opts.verbose,
103112
+ limit: opts.limit,
103113
+ noOrderByCost: opts.noOrderByCost,
103114
+ level: opts.level,
103115
+ audienceType: opts.audienceType,
103116
+ costGreater: opts.costGreater,
103117
+ clickGreater: opts.clickGreater,
103118
+ conversionsGreater: opts.conversionsGreater
103119
+ });
103120
+ const summary = await writeGoogleAnalysisSnapshot({
103121
+ snapshotDir: opts.jsonOut,
103122
+ section: def.name,
103123
+ accountId: id,
103124
+ dateRange: manifestDateRange(def, opts.start, opts.end),
103125
+ payload,
103126
+ cliVersion,
103127
+ endpointHint: endpointHintForSection(def)
103128
+ });
103129
+ return {
103130
+ section: def.name,
103131
+ ok: true,
103132
+ elapsedMs: performance4.now() - t0,
103133
+ file: summary.writtenFiles[0]
103134
+ };
103135
+ } catch (err) {
103136
+ return {
103137
+ section: def.name,
103138
+ ok: false,
103139
+ elapsedMs: performance4.now() - t0,
103140
+ error: sectionFetchErrorMessage(err)
103141
+ };
103142
+ }
103143
+ }
103049
103144
  async function runAllSections(opts) {
103050
103145
  const config = loadConfig(opts.token);
103051
103146
  const id = opts.account.split(",")[0].trim();
@@ -103070,35 +103165,20 @@ async function runAllSections(opts) {
103070
103165
  );
103071
103166
  const cliVersion = getCurrentVersion2();
103072
103167
  const overallT0 = performance4.now();
103073
- const tasks = targets.map((def) => async () => {
103074
- const t0 = performance4.now();
103075
- try {
103076
- const payload = await fetchSectionPayload(def, opts, config, id);
103077
- const summary2 = await writeGoogleAnalysisSnapshot({
103078
- snapshotDir: opts.jsonOut,
103079
- section: def.name,
103080
- accountId: id,
103081
- dateRange: manifestDateRange(def, opts.start, opts.end),
103082
- payload,
103083
- cliVersion,
103084
- endpointHint: endpointHintForSection(def)
103085
- });
103086
- return {
103087
- section: def.name,
103088
- ok: true,
103089
- elapsedMs: performance4.now() - t0,
103090
- file: summary2.writtenFiles[0]
103091
- };
103092
- } catch (err) {
103093
- return {
103094
- section: def.name,
103095
- ok: false,
103096
- elapsedMs: performance4.now() - t0,
103097
- error: err instanceof Error ? err.message : String(err)
103098
- };
103168
+ const tasks = targets.map(
103169
+ (def) => () => fetchAndWriteSection(def, opts, config, id, cliVersion)
103170
+ );
103171
+ let results = await runWithConcurrency(tasks, concurrency);
103172
+ const failedIndices = results.map((r, i) => r.ok ? -1 : i).filter((i) => i >= 0);
103173
+ if (failedIndices.length > 0) {
103174
+ console.error(`
103175
+ \u26A0\uFE0F ${failedIndices.length} \u4E2A\u7EF4\u5EA6\u9996\u8F6E\u5931\u8D25\uFF0C\u4E32\u884C\u4E8C\u8F6E\u91CD\u8BD5\u2026
103176
+ `);
103177
+ for (const i of failedIndices) {
103178
+ const retry = await fetchAndWriteSection(targets[i], opts, config, id, cliVersion);
103179
+ if (retry.ok) results = [...results.slice(0, i), retry, ...results.slice(i + 1)];
103099
103180
  }
103100
- });
103101
- const results = await runWithConcurrency(tasks, concurrency);
103181
+ }
103102
103182
  const succeeded = results.filter((r) => r.ok).length;
103103
103183
  const failed = results.length - succeeded;
103104
103184
  const absoluteSnapshotDir = path27.resolve(opts.jsonOut);
@@ -126276,43 +126356,51 @@ async function runGoogleAdsDiagnosisCollect(opts) {
126276
126356
  }
126277
126357
  const snapDir = path22.resolve(opts.jsonOut);
126278
126358
  await fs17.mkdir(snapDir, { recursive: true });
126279
- if (!opts.skipFetch) {
126280
- console.error(`
126359
+ let degraded = false;
126360
+ console.error(`
126281
126361
  \u{1F4E5} google-analysis \u62C9\u6570\uFF1A\u8D26\u6237 ${accountId}\uFF0C${opts.start} ~ ${opts.end}
126282
126362
  `);
126283
- const main = invokeGoogleAnalysisCli({
126363
+ const main = invokeGoogleAnalysisCli({
126364
+ account: accountId,
126365
+ start: opts.start,
126366
+ end: opts.end,
126367
+ jsonOut: snapDir,
126368
+ token: opts.token,
126369
+ verbose: opts.verbose
126370
+ });
126371
+ const mainPartial = !main.ok && main.status === 2;
126372
+ if (!main.ok && !mainPartial) {
126373
+ console.error(main.stderr || main.stdout);
126374
+ console.error("\n\u274C google-analysis \u62C9\u6570\u5931\u8D25\u3002\n");
126375
+ process.exit(main.status || 1);
126376
+ }
126377
+ if (mainPartial) {
126378
+ console.error(main.stderr || main.stdout);
126379
+ console.error(
126380
+ "\n\u26A0\uFE0F google-analysis \u90E8\u5206\u7EF4\u5EA6\u62C9\u53D6\u5931\u8D25\uFF0C\u5C06\u7EE7\u7EED\u62C9\u53D6\u5BF9\u6BD4\u5468\u671F\u5E76\u751F\u6210 collect\u3002\n"
126381
+ );
126382
+ degraded = true;
126383
+ }
126384
+ if (opts.fetchPreviousPeriod !== false) {
126385
+ const prev = previousDateRange2(opts.start, opts.end);
126386
+ console.error(
126387
+ `
126388
+ \u{1F4E5} \u62C9\u53D6\u5BF9\u6BD4\u5468\u671F campaigns/geographic/keywords\uFF1A${prev.start} ~ ${prev.end}
126389
+ `
126390
+ );
126391
+ const prevRun = invokeGoogleAnalysisCli({
126284
126392
  account: accountId,
126285
- start: opts.start,
126286
- end: opts.end,
126393
+ start: prev.start,
126394
+ end: prev.end,
126287
126395
  jsonOut: snapDir,
126396
+ sections: "campaigns,geographic,keywords",
126288
126397
  token: opts.token,
126289
126398
  verbose: opts.verbose
126290
126399
  });
126291
- if (!main.ok) {
126292
- console.error(main.stderr || main.stdout);
126293
- console.error("\n\u274C google-analysis \u62C9\u6570\u5931\u8D25\u3002\n");
126294
- process.exit(main.status || 1);
126295
- }
126296
- if (opts.fetchPreviousPeriod !== false) {
126297
- const prev = previousDateRange2(opts.start, opts.end);
126298
- console.error(
126299
- `
126300
- \u{1F4E5} \u62C9\u53D6\u5BF9\u6BD4\u5468\u671F campaigns/geographic/keywords\uFF1A${prev.start} ~ ${prev.end}
126301
- `
126302
- );
126303
- const prevRun = invokeGoogleAnalysisCli({
126304
- account: accountId,
126305
- start: prev.start,
126306
- end: prev.end,
126307
- jsonOut: snapDir,
126308
- sections: "campaigns,geographic,keywords",
126309
- token: opts.token,
126310
- verbose: opts.verbose
126311
- });
126312
- if (!prevRun.ok) {
126313
- console.error(prevRun.stderr || prevRun.stdout);
126314
- console.error("\n\u26A0\uFE0F \u4E0A\u4E00\u5468\u671F\u5BF9\u6BD4\u7EF4\u5EA6\u62C9\u53D6\u5931\u8D25\uFF0C\u73AF\u6BD4\u5217\u53EF\u80FD\u4E3A\u7A7A\u3002\n");
126315
- }
126400
+ if (!prevRun.ok) {
126401
+ console.error(prevRun.stderr || prevRun.stdout);
126402
+ console.error("\n\u26A0\uFE0F \u4E0A\u4E00\u5468\u671F\u5BF9\u6BD4\u7EF4\u5EA6\u62C9\u53D6\u5931\u8D25\uFF0C\u73AF\u6BD4\u5217\u53EF\u80FD\u4E3A\u7A7A\u3002\n");
126403
+ degraded = true;
126316
126404
  }
126317
126405
  }
126318
126406
  const websiteUrl = await resolveWebsiteUrl(opts, snapDir, accountId);
@@ -126364,6 +126452,7 @@ async function runGoogleAdsDiagnosisCollect(opts) {
126364
126452
  ` \u7EC8\u7A3F\uFF1Asiluzan-tso google-ads-diagnosis render --data ${path22.join(snapDir, GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME)}
126365
126453
  `
126366
126454
  );
126455
+ if (degraded) process.exit(2);
126367
126456
  }
126368
126457
 
126369
126458
  // src/commands/google-ads-diagnosis/render-report.ts
@@ -126707,7 +126796,7 @@ function registerGoogleAdsDiagnosisCommands(program2) {
126707
126796
  ).requiredOption("-a, --account <id>", "Google mediaCustomerId").requiredOption("--start <date>", "\u8BCA\u65AD\u5F00\u59CB\u65E5\u671F YYYY-MM-DD").requiredOption("--end <date>", "\u8BCA\u65AD\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--company-name <name>", "\u8D26\u6237\u540D\u79F0\uFF08\u9ED8\u8BA4\u4ECE list-accounts \u5FEB\u7167\u8BFB\u53D6\uFF09").option("--website <url>", "\u5B98\u7F51 URL\uFF08\u9ED8\u8BA4\u4ECE ads \u5FEB\u7167 finalurl \u8BFB\u53D6\uFF09").option(
126708
126797
  "--json-out <dir>",
126709
126798
  "\u843D\u76D8\u76EE\u5F55\uFF08google-analysis \u5FEB\u7167 + google-ads-diagnosis-collect.json\uFF09"
126710
- ).option("--skip-fetch", "\u4EC5\u6620\u5C04\u76EE\u5F55\u5185\u5DF2\u6709 google-analysis \u5FEB\u7167\uFF0C\u4E0D\u53D1\u8D77 API \u8BF7\u6C42").option("--skip-landing-page", "\u4E0D\u62C9\u53D6 WebsiteDiagnosisReports/performance").option("--no-fetch-previous-period", "\u4E0D\u62C9\u4E0A\u4E00\u5468\u671F campaigns/geographic/keywords \u5BF9\u6BD4\u6570\u636E").option("--token <token>", "JWT").option("--verbose", "\u6253\u5370\u8BF7\u6C42\u8BE6\u60C5").action(
126799
+ ).option("--skip-landing-page", "\u4E0D\u62C9\u53D6 WebsiteDiagnosisReports/performance").option("--no-fetch-previous-period", "\u4E0D\u62C9\u4E0A\u4E00\u5468\u671F campaigns/geographic/keywords \u5BF9\u6BD4\u6570\u636E").option("--token <token>", "JWT").option("--verbose", "\u6253\u5370\u8BF7\u6C42\u8BE6\u60C5").action(
126711
126800
  async (opts) => {
126712
126801
  if (!opts.jsonOut?.trim()) {
126713
126802
  console.error("\n\u274C collect \u987B\u6307\u5B9A --json-out <\u76EE\u5F55>\u3002\n");
@@ -126720,7 +126809,6 @@ function registerGoogleAdsDiagnosisCommands(program2) {
126720
126809
  companyName: opts.companyName,
126721
126810
  website: opts.website,
126722
126811
  jsonOut: opts.jsonOut,
126723
- skipFetch: opts.skipFetch,
126724
126812
  skipLandingPage: opts.skipLandingPage,
126725
126813
  fetchPreviousPeriod: opts.fetchPreviousPeriod,
126726
126814
  token: opts.token,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.34-beta.4",
4
- "publishedAt": 1782984153033
3
+ "version": "1.1.35-beta.1",
4
+ "publishedAt": 1783063300004
5
5
  }
@@ -29,7 +29,7 @@
29
29
  1. 确认统计区间(规则见 conventions §五)。
30
30
  2. `list-accounts -m Google -k <mediaCustomerId> --json-out ./snap-p1`(取 `currencyCode`)。
31
31
  3. `stats -m Google -a <mediaCustomerId> --start <S> --end <D> --json-out ./snap-p1`。
32
- 4. `google-ads-diagnosis collect -a <mediaCustomerId> --start <S> --end <D> --json-out ./snap-p1`(含 `daily-metrics` 按日趋势;产出 `google-ads-diagnosis-collect.json`,**仅事实**)。
32
+ 4. `google-ads-diagnosis collect -a <mediaCustomerId> --start <S> --end <D> --json-out ./snap-p1`(含对比周期 campaigns/geographic/keywords + `daily-metrics` 按日趋势;产出 `google-ads-diagnosis-collect.json`,**仅事实**)。部分维度失败(exit 2)时 collect 仍产出 JSON,**禁止**用 `--skip-fetch` 或脚本绕过 CLI 重映射快照。
33
33
  5. **outline 门禁**(若手写脚本读盘):对**每个** section Read 其 `<section>-<accountId>_*.outline.txt` 确认字段树——字段名以 outline 为准。
34
34
  6. **Agent 撰写**:读 `google-ads-diagnosis-collect.json`(`reportData` + `agentBrief`)+ `google-ads-diagnosis.md`,填写全部 `analysis` / `suggestions` / `diagnosisOverview` / `summary` 等,保存 `google-ads-diagnosis.json`。
35
35
  7. **渲染终稿**:`google-ads-diagnosis render --data ./snap-p1/google-ads-diagnosis.json --out ./snap-p1/google-ads-diagnosis-report.html`(模板与 MarkAI `GoogleAdsDiagnosisReport.html` 一致)。
@@ -30,9 +30,22 @@ siluzan-tso google-ads-diagnosis render \
30
30
 
31
31
  | 子命令 | 说明 |
32
32
  | -------- | -------------------------------------------------------------------- |
33
- | `collect` | 拉 google-analysis + 可选 Lighthouse;输出 `google-ads-diagnosis-collect.json` |
33
+ | `collect` | 拉 google-analysis + 对比周期 campaigns/geographic/keywords + 可选 Lighthouse;输出 `google-ads-diagnosis-collect.json` |
34
34
  | `render` | 读取 Agent 产出的 `google-ads-diagnosis.json`,注入 `GoogleAdsDiagnosisReport.html` |
35
35
 
36
+ **collect 选项**:
37
+
38
+ | 选项 | 说明 |
39
+ | ---- | ---- |
40
+ | `--no-fetch-previous-period` | 不拉上一周期 campaigns/geographic/keywords 对比数据(环比列将为空) |
41
+ | `--skip-landing-page` | 不拉取 WebsiteDiagnosisReports/performance |
42
+
43
+ **collect 行为约束**:
44
+
45
+ - **禁止**使用已移除的 `--skip-fetch`;环比对比数据须由 CLI 向 API 拉取上一周期快照(`campaigns/geographic/keywords` 的 `*_YYYYMMDD-YYYYMMDD.json`),不可仅靠目录内当期快照映射。
46
+ - `google-analysis` **部分维度失败**(exit 2,如 `daily-metrics` HTTP 400)时,collect **仍会继续**拉取对比周期并生成 `google-ads-diagnosis-collect.json`;Agent 可基于 collect 继续撰写,缺失维度在报告中注明。
47
+ - 若环比列全为 `null`,检查落盘目录是否存在上一周期文件(如 `campaigns-<id>_20260503-20260602.json`);不存在则**重新执行 collect**(勿用手动拼装或 Python 脚本绕过 CLI)。
48
+
36
49
  - **render 自动合并(默认开启)**:若同目录存在 `google-ads-diagnosis-collect.json`,且 Agent 改坏了 `campaigns` / `geographic` / `keywords` 的 `items`(缺 `title`/`currentCost` 或环比写成字符串),CLI **自动从 collect.reportData 恢复 items**,保留 Agent 的 `analysis`/`suggestions`。可用 `--no-merge-collect` 关闭;`--collect <file>` 指定 collect 路径。
37
50
  - **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空)。
38
51
 
@@ -30,9 +30,22 @@ siluzan-tso google-ads-diagnosis render \
30
30
 
31
31
  | 子命令 | 说明 |
32
32
  | -------- | -------------------------------------------------------------------- |
33
- | `collect` | 拉 google-analysis + 可选 Lighthouse;输出 `google-ads-diagnosis-collect.json` |
33
+ | `collect` | 拉 google-analysis + 对比周期 campaigns/geographic/keywords + 可选 Lighthouse;输出 `google-ads-diagnosis-collect.json` |
34
34
  | `render` | 读取 Agent 产出的 `google-ads-diagnosis.json`,注入 `GoogleAdsDiagnosisReport.html` |
35
35
 
36
+ **collect 选项**:
37
+
38
+ | 选项 | 说明 |
39
+ | ---- | ---- |
40
+ | `--no-fetch-previous-period` | 不拉上一周期 campaigns/geographic/keywords 对比数据(环比列将为空) |
41
+ | `--skip-landing-page` | 不拉取 WebsiteDiagnosisReports/performance |
42
+
43
+ **collect 行为约束**:
44
+
45
+ - **禁止**使用已移除的 `--skip-fetch`;环比对比数据须由 CLI 向 API 拉取上一周期快照(`campaigns/geographic/keywords` 的 `*_YYYYMMDD-YYYYMMDD.json`),不可仅靠目录内当期快照映射。
46
+ - `google-analysis` **部分维度失败**(exit 2,如 `daily-metrics` HTTP 400)时,collect **仍会继续**拉取对比周期并生成 `google-ads-diagnosis-collect.json`;Agent 可基于 collect 继续撰写,缺失维度在报告中注明。
47
+ - 若环比列全为 `null`,检查落盘目录是否存在上一周期文件(如 `campaigns-<id>_20260503-20260602.json`);不存在则**重新执行 collect**(勿用手动拼装或 Python 脚本绕过 CLI)。
48
+
36
49
  - **render 自动合并(默认开启)**:若同目录存在 `google-ads-diagnosis-collect.json`,且 Agent 改坏了 `campaigns` / `geographic` / `keywords` 的 `items`(缺 `title`/`currentCost` 或环比写成字符串),CLI **自动从 collect.reportData 恢复 items**,保留 Agent 的 `analysis`/`suggestions`。可用 `--no-merge-collect` 关闭;`--collect <file>` 指定 collect 路径。
37
50
  - **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空)。
38
51
 
@@ -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.34-beta.4'
12
+ $PKG_VERSION = '1.1.35-beta.1'
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.34-beta.4"
12
+ readonly PKG_VERSION="1.1.35-beta.1"
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.34-beta.4",
3
+ "version": "1.1.35-beta.1",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",