siluzan-tso-cli 1.1.13 → 1.1.14-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,7 @@ HTML 报告模板引用以下 CDN:`cdn.tailwindcss.com`、`cdnjs.cloudflare.co
45
45
  在**用户的目标项目根目录**执行(根据用户使用的助手选择 `--ai`):
46
46
 
47
47
  ```bash
48
- npm install -g siluzan-tso-cli
48
+ npm install -g siluzan-tso-cli@beta
49
49
  siluzan-tso init --ai cursor # 写入 Cursor(默认)
50
50
  siluzan-tso init --ai cursor,claude # 同时写入多个平台
51
51
  siluzan-tso init --ai all # 写入所有支持的平台
@@ -53,6 +53,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
53
53
  siluzan-tso init --force # 强制覆盖已存在文件
54
54
  ```
55
55
 
56
+ > **注意**:当前为测试版(1.1.14-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
56
57
 
57
58
  | 助手 | 建议 `--ai` |
58
59
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -1962,7 +1962,7 @@ import { fileURLToPath as fileURLToPath4 } from "url";
1962
1962
  import { Command } from "commander";
1963
1963
 
1964
1964
  // src/config/defaults.ts
1965
- var DEFAULT_API_BASE = "https://tso-api.siluzan.com";
1965
+ var DEFAULT_API_BASE = "https://tso-api-ci.siluzan.com";
1966
1966
 
1967
1967
  // ../common/dist/index.js
1968
1968
  import * as fs from "fs";
@@ -2441,19 +2441,19 @@ import * as fs3 from "fs/promises";
2441
2441
  import * as path3 from "path";
2442
2442
  async function getSkillFiles(skillDir) {
2443
2443
  const out = {};
2444
- async function walk(dir, prefix) {
2444
+ async function walk2(dir, prefix) {
2445
2445
  const entries = await fs3.readdir(dir, { withFileTypes: true });
2446
2446
  for (const ent of entries) {
2447
2447
  const rel = prefix ? `${prefix}/${ent.name}` : ent.name;
2448
2448
  const full = path3.join(dir, ent.name);
2449
2449
  if (ent.isDirectory()) {
2450
- await walk(full, rel);
2450
+ await walk2(full, rel);
2451
2451
  } else {
2452
2452
  out[rel] = await fs3.readFile(full, "utf8");
2453
2453
  }
2454
2454
  }
2455
2455
  }
2456
- await walk(skillDir, "");
2456
+ await walk2(skillDir, "");
2457
2457
  return out;
2458
2458
  }
2459
2459
 
@@ -3676,18 +3676,25 @@ var VALID_MEDIA_TYPES2 = ["Google", "TikTok", "Yandex", "MetaAd", "BingV2", "Kwa
3676
3676
  async function runBalance(opts) {
3677
3677
  const config = loadConfig(opts.token);
3678
3678
  if (!VALID_MEDIA_TYPES2.includes(opts.media)) {
3679
- console.error(
3680
- `
3681
- \u274C \u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B\uFF1A${opts.media}
3682
- \u53EF\u9009\u503C\uFF1A${VALID_MEDIA_TYPES2.join(" | ")}
3683
- `
3684
- );
3679
+ const msg = `\u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B\uFF1A${opts.media}\uFF08\u53EF\u9009\uFF1A${VALID_MEDIA_TYPES2.join(" | ")}\uFF09`;
3680
+ if (opts.json) {
3681
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
3682
+ process.exit(1);
3683
+ }
3684
+ console.error(`
3685
+ \u274C ${msg}
3686
+ `);
3685
3687
  process.exit(1);
3686
3688
  }
3687
3689
  const media = opts.media;
3688
3690
  if (!BALANCE_SUPPORTED_MEDIA.includes(media)) {
3691
+ const msg = `${media} \u6682\u4E0D\u652F\u6301\u4F59\u989D\u67E5\u8BE2`;
3692
+ if (opts.json) {
3693
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
3694
+ process.exit(1);
3695
+ }
3689
3696
  console.error(`
3690
- \u26A0\uFE0F ${media} \u6682\u4E0D\u652F\u6301\u4F59\u989D\u67E5\u8BE2
3697
+ \u26A0\uFE0F ${msg}
3691
3698
  `);
3692
3699
  process.exit(1);
3693
3700
  }
@@ -3707,8 +3714,13 @@ async function runBalance(opts) {
3707
3714
  opts.verbose
3708
3715
  );
3709
3716
  } catch (err) {
3717
+ const message = err instanceof Error ? err.message : String(err);
3718
+ if (opts.json) {
3719
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
3720
+ process.exit(1);
3721
+ }
3710
3722
  console.error(`
3711
- \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
3723
+ \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${message}
3712
3724
  `);
3713
3725
  process.exit(1);
3714
3726
  }
@@ -3948,18 +3960,54 @@ async function runBalanceScan(opts) {
3948
3960
  const CHUNK = 100;
3949
3961
  const chunks = [];
3950
3962
  for (let i = 0; i < validIds.length; i += CHUNK) chunks.push(validIds.slice(i, i + CHUNK));
3963
+ process.stderr.write(
3964
+ `\u23F3 [balance-scan] \u6709\u6548\u8D26\u6237 ${validIds.length} \u4E2A\uFF0C\u5206 ${chunks.length} \u6279\uFF1B\u4F59\u989D\u4E0E\u8FD1 7 \u65E5\u6D88\u8017\u5E76\u884C\u8BF7\u6C42\uFF08\u5355\u8BF7\u6C42\u6700\u957F\u7EA6 10 \u5206\u949F\uFF09\u3002
3965
+ `
3966
+ );
3967
+ const logBalanceChunk = (idx, ids, m) => {
3968
+ process.stderr.write(
3969
+ ` \u2713 [\u4F59\u989D] \u7B2C ${idx + 1}/${chunks.length} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${m.size} \u6761\uFF09
3970
+ `
3971
+ );
3972
+ };
3973
+ const logOverviewChunk = (idx, ids, m) => {
3974
+ process.stderr.write(
3975
+ ` \u2713 [\u8FD17\u65E5\u6D88\u8017] \u7B2C ${idx + 1}/${chunks.length} \u6279\u5B8C\u6210\uFF08${ids.length} \u6237 \u2192 ${m.size} \u6761\uFF09
3976
+ `
3977
+ );
3978
+ };
3951
3979
  const [bMaps, oMaps] = await Promise.all([
3952
3980
  Promise.all(
3953
- chunks.map(
3954
- (ids) => fetchBalanceMap(media, ids, config, void 0, void 0, opts.verbose)
3955
- )
3981
+ chunks.map((ids, chunkIdx) => {
3982
+ process.stderr.write(
3983
+ ` \u2192 [\u4F59\u989D] \u7B2C ${chunkIdx + 1}/${chunks.length} \u6279\u8BF7\u6C42\u4E2D\uFF08${ids.length} \u6237\uFF09\u2026
3984
+ `
3985
+ );
3986
+ return fetchBalanceMap(media, ids, config, void 0, void 0, opts.verbose).then(
3987
+ (m) => {
3988
+ logBalanceChunk(chunkIdx, ids, m);
3989
+ return m;
3990
+ }
3991
+ );
3992
+ })
3956
3993
  ),
3957
3994
  Promise.all(
3958
- chunks.map(
3959
- (ids) => fetchOverviewMap(media, ids, config, void 0, void 0, opts.verbose)
3960
- )
3995
+ chunks.map((ids, chunkIdx) => {
3996
+ process.stderr.write(
3997
+ ` \u2192 [\u8FD17\u65E5\u6D88\u8017] \u7B2C ${chunkIdx + 1}/${chunks.length} \u6279\u8BF7\u6C42\u4E2D\uFF08${ids.length} \u6237\uFF09\u2026
3998
+ `
3999
+ );
4000
+ return fetchOverviewMap(media, ids, config, void 0, void 0, opts.verbose).then(
4001
+ (m) => {
4002
+ logOverviewChunk(chunkIdx, ids, m);
4003
+ return m;
4004
+ }
4005
+ );
4006
+ })
3961
4007
  )
3962
4008
  ]);
4009
+ process.stderr.write(`\u23F3 [balance-scan] \u4F59\u989D\u4E0E\u6D88\u8017\u5DF2\u9F50\uFF0C\u6B63\u5728\u6309\u9608\u503C\u7B5B\u9009\u2026
4010
+ `);
3963
4011
  for (const m of bMaps) for (const [k, v] of m) balanceMap.set(k, v);
3964
4012
  for (const m of oMaps) for (const [k, v] of m) overviewMap.set(k, v);
3965
4013
  }
@@ -4382,8 +4430,13 @@ async function runStats(opts) {
4382
4430
  try {
4383
4431
  raw = await apiFetch2(url, config, {}, opts.verbose);
4384
4432
  } catch (err) {
4433
+ const message = err instanceof Error ? err.message : String(err);
4434
+ if (opts.json) {
4435
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
4436
+ process.exit(1);
4437
+ }
4385
4438
  console.error(`
