siluzan-tso-cli 1.1.37-beta.12 → 1.1.37-beta.13

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.37-beta.12),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.37-beta.13),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -4385,6 +4385,40 @@ var init_currency_display = __esm({
4385
4385
  }
4386
4386
  });
4387
4387
 
4388
+ // src/utils/media-customer-id.ts
4389
+ function stripMediaCustomerIdDecorators(raw) {
4390
+ return raw.trim().replace(/[\s-]+/g, "");
4391
+ }
4392
+ function looksLikeNumericMediaCustomerId(raw) {
4393
+ return /^\d+$/.test(stripMediaCustomerIdDecorators(raw));
4394
+ }
4395
+ function normalizeNumericMediaCustomerId(raw) {
4396
+ if (!looksLikeNumericMediaCustomerId(raw)) return null;
4397
+ return stripMediaCustomerIdDecorators(raw);
4398
+ }
4399
+ function parseNumericMediaCustomerIds(input) {
4400
+ if (!input || !input.trim()) return [];
4401
+ const seen = /* @__PURE__ */ new Set();
4402
+ const out = [];
4403
+ for (const raw of input.split(",")) {
4404
+ const piece = raw.trim();
4405
+ if (!piece) continue;
4406
+ const id = normalizeNumericMediaCustomerId(piece);
4407
+ if (!id) {
4408
+ throw new Error(`mediaCustomerId \u5FC5\u987B\u4E3A\u7EAF\u6570\u5B57\uFF08\u53EF\u542B\u8FDE\u5B57\u7B26\uFF09\uFF1A${piece}`);
4409
+ }
4410
+ if (seen.has(id)) continue;
4411
+ seen.add(id);
4412
+ out.push(id);
4413
+ }
4414
+ return out;
4415
+ }
4416
+ var init_media_customer_id = __esm({
4417
+ "src/utils/media-customer-id.ts"() {
4418
+ "use strict";
4419
+ }
4420
+ });
4421
+
4388
4422
  // src/utils/balance.ts