4386
- \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
4439
+ \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${message}
4387
4440
  `);
4388
4441
  process.exit(1);
4389
4442
  }
@@ -5070,6 +5123,37 @@ async function runReportPushReceiveEmails(opts) {
5070
5123
  console.log();
5071
5124
  }
5072
5125
 
5126
+ // src/utils/strip-legacy-google-fields.ts
5127
+ var LEGACY_WHEN_V2_PRESENT = [
5128
+ ["status", "statusV2"],
5129
+ ["channelType", "channelTypeV2"],
5130
+ ["subChannelType", "subChannelTypeV2"],
5131
+ ["biddingStrategyType", "biddingStrategyTypeV2"],
5132
+ ["campaignStatus", "campaignStatusV2"],
5133
+ ["adGroupStatus", "adGroupStatusV2"],
5134
+ ["matchType", "matchTypeV2"],
5135
+ ["type", "typeV2"]
5136
+ ];
5137
+ function stripLegacyGoogleFieldsIfV2Present(value) {
5138
+ return walk(value);
5139
+ }
5140
+ function walk(obj) {
5141
+ if (Array.isArray(obj)) return obj.map(walk);
5142
+ if (obj !== null && typeof obj === "object") {
5143
+ const o = { ...obj };
5144
+ for (const [legacy, modern] of LEGACY_WHEN_V2_PRESENT) {
5145
+ if (modern in o && legacy in o) {
5146
+ delete o[legacy];
5147
+ }
5148
+ }
5149
+ for (const k of Object.keys(o)) {
5150
+ o[k] = walk(o[k]);
5151
+ }
5152
+ return o;
5153
+ }
5154
+ return obj;
5155
+ }
5156
+
5073
5157
  // src/commands/google-analysis.ts
5074
5158
  var SECTIONS = [
5075
5159
  {
@@ -5292,7 +5376,7 @@ async function runOneSection(def, opts) {
5292
5376
  ]);
5293
5377
  const merged = { images, videos };
5294
5378
  if (opts.json) {
5295
- console.log(JSON.stringify(merged, null, 2));
5379
+ console.log(JSON.stringify(stripLegacyGoogleFieldsIfV2Present(merged), null, 2));
5296
5380
  return;
5297
5381
  }
5298
5382
  const iLen = Array.isArray(images) ? images.length : 0;
@@ -5307,7 +5391,7 @@ async function runOneSection(def, opts) {
5307
5391
  }
5308
5392
  const data = await fetchJson(config, fullPath, !!opts.verbose);
5309
5393
  if (opts.json) {
5310
- console.log(JSON.stringify(data, null, 2));
5394
+ console.log(JSON.stringify(stripLegacyGoogleFieldsIfV2Present(data), null, 2));
5311
5395
  return;
5312
5396
  }
5313
5397
  console.log(
@@ -6806,6 +6890,39 @@ function requireGoogleApi(config) {
6806
6890
  }
6807
6891
  return config.googleApiUrl;
6808
6892
  }
6893
+ function formatGoogleCampaignListStatus(row) {
6894
+ let result = "-";
6895
+ const raw = row.statusV2;
6896
+ if (raw == null || String(raw).trim() === "") return result;
6897
+ const statusV2 = String(raw).toUpperCase();
6898
+ const start = parseCampaignTimeMs(row.startTime);
6899
+ const end = parseCampaignTimeMs(row.endTime);
6900
+ if (start == null || end == null) return result;
6901
+ const now = Date.now();
6902
+ if (statusV2 === "PAUSED") {
6903
+ if (now > start && now < end) {
6904
+ result = "\u5DF2\u6682\u505C";
6905
+ } else if (now > end) {
6906
+ result = "\u5DF2\u7ED3\u675F\u4F7F\u7528";
6907
+ } else if (now < start) {
6908
+ result = "\u672A\u6295\u653E";
6909
+ }
6910
+ } else if (statusV2 === "ENABLED") {
6911
+ if (now > start && now < end) {
6912
+ result = "\u6709\u6548";
6913
+ } else if (now > end) {
6914
+ result = "\u5DF2\u7ED3\u675F\u4F7F\u7528";
6915
+ } else if (now < start) {
6916
+ result = "\u672A\u6295\u653E";
6917
+ }
6918
+ }
6919
+ return result;
6920
+ }
6921
+ function parseCampaignTimeMs(v) {
6922
+ if (v == null || v === "") return null;
6923
+ const t = new Date(v).getTime();
6924
+ return Number.isFinite(t) ? t : null;
6925
+ }
6809
6926
  async function runAdCampaigns(opts) {
6810
6927
  const config = loadConfig(opts.token);
6811
6928
  const googleApiUrl = requireGoogleApi(config);
@@ -6828,18 +6945,19 @@ async function runAdCampaigns(opts) {
6828
6945
  return {
6829
6946
  ...item,
6830
6947
  budgetDisplay,
6831
- budgetUnit: "display"
6948
+ budgetUnit: "display",
6949
+ statusDisplay: formatGoogleCampaignListStatus(item)
6832
6950
  };
6833
6951
  });
6834
6952
  const n = items.length;
6835
6953
  if (opts.json) {
6836
6954
  console.log(
6837
6955
  JSON.stringify(
6838
- {
6956
+ stripLegacyGoogleFieldsIfV2Present({
6839
6957
  ...wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items }),
6840
6958
  code: data.code ?? null,
6841
6959
  message: data.message ?? null
6842
- },
6960
+ }),
6843
6961
  null,
6844
6962
  2
6845
6963
  )
@@ -6874,7 +6992,7 @@ async function runAdCampaigns(opts) {
6874
6992
  const budget = item.budgetDisplay != null ? item.budgetDisplay.toFixed(2) : "\u2014";
6875
6993
  return {
6876
6994
  name: (item.name ?? "").slice(0, nameW),
6877
- status: item.statusV2 ?? "",
6995
+ status: item.statusDisplay ?? formatGoogleCampaignListStatus(item),
6878
6996
  channelType: item.channelTypeV2 ?? "",
6879
6997
  bidding: String(item.biddingStrategyTypeV2 ?? ""),
6880
6998
  budget,
@@ -6917,10 +7035,10 @@ async function runAdGroups(opts) {
6917
7035
  if (opts.json) {
6918
7036
  console.log(
6919
7037
  JSON.stringify(
6920
- {
7038
+ stripLegacyGoogleFieldsIfV2Present({
6921
7039
  ...wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items }),
6922
7040
  code: data.code ?? null
6923
- },
7041
+ }),
6924
7042
  null,
6925
7043
  2
6926
7044
  )
@@ -6990,7 +7108,13 @@ async function runAdList(opts) {
6990
7108
  const n = items.length;
6991
7109
  if (opts.json) {
6992
7110
  console.log(
6993
- JSON.stringify(wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items }), null, 2)
7111
+ JSON.stringify(
7112
+ stripLegacyGoogleFieldsIfV2Present(
7113
+ wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
7114
+ ),
7115
+ null,
7116
+ 2
7117
+ )
6994
7118
  );
6995
7119
  return;
6996
7120
  }
@@ -7027,7 +7151,7 @@ async function runAdList(opts) {
7027
7151
  campaign: String(item["campaign"] ?? "").slice(0, campW),
7028
7152
  adGroup: String(item["adGroup"] ?? "").slice(0, grpW),
7029
7153
  status: String(item["statusV2"]),
7030
- type: String(item["typeV2"] ?? item["type"] ?? ""),
7154
+ type: String(item["typeV2"] ?? ""),
7031
7155
  impressions: String(item["impressions"] ?? 0),
7032
7156
  clicks: String(item["clicks"] ?? 0),
7033
7157
  ctr,
@@ -7066,7 +7190,13 @@ async function runAdKeywords(opts) {
7066
7190
  const n = items.length;
7067
7191
  if (opts.json) {
7068
7192
  console.log(
7069
- JSON.stringify(wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items }), null, 2)
7193
+ JSON.stringify(
7194
+ stripLegacyGoogleFieldsIfV2Present(
7195
+ wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
7196
+ ),
7197
+ null,
7198
+ 2
7199
+ )
7070
7200
  );
7071
7201
  return;
7072
7202
  }
@@ -7082,7 +7212,7 @@ ${label}\uFF08\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672C
7082
7212
  if (opts.negative) {
7083
7213
  items.forEach((item) => {
7084
7214
  const kwText = Array.isArray(item["keywordText"]) ? item["keywordText"].join(", ") : String(item["text"] ?? item["keywordText"] ?? item["id"] ?? "\u2014");
7085
- const matchType = item["matchTypeV2"] ?? item["matchType"] ?? "\u2014";
7215
+ const matchType = item["matchTypeV2"] ?? "\u2014";
7086
7216
  console.log(` [${matchType}] ${kwText} id: ${String(item["id"] ?? "")}`);
7087
7217
  });
7088
7218
  } else {
@@ -7113,8 +7243,8 @@ ${label}\uFF08\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672C
7113
7243
  const kwText = String(item["text"] ?? item["keywordText"] ?? "\u2014");
7114
7244
  const campaign = String(item["campaignName"] ?? item["campaign"] ?? "");
7115
7245
  const adGroup = String(item["adGroupName"] ?? item["adGroup"] ?? "");
7116
- const status = String(item["status"] ?? item["userStatus"] ?? "");
7117
- const matchType = String(item["matchTypeV2"] ?? item["matchType"] ?? "");
7246
+ const status = String(item["userStatus"] ?? "");
7247
+ const matchType = String(item["matchTypeV2"] ?? "");
7118
7248
  const ctr = item["ctr"] != null ? (Number(item["ctr"]) * 100).toFixed(2) + "%" : "\u2014";
7119
7249
  const spend = item["spend"] != null ? Number(item["spend"]).toFixed(2) : "\u2014";
7120
7250
  return {
@@ -8145,7 +8275,13 @@ async function runAdExtensionList(opts) {
8145
8275
  const n = items.length;
8146
8276
  if (opts.json) {
8147
8277
  console.log(
8148
- JSON.stringify(wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items }), null, 2)
8278
+ JSON.stringify(
8279
+ stripLegacyGoogleFieldsIfV2Present(
8280
+ wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
8281
+ ),
8282
+ null,
8283
+ 2
8284
+ )
8149
8285
  );
8150
8286
  return;
8151
8287
  }
@@ -8278,7 +8414,13 @@ async function runAdSearchTerms(opts) {
8278
8414
  const n = items.length;
8279
8415
  if (opts.json) {
8280
8416
  console.log(
8281
- JSON.stringify(wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items }), null, 2)
8417
+ JSON.stringify(
8418
+ stripLegacyGoogleFieldsIfV2Present(
8419
+ wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
8420
+ ),
8421
+ null,
8422
+ 2
8423
+ )
8282
8424
  );
8283
8425
  return;
8284
8426
  }
@@ -8321,7 +8463,7 @@ async function runAdSearchTerms(opts) {
8321
8463
  term: term.slice(0, termW),
8322
8464
  campaign: String(item["campaignName"] ?? "").slice(0, campW),
8323
8465
  adGroup: String(item["adGroupName"] ?? "").slice(0, grpW),
8324
- matchType: String(item["matchType"] ?? ""),
8466
+ matchType: String(item["matchTypeV2"] ?? ""),
8325
8467
  impressions: String(item["impressions"] ?? 0),
8326
8468
  clicks: String(item["clicks"] ?? 0),
8327
8469
  ctr,
@@ -8355,7 +8497,7 @@ async function runAdGeoSearch(opts) {
8355
8497
  for (const item of items) {
8356
8498
  const id = String(item["id"] ?? "");
8357
8499
  const name = String(item["locationName"] ?? item["canonicalName"] ?? item["name"] ?? "");
8358
- const type = String(item["targetType"] ?? item["typeV2"] ?? item["type"] ?? "");
8500
+ const type = String(item["targetType"] ?? item["typeV2"] ?? "");
8359
8501
  console.log(` id:${id} ${name} [${type}]`);
8360
8502
  }
8361
8503
  console.log();
@@ -8391,7 +8533,13 @@ async function runAdGeoList(opts) {
8391
8533
  const n = items.length;
8392
8534
  if (opts.json) {
8393
8535
  console.log(
8394
- JSON.stringify(wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items }), null, 2)
8536
+ JSON.stringify(
8537
+ stripLegacyGoogleFieldsIfV2Present(
8538
+ wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
8539
+ ),
8540
+ null,
8541
+ 2
8542
+ )
8395
8543
  );
8396
8544
  return;
8397
8545
  }
@@ -9415,8 +9563,13 @@ async function fetchTikTokClues(opts, config) {
9415
9563
  );
9416
9564
  leads = Array.isArray(res) ? res : res.data ?? [];
9417
9565
  } catch (err) {
9566
+ const message = err instanceof Error ? err.message : String(err);
9567
+ if (opts.json) {
9568
+ console.log(JSON.stringify({ ok: false, error: message, items: [] }, null, 2));
9569
+ process.exit(1);
9570
+ }
9418
9571
  console.error(`
9419
- \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
9572
+ \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${message}
9420
9573
  `);
9421
9574
  process.exit(1);
9422
9575
  }
@@ -9463,8 +9616,13 @@ async function fetchMetaClues(opts, config) {
9463
9616
  try {
9464
9617
  raw = await apiFetch2(url, config, {}, opts.verbose);
9465
9618
  } catch (err) {
9619
+ const message = err instanceof Error ? err.message : String(err);
9620
+ if (opts.json) {
9621
+ console.log(JSON.stringify({ ok: false, error: message, items: [] }, null, 2));
9622
+ process.exit(1);
9623
+ }
9466
9624
  console.error(`
9467
- \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
9625
+ \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${message}
9468
9626
  `);
9469
9627
  process.exit(1);
9470
9628
  }
@@ -12121,14 +12279,14 @@ program.command("accounts-digest").description(
12121
12279
  program.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(
12122
12280
  "-m, --media <type>",
12123
12281
  "\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Yandex | MetaAd | BingV2 | Kwai"
12124
- ).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("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA\u539F\u59CB\u54CD\u5E94", false).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
12282
+ ).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("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA\u539F\u59CB\u54CD\u5E94", false).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
12125
12283
  async (opts) => {
12126
12284
  await runStats({
12127
12285
  token: opts.token,
12128
12286
  media: opts.media,
12129
12287
  accounts: opts.accounts,
12130
- startDate: opts.start,
12131
- endDate: opts.end,
12288
+ startDate: opts.start ?? opts.startDate,
12289
+ endDate: opts.end ?? opts.endDate,
12132
12290
  json: opts.json,
12133
12291
  verbose: opts.verbose
12134
12292
  });
@@ -75,6 +75,8 @@ allowed-tools: Bash(siluzan-tso:*) Read
75
75
  - 这种报告你无法用它来做数据分析除非用户明确要求(Siluzan平台的优化报告)
76
76
  - (推荐,默认生成这种报告)由你主动拉取数据,并按照skill给出的格式,输出给用户:详情请查看(`references/account-analytics.md`)
77
77
 
78
+ **写报告前必读(账户 vs 系列)**:`stats` / `balance` / `list-accounts` 里的账户 `status` 只表示**广告账户**是否可用,**不能**当作**广告系列**是否启用;系列是否投放须用 `ad campaigns`(及 CLI 派生的 `statusDisplay` 等)。详见 `references/account-analytics.md`「账户状态 ≠ 广告系列状态」。
79
+
78
80
  ### 广告账户相关
79
81
 
80
82
  - 广告账户开户请阅读: `references/open-account-by-media.md`
@@ -121,9 +123,12 @@ allowed-tools: Bash(siluzan-tso:*) Read
121
123
 
122
124
  ### 硬规范
123
125
 
126
+ - **出报告时账户状态 ≠ 广告系列状态**:`stats` / `balance` / `list-accounts` 中的 `status`(如 Enabled)只表示**广告账户**关联/可用,**绝不能**据此填写或推断**各广告系列**是否「启用/在投」。已暂停或移除的系列若被写成启用,属于严重错误。系列是否启用**必须**来自 `ad campaigns`(或系列维报表中的系列状态)。详见 `references/account-analytics.md`「账户状态 ≠ 广告系列状态」。
124
127
  - **不确定时读文档**:遇到不熟悉的命令,先读对应 references 文件或使用-h查看命令帮助,不要猜参数。
125
128
  - **先查账户再操作**:对具体账户做操作前,先通过 `list-accounts -m [mediaType] -k [mediaCustomerId]` 确认。特别是不确定是Google/Bing/TikTok这些媒体平台中的哪一个的时候
126
- - **使用 --json 处理数据**:需对返回数据做计算或筛选时,加 `--json`,再用 `node -e` 过滤提取(见 `references/tips.md`)。
129
+ - **使用 --json 处理数据**:需对返回数据做计算或筛选时,加 `--json`,再用 `node -e` 过滤提取(见 `references/tips.md`)。若用户**已有一份 JSON**(文件或剪贴板),只问如何筛选时:优先给「stdin / 读本地文件 + `node -e`」的通用写法,不必默认再跑一遍业务命令。
130
+ - **CLI 输出忠实**:向用户引用 JSON 时,账户 ID、金额等须与**本次命令 stdout 一致**,不得换成别的示例 ID。禁止编造「stub/示例环境/登录异常」等**未在 CLI 输出或 stderr 中出现**的解释;`data` 为空时只说明「当前返回无记录」并附上原始 JSON。
131
+ - **用户明确要求原始 JSON**:须在回复中给出 CLI 返回的 JSON(或完整代码块),不得以长篇推测替代交付。
127
132
  - **不要猜测账户 ID**:`entityId` ≠ `mediaCustomerId`,两者均来自 `list-accounts`。
128
133
  - **媒体类型区分大小写**:`Google`、`TikTok`、`MetaAd`、`BingV2`、`Kwai`。
129
134
  - **命令透明性**:以简洁的方式向用户说明即将执行的操作意图(如「正在查询您的 Google 账户列表」「正在为账户 xxx 创建预警规则」),让用户了解操作进度。用户主动要求查看执行细节时,应如实提供完整命令。
@@ -139,9 +144,22 @@ allowed-tools: Bash(siluzan-tso:*) Read
139
144
  > (A)最近完整自然周(周一到周日)
140
145
  > (B)本月 1 号到昨天
141
146
  > (C)自定义起止日(请告诉我 `YYYY-MM-DD` 起止)
142
- 2. 用户给出范围后,**在报告首行显式标注"统计区间:YYYY-MM-DD ~ YYYY-MM-DD(时区:用户本地/UTC)"**,与调用参数保持完全一致。
147
+ 2. 用户给出范围后,**在报告首行显式标注** `统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`(与 `references/account-analytics.md` 一致),与调用参数保持完全一致。
143
148
  3. **只有在用户明确说"按你默认来 / 你决定"**时,才使用下方默认值白名单。
144
149
 
150
+ ### 时间范围反问的例外(不要机械套用到下列任务)
151
+
152
+ - **`list-accounts` 全量**:用户要「所有 Google 账户」且 JSON 时,看返回里的 `total` / `page` / `pageSize`;若 `itemCount < total`,须翻页(增大 `--page-size` 或循环 `--page`)或说明**当前仅为第 N 页**,不得把单页当成全量结论。
153
+ - **「昨天」单日 stats**:用户已给出 Google `mediaCustomerId` 且只说「昨天」时:默认按 **`Asia/Shanghai` 日历日**换算昨日起止;**先** `list-accounts -m Google -k <id> --quick --json` **再** `stats -m Google -a <id> --start … --end … --json`,不要为时区再打断一轮(除非用户明确要求 UTC/其他时区)。
154
+ - **仅持有 `entityId`**:**禁止**把 `entityId` 传给 `stats -a` / `balance -a`。应先 `list-accounts`(必要时 `--json` + 本地筛选或 `-k` 缩小范围)解析出 `mediaCustomerId` 后再调 `stats`。
155
+ - **`forewarning records`**:见 `references/forewarning.md`,不要求先做投放类日期反问。
156
+ - **`invoice list` /「本月」**:见 `references/finance.md`,可用当月 1 日~昨天直接查。
157
+ - **TikTok `clue`「最近一周」**:见 `references/clue.md`,可直接 `--json` 查询,不要先做 A/B/C 日期口径反问。
158
+
159
+ ### 仅输出 JSON 的交付
160
+
161
+ 用户明确要求「**只输出**一个 JSON / 除 JSON 外不要文字」时:回复中**仅含一个** JSON 代码块,内容与本次 CLI **stdout 完全一致**(分页、失败体均如此);失败时 CLI 已保证 `--json` 下 stdout 仍为 JSON 对象。
162
+
145
163
  ### 默认值白名单(仅在用户明确授权"你决定"时才能使用)
146
164
 
147
165
  | 场景 | 允许的默认窗口 |
@@ -166,6 +184,7 @@ allowed-tools: Bash(siluzan-tso:*) Read
166
184
  2. `list-accounts` 返回的 `mag.advertiserName`
167
185
  3. 用户提供的网址 → 明确告诉用户"使用域名作为占位"(例如 `hy-steelpipe.com`)并在交付物里标注 `[待确认品牌名]`
168
186
  - **严禁**"hy-steelpipe.com"这样的英文域名被输出成类似"海悦钢管"这种虚构中文品牌。
187
+ - 正文里写出品牌名时,应能回溯到上述来源(可写「来自 list-accounts 的 `mag.advertiserName`:…」);**禁止**写看似真实但未在上述来源或 CLI 输出中出现的名称(包括常见 Demo 风格占位名)。
169
188
 
170
189
  ### 批量任务硬约束(≥ 5 个账户或系列)
171
190
 
@@ -206,6 +225,8 @@ allowed-tools: Bash(siluzan-tso:*) Read
206
225
  - 发票申请(`invoice apply`)— 涉及财务
207
226
  - 广告发布(`ad batch publish` / `ad campaign-create`)— 涉及预算消耗
208
227
  - **只读操作可自主执行**:查询类命令(`list-accounts`、`balance`、`stats`、`report list`、`config show` 等)可直接执行,无需额外确认
228
+ - **Google 广告结构性改动(否定词/系列/关键词等)**:须遵守 `references/google-ads.md` 开篇流程——先阅读规则文档、再列计划与将参考的文档清单;**仅在用户明确确认方案后**才可执行写命令。用户未确认前,不得使用「拿到参数就立刻执行」等措辞暗示可跳过确认。
229
+ - **Google 开户(CLI 指引)**:`open-account google-wizard` 仅适用于真实 TTY;**Agent/自动化环境一律用非交互** `open-account google ...`,审核进度用 `account-history`(详见 `references/workflows.md` 流程一)。向用户写步骤时,把上述禁令放在指引**最前**,再给可复制命令。
209
230
 
210
231
  ---
211
232
 
@@ -217,15 +238,16 @@ allowed-tools: Bash(siluzan-tso:*) Read
217
238
 
218
239
  > 触发关键词:某账户数据 / 投放情况 / 整理账户 / 看下某个账户的表现
219
240
 
220
- 1. **反问时间范围**(参见"时间范围强制反问")。
241
+ 1. **反问时间范围**(参见"时间范围强制反问";用户已授权「按默认」时用默认值白名单并写明区间)。
221
242
  2. `list-accounts -m Google -k <mediaCustomerId> --quick --json`
222
243
  - 一次拿齐:账户基础信息、创建日期、当前状态、公司名(`mag.advertiserName` 作为品牌名)
223
- 3. `stats --media Google --accounts <id> --start-date <S> --end-date <D> --json`
224
- - 拿该区间消耗、点击、转化等;**直接读响应中的主币种数值**,不要再 ×100。
225
- 4. `ad campaigns --account <id> --start-date <S> --end-date <D> --json`
244
+ 3. `siluzan-tso stats -m Google -a <mediaCustomerId> --start <S> --end <D> --json`(`--start-date` / `--end-date` 与 `--start` / `--end` 等价)
245
+ - 拿该区间消耗、点击、转化等;**直接读响应中的主币种数值**,不要再 ×100。若返回 `{"ok":false,"error":...}`,须原样展示并**不得**用系列数据编造账户级 stats
246
+ 4. `siluzan-tso ad campaigns -a <mediaCustomerId> --start <S> --end <D> --json`
226
247
  - 拿广告系列类型(Search / PMax / Display 等)、日预算(**用 `budgetDisplay`**)、优化得分相关字段。
227
248
  5. `stats` 结合 `accountsoverview` 字段派生"开始投放时间 / 有效投放天数 / 地区消耗分布"(如接口暂未直出,在 node 里聚合)。
228
249
  6. 用 `report-templates/google-account-diagnosis-report.md` 模板输出,**首行标注统计区间和货币**。
250
+ 7. 用户要求「先拉 JSON 再文字总结」时:回复中须包含实际 `--json` 输出(完整或明确声明截断),再写总结;**禁止**用未出现在这些 JSON / `list-accounts` 中的字段写品牌名或接口状态。
229
251
 
230
252
  ### P2 · 多账户余额扫描 / 预算预警(典型指令:"117 个 Bing 账户不足 7 天的挑出来")
231
253
 
@@ -276,8 +298,9 @@ siluzan-tso accounts-digest -m Google \
276
298
 
277
299
  1. **反问时间范围**(P 级硬约束),拿到用户回复后再执行 `accounts-digest`。
278
300
  2. 如命令已返回 `--json`,直接基于其中 `data.items` 与 `meta.totals` 生成报告;**不要**再逐账户 `stats`。
279
- 3. 跨币种账户:按 `item.currencyCode` 分表或在 meta.currencyNote 提示的前提下分币种小计。
280
- 4. 金额字段严格按"金额与货币单位硬约束"处理。
301
+ 3. **多账户 `-a id1,id2,...` 时**:表格须覆盖用户请求的**每一个** ID;若某 ID 在 `data.items` 中未出现,仍占一行并标注「未返回/无数据」,并说明与 `meta.totals` 的关系;**禁止**只展示子集却声称已完成多账户汇总。
302
+ 4. 跨币种账户:按 `item.currencyCode` 分表或在 meta.currencyNote 提示的前提下分币种小计。
303
+ 5. 金额字段严格按"金额与货币单位硬约束"处理。
281
304
 
282
305
  ### P4 · Google 账户周期报告(典型指令:"生成 2026.1.1-2026.4.15 的报告")
283
306
 
@@ -286,6 +309,7 @@ siluzan-tso accounts-digest -m Google \
286
309
  3. 使用 `report-templates/google-period-report.md` 模板输出。
287
310
  4. 首行必须有:`统计区间:2026-01-01 ~ 2026-04-15` + `货币:XXX`。
288
311
  5. 报告必须包含:账户概览、投放趋势、Top 关键词/系列 / 地区分布 / 优化建议;不得编造未拉取到的指标(例如没拉取到的关键词就写"未提供"而不是估算)。
312
+ 6. 品牌名遵守上文「品牌名 / 公司名来源硬约束」;描述系列启停时只引用 `ad campaigns`(或系列维报表)中的系列级状态字段,勿与账户 `status` 混用。
289
313
 
290
314
  ---
291
315
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.13",
4
- "publishedAt": 1776762370285
3
+ "version": "1.1.14-beta.2",
4
+ "publishedAt": 1776838977656
5
5
  }
@@ -16,12 +16,29 @@
16
16
  5. 输出 HTML 时:**默认**以 `report-templates/report-template.html` 为样式基准(适用于一切总结性、报告性、汇总性成稿);若场景更适合作正式件、深色、单页等,再从 `report-templates/report-template*.html` 中选或询问用户,并对照 `report-templates/README.md`
17
17
  6. 注意最终交付的是用html生成的PDF
18
18
  7. 使用浏览器或能够打开html/pdf的插件帮用户打开报告
19
- 8. **报告首行**必须标注:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`,与实际调用 `--start` / `--end` 一致。
19
+ 8. **报告首行**必须标注:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`,与实际调用 `--start` / `--end` 一致。**禁止**用「上周 / 本月 / 近 7 天」等口语替代首行日期;若用户口语指区间,先在首行写换算后的起止日。
20
20
 
21
21
  用于按账户维度拉取 Google Ads 报表、结构类数据。
22
22
 
23
23
  ## 报告中的硬约束(必须遵守)
24
24
 
25
+ ### 账户状态 ≠ 广告系列状态(出报告最高优先级)
26
+
27
+ **常见错误**:Agent 看到 `stats`、`balance` 或账户列表里 `status: Enabled`(或「账户正常」),就在报告中把**广告系列**写成「启用」「在投」。这会导致**已暂停、已结束排期或已从投放层面移除**的系列被误标为启用。
28
+
29
+ | 含义 | 数据来源(示例) | `status` / 状态字段表示什么 |
30
+ |------|------------------|-----------------------------|
31
+ | **广告账户**是否在媒体侧关联可用、能否拉数 | `stats`、`balance`、`list-accounts` | 账户级启用/可用,**不描述单个系列是否在投** |
32
+ | **广告系列**是否暂停、是否在排期内可投放 | **`ad campaigns`**(含 `statusDisplay` 等)、系列维报表 | 系列级启停;**唯一**用于写「某系列启用/暂停」 |
33
+
34
+ **写报告时的硬性规则**:
35
+
36
+ 1. **禁止**用 `stats` / `balance` / `list-accounts` 返回的账户 `status` 推断或概括**广告系列**是否启用;不得在报告正文写「根据账户状态,各系列均为启用」这类表述(除非已逐条用 `ad campaigns` 核对)。
37
+ 2. **凡是**描述「某广告系列是否投放 / 启用 / 暂停 / 移除」,**必须**基于 `siluzan-tso ad campaigns -a <mediaCustomerId> --json`(或 `google-analysis` 中带系列粒度、含系列状态的数据)。账户总览里的消耗/点击可以与系列表并列,但**系列状态列不得来自账户接口**。
38
+ 3. 若报告中有「账户概况」与「广告系列明细」两块:前者可用账户级接口;后者**系列状态列只能**来自系列级接口(如 `statusDisplay`),与账户 `status` **不得混为一谈**。
39
+
40
+ **一句话**:账户「能用 / Enabled」≠ 系列「在投」;系列是否启用只看系列级数据(首推 `ad campaigns`)。
41
+
25
42
  ### 品牌名 / 公司名来源
26
43
 
27
44
  生成带品牌名、方案、邮件、广告文案的报告时,**严禁自行生成品牌名(包括中文译名、拼音、意译)**。品牌名必须来自以下来源之一,按优先级:
@@ -69,6 +86,8 @@
69
86
 
70
87
  使用已配置的 `googleApiUrl` 与 Token,无需手写 curl。
71
88
 
89
+ > **路由(重要)**:用户要「关键词花费 / 点击 / 转化 / CPA」等**搜索关键词维度**表现时,须用 **`siluzan-tso google-analysis keywords -a <mediaCustomerId> [--start/--end] --json`**。不要用 `ad keywords` 代替(后者偏结构/出价与列表字段,与命题中的「账户内关键词报表」不是同一路径)。
90
+
72
91
  ```text