4389
4423
  function fmt(d) {
4390
4424
  return d.toISOString().slice(0, 10);
@@ -103126,20 +103160,7 @@ function parseSections(input) {
103126
103160
  return list;
103127
103161
  }
103128
103162
  function parseAccountIds(input) {
103129
- if (!input || !input.trim()) return [];
103130
- const seen = /* @__PURE__ */ new Set();
103131
- const out = [];
103132
- for (const raw of input.split(",")) {
103133
- const id = raw.trim();
103134
- if (!id) continue;
103135
- if (!/^\d+$/.test(id)) {
103136
- throw new Error(`mediaCustomerId \u5FC5\u987B\u4E3A\u7EAF\u6570\u5B57\uFF1A${id}`);
103137
- }
103138
- if (seen.has(id)) continue;
103139
- seen.add(id);
103140
- out.push(id);
103141
- }
103142
- return out;
103163
+ return parseNumericMediaCustomerIds(input);
103143
103164
  }
103144
103165
  function parseDeadline(s) {
103145
103166
  if (!s) return void 0;
@@ -103644,6 +103665,7 @@ var init_google_analysis_batch = __esm({
103644
103665
  init_google_analysis3();
103645
103666
  init_balance();
103646
103667
  init_version();
103668
+ init_media_customer_id();
103647
103669
  DEFAULT_SECTIONS = ["campaigns", "geographic", "keywords"];
103648
103670
  DEFAULT_ACCOUNT_CONCURRENCY = 4;
103649
103671
  DEFAULT_SECTION_CONCURRENCY = 6;
@@ -103795,23 +103817,15 @@ function finiteOpt(n) {
103795
103817
  return typeof n === "number" && Number.isFinite(n) ? n : void 0;
103796
103818
  }
103797
103819
  function parseAccountIdsForAnalysis(input) {
103798
- const out = [];
103799
- const seen = /* @__PURE__ */ new Set();
103800
- for (const raw of input.split(",")) {
103801
- const id = raw.trim();
103802
- if (!id) continue;
103803
- if (!/^\d+$/.test(id)) {
103804
- console.error(`
103805
- \u274C --account \u987B\u4E3A\u6570\u5B57 mediaCustomerId\uFF08\u4E0D\u5408\u6CD5\uFF1A${id}\uFF09\u3002
103820
+ try {
103821
+ return parseNumericMediaCustomerIds(input);
103822
+ } catch (err) {
103823
+ const message = err instanceof Error ? err.message : String(err);
103824
+ console.error(`
103825
+ \u274C --account \u987B\u4E3A\u6570\u5B57 mediaCustomerId\uFF1A${message}
103806
103826
  `);
103807
- process.exit(1);
103808
- }
103809
- if (!seen.has(id)) {
103810
- seen.add(id);
103811
- out.push(id);
103812
- }
103827
+ process.exit(1);
103813
103828
  }
103814
- return out;
103815
103829
  }
103816
103830
  async function runMultiAccountViaBatch(accountIds, opts) {
103817
103831
  const targets = resolveSectionList(opts.sections, opts.exclude);
@@ -103997,6 +104011,7 @@ var init_register_cli = __esm({
103997
104011
  init_auth();
103998
104012
  init_google_analysis_snapshot();
103999
104013
  init_version();
104014
+ init_media_customer_id();
104000
104015
  init_http_retry();
104001
104016
  init_sections();
104002
104017
  init_fetch();
@@ -105963,6 +105978,7 @@ function wrapListJson(params) {
105963
105978
  }
105964
105979
 
105965
105980
  // src/commands/list-accounts/main.ts
105981
+ init_media_customer_id();
105966
105982
  async function runListAccounts(opts) {
105967
105983
  let config = loadConfig(opts.token);
105968
105984
  if (opts.refreshDp) {
@@ -105993,9 +106009,9 @@ async function runListAccounts(opts) {
105993
106009
  params.set("status", opts.status);
105994
106010
  }
105995
106011
  if (opts.keyword) {
105996
- const isNumeric = /^\d+$/.test(opts.keyword.trim());
105997
- if (isNumeric) {
105998
- params.set(platformCfg.idSearchParam, opts.keyword.trim());
106012
+ const numericId = normalizeNumericMediaCustomerId(opts.keyword);
106013
+ if (numericId) {
106014
+ params.set(platformCfg.idSearchParam, numericId);
105999
106015
  } else {
106000
106016
  params.set("mediaCustomerName", opts.keyword.trim());
106001
106017
  }
@@ -106033,7 +106049,9 @@ async function runListAccounts(opts) {
106033
106049
  }
106034
106050
  } else {
106035
106051
  const params = new URLSearchParams();
106036
- if (opts.keyword) params.set("keyword", opts.keyword);
106052
+ if (opts.keyword) {
106053
+ params.set("keyword", normalizeNumericMediaCustomerId(opts.keyword) ?? opts.keyword.trim());
106054
+ }
106037
106055
  if (opts.status && opts.status !== "all") params.set("status", opts.status);
106038
106056
  params.set("pageNo", String(page));
106039
106057
  params.set("pageSize", String(pageSize));
@@ -106280,6 +106298,7 @@ init_auth();
106280
106298
  init_cli_json_snapshot();
106281
106299
  init_cli_table();
106282
106300
  init_balance();
106301
+ init_media_customer_id();
106283
106302
  var VALID_MEDIA_TYPES2 = ["Google", "TikTok", "Yandex", "MetaAd", "BingV2", "Kwai"];
106284
106303
  async function runBalance(opts) {
106285
106304
  const config = loadConfig(opts.token);
@@ -106316,7 +106335,7 @@ async function runBalance(opts) {
106316
106335
  `);
106317
106336
  process.exit(1);
106318
106337
  }
106319
- const accountIds = opts.accounts.split(",").map((id) => id.trim()).filter(Boolean);
106338
+ const accountIds = opts.accounts.split(",").map((id) => id.trim()).filter(Boolean).map((id) => normalizeNumericMediaCustomerId(id) ?? id);
106320
106339
  if (accountIds.length === 0) {
106321
106340
  console.error("\n\u274C \u8BF7\u901A\u8FC7 --accounts \u63D0\u4F9B\u81F3\u5C11\u4E00\u4E2A\u8D26\u6237 ID\uFF08\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF09\n");
106322
106341
  process.exit(1);
@@ -106874,7 +106893,10 @@ init_cli_json_snapshot();
106874
106893
  init_cli_table();
106875
106894
  init_balance();
106876
106895
  init_media_account_lookup();
106896
+ init_media_customer_id();
106877
106897
  var VALID_MEDIA_TYPES3 = ["Google", "TikTok", "Yandex", "MetaAd", "BingV2", "Kwai"];
106898
+ var MAX_BY_DAY_SPAN = 366;
106899
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
106878
106900
  function defaultDateRange2() {
106879
106901
  const end = /* @__PURE__ */ new Date();
106880
106902
  end.setDate(end.getDate() - 1);
@@ -106883,6 +106905,77 @@ function defaultDateRange2() {
106883
106905
  const fmt2 = (d) => d.toISOString().slice(0, 10);
106884
106906
  return { startDate: fmt2(start), endDate: fmt2(end) };
106885
106907
  }
106908
+ function eachDateInclusive(startDate, endDate) {
106909
+ if (!DATE_RE.test(startDate) || !DATE_RE.test(endDate)) {
106910
+ throw new Error(`\u65E5\u671F\u683C\u5F0F\u987B\u4E3A YYYY-MM-DD\uFF0C\u6536\u5230\uFF1A${startDate} ~ ${endDate}`);
106911
+ }
106912
+ const [ys, ms, ds] = startDate.split("-").map(Number);
106913
+ const [ye, me, de] = endDate.split("-").map(Number);
106914
+ const cur = new Date(Date.UTC(ys, ms - 1, ds));
106915
+ const end = new Date(Date.UTC(ye, me - 1, de));
106916
+ if (Number.isNaN(cur.getTime()) || Number.isNaN(end.getTime())) {
106917
+ throw new Error(`\u65E0\u6548\u65E5\u671F\uFF1A${startDate} ~ ${endDate}`);
106918
+ }
106919
+ if (cur > end) {
106920
+ throw new Error(`\u5F00\u59CB\u65E5\u671F\u4E0D\u80FD\u665A\u4E8E\u7ED3\u675F\u65E5\u671F\uFF1A${startDate} > ${endDate}`);
106921
+ }
106922
+ const out = [];
106923
+ while (cur <= end) {
106924
+ out.push(cur.toISOString().slice(0, 10));
106925
+ cur.setUTCDate(cur.getUTCDate() + 1);
106926
+ }
106927
+ if (out.length > MAX_BY_DAY_SPAN) {
106928
+ throw new Error(
106929
+ `--by-day \u8DE8\u5EA6\u6700\u591A ${MAX_BY_DAY_SPAN} \u5929\uFF08\u542B\u8D77\u6B62\uFF09\uFF0C\u5F53\u524D ${out.length} \u5929\uFF1B\u8BF7\u7F29\u5C0F\u533A\u95F4\u6216\u5206\u6BB5\u62C9\u53D6`
106930
+ );
106931
+ }
106932
+ return out;
106933
+ }
106934
+ async function fetchStatsWindow(media, ids, config, startDate, endDate, verbose) {
106935
+ const isGoogle = media === "Google";
106936
+ const isMetaAd = media === "MetaAd";
106937
+ if (isGoogle) {
106938
+ const params = new URLSearchParams({ startDate, endDate, mediaCustomerIds: ids.join("|") });
106939
+ const url = `${config.apiBaseUrl}/report/media-account/google/account-spend-overview?${params.toString()}`;
106940
+ const raw = await apiFetch2(url, config, {}, verbose);
106941
+ return parseGoogleAccountSpendOverviewRows(raw).map((row) => ({
106942
+ mediaAccountId: row.mediaAccountId,
106943
+ mediaCustomerName: row.mediaCustomerName,
106944
+ spend: row.spend,
106945
+ impressions: row.impressions,
106946
+ clicks: row.clicks,
106947
+ conversions: row.conversions,
106948
+ costPerClick: row.costPerClick,
106949
+ currencyCode: row.currencyCode,
106950
+ remainingAccountBudget: row.remainingAccountBudget,
106951
+ status: row.status
106952
+ }));
106953
+ }
106954
+ if (isMetaAd) {
106955
+ const accountMediaTypes = await lookupMetaMediaAccountTypes(config, ids, verbose);
106956
+ const items2 = [];
106957
+ for (const [route, routeIds] of groupAccountIdsByOverviewRoute(
106958
+ "MetaAd",
106959
+ ids,
106960
+ accountMediaTypes
106961
+ )) {
106962
+ const map2 = await fetchOverviewMap(route, routeIds, config, startDate, endDate, verbose);
106963
+ for (const item of map2.values()) {
106964
+ items2.push(item);
106965
+ }
106966
+ }
106967
+ return items2;
106968
+ }
106969
+ const map = await fetchOverviewMap(
106970
+ media,
106971
+ ids,
106972
+ config,
106973
+ startDate,
106974
+ endDate,
106975
+ verbose
106976
+ );
106977
+ return [...map.values()];
106978
+ }
106886
106979
  async function runStats(opts) {
106887
106980
  const config = loadConfig(opts.token);
106888
106981
  if (!VALID_MEDIA_TYPES3.includes(opts.media)) {
@@ -106909,57 +107002,35 @@ async function runStats(opts) {
106909
107002
  );
106910
107003
  process.exit(1);
106911
107004
  }
106912
- const ids = opts.accounts.split(",").map((id) => id.trim()).filter(Boolean);
106913
- const isGoogle = opts.media === "Google";
106914
- const isMetaAd = opts.media === "MetaAd";
107005
+ const ids = opts.accounts.split(",").map((id) => id.trim()).filter(Boolean).map((id) => normalizeNumericMediaCustomerId(id) ?? id);
106915
107006
  let items2;
106916
107007
  try {
106917
- if (isGoogle) {
106918
- const params = new URLSearchParams({ startDate, endDate, mediaCustomerIds: ids.join("|") });
106919
- const url = `${config.apiBaseUrl}/report/media-account/google/account-spend-overview?${params.toString()}`;
106920
- const raw = await apiFetch2(url, config, {}, opts.verbose);
106921
- items2 = parseGoogleAccountSpendOverviewRows(raw).map((row) => ({
106922
- mediaAccountId: row.mediaAccountId,
106923
- mediaCustomerName: row.mediaCustomerName,
106924
- spend: row.spend,
106925
- impressions: row.impressions,
106926
- clicks: row.clicks,
106927
- conversions: row.conversions,
106928
- costPerClick: row.costPerClick,
106929
- currencyCode: row.currencyCode,
106930
- remainingAccountBudget: row.remainingAccountBudget,
106931
- status: row.status
106932
- }));
106933
- } else if (isMetaAd) {
106934
- const accountMediaTypes = await lookupMetaMediaAccountTypes(config, ids, opts.verbose);
107008
+ if (opts.byDay) {
107009
+ const days = eachDateInclusive(startDate, endDate);
106935
107010
  items2 = [];
106936
- for (const [route, routeIds] of groupAccountIdsByOverviewRoute(
106937
- "MetaAd",
106938
- ids,
106939
- accountMediaTypes
106940
- )) {
106941
- const map = await fetchOverviewMap(
106942
- route,
106943
- routeIds,
107011
+ console.error(
107012
+ `\u23F3 [stats --by-day] ${opts.media} ${ids.length} \u6237 \xD7 ${days.length} \u5929\uFF08${startDate} ~ ${endDate}\uFF09\u2026`
107013
+ );
107014
+ for (let i = 0; i < days.length; i++) {
107015
+ const day = days[i];
107016
+ if (opts.verbose || days.length > 7) {
107017
+ console.error(` \u2192 [${i + 1}/${days.length}] ${day}`);
107018
+ }
107019
+ const dayItems = await fetchStatsWindow(
107020
+ opts.media,
107021
+ ids,
106944
107022
  config,
106945
- startDate,
106946
- endDate,
107023
+ day,
107024
+ day,
106947
107025
  opts.verbose
106948
107026
  );
106949
- for (const item of map.values()) {
106950
- items2.push(item);
107027
+ for (const row of dayItems) {
107028
+ items2.push({ ...row, date: day });
106951
107029
  }
106952
107030
  }
107031
+ console.error(` \u2713 \u5B8C\u6210\uFF1A${items2.length} \u6761\u6309\u65E5\u884C`);
106953
107032
  } else {
106954
- const map = await fetchOverviewMap(
106955
- opts.media,
106956
- ids,
106957
- config,
106958
- startDate,
106959
- endDate,
106960
- opts.verbose
106961
- );
106962
- items2 = [...map.values()];
107033
+ items2 = await fetchStatsWindow(opts.media, ids, config, startDate, endDate, opts.verbose);
106963
107034
  }
106964
107035
  } catch (err) {
106965
107036
  const message = err instanceof Error ? err.message : String(err);
@@ -106978,17 +107049,18 @@ async function runStats(opts) {
106978
107049
  process.exit(1);
106979
107050
  }
106980
107051
  const n = items2.length;
107052
+ const section = opts.byDay ? `stats-${opts.media}-by-day` : `stats-${opts.media}`;
106981
107053
  const statsPayload = wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items: items2 });
106982
107054
  if (await emitCliJsonOrSnapshot(opts, {
106983
- section: `stats-${opts.media}`,
107055
+ section,
106984
107056
  commandLabel: "stats",
106985
- commandHint: opts.media,
107057
+ commandHint: opts.byDay ? `${opts.media}-by-day` : opts.media,
106986
107058
  payload: statsPayload,
106987
- idSuffix: opts.media
107059
+ idSuffix: opts.byDay ? `${opts.media}-by-day` : opts.media
106988
107060
  })) {
106989
107061
  return;
106990
107062
  }
106991
- const rangeLabel = `${startDate} ~ ${endDate}`;
107063
+ const rangeLabel = opts.byDay ? `${startDate} ~ ${endDate}\uFF08\u6309\u65E5\uFF09` : `${startDate} ~ ${endDate}`;
106992
107064
  if (items2.length === 0) {
106993
107065
  console.log(`
106994
107066
  ${opts.media} \u6295\u653E\u6570\u636E\uFF08${rangeLabel}\uFF09\uFF08\u7B2C 1 \u9875\uFF0C\u672C\u9875 0 \u6761\uFF0C\u5171 0 \u6761\uFF09
@@ -107001,7 +107073,16 @@ ${opts.media} \u6295\u653E\u6570\u636E\uFF08${rangeLabel}\uFF09\uFF08\u7B2C 1 \u
107001
107073
  ${opts.media} \u6295\u653E\u6570\u636E\uFF08${rangeLabel}\uFF09\uFF08\u7B2C 1 \u9875\uFF0C\u672C\u9875 ${items2.length} \u6761\uFF0C\u5171 ${items2.length} \u6761\uFF09
107002
107074
  `
107003
107075
  );
107004
- const cols = [
107076
+ const cols = opts.byDay ? [
107077
+ { key: "date", header: "\u65E5\u671F" },
107078
+ { key: "id", header: "\u8D26\u6237ID" },
107079
+ { key: "name", header: "\u8D26\u6237\u540D\u79F0" },
107080
+ { key: "spend", header: "\u6D88\u8017" },
107081
+ { key: "impressions", header: "\u5C55\u793A\u6B21\u6570" },
107082
+ { key: "clicks", header: "\u70B9\u51FB\u6B21\u6570" },
107083
+ { key: "conversions", header: "\u8F6C\u5316\u6B21\u6570" },
107084
+ { key: "cpc", header: "\u70B9\u51FB\u5747\u4EF7" }
107085
+ ] : [
107005
107086
  { key: "id", header: "\u8D26\u6237ID" },
107006
107087
  { key: "name", header: "\u8D26\u6237\u540D\u79F0" },
107007
107088
  { key: "spend", header: "\u6D88\u8017" },
@@ -107019,7 +107100,7 @@ ${opts.media} \u6295\u653E\u6570\u636E\uFF08${rangeLabel}\uFF09\uFF08\u7B2C 1 \u
107019
107100
  totalImpressions += item.impressions ?? 0;
107020
107101
  totalClicks += item.clicks ?? 0;
107021
107102
  totalConversions += item.conversions ?? 0;
107022
- return {
107103
+ const base = {
107023
107104
  id: item.mediaAccountId ?? "",
107024
107105
  name: item.mediaCustomerName ?? "",
107025
107106
  spend: item.spend?.toFixed(2) ?? "0.00",
@@ -107028,27 +107109,45 @@ ${opts.media} \u6295\u653E\u6570\u636E\uFF08${rangeLabel}\uFF09\uFF08\u7B2C 1 \u
107028
107109
  conversions: String(item.conversions ?? 0),
107029
107110
  cpc: item.costPerClick?.toFixed(4) ?? "0.0000"
107030
107111
  };
107112
+ return opts.byDay ? { date: item.date ?? "", ...base } : base;
107031
107113
  });
107032
107114
  if (items2.length > 1) {
107033
107115
  const avgCpc = totalClicks > 0 ? (totalSpend / totalClicks).toFixed(4) : "0.0000";
107034
- rows.push({
107035
- id: "\u5408\u8BA1",
107036
- name: "",
107037
- spend: totalSpend.toFixed(2),
107038
- impressions: String(totalImpressions),
107039
- clicks: String(totalClicks),
107040
- conversions: String(totalConversions),
107041
- cpc: avgCpc
107042
- });
107116
+ rows.push(
107117
+ opts.byDay ? {
107118
+ date: "\u5408\u8BA1",
107119
+ id: "",
107120
+ name: "",
107121
+ spend: totalSpend.toFixed(2),
107122
+ impressions: String(totalImpressions),
107123
+ clicks: String(totalClicks),
107124
+ conversions: String(totalConversions),
107125
+ cpc: avgCpc
107126
+ } : {
107127
+ id: "\u5408\u8BA1",
107128
+ name: "",
107129
+ spend: totalSpend.toFixed(2),
107130
+ impressions: String(totalImpressions),
107131
+ clicks: String(totalClicks),
107132
+ conversions: String(totalConversions),
107133
+ cpc: avgCpc
107134
+ }
107135
+ );
107043
107136
  }
107044
107137
  printCliTable(rows, cols);
107045
107138
  console.log();
107046
107139
  }
107047
107140
  function register11(program2) {
107048
- program2.command("stats").description("\u67E5\u8BE2\u5E7F\u544A\u6D88\u8017\u3001\u70B9\u51FB\u3001\u8F6C\u5316\u7B49\u6295\u653E\u6570\u636E\uFF08\u9ED8\u8BA4\u8FD1 7 \u5929\uFF0C\u4E0D\u542B\u4ECA\u5929\uFF09").requiredOption(
107141
+ program2.command("stats").description(
107142
+ "\u67E5\u8BE2\u5E7F\u544A\u6D88\u8017\u3001\u70B9\u51FB\u3001\u8F6C\u5316\u7B49\u6295\u653E\u6570\u636E\uFF08\u9ED8\u8BA4\u8FD1 7 \u5929\u533A\u95F4\u5408\u8BA1\uFF0C\u4E0D\u542B\u4ECA\u5929\uFF1B\u8981\u6309\u65E5\u660E\u7EC6\u52A0 --by-day\uFF09"
107143
+ ).requiredOption(
107049
107144
  "-m, --media <type>",
107050
107145
  "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Yandex | MetaAd | BingV2 | Kwai"
107051
- ).option("-a, --accounts <ids>", "\u8D26\u6237 ID\uFF0C\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF08\u7559\u7A7A\u5219\u67E5\u5168\u90E8\u8D26\u6237\uFF09").option("--start <date>", "\u5F00\u59CB\u65E5\u671F\uFF0C\u683C\u5F0F YYYY-MM-DD\uFF08\u9ED8\u8BA4 7 \u5929\u524D\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F\uFF0C\u683C\u5F0F YYYY-MM-DD\uFF08\u9ED8\u8BA4\u6628\u5929\uFF09").option("--start-date <date>", "\u540C --start\uFF08\u6587\u6863/Playbook \u517C\u5BB9\u522B\u540D\uFF09").option("--end-date <date>", "\u540C --end\uFF08\u6587\u6863/Playbook \u517C\u5BB9\u522B\u540D\uFF09").option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option(
107146
+ ).option("-a, --accounts <ids>", "\u8D26\u6237 mediaCustomerId\uFF0C\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF08\u5FC5\u586B\uFF09").option("--start <date>", "\u5F00\u59CB\u65E5\u671F\uFF0C\u683C\u5F0F YYYY-MM-DD\uFF08\u9ED8\u8BA4 7 \u5929\u524D\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F\uFF0C\u683C\u5F0F YYYY-MM-DD\uFF08\u9ED8\u8BA4\u6628\u5929\uFF09").option("--start-date <date>", "\u540C --start\uFF08\u6587\u6863/Playbook \u517C\u5BB9\u522B\u540D\uFF09").option("--end-date <date>", "\u540C --end\uFF08\u6587\u6863/Playbook \u517C\u5BB9\u522B\u540D\uFF09").option(
107147
+ "--by-day",
107148
+ "\u6309\u65E5\u660E\u7EC6\uFF1A\u5BF9\u533A\u95F4\u5185\u6BCF\u4E00\u5929\u5206\u522B\u62C9\u6570\uFF0Citems[] \u6BCF\u884C\u5E26 date\uFF1B\u5BFC\u51FA Excel/\u6309\u65E5\u8D8B\u52BF\u5FC5\u52A0\u3002\u9ED8\u8BA4\u4E0D\u52A0\u5219\u53EA\u8FD4\u56DE\u533A\u95F4\u5408\u8BA1\u4E00\u884C",
107149
+ false
107150
+ ).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option(
107052
107151
  "--json-out <path>",
107053
107152
  "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json\uFF1B\u76EE\u5F55\u6A21\u5F0F\u6587\u4EF6\u540D\u4E3A `<section>[-<\u67E5\u8BE2id>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09",
107054
107153
  void 0
@@ -107060,6 +107159,7 @@ function register11(program2) {
107060
107159
  accounts: opts.accounts,
107061
107160
  startDate: opts.start ?? opts.startDate,
107062
107161
  endDate: opts.end ?? opts.endDate,
107162
+ byDay: opts.byDay,
107063
107163
  jsonOut: opts.jsonOut,
107064
107164
  verbose: opts.verbose
107065
107165
  });
@@ -38,6 +38,7 @@
38
38
  | 网站诊断 | `references/report-templates/website-diagnosis-report.md` |
39
39
  | 战略市场分析 | `references/report-templates/market-analysis-report.md` |
40
40
  | Google 周期 Excel | `references/report-templates/google-period-report-excel.md`(P4) |
41
+ | 账户按日 Excel | `references/report-templates/stats-daily-excel.md`(P4-DAILY) |
41
42
  | OKKI 周报 | `references/report-templates/okki-weekly-google-client.md` |
42
43
  | 询盘分析 | `references/report-templates/google-inquiry-analysis.md` |
43
44
 
@@ -98,6 +98,7 @@ siluzan-tso -h # 查看帮助
98
98
  | Google 账户 **周期** / **月度** / **季度** 报告 / 月报(**非** OKKI / **非** 询盘 / **非** 强调健康诊断) | **P4** | `references/report-templates/google-period-report.md` + `references/analytics/account-analytics.md`;要 **Excel** 加 `references/report-templates/google-period-report-excel.md` |
99
99
  | Meta/Facebook 周期或诊断报告 | **P4-FB** | `references/report-templates/meta-period-report.md` + `assets/meta-period-report-rules.md` + `references/analytics/facebook-analysis-guide.md`;要 Excel 加 `references/report-templates/meta-period-report-excel.md` |
100
100
  | TikTok / Bing 周期报告 | **P4** | `references/analytics/account-analytics.md`(tiktok-analysis/bing-analysis)+ 对应 `references/report-templates/*-period-report.md` |
101
+ | 账户级按日 Excel / 按天导出投放数据 | **P4-DAILY** | `references/report-templates/stats-daily-excel.md` |
101
102
  | 多账户 × 多维度批处理 | **P5** | `references/analytics/google-analysis-batch.md` + `references/analytics/account-analytics.md` |
102
103
  | OKKI 周报 | **P6** | `references/report-templates/okki-weekly-google-client.md`(全文) |
103
104
  | Google 询盘分析 | **P7** | `references/report-templates/google-inquiry-analysis.md`(全文)+ `references/analytics/geo-continents.json` |
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.37-beta.12",
4
- "publishedAt": 1784276742609
3
+ "version": "1.1.37-beta.13",
4
+ "publishedAt": 1784533653914
5
5
  }
@@ -85,6 +85,7 @@
85
85
  | ---------------------------------------------------------------- | ------------------------------ |
86
86
  | `google-period-report.md` | Google 账户周期/月度报告(P4) |
87
87
  | `google-period-report-excel.md` | P4 用户要 Excel |
88
+ | `stats-daily-excel.md` | P4-DAILY 按日 Excel |
88
89
  | `meta-period-report.md` / `meta-period-report-excel.md` | Meta 周期(P4-FB) |
89
90
  | `google-ads-diagnosis.md` / `google-account-diagnosis-report.md` | Google 诊断(P1) |
90
91
  | `website-diagnosis-report.md` | 网站诊断(P8) |
@@ -118,11 +118,8 @@ siluzan-tso account-active-bills -m <媒体> --id <entityId> [--json-out ./snap]
118
118
  # 从列表取 entityId
119
119
  siluzan-tso list-accounts -m Google --json-out ./snap
120
120
 
121
- # 查询该账户激活账单(将下方 UUID 换成实际 entityId)
122
- siluzan-tso account-active-bills -m Google --id 18176820-6204-43c2-9a1f-0d0f5e9eb957
123
-
124
- # 原始 JSON,便于脚本解析
125
- siluzan-tso account-active-bills -m Google --id 18176820-6204-43c2-9a1f-0d0f5e9eb957 --json-out ./snap
121
+ siluzan-tso account-active-bills -m Google --id <entityId>
122
+ siluzan-tso account-active-bills -m Google --id <entityId> --json-out ./snap
126
123
  ```
127
124
 
128
125
  > \*\*勿在文档或聊天中粘贴真实 JWT。
@@ -233,24 +230,19 @@ siluzan-tso stats -m <媒体类型> [选项]
233
230
  | `--start <YYYY-MM-DD>` | 开始日期 | 7 天前 |
234
231
  | `--end <YYYY-MM-DD>` | 结束日期 | 昨天 |
235
232
  | `--start-date` / `--end-date` | 与 `--start` / `--end` 同义(CLI 别名,与 SKILL Playbook 一致) | — |
233
+ | `--by-day` | 按日明细(`items[]` 带 `date`);按日 Excel 见 `stats-daily-excel.md` | 关 |
236
234
  | `--json-out` | 输出原始 JSON;**失败时 stdout 仍为 JSON**(`{"ok":false,"error":"..."}`) | — |
237
235
 
238
- **口径(易错)**:返回的 **`spend` = `--start`~`--end` 闭区间合计消耗**,默认近 7 天即约 **7 日之和**,**不是**「一天的消耗」。要日消耗须 `start=end`;要日均用 `spend/天数` 或 `balance-scan` 的 `dailySpend`。**禁止**把默认窗口的 `spend` 当成日消耗去比日预算或写「今天花了 X」。
236
+ **口径(易错)**:默认 **`spend` = 区间合计**(非日消耗)。按日加 `--by-day`;单日可用 `start=end`;日均用 `spend/天数` 或 `balance-scan` 的 `dailySpend`。
239
237
 
240
238
  **示例:**
241
239
 
242
240
  ```bash
243
- # 查询 Google 账户最近 7 天消耗
244
- siluzan-tso stats -m Google -a 6326027735
245
-
246
- # 查询 Google 指定月份消耗
247
- siluzan-tso stats -m Google -a 6326027735 --start 2026-03-01 --end 2026-03-31
248
-
249
- # 查询多个 Bing 账户消耗
250
- siluzan-tso stats -m BingV2 -a 1234567890,9876543210 --start 2026-03-01
251
-
252
- # JSON 输出
253
- siluzan-tso stats -m Google -a 6326027735 --json-out ./snap
241
+ siluzan-tso stats -m Google -a <id>
242
+ siluzan-tso stats -m Google -a <id> --start 2026-03-01 --end 2026-03-31
243
+ siluzan-tso stats -m Yandex -a <id> --start <S> --end <E> --by-day --json-out ./snap/daily.json
244
+ siluzan-tso stats -m BingV2 -a <id1>,<id2> --start 2026-03-01
245
+ siluzan-tso stats -m Google -a <id> --json-out ./snap
254
246
  ```
255
247
 
256
248
  ---
@@ -110,7 +110,7 @@
110
110
  - **数据时效性**:涉及「今天/当天/今日消耗」「实时消耗排行」前,必读 `references/analytics/account-analytics.md` 顶部「数据时效性」表。TikTok / Yandex / BingV2 / Kwai 是 `accountsoverview` 同步昨天数据,**不能查今天**。
111
111
  - **先查账户再操作**:`list-accounts -m [mediaType] -k [mediaCustomerId]`;用户给出的 `mediaCustomerId` 必须 `-k` 核验,无结果则告知用户并停止,**禁止**翻页 grep 自行换 ID(会导致报告错户);拉数、脚本、报告文件名全链路用同一 ID(以 stdout `accountId` 为准)。**Google 额外建议**:`list-accounts` 命中后、拉数前可 `account check-access -a <mediaCustomerId>` 校验 Google 网关访问权限;`no_permission`(403)通常表示账户不在当前丝路赞账号下。
112
112
  - **不猜测账户 ID**:`entityId` ≠ `mediaCustomerId`,两者均来自 `list-accounts`;**禁止**把 `entityId` 传给 `stats -a` / `balance -a`。
113
- - **媒体类型区分大小写**:`Google`、`TikTok`、`MetaAd`、`BingV2`、`Kwai`。
113
+ - **媒体类型区分大小写**:`Google`、`TikTok`、`Yandex`、`MetaAd`、`BingV2`、`Kwai`。
114
114
  - **CLI 输出忠实**:数值与 ID 须与本次落盘 JSON / stdout 一致,不编造示例 ID;`data` 为空时只说「当前返回无记录」并附 JSON 路径。
115
115
  - **破坏性操作必须确认 + `--commit`**:账户解绑/关闭/取消分享、BC/MCC 解绑、删除预警/报告/广告/关键词、发票申请、广告发布等。
116
116
  - **不确定时读文档**:先读对应 references 或用 `-h`,不要猜参数。
@@ -199,6 +199,7 @@
199
199
  | --------------------------- | -------------------------------------------------------------------------------------- | -------------------------------- |
200
200
  | 多账户余额 / 预算不足预警 | `balance-scan -m <媒体> --threshold-days 7`(用 **`dailySpend`** / `remainingDays`) | 逐账户 `balance --accounts ...`;把 `stats` 多日 **`spend` 当「日消耗」** |
201
201
  | 多账户投放画像 | `accounts-digest -m <媒体> [-a ...] --start --end --json-out`(**`spend`=区间合计**) | 逐账户 `stats`;把合计 `spend` 说成「每天花了」 |
202
+ | 单户按日 Excel | `stats … --by-day --json-out`(P4-DAILY) | 默认 `stats` 当按日 |
202
203
  | 多账户 × 多维度 Google 数据 | 全量:`google-analysis-batch run`(省略 `-a`);2~10:`google-analysis -a id1,id2,...` | 外层 for-loop;先 list 再拼 `-a` |
203
204
  | 多系列诊断 | `ad campaigns --json-out` + node 读文件过滤 | 逐系列 `ad campaign-get` |
204
205
  | 单日预算熔断 / 当日花费比日预算 | `ad campaigns --start <当日> --end <当日>`(**start=end**)后再比 `spend` 与 `budget` | 用默认近 7 天窗口的 `spend` 对比日预算 |
@@ -219,7 +220,7 @@
219
220
 
220
221
  先用位数判断类型,不确定再 `list-accounts -m [mediaType] -k [id]`:
221
222
 
222
- - Google: `454xxx5137` 或 `270-xxx-0720`(去掉 `-`)
223
+ - Google: `454xxx5137` 或 `270-xxx-0720`
223
224
  - TikTok: `70083497xxx59820033`
224
225
  - Meta(Facebook): `1716030xxx734076`
225
226
  - Bing: `138xxx763`
@@ -164,6 +164,7 @@
164
164
  | **P9** | **行业 / 市场 / 战略**(**无网址主诉求**) | 市场分析、**行业分析**、**行业分析报告**、市场调查报告、战略市场报告、KA 市场报告、竞品/GTM、「写一份 **XX 行业** 报告」(电商/制造/医疗…) | 客户名 / 行业 / 核心产品 **至少一项**(**不是** URL+诊断) | HTML(`market-analysis render`) |
165
165
  | **P1** | **单个 Google 广告账户** 的投放健康 / 诊断 | Google **广告**诊断、**账户**诊断报告、投放诊断、**账户**健康检查、**账户**体检(**且含 Google 账户 ID**) | 账户 ID + 统计区间 | HTML(`google-ads-diagnosis render`) |
166
166
  | **P4** | **单个媒体广告账户** 某区间的 **周期汇总** | 周期报告、月度报告、季度报告、月报、投放月报、Google 账户报告(**未强调「诊断/健康/网站/网址」**) | 媒体 + 账户 ID + 区间 | HTML(默认);用户要 Excel 见 P4 excel 纲要 |
167
+ | **P4-DAILY** | **账户级按日表**(无多维) | 按天导出、投放数据 Excel(非 Google/Meta 周期报告) | 媒体 + 账户 ID + 区间 | xlsx(`stats --by-day`) |
167
168
  | **P4-FB** | **Meta/Facebook 账户** 周期或诊断 | Facebook 报告、Meta 月报、FB **账户**诊断报告 | Meta 账户 ID + 区间 | HTML(`facebook-analysis render`) |
168
169
  | **P6** | **OKKI 周报**(固定客户模板) | 见 §三魔法短语 | 账户 ID + 区间 | 多 Sheet xlsx |
169
170
  | **P7** | **Google 账户 + 询盘** 效果 | 见 §三魔法短语 | 账户 + 分析月 + 询盘资料 | 8 Sheet xlsx |
@@ -190,7 +191,7 @@
190
191
  | 用户侧重点 | 正确工作流 |
191
192
  | ---------------------------------------------- | -------------------------------- |
192
193
  | 给了 **Google 账户 ID**,要健康/诊断/优化建议 | **P1**(`google-ads-diagnosis`) |
193
- | 给了 **账户 ID**,要某月/某季 **投放数据汇总** | **P4** |
194
+ | 给了 **账户 ID**,要某月/某季 **投放数据汇总** | **P4**(按日表无多维 → **P4-DAILY**) |
194
195
  | 给了 **URL / 网址 / 域名** + 诊断类词(§零) | **P8** |
195
196
  | 只给 **行业名**(无 URL),要行业分析 | **P9** |
196
197
 
@@ -73,6 +73,15 @@
73
73
  - **交付/确认**:按 conventions §七 自检;`accountId` 须与用户给的 `mediaCustomerId` 一致。
74
74
 
75
75
  > 用户用「OKKI 周报」固定话术 → 改走 **P6**,不按默认 8 维追问。
76
+ > 只要账户级按日 Excel(无多维章节)→ **P4-DAILY**。
77
+
78
+ ---
79
+
80
+ ## P4-DAILY · 按日投放 Excel
81
+
82
+ - **触发**:按天导出 / 投放数据做成 Excel(非 Google/Meta 多维报告)。
83
+ - **必读**:`references/report-templates/stats-daily-excel.md`。
84
+ - **步骤**:核验账户 → `stats … --by-day --json-out` → 脚本写 xlsx。
76
85
 
77
86
  ---
78
87
 
@@ -28,7 +28,7 @@
28
28
  - **步骤**:
29
29
  1. 列表/数量:`list-accounts -m <媒体> --page-size 999 --json-out ./snap`,脚本读 `list-accounts-*.json` 的 `total` / `items[]`(**禁止**默认 20 条再翻页)。
30
30
  2. 单户余额:`balance -m <媒体> -a <mediaCustomerId>`。
31
- 3. 单户消耗:`stats -m <媒体> -a <mediaCustomerId> --start <S> --end <D>`(多账户对比走 **P3**)。
31
+ 3. 单户消耗:`stats -m <媒体> -a <mediaCustomerId> --start <S> --end <D>`(按日 Excel → **P4-DAILY**;多账户对比 → **P3**)。
32
32
  4. 激活账单:先取 `entityId` → `account-active-bills -m <媒体> --id <entityId> --json-out ./snap`。
33
33
  - **交付/确认**:多账户余额预警走 **P2**、消耗汇总走 **P3**。
34
34
 
@@ -21,6 +21,7 @@
21
21
  | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
22
  | `google-period-report.md` | **Google 周期分析报告(默认)**:终稿=HTML,四步流程 → `google-analysis render` 出 HTML |
23
23
  | `google-period-report-excel.md` | **仅用户要 Excel 时**(P4 定制 Sheet):先 `*.outline.txt` 后脚本写 xlsx;账户须 `-k` 核验,无 CLI excel 子命令 |
24
+ | `stats-daily-excel.md` | **P4-DAILY**:账户级按日 Excel(`stats --by-day`) |
24
25
  | `google-account-diagnosis-report.md` | Google 账户深度诊断(健康度/转化/结构等) |
25
26
  | `google-ads-diagnosis.md` | Google **广告诊断**完整纲要(HTML 区块、**每日趋势 2 位小数**、**每模块必填分析/建议**) |
26
27
  | `meta-period-report.md` / `.html` | **Meta(Facebook)周期报告(默认)**:四步流程 → `facebook-analysis render` 出 HTML,见 P4-FB |
@@ -129,7 +129,7 @@ siluzan-tso google-ads-diagnosis render \
129
129
 
130
130
  ### 6. `landingPageAnalysis.desktop` / `mobile` 禁止被整体覆盖丢弃
131
131
 
132
- - `collect` 产出的 `landingPageAnalysis` 结构固定为 `{ desktop: {score, firstContentfulPaint, firstMeaningfulPaint, speedIndex}, mobile: {...}, source?, simpleDiagnosis? }`;Agent 撰写终稿时**只能在这个对象上追加** `analysis`/`suggestions`,**禁止**把整个 `landingPageAnalysis` 替换成仅剩 `{analysis, suggestions}`(真实案例:账户 1029225790 报告因此丢失了全部 Lighthouse 测速数字)。
132
+ - `collect` 产出的 `landingPageAnalysis` 结构固定为 `{ desktop: {score, firstContentfulPaint, firstMeaningfulPaint, speedIndex}, mobile: {...}, source?, simpleDiagnosis? }`;Agent 撰写终稿时**只能在这个对象上追加** `analysis`/`suggestions`,**禁止**把整个 `landingPageAnalysis` 替换成仅剩 `{analysis, suggestions}`(否则会丢掉全部 Lighthouse 测速数字)。
133
133
  - `render` 会在同目录存在 `google-ads-diagnosis-collect.json` 时自动从 collect 恢复缺失/损坏的 `desktop`/`mobile`(保留 Agent 写的 `analysis`/`suggestions`),但**不能依赖这个兜底**——终稿应本就保留完整字段。
134
134
  - 若 `source` 为 `simple`/`none`,或 `desktop`/`mobile` 全 0(未测到),`analysis` 须说明数据未获取/已降级,并引用 `simpleDiagnosis`(若有);**禁止**编造评分,**禁止**把「数据缺失」写进 `keyIssues`。
135
135
  - **`analysis` / `suggestions` 只能谈着陆页**(测速、HTTPS、表单、追踪、加载体验)。**禁止**在 §07 写关键词花费/CPA/匹配类型等(那些属于 §08);render 会拦截「只谈关键词、不谈着陆页」的 analysis。
@@ -22,16 +22,9 @@
22
22
 
23
23
  ---
24
24
 
25
- ## 账户 ID 纪律(来自真实踩坑)
25
+ ## 账户 ID
26
26
 
27
- 用户给出的 `mediaCustomerId`(如 `2702960720`)**必须**与后续全流程一致:
28
-
29
- - **必须**:`list-accounts -m Google -k <该 ID>` 单户查询;404 / 空列表 → 告知用户 ID 不存在,**停止**,不得换号继续。
30
- - **禁止**:`list-accounts` 翻页 + `grep` 猜账户;禁止把列表里「看起来像」的另一账户(如 `9322098303`)当作用户要的 ID。
31
- - **禁止**:目录名用用户 ID、但 `google-analysis -a` 或脚本里读文件用另一个 ID。
32
- - **禁止**:`entityId` 与 `mediaCustomerId` 混用(见 `agent-conventions.md`)。
33
-
34
- 核验通过后,在报告首行/封面写:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`,且账户 ID 与 `list-accounts` 输出一致。
27
+ `list-accounts -m Google -k <id>` 核验;全流程用同一 `mediaCustomerId`(见 `agent-conventions.md`)。报告首行:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`。
35
28
 
36
29
  ---
37
30
 
@@ -16,6 +16,7 @@
16
16
  2. **统计区间**:`--start` / `--end`(用户未给齐时按 SKILL 反问;授权默认时可用「上一完整自然周」白名单并写明)。
17
17
  3. **询盘口径**:默认使用区间内 `conversions`(转化次数,来自 `stats` 或 `overview`)。若运营定义为某类转化,须在首段脚注说明(必要时补拉 `google-analysis --sections conversion-actions` 仅作说明)。
18
18
  4. **日期写法**:`2026.4.1`、`2026/4/1` 等先规范为 `YYYY-MM-DD`(如 `2026-04-01`)再传 `--start` / `--end`。
19
+ 5. **交付**:用户要可复制话术 → 优先纯文本;否则默认话术 + Excel。
19
20
 
20
21
  ---
21
22
 
@@ -0,0 +1,10 @@
1
+ # 账户按日投放数据 Excel(stats --by-day)
2
+
3
+ > 账户级**按日**消耗表 → Excel。≠ P4 / P4-FB / P1。无 CLI excel 子命令。
4
+
5
+ ```bash
6
+ siluzan-tso list-accounts -m <媒体> -k <id> --json-out ./snap
7
+ siluzan-tso stats -m <媒体> -a <id> --start <S> --end <E> --by-day --json-out ./snap/daily.json
8
+ ```
9
+
10
+ 脚本读 `items[]`(含 `date`、`spend`、`impressions`、`clicks`、`conversions` 等)写 xlsx。建议 Sheet:每日明细 + 汇总。ID 列文本;CTR/CPA 由脚本派生。
@@ -21,6 +21,7 @@
21
21
  | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
22
  | `google-period-report.md` | **Google 周期分析报告(默认)**:终稿=HTML,四步流程 → `google-analysis render` 出 HTML |
23
23
  | `google-period-report-excel.md` | **仅用户要 Excel 时**(P4 定制 Sheet):先 `*.outline.txt` 后脚本写 xlsx;账户须 `-k` 核验,无 CLI excel 子命令 |
24
+ | `stats-daily-excel.md` | **P4-DAILY**:账户级按日 Excel(`stats --by-day`) |
24
25
  | `google-account-diagnosis-report.md` | Google 账户深度诊断(健康度/转化/结构等) |
25
26
  | `google-ads-diagnosis.md` | Google **广告诊断**完整纲要(HTML 区块、**每日趋势 2 位小数**、**每模块必填分析/建议**) |
26
27
  | `meta-period-report.md` / `.html` | **Meta(Facebook)周期报告(默认)**:四步流程 → `facebook-analysis render` 出 HTML,见 P4-FB |
@@ -129,7 +129,7 @@ siluzan-tso google-ads-diagnosis render \
129
129
 
130
130
  ### 6. `landingPageAnalysis.desktop` / `mobile` 禁止被整体覆盖丢弃
131
131
 
132
- - `collect` 产出的 `landingPageAnalysis` 结构固定为 `{ desktop: {score, firstContentfulPaint, firstMeaningfulPaint, speedIndex}, mobile: {...}, source?, simpleDiagnosis? }`;Agent 撰写终稿时**只能在这个对象上追加** `analysis`/`suggestions`,**禁止**把整个 `landingPageAnalysis` 替换成仅剩 `{analysis, suggestions}`(真实案例:账户 1029225790 报告因此丢失了全部 Lighthouse 测速数字)。
132
+ - `collect` 产出的 `landingPageAnalysis` 结构固定为 `{ desktop: {score, firstContentfulPaint, firstMeaningfulPaint, speedIndex}, mobile: {...}, source?, simpleDiagnosis? }`;Agent 撰写终稿时**只能在这个对象上追加** `analysis`/`suggestions`,**禁止**把整个 `landingPageAnalysis` 替换成仅剩 `{analysis, suggestions}`(否则会丢掉全部 Lighthouse 测速数字)。
133
133
  - `render` 会在同目录存在 `google-ads-diagnosis-collect.json` 时自动从 collect 恢复缺失/损坏的 `desktop`/`mobile`(保留 Agent 写的 `analysis`/`suggestions`),但**不能依赖这个兜底**——终稿应本就保留完整字段。
134
134
  - 若 `source` 为 `simple`/`none`,或 `desktop`/`mobile` 全 0(未测到),`analysis` 须说明数据未获取/已降级,并引用 `simpleDiagnosis`(若有);**禁止**编造评分,**禁止**把「数据缺失」写进 `keyIssues`。
135
135
  - **`analysis` / `suggestions` 只能谈着陆页**(测速、HTTPS、表单、追踪、加载体验)。**禁止**在 §07 写关键词花费/CPA/匹配类型等(那些属于 §08);render 会拦截「只谈关键词、不谈着陆页」的 analysis。
@@ -22,16 +22,9 @@
22
22
 
23
23
  ---
24
24
 
25
- ## 账户 ID 纪律(来自真实踩坑)
25
+ ## 账户 ID
26
26
 
27
- 用户给出的 `mediaCustomerId`(如 `2702960720`)**必须**与后续全流程一致:
28
-
29
- - **必须**:`list-accounts -m Google -k <该 ID>` 单户查询;404 / 空列表 → 告知用户 ID 不存在,**停止**,不得换号继续。
30
- - **禁止**:`list-accounts` 翻页 + `grep` 猜账户;禁止把列表里「看起来像」的另一账户(如 `9322098303`)当作用户要的 ID。
31
- - **禁止**:目录名用用户 ID、但 `google-analysis -a` 或脚本里读文件用另一个 ID。
32
- - **禁止**:`entityId` 与 `mediaCustomerId` 混用(见 `agent-conventions.md`)。
33
-
34
- 核验通过后,在报告首行/封面写:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`,且账户 ID 与 `list-accounts` 输出一致。
27
+ `list-accounts -m Google -k <id>` 核验;全流程用同一 `mediaCustomerId`(见 `agent-conventions.md`)。报告首行:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`。
35
28
 
36
29
  ---
37
30
 
@@ -16,6 +16,7 @@
16
16
  2. **统计区间**:`--start` / `--end`(用户未给齐时按 SKILL 反问;授权默认时可用「上一完整自然周」白名单并写明)。
17
17
  3. **询盘口径**:默认使用区间内 `conversions`(转化次数,来自 `stats` 或 `overview`)。若运营定义为某类转化,须在首段脚注说明(必要时补拉 `google-analysis --sections conversion-actions` 仅作说明)。
18
18
  4. **日期写法**:`2026.4.1`、`2026/4/1` 等先规范为 `YYYY-MM-DD`(如 `2026-04-01`)再传 `--start` / `--end`。
19
+ 5. **交付**:用户要可复制话术 → 优先纯文本;否则默认话术 + Excel。
19
20
 
20
21
  ---
21
22
 
@@ -0,0 +1,10 @@
1
+ # 账户按日投放数据 Excel(stats --by-day)
2
+
3
+ > 账户级**按日**消耗表 → Excel。≠ P4 / P4-FB / P1。无 CLI excel 子命令。
4
+
5
+ ```bash
6
+ siluzan-tso list-accounts -m <媒体> -k <id> --json-out ./snap
7
+ siluzan-tso stats -m <媒体> -a <id> --start <S> --end <E> --by-day --json-out ./snap/daily.json
8
+ ```
9
+
10
+ 脚本读 `items[]`(含 `date`、`spend`、`impressions`、`clicks`、`conversions` 等)写 xlsx。建议 Sheet:每日明细 + 汇总。ID 列文本;CTR/CPA 由脚本派生。
@@ -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.37-beta.12'
12
+ $PKG_VERSION = '1.1.37-beta.13'
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.37-beta.12"
12
+ readonly PKG_VERSION="1.1.37-beta.13"
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.37-beta.12",
3
+ "version": "1.1.37-beta.13",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",