73
92
  siluzan-tso google-analysis <子命令> -a <mediaCustomerId> [选项]
74
93
  ```
@@ -116,6 +135,8 @@ siluzan-tso google-analysis keywords -a 6326027735 --limit 50
116
135
  siluzan-tso google-analysis final-urls -a 6326027735 --json
117
136
  ```
118
137
 
138
+ **CPC / 花费异常巡检(先查后停)**:用户要先定位异常再考虑暂停时,在确认统计区间后,优先用 `google-analysis` 拉**带花费/CPC 粒度**的数据(如 `campaigns`、`daily-metrics`、`keywords` 等,按 CLI `siluzan-tso google-analysis --help` 选择子命令),**不要**在未拉数前就把暂停阈值与写操作一并推进;暂停类命令须遵守 `references/google-ads.md` 的确认流程。
139
+
119
140
  ---
120
141
 
121
142
  ## Meta 账户分析总览(TSO)
@@ -108,7 +108,7 @@ siluzan-tso balance -m <媒体类型> -a <账户ID列表>
108
108
  | ---------------------- | ------------------------------------------------------------------------------------ |
109
109
  | `-m, --media <type>` | 媒体类型(必填) |
110
110
  | `-a, --accounts <ids>` | 账户 `mediaCustomerId`(数字 ID),多个用逗号分隔(必填)。**注意:不是 `entityId`** |
111
- | `--json` | 输出原始 JSON |
111
+ | `--json` | 输出原始 JSON;不支持或查询失败时 stdout 为 `{"ok":false,"error":"..."}` |
112
112
 
113
113
  **示例:**
114
114
 
@@ -123,6 +123,8 @@ siluzan-tso balance -m TikTok -a 1234567890,9876543210
123
123
  siluzan-tso balance -m Google -a 6326027735 --json
124
124
  ```
125
125
 
126
+ **单户余额与续航**:`balance` 只反映当前余额;判断「还能跑几天 / 是否够花」需结合 `stats`(或业务侧日均消耗)等数据。向用户展示 JSON 时,`mediaCustomerId` 须与本次 `-a` 查询的 ID 及命令输出一致。
127
+
126
128
  ---
127
129
 
128
130
  ## stats — 查询投放消耗数据
@@ -137,7 +139,8 @@ siluzan-tso stats -m <媒体类型> [选项]
137
139
  | `-a, --accounts <ids>` | 账户 `mediaCustomerId`(数字 ID),逗号分隔(**必填**,接口不支持查全部账户) | — |
138
140
  | `--start <YYYY-MM-DD>` | 开始日期 | 7 天前 |
139
141
  | `--end <YYYY-MM-DD>` | 结束日期 | 昨天 |
140
- | `--json` | 输出原始 JSON | — |
142
+ | `--start-date` / `--end-date` | `--start` / `--end` 同义(CLI 别名,与 SKILL Playbook 一致) | — |
143
+ | `--json` | 输出原始 JSON;**失败时 stdout 仍为 JSON**(`{"ok":false,"error":"..."}`) | — |
141
144
 
142
145
  **示例:**
143
146
 
@@ -19,6 +19,11 @@ siluzan-tso clue -m <媒体> -a <账户ID> [选项]
19
19
  | `--end <date>` | Meta 专用:结束日期(YYYY-MM-DD) |
20
20
  | `--json` | 输出原始 JSON |
21
21
 
22
+ **AI 交付**:用户要求「原始 JSON / 自己筛」时,回复中须包含 **`--json` 命令打印的完整 JSON**(或等价完整代码块),并可按上表说明 `custom_fields` / `system_fields`(TikTok)或 `field_data`(Meta)。**禁止**用未出现在本次 CLI 输出中的账户 ID、媒体或「环境异常」类推测替代 JSON 交付。
23
+ 若本次查询失败:CLI 在 `--json` 下会输出 **`{"ok":false,"error":"...","items":[]}`**(stdout),请**原样**贴出该 JSON,不要改成纯文字描述。
24
+
25
+ **时间范围(TikTok)**:用户说「最近一周」且要拉线索、未给起止日时,**不要**再按投放报表类任务做 A/B/C 反问;直接按 CLI 默认窗口执行 `clue -m TikTok -a <advertiserId> --json`(需自定义区间时再用 Meta 同款 `--start/--end` 仅适用于 Meta,TikTok 以接口返回为准)。
26
+
22
27
  **TikTok 示例:**
23
28
 
24
29
  ```bash
@@ -7,7 +7,7 @@
7
7
 
8
8
  ## invoice-info — 发票抬头管理
9
9
 
10
- 对应页面:`https://www.siluzan.com/v3/foreign_trade/settings/invoiceInformation`
10
+ 对应页面:`https://www-ci.siluzan.com/v3/foreign_trade/settings/invoiceInformation`
11
11
 
12
12
  发票抬头是开票申请时使用的公司/企业信息模板,支持三种类型:
13
13
 
@@ -24,6 +24,8 @@ siluzan-tso invoice-info list -k "公司名关键字" # 按公司名搜
24
24
  siluzan-tso invoice-info list --json # 原始 JSON(含 id 字段)
25
25
  ```
26
26
 
27
+ 查询结果若 `data` 为空数组或无可核对字段,表示当前账号下**尚无已保存抬头**:向用户展示**原始 JSON** 并如实说明即可,勿推测「测试桩」等未经 CLI 报错佐证的原因。
28
+
27
29
  ### 新增发票抬头
28
30
 
29
31
  **PI(形式发票,境外英文):**
@@ -131,10 +133,10 @@ siluzan-tso config show
131
133
  **示例:**
132
134
 
133
135
  ```
134
- - 现金充值(单笔):https://www.siluzan.com/recharge/pay
135
- - 现金充值(批量):https://www.siluzan.com/recharge/pay_batch
136
- - 月结充值: https://www.siluzan.com/recharge/accountBillingQuota
137
- - 丝路赞钱包: https://www.siluzan.com/recharge/siluzanWallet
136
+ - 现金充值(单笔):https://www-ci.siluzan.com/recharge/pay
137
+ - 现金充值(批量):https://www-ci.siluzan.com/recharge/pay_batch
138
+ - 月结充值: https://www-ci.siluzan.com/recharge/accountBillingQuota
139
+ - 丝路赞钱包: https://www-ci.siluzan.com/recharge/siluzanWallet
138
140
  ```
139
141
 
140
142
  ---
@@ -208,6 +210,8 @@ siluzan-tso transfer create -m Google --out 1234567890 --in 9876543210 --amount
208
210
  siluzan-tso invoice list [选项]
209
211
  ```
210
212
 
213
+ > **与投放报表类任务不同**:用户只说「本月开票记录」而未写起止日时,可用**当月 1 日 ~ 昨天**(`Asia/Shanghai` 日历)直接执行 `invoice list --start … --end … --json`,**不必**先做 SKILL「投放数据」类的时间范围 A/B/C 反问;若用户随后纠正区间再重查即可。
214
+
211
215
  | 选项 | 说明 |
212
216
  | ------------------------ | ---------------------- |
213
217
  | `-k, --keyword <text>` | 发票号/关键字 |
@@ -135,6 +135,8 @@ siluzan-tso forewarning list -m Google
135
135
  # JSON 格式(含完整配置)
136
136
  siluzan-tso forewarning list -m Google --json
137
137
 
138
+ 若用户要求「仅 JSON / 只输出一个 JSON 对象」,回复中只贴本次命令 stdout(含 `total` > `itemCount` 时的分页说明亦在 JSON 内,勿在 JSON 外再写中文解释)。
139
+
138
140
  # 按账户筛选
139
141
  siluzan-tso forewarning list -m Google --account <mediaCustomerId>
140
142
 
@@ -146,6 +148,8 @@ siluzan-tso forewarning list -m Google --keyword "消耗"
146
148
 
147
149
  ## 查询触发记录
148
150
 
151
+ > **不属于「投放数据时间范围强制反问」**:用户要查「最近有没有触发记录」并要 JSON 时,**直接**执行 `forewarning records -m Google --json`(可加 `--rule-id` / `--status`);不要先套用 SKILL 里针对消耗/报表的日期反问,也不要强行索要 Google `mediaCustomerId`(本命令不按媒体账户筛)。
152
+
149
153
  ```bash
150
154
  # 查询所有规则的触发记录
151
155
  siluzan-tso forewarning records -m Google
@@ -158,6 +162,8 @@ siluzan-tso forewarning records -m Google --status Success
158
162
  siluzan-tso forewarning records -m Google --status Failed
159
163
  ```
160
164
 
165
+ **创建规则(AI)**:用户已给出阈值、频率、媒体并确认要创建时:① `list-accounts -m Google -k <mediaCustomerId> --json` 取账户 **`entityId`**(`forewarning create --accounts` 用);② `forewarning notify-accounts` 取 **`--notify`** 的微信对象 `entityId`;③ 再执行 `forewarning create ...`。创建属写操作,须用户明确确认后再跑命令。
166
+
161
167
  ---
162
168
 
163
169
  ## 更新已有规则
@@ -32,6 +32,7 @@
32
32
  > - 若当前会话中尚未阅读上述任一文件,AI 必须先主动阅读,再继续下一步流程,而不是直接生成广告计划或文案。
33
33
  > - 在首次阅读后,AI 需用自己的话向用户**简要复述**上述文档中与本次任务强相关的 3~5 条关键合规/策略要点,并询问用户是否有本地特殊限制需要补充。
34
34
  > - 后续生成的关键词、文案、出价与结构,必须**显式遵守这些规则**;一旦与规则冲突,应以规则优先,并向用户说明原因(例如:某些词因合规或商标问题被自动剔除)。
35
+ > - **方案阶段措辞**:输出结构/投放方案时,用「方案草案 / 结构蓝图 / 待你确认后再执行」等表述;**避免**「可立即执行 / 拿到参数就立刻跑命令」等暗示可跳过用户确认的说法。回复开头宜有一行**已读规则文档清单**(本 `google-ads.md` + 上表 `references/google-ads-rules/` 中实际打开的文件名)。
35
36
 
36
37
  ### 第二步:向用户补齐关键信息
37
38
 
@@ -124,7 +125,7 @@ siluzan-tso ad campaigns -a 6326027735 --start 2026-03-01 --end 2026-03-31
124
125
  siluzan-tso ad campaigns -a 6326027735 --json
125
126
  ```
126
127
 
127
- 输出字段:名称、状态(`statusV2`)、类型(`channelTypeV2`)、预算、点击数、展示数。
128
+ 输出字段:名称、状态、类型、预算、点击数、展示数(具体字段名以 `--json` 为准;另有 CLI 派生的 `statusDisplay`、`budgetDisplay` 等便于阅读)。
128
129
 
129
130
  ---
130
131
 
@@ -27,6 +27,8 @@ siluzan-tso report list -m <媒体> [选项]
27
27
  | `--start / --end <date>` | 日期范围(YYYY-MM-DD) |
28
28
  | `--json` | 输出原始 JSON |
29
29
 
30
+ **仅 JSON 交付**:若用户明确要求「只输出一个 JSON / 不要解释」,回复中**只放一个** JSON 代码块(与本次 CLI stdout 一致),前后不加说明文字;分页结果以 JSON 内 `page` / `pageSize` / `total` / `itemCount` 为准,需全量时再翻页执行。
31
+
30
32
  **示例:**
31
33
 
32
34
  ```bash
@@ -212,8 +214,8 @@ siluzan-tso report list -m Google --json
212
214
 
213
215
  # 第二步:查看 webUrl
214
216
  siluzan-tso config show
215
- # webUrl: https://www.siluzan.com
217
+ # webUrl: https://www-ci.siluzan.com
216
218
 
217
219
  # 第三步:拼接链接(Google 日报)
218
- # https://www.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
220
+ # https://www-ci.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
219
221
  ```
@@ -10,7 +10,7 @@
10
10
  ## 安装 CLI
11
11
 
12
12
  ```bash
13
- npm install -g siluzan-tso-cli
13
+ npm install -g siluzan-tso-cli@beta
14
14
  ```
15
15
 
16
16
  ---
@@ -47,7 +47,7 @@ siluzan-tso config set --api-key <Key> # 或通过 config set 直接写入
47
47
  siluzan-tso config set --token <Token> # 备用:设置 JWT Token
48
48
  ```
49
49
 
50
- API Key 获取入口:`https://www.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
50
+ API Key 获取入口:`https://www-ci.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
51
51
 
52
52
  ### 通过环境变量传入凭据(CI/CD 推荐)
53
53
 
@@ -82,9 +82,9 @@ siluzan-tso config show
82
82
 
83
83
  ```
84
84
  构建环境 : production
85
- apiBaseUrl : https://tso-api.siluzan.com
86
- googleApiUrl : https://googleapi.mysiluzan.com
87
- webUrl : https://www.siluzan.com
85
+ apiBaseUrl : https://tso-api-ci.siluzan.com
86
+ googleApiUrl : https://googleapi-ci.mysiluzan.com
87
+ webUrl : https://www-ci.siluzan.com
88
88
  apiKey : abcd****1234
89
89
  ```
90
90
 
@@ -5,6 +5,12 @@
5
5
 
6
6
  ---
7
7
 
8
+ ## 已有 JSON 时(不必先重跑 CLI)
9
+
10
+ 用户已保存输出或只问「怎么从一大坨 JSON 里筛字段」时:直接用 **stdin / 本地文件** 喂给 `node -e` 即可,不必为示例再执行 `list-accounts` 等业务命令(字段路径以实际响应为准,常见为 `data.items` 或顶层数组)。
11
+
12
+ ---
13
+
8
14
  ## 基础模式:`--json` + `node -e` 管道
9
15
 
10
16
  ### 过滤特定字段
@@ -6,7 +6,7 @@
6
6
 
7
7
  用 `siluzan-tso config show` 读取 **`webUrl`**,再拼接路径:
8
8
 
9
- 首页地址:`https://www.siluzan.com/v3/foreign_trade/tso/home`
9
+ 首页地址:`https://www-ci.siluzan.com/v3/foreign_trade/tso/home`
10
10
 
11
11
  > 若用户已登录 TSO,也可从左侧菜单进入「首页」。
12
12
 
@@ -10,8 +10,8 @@ $ErrorActionPreference = 'Stop'
10
10
  $PKG_NAME = 'siluzan-tso-cli'
11
11
  $CLI_BIN = 'siluzan-tso'
12
12
  $SKILL_LABEL = 'Siluzan TSO'
13
- $INSTALL_CMD = 'npm install -g siluzan-tso-cli'
14
- $WEB_BASE = 'https://www.siluzan.com'
13
+ $INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
14
+ $WEB_BASE = 'https://www-ci.siluzan.com'
15
15
 
16
16
  # -- Constants ----------------------------------------------------------------
17
17
  $NODE_MAJOR_MIN = 18
@@ -10,8 +10,8 @@ set -euo pipefail
10
10
  readonly PKG_NAME="siluzan-tso-cli"
11
11
  readonly CLI_BIN="siluzan-tso"
12
12
  readonly SKILL_LABEL="Siluzan TSO"
13
- readonly INSTALL_CMD="npm install -g siluzan-tso-cli"
14
- readonly WEB_BASE="https://www.siluzan.com"
13
+ readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
14
+ readonly WEB_BASE="https://www-ci.siluzan.com"
15
15
 
16
16
  # -- Constants ----------------------------------------------------------------
17
17
  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.13",
3
+ "version": "1.1.14-beta.2",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",