siluzan-tso-cli 1.0.0-beta.26 → 1.0.0-beta.28

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
@@ -20,7 +20,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
20
20
  siluzan-tso init --force # 强制覆盖已存在文件
21
21
  ```
22
22
 
23
- > **注意**:当前为测试版(1.0.0-beta.26),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
23
+ > **注意**:当前为测试版(1.0.0-beta.28),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
24
24
 
25
25
  | 助手 | 建议 `--ai` |
26
26
  |------|-------------|
package/dist/index.js CHANGED
@@ -124,7 +124,15 @@ function rawRequest(url, options) {
124
124
  res.on("data", (chunk) => {
125
125
  data += chunk;
126
126
  });
127
- res.on("end", () => resolve2({ status: res.statusCode ?? 0, text: data }));
127
+ res.on("end", () => {
128
+ const headers = {};
129
+ for (const [key, val] of Object.entries(res.headers)) {
130
+ if (val !== void 0) {
131
+ headers[key.toLowerCase()] = Array.isArray(val) ? val[0] : val;
132
+ }
133
+ }
134
+ resolve2({ status: res.statusCode ?? 0, text: data, headers });
135
+ });
128
136
  });
129
137
  req.on("error", reject);
130
138
  if (options.body) req.write(options.body);
@@ -588,13 +596,52 @@ async function apiFetch(url, config, options = {}, verbose = false) {
588
596
  return text;
589
597
  }
590
598
  }
599
+ async function apiFetchWithHeaders(url, config, options = {}, verbose = false) {
600
+ await prepareSiluzanSentryForApiFetch(url, config, options);
601
+ const method = options.method ?? "GET";
602
+ const authHeaders = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
603
+ const reqHeaders = {
604
+ "Content-Type": "application/json",
605
+ "Accept-Language": "zh-CN",
606
+ ...authHeaders,
607
+ Datapermission: config.dataPermission ?? "",
608
+ ...options.headers ?? {}
609
+ };
610
+ const body = typeof options.body === "string" ? options.body : void 0;
611
+ const res = await rawRequest(url, { method, headers: reqHeaders, body });
612
+ const text = res.text;
613
+ await reportSiluzanApiCall({
614
+ url,
615
+ config,
616
+ method,
617
+ reqHeaders,
618
+ requestBody: body,
619
+ status: res.status,
620
+ responseText: text
621
+ });
622
+ if (res.status < 200 || res.status >= 300) {
623
+ const detail = verbose ? `\uFF1A${redactSensitive(text).slice(0, 300)}` : "";
624
+ throw new Error(`HTTP ${res.status}${detail}`);
625
+ }
626
+ let data;
627
+ if (!text.trim()) {
628
+ data = null;
629
+ } else {
630
+ try {
631
+ data = JSON.parse(text);
632
+ } catch {
633
+ data = text;
634
+ }
635
+ }
636
+ return { data, headers: res.headers };
637
+ }
591
638
  function sleep(ms) {
592
639
  return new Promise((resolve2) => setTimeout(resolve2, ms));
593
640
  }
594
641
  function getCurrentVersion(importMetaUrl) {
595
642
  try {
596
643
  const __dirname2 = path2.dirname(fileURLToPath(importMetaUrl));
597
- const pkgPath = path2.join(__dirname2, "..", "..", "package.json");
644
+ const pkgPath = path2.join(__dirname2, "..", "package.json");
598
645
  const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
599
646
  return pkg.version ?? "0.0.0";
600
647
  } catch {
@@ -809,6 +856,9 @@ function loadConfig(tokenArg) {
809
856
  function apiFetch2(url, config, options = {}, verbose = false) {
810
857
  return apiFetch(url, config, options, verbose);
811
858
  }
859
+ function apiFetchWithHeaders2(url, config, options = {}, verbose = false) {
860
+ return apiFetchWithHeaders(url, config, options, verbose);
861
+ }
812
862
 
813
863
  // src/commands/config.ts
814
864
  function deriveWebUrl(apiBaseUrl) {
@@ -1168,6 +1218,81 @@ async function runUpdate(options) {
1168
1218
  console.log(" \u5982\u679C AI \u52A9\u624B\u6B63\u5728\u8FD0\u884C\uFF0C\u5EFA\u8BAE\u91CD\u542F\u4EE5\u4F7F\u65B0 Skill \u6587\u4EF6\u751F\u6548\u3002\n");
1169
1219
  }
1170
1220
 
1221
+ // src/utils/balance.ts
1222
+ var BALANCE_SUPPORTED_MEDIA = ["Google", "Yandex", "TikTok", "BingV2", "Kwai"];
1223
+ function fmt(d) {
1224
+ return d.toISOString().slice(0, 10);
1225
+ }
1226
+ function defaultDateRange() {
1227
+ const end = /* @__PURE__ */ new Date();
1228
+ const start = /* @__PURE__ */ new Date();
1229
+ start.setDate(start.getDate() - 7);
1230
+ return { startDate: fmt(start), endDate: fmt(end) };
1231
+ }
1232
+ async function fetchBalanceMap(media, accountIds, config, startDate, endDate, verbose) {
1233
+ const result = /* @__PURE__ */ new Map();
1234
+ if (accountIds.length === 0 || media === "MetaAd") return result;
1235
+ try {
1236
+ if (["Google", "Yandex", "TikTok", "BingV2"].includes(media)) {
1237
+ const params = new URLSearchParams({ ids: accountIds.join("|") });
1238
+ const url = `${config.apiBaseUrl}/query/media-account/${media}/GetMediaAccountInfo?${params}`;
1239
+ const raw = await apiFetch2(url, config, {}, verbose);
1240
+ const items = Array.isArray(raw) ? raw : raw.items ?? [];
1241
+ for (const item of items) {
1242
+ if (item.mediaCustomerId != null) {
1243
+ result.set(String(item.mediaCustomerId), item);
1244
+ }
1245
+ }
1246
+ } else if (media === "Kwai") {
1247
+ const range = defaultDateRange();
1248
+ const params = new URLSearchParams({
1249
+ period: "true",
1250
+ startDate: startDate ?? range.startDate,
1251
+ endDate: endDate ?? range.endDate,
1252
+ mediaCustomerIds: accountIds.join(",")
1253
+ });
1254
+ const url = `${config.apiBaseUrl}/report/media-account/Kwai/accountsoverview?${params}`;
1255
+ const raw = await apiFetch2(url, config, {}, verbose);
1256
+ const items = Array.isArray(raw) ? raw : [];
1257
+ for (const item of items) {
1258
+ if (item.mediaAccountId != null) {
1259
+ result.set(String(item.mediaAccountId), {
1260
+ mediaCustomerId: item.mediaAccountId,
1261
+ remainingAccountBudget: item.remainingAccountBudget,
1262
+ status: item.status,
1263
+ currencyCode: item.currencyCode
1264
+ });
1265
+ }
1266
+ }
1267
+ }
1268
+ } catch {
1269
+ }
1270
+ return result;
1271
+ }
1272
+ async function fetchOverviewMap(media, accountIds, config, startDate, endDate, verbose) {
1273
+ const result = /* @__PURE__ */ new Map();
1274
+ if (accountIds.length === 0 || media === "MetaAd") return result;
1275
+ const range = defaultDateRange();
1276
+ const params = new URLSearchParams({
1277
+ period: "true",
1278
+ startDate: startDate ?? range.startDate,
1279
+ endDate: endDate ?? range.endDate,
1280
+ mediaCustomerIds: accountIds.join(",")
1281
+ });
1282
+ const url = `${config.apiBaseUrl}/report/media-account/${media}/accountsoverview?${params}`;
1283
+ try {
1284
+ const raw = await apiFetch2(url, config, {}, verbose);
1285
+ const items = Array.isArray(raw) ? raw : [];
1286
+ for (const item of items) {
1287
+ if (item.mediaAccountId != null) {
1288
+ result.set(String(item.mediaAccountId), item);
1289
+ }
1290
+ }
1291
+ } catch {
1292
+ }
1293
+ return result;
1294
+ }
1295
+
1171
1296
  // src/commands/list-accounts.ts
1172
1297
  var VALID_MEDIA_TYPES = ["Google", "TikTok", "Yandex", "MetaAd", "BingV2", "Kwai"];
1173
1298
  var PLATFORM_CONFIG = {
@@ -1226,6 +1351,8 @@ var PLATFORM_CONFIG = {
1226
1351
  path: "/query/media-account/SearchMediaAcountByCriteria",
1227
1352
  pageParam: "pageNum",
1228
1353
  fixedParams: {
1354
+ // 与 Meta 同源接口,后端要求 MediaTypes(否则会 400:mediaTypes is required)
1355
+ MediaTypes: "Kwai",
1229
1356
  MediaType: "Kwai",
1230
1357
  advStatus: "",
1231
1358
  mediaAccountState: "Approved,Linked",
@@ -1235,6 +1362,31 @@ var PLATFORM_CONFIG = {
1235
1362
  idSearchParam: "mediaCustomerIds"
1236
1363
  }
1237
1364
  };
1365
+ function mergeTikTokAdList(items, adList) {
1366
+ if (!Array.isArray(adList) || adList.length === 0) return;
1367
+ for (const item of items) {
1368
+ const id = item.ma.mediaCustomerId;
1369
+ if (!id) continue;
1370
+ const ad = adList.find((a) => String(a.mediaCustomerId ?? "") === String(id));
1371
+ if (ad?.status != null) {
1372
+ item.ma.TTADInfo = { status: String(ad.status) };
1373
+ }
1374
+ }
1375
+ }
1376
+ function mergeMetaFacebookAdList(items, adList) {
1377
+ if (!Array.isArray(adList) || adList.length === 0) return;
1378
+ for (const item of items) {
1379
+ const id = item.ma.mediaCustomerId;
1380
+ if (!id) continue;
1381
+ const ad = adList.find((a) => String(a.mediaCustomerId ?? "") === String(id));
1382
+ if (ad) {
1383
+ item.ma.FacebookADInfo = {
1384
+ status: ad.status,
1385
+ reason: ad.reason
1386
+ };
1387
+ }
1388
+ }
1389
+ }
1238
1390
  async function fetchTikTokAccountByMediaCustomerId(config, mediaCustomerId, verbose) {
1239
1391
  const cfg = PLATFORM_CONFIG.TikTok;
1240
1392
  const params = new URLSearchParams();
@@ -1288,7 +1440,10 @@ async function runListAccounts(opts) {
1288
1440
  const url = `${config.apiBaseUrl}${platformCfg.path}?${params}`;
1289
1441
  if (platformCfg.responseType === "array") {
1290
1442
  try {
1291
- items = await apiFetch2(url, config, {}, opts.verbose);
1443
+ const res = await apiFetchWithHeaders2(url, config, {}, opts.verbose);
1444
+ items = res.data ?? [];
1445
+ const hit = res.headers["s-total-hits"];
1446
+ if (hit !== void 0) total = parseInt(hit, 10) || void 0;
1292
1447
  } catch (err) {
1293
1448
  console.error(`
1294
1449
  \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -1307,6 +1462,11 @@ async function runListAccounts(opts) {
1307
1462
  }
1308
1463
  items = Array.isArray(res?.mas) ? res.mas : [];
1309
1464
  total = typeof res?.total === "number" ? res.total : void 0;
1465
+ if (opts.media === "TikTok") {
1466
+ mergeTikTokAdList(items, res.adList);
1467
+ } else if (opts.media === "MetaAd") {
1468
+ mergeMetaFacebookAdList(items, res.adList);
1469
+ }
1310
1470
  }
1311
1471
  } else {
1312
1472
  const params = new URLSearchParams();
@@ -1316,7 +1476,10 @@ async function runListAccounts(opts) {
1316
1476
  params.set("pageSize", String(pageSize));
1317
1477
  const url = `${config.apiBaseUrl}/query/media-account/?${params}`;
1318
1478
  try {
1319
- items = await apiFetch2(url, config, {}, opts.verbose);
1479
+ const res = await apiFetchWithHeaders2(url, config, {}, opts.verbose);
1480
+ items = res.data ?? [];
1481
+ const hit = res.headers["s-total-hits"];
1482
+ if (hit !== void 0) total = parseInt(hit, 10) || void 0;
1320
1483
  } catch (err) {
1321
1484
  console.error(`
1322
1485
  \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -1324,6 +1487,79 @@ async function runListAccounts(opts) {
1324
1487
  process.exit(1);
1325
1488
  }
1326
1489
  }
1490
+ if (items.length > 0) {
1491
+ const groups = /* @__PURE__ */ new Map();
1492
+ for (const item of items) {
1493
+ const id = item.ma.mediaCustomerId;
1494
+ const mediaType = item.ma.mediaAccountType ?? opts.media ?? "";
1495
+ if (id && mediaType) {
1496
+ if (!groups.has(mediaType)) groups.set(mediaType, []);
1497
+ groups.get(mediaType).push(id);
1498
+ }
1499
+ }
1500
+ const diagnoseIdToItem = /* @__PURE__ */ new Map();
1501
+ const aritDiagnoseIds = [];
1502
+ for (const item of items) {
1503
+ for (const report of item.ma.diagnoseReports ?? []) {
1504
+ if (report.reportSource === "ARIT") {
1505
+ diagnoseIdToItem.set(report.websiteDiagnoseId, item);
1506
+ aritDiagnoseIds.push(report.websiteDiagnoseId);
1507
+ }
1508
+ }
1509
+ }
1510
+ const [balanceMaps, overviewMaps, aritReports] = await Promise.all([
1511
+ // 各媒体类型余额
1512
+ Promise.all(
1513
+ Array.from(groups.entries()).map(
1514
+ ([mediaType, ids]) => fetchBalanceMap(mediaType, ids, config, void 0, void 0, opts.verbose)
1515
+ )
1516
+ ),
1517
+ // 各媒体类型消耗数据(MetaAd 不支持,fetchOverviewMap 内部会跳过)
1518
+ Promise.all(
1519
+ Array.from(groups.entries()).map(
1520
+ ([mediaType, ids]) => fetchOverviewMap(mediaType, ids, config, void 0, void 0, opts.verbose)
1521
+ )
1522
+ ),
1523
+ // ARIT 得分
1524
+ aritDiagnoseIds.length > 0 ? apiFetch2(
1525
+ `${config.apiBaseUrl}/query/WebsiteDiagnoseReport/search?ids=${aritDiagnoseIds.join(",")}`,
1526
+ config,
1527
+ {},
1528
+ opts.verbose
1529
+ ).catch(() => []) : Promise.resolve([])
1530
+ ]);
1531
+ const globalBalance = /* @__PURE__ */ new Map();
1532
+ for (const m of balanceMaps) {
1533
+ for (const [id, info] of m) globalBalance.set(id, info);
1534
+ }
1535
+ const globalOverview = /* @__PURE__ */ new Map();
1536
+ for (const m of overviewMaps) {
1537
+ for (const [id, info] of m) globalOverview.set(id, info);
1538
+ }
1539
+ for (const item of items) {
1540
+ const id = item.ma.mediaCustomerId;
1541
+ if (!id) continue;
1542
+ const balanceInfo = globalBalance.get(id);
1543
+ if (balanceInfo) {
1544
+ item.ma.remainingAccountBudget = balanceInfo.remainingAccountBudget;
1545
+ item.ma.platformStatus = balanceInfo.status ?? balanceInfo.accountStatus;
1546
+ }
1547
+ const overviewInfo = globalOverview.get(id);
1548
+ if (overviewInfo) {
1549
+ item.ma["spend"] = overviewInfo.spend;
1550
+ item.ma["impressions"] = overviewInfo.impressions;
1551
+ item.ma["clicks"] = overviewInfo.clicks;
1552
+ item.ma["conversions"] = overviewInfo.conversions;
1553
+ item.ma["costPerClick"] = overviewInfo.costPerClick;
1554
+ }
1555
+ }
1556
+ for (const report of Array.isArray(aritReports) ? aritReports : []) {
1557
+ const targetItem = diagnoseIdToItem.get(report.websiteDiagnoseId);
1558
+ if (targetItem) {
1559
+ targetItem.ma.aritScore = report.summaryScore ?? "-";
1560
+ }
1561
+ }
1562
+ }
1327
1563
  if (opts.json) {
1328
1564
  console.log(JSON.stringify(items, null, 2));
1329
1565
  return;
@@ -1336,61 +1572,512 @@ async function runListAccounts(opts) {
1336
1572
  console.log(`
1337
1573
  \u5E7F\u544A\u8D26\u6237\u5217\u8868\uFF08\u7B2C ${page} \u9875\uFF0C\u672C\u9875 ${items.length} \u6761${totalInfo}\uFF09
1338
1574
  `);
1575
+ const isGoogle = opts.media === "Google";
1576
+ const isYandex = opts.media === "Yandex";
1577
+ const isTikTok = opts.media === "TikTok";
1578
+ const isMetaAd = opts.media === "MetaAd";
1579
+ const isBingV2 = opts.media === "BingV2";
1580
+ const isKwai = opts.media === "Kwai";
1581
+ if (isGoogle) {
1582
+ printGoogleTable(items);
1583
+ } else if (isYandex) {
1584
+ printYandexTable(items);
1585
+ } else if (isTikTok) {
1586
+ printTikTokTable(items);
1587
+ } else if (isMetaAd) {
1588
+ printMetaTable(items);
1589
+ } else if (isBingV2 || isKwai) {
1590
+ printSpendMetricsAccountTable(items);
1591
+ } else {
1592
+ printDefaultTable(items, opts.media);
1593
+ }
1594
+ const hasMore = total !== void 0 ? page * pageSize < total : items.length >= pageSize;
1595
+ if (hasMore) {
1596
+ console.log(`
1597
+ \u4F7F\u7528 --page <n> \u7FFB\u9875\uFF0C--page-size <n> \u8C03\u6574\u6BCF\u9875\u6570\u91CF\u3002`);
1598
+ }
1599
+ if (isGoogle) {
1600
+ console.log(`
1601
+ \u2139\uFE0F \u6B64\u5217\u8868\u4E0D\u663E\u793A Google \u5C01\u53F7\uFF08Suspended\uFF09\u72B6\u6001\uFF0C\u5982\u9700\u67E5\u770B\u53EF\u63D0\u73B0\u7684\u88AB\u5C01\u8D26\u6237\u8BF7\u8FD0\u884C\uFF1A`);
1602
+ console.log(` siluzan-tso account withdraw-list`);
1603
+ }
1604
+ console.log();
1605
+ }
1606
+ function fmtNum(v, decimals = 2) {
1607
+ const n = Number(v);
1608
+ return isNaN(n) ? "-" : n.toFixed(decimals);
1609
+ }
1610
+ function fmtDate(v) {
1611
+ if (!v) return "-";
1612
+ return v.slice(0, 10);
1613
+ }
1614
+ function formatTikTokAuditStatus(ma) {
1615
+ const state = ma.mediaAccountState ?? "";
1616
+ const tt = ma.TTADInfo?.status ?? "";
1617
+ if (state === "Created") return "\u5BA1\u6838\u4E2D";
1618
+ if (state === "Denied") return "\u5DF2\u62D2\u7EDD";
1619
+ if (state === "Approved" || state === "Linked") {
1620
+ if (tt === "STATUS_ENABLE") return "\u6B63\u5E38\u542F\u7528";
1621
+ if (tt === "STATUS_LIMIT") return "\u9650\u5236\u6295\u653E";
1622
+ if (tt === "STATUS_DISABLE") return "\u5DF2\u7981\u7528";
1623
+ if (tt) return tt;
1624
+ if (state === "Approved") return "\u4EBA\u5DE5\u5BA1\u6838\u6210\u529F";
1625
+ if (state === "Linked") return "\u5DF2\u5173\u8054";
1626
+ }
1627
+ return state || "-";
1628
+ }
1629
+ function formatTikTokBcColumn(ma) {
1630
+ const infos = ma.bcInfos;
1631
+ if (!Array.isArray(infos) || infos.length === 0) return "-";
1632
+ const first = infos[0];
1633
+ const id = String(first?.bcId ?? first?.bc_id ?? "").trim();
1634
+ const primary = id || "\u2014";
1635
+ if (infos.length <= 1) return primary;
1636
+ return `${primary}(+${infos.length - 1})`;
1637
+ }
1638
+ function displayMetaAccountId(id) {
1639
+ if (id == null || id === "") return "(\u672A\u5F00\u901A)";
1640
+ const s = String(id);
1641
+ return s.startsWith("act_") ? s.slice(4) : s;
1642
+ }
1643
+ function formatMetaAuthType(mediaAccountType) {
1644
+ if (mediaAccountType === "MetaAd") return "Meta \u8D26\u6237";
1645
+ if (mediaAccountType === "FacebookAds") return "Facebook \u5E7F\u544A\u8D26\u6237";
1646
+ return mediaAccountType ?? "-";
1647
+ }
1648
+ function formatMetaOAuthColumn(ma) {
1649
+ return ma.invalidOAuthToken ? "\u5931\u6548" : "\u6B63\u5E38";
1650
+ }
1651
+ function formatMetaAuditStatus(ma) {
1652
+ const ms = ma.mediaAccountState ?? "";
1653
+ if (ms !== "Approved" && ms !== "Linked") {
1654
+ return "\u672A\u77E5\u72B6\u6001";
1655
+ }
1656
+ const raw = ma.FacebookADInfo?.status;
1657
+ if (raw === void 0 || raw === null || raw === "") {
1658
+ return "\u672A\u77E5\u72B6\u6001";
1659
+ }
1660
+ if (typeof raw === "number") {
1661
+ const numMap = {
1662
+ 1: "\u6D3B\u8DC3",
1663
+ 2: "\u5DF2\u7981\u7528",
1664
+ 3: "\u672A\u7ED3\u6E05",
1665
+ 7: "\u5F85\u98CE\u63A7\u5BA1\u6838",
1666
+ 8: "\u4F59\u989D\u5DF2\u6E05\u96F6",
1667
+ 9: "\u5DF2\u5173\u95ED",
1668
+ 100: "\u5F85\u4ED8\u6B3E",
1669
+ 101: "\u5DF2\u5173\u95ED",
1670
+ 201: "\u4EFB\u4F55",
1671
+ 202: "\u5DF2\u5173\u95ED"
1672
+ };
1673
+ return numMap[raw] ?? `\u72B6\u6001\u7801 ${raw}`;
1674
+ }
1675
+ const u = String(raw).toUpperCase();
1676
+ const strMap = {
1677
+ ACTIVE: "\u6D3B\u8DC3",
1678
+ DISABLED: "\u5DF2\u505C\u7528",
1679
+ UNSETTLED: "\u672A\u7ED3\u6E05",
1680
+ PENDING_RISK_REVIEW: "\u98CE\u63A7\u5BA1\u6838\u4E2D",
1681
+ PENDING_SETTLEMENT: "\u5F85\u7ED3\u7B97",
1682
+ IN_GRACE_PERIOD: "\u5BBD\u9650\u671F",
1683
+ PENDING_CLOSURE: "\u5F85\u5173\u95ED",
1684
+ CLOSED: "\u5DF2\u5173\u95ED",
1685
+ ANY: "\u4EFB\u4F55"
1686
+ };
1687
+ return strMap[u] ?? String(raw);
1688
+ }
1689
+ function printMetaTable(items) {
1690
+ const rows = items.map((item) => {
1691
+ const ma = item.ma;
1692
+ return {
1693
+ company: item.mag?.advertiserName ?? "-",
1694
+ id: displayMetaAccountId(ma.mediaCustomerId),
1695
+ name: ma.mediaCustomerName ?? "-",
1696
+ oauthState: formatMetaOAuthColumn(ma),
1697
+ auditState: formatMetaAuditStatus(ma),
1698
+ authType: formatMetaAuthType(ma.mediaAccountType),
1699
+ createdAt: fmtDate(ma.createdDateTime)
1700
+ };
1701
+ });
1702
+ const cw = {
1703
+ company: Math.max(8, ...rows.map((r) => r.company.length)),
1704
+ id: Math.max(10, ...rows.map((r) => r.id.length)),
1705
+ name: Math.max(8, ...rows.map((r) => r.name.length)),
1706
+ oauthState: Math.max(4, ...rows.map((r) => r.oauthState.length)),
1707
+ auditState: Math.max(8, ...rows.map((r) => r.auditState.length)),
1708
+ authType: Math.max(12, ...rows.map((r) => r.authType.length)),
1709
+ createdAt: 12
1710
+ };
1711
+ const sep = " ";
1712
+ const header = [
1713
+ "\u516C\u53F8\u540D\u79F0".padEnd(cw.company),
1714
+ "\u8D26\u6237ID".padEnd(cw.id),
1715
+ "\u8D26\u6237\u540D\u79F0".padEnd(cw.name),
1716
+ "\u72B6\u6001".padEnd(cw.oauthState),
1717
+ "\u5BA1\u6838\u72B6\u6001".padEnd(cw.auditState),
1718
+ "\u6388\u6743\u7C7B\u578B".padEnd(cw.authType),
1719
+ "\u6388\u6743/\u5F00\u6237\u65F6\u95F4".padEnd(cw.createdAt)
1720
+ ].join(sep);
1721
+ console.log(" " + header);
1722
+ console.log(" " + "-".repeat(header.length));
1723
+ for (const r of rows) {
1724
+ console.log(" " + [
1725
+ r.company.padEnd(cw.company),
1726
+ r.id.padEnd(cw.id),
1727
+ r.name.padEnd(cw.name),
1728
+ r.oauthState.padEnd(cw.oauthState),
1729
+ r.auditState.padEnd(cw.auditState),
1730
+ r.authType.padEnd(cw.authType),
1731
+ r.createdAt.padEnd(cw.createdAt)
1732
+ ].join(sep));
1733
+ }
1734
+ }
1735
+ function printSpendMetricsAccountTable(items) {
1339
1736
  const rows = items.map((item) => {
1340
1737
  const ma = item.ma;
1341
- let status;
1738
+ const authStatus = ma.invalidOAuthToken ? "\u26A0\uFE0F \u5931\u6548" : "\u2705 \u6B63\u5E38";
1739
+ const balance = ma.remainingAccountBudget != null ? `${ma.currencyCode ?? ""} ${ma.remainingAccountBudget.toFixed(2)}`.trim() : "-";
1740
+ const sharedCount = Array.isArray(ma.accountIds) ? String(Math.max(0, ma.accountIds.length - 1)) : "-";
1741
+ return {
1742
+ company: item.mag?.advertiserName ?? "-",
1743
+ id: ma.mediaCustomerId != null && String(ma.mediaCustomerId).trim() !== "" ? String(ma.mediaCustomerId) : "(\u672A\u5F00\u901A)",
1744
+ name: ma.mediaCustomerName ?? "-",
1745
+ spend: fmtNum(ma["spend"]),
1746
+ impressions: fmtNum(ma["impressions"], 0),
1747
+ conversions: fmtNum(ma["conversions"], 0),
1748
+ clicks: fmtNum(ma["clicks"], 0),
1749
+ cpc: fmtNum(ma["costPerClick"]),
1750
+ balance,
1751
+ authStatus,
1752
+ createdAt: fmtDate(ma.createdDateTime),
1753
+ shared: sharedCount
1754
+ };
1755
+ });
1756
+ const cw = {
1757
+ company: Math.max(8, ...rows.map((r) => r.company.length)),
1758
+ id: Math.max(10, ...rows.map((r) => r.id.length)),
1759
+ name: Math.max(8, ...rows.map((r) => r.name.length)),
1760
+ spend: Math.max(6, ...rows.map((r) => r.spend.length)),
1761
+ impressions: Math.max(8, ...rows.map((r) => r.impressions.length)),
1762
+ conversions: Math.max(8, ...rows.map((r) => r.conversions.length)),
1763
+ clicks: Math.max(8, ...rows.map((r) => r.clicks.length)),
1764
+ cpc: Math.max(8, ...rows.map((r) => r.cpc.length)),
1765
+ balance: Math.max(8, ...rows.map((r) => r.balance.length)),
1766
+ authStatus: Math.max(6, ...rows.map((r) => r.authStatus.length)),
1767
+ createdAt: 12,
1768
+ shared: 6
1769
+ };
1770
+ const sep = " ";
1771
+ const header = [
1772
+ "\u516C\u53F8\u540D\u79F0".padEnd(cw.company),
1773
+ "\u8D26\u6237ID".padEnd(cw.id),
1774
+ "\u8D26\u6237\u540D\u79F0".padEnd(cw.name),
1775
+ "\u6D88\u8017".padEnd(cw.spend),
1776
+ "\u5C55\u793A\u6B21\u6570".padEnd(cw.impressions),
1777
+ "\u8F6C\u5316\u6B21\u6570".padEnd(cw.conversions),
1778
+ "\u70B9\u51FB\u6B21\u6570".padEnd(cw.clicks),
1779
+ "\u70B9\u51FB\u5747\u4EF7".padEnd(cw.cpc),
1780
+ "\u4F59\u989D".padEnd(cw.balance),
1781
+ "\u6388\u6743\u72B6\u6001".padEnd(cw.authStatus),
1782
+ "\u6388\u6743/\u5F00\u6237\u65F6\u95F4".padEnd(cw.createdAt),
1783
+ "\u5206\u4EAB\u8D26\u6237".padEnd(cw.shared)
1784
+ ].join(sep);
1785
+ console.log(" " + header);
1786
+ console.log(" " + "-".repeat(header.length));
1787
+ for (const r of rows) {
1788
+ console.log(" " + [
1789
+ r.company.padEnd(cw.company),
1790
+ r.id.padEnd(cw.id),
1791
+ r.name.padEnd(cw.name),
1792
+ r.spend.padEnd(cw.spend),
1793
+ r.impressions.padEnd(cw.impressions),
1794
+ r.conversions.padEnd(cw.conversions),
1795
+ r.clicks.padEnd(cw.clicks),
1796
+ r.cpc.padEnd(cw.cpc),
1797
+ r.balance.padEnd(cw.balance),
1798
+ r.authStatus.padEnd(cw.authStatus),
1799
+ r.createdAt.padEnd(cw.createdAt),
1800
+ r.shared.padEnd(cw.shared)
1801
+ ].join(sep));
1802
+ }
1803
+ }
1804
+ function printYandexTable(items) {
1805
+ const rows = items.map((item) => {
1806
+ const ma = item.ma;
1807
+ const authStatus = ma.invalidOAuthToken ? "\u26A0\uFE0F \u5931\u6548" : "\u2705 \u6B63\u5E38";
1808
+ const balance = ma.remainingAccountBudget != null ? `${ma.currencyCode ?? ""} ${ma.remainingAccountBudget.toFixed(2)}`.trim() : "-";
1809
+ const sharedCount = Array.isArray(ma.accountIds) ? String(Math.max(0, ma.accountIds.length - 1)) : "-";
1810
+ const pwd = ma.mediaPassword != null && String(ma.mediaPassword).trim() !== "" ? String(ma.mediaPassword) : "-";
1811
+ return {
1812
+ company: item.mag?.advertiserName ?? "-",
1813
+ firstPwd: pwd,
1814
+ name: ma.mediaCustomerName ?? "-",
1815
+ spend: fmtNum(ma["spend"]),
1816
+ impressions: fmtNum(ma["impressions"], 0),
1817
+ conversions: fmtNum(ma["conversions"], 0),
1818
+ clicks: fmtNum(ma["clicks"], 0),
1819
+ cpc: fmtNum(ma["costPerClick"]),
1820
+ balance,
1821
+ authStatus,
1822
+ createdAt: fmtDate(ma.createdDateTime),
1823
+ shared: sharedCount
1824
+ };
1825
+ });
1826
+ const cw = {
1827
+ company: Math.max(8, ...rows.map((r) => r.company.length)),
1828
+ firstPwd: Math.max(12, ...rows.map((r) => r.firstPwd.length)),
1829
+ name: Math.max(8, ...rows.map((r) => r.name.length)),
1830
+ spend: Math.max(6, ...rows.map((r) => r.spend.length)),
1831
+ impressions: Math.max(8, ...rows.map((r) => r.impressions.length)),
1832
+ conversions: Math.max(8, ...rows.map((r) => r.conversions.length)),
1833
+ clicks: Math.max(8, ...rows.map((r) => r.clicks.length)),
1834
+ cpc: Math.max(8, ...rows.map((r) => r.cpc.length)),
1835
+ balance: Math.max(8, ...rows.map((r) => r.balance.length)),
1836
+ authStatus: Math.max(6, ...rows.map((r) => r.authStatus.length)),
1837
+ createdAt: 12,
1838
+ shared: 6
1839
+ };
1840
+ const sep = " ";
1841
+ const header = [
1842
+ "\u516C\u53F8\u540D\u79F0".padEnd(cw.company),
1843
+ "\u9996\u6B21\u767B\u9646\u5BC6\u7801".padEnd(cw.firstPwd),
1844
+ "\u8D26\u6237\u540D\u79F0".padEnd(cw.name),
1845
+ "\u6D88\u8017".padEnd(cw.spend),
1846
+ "\u5C55\u793A\u6B21\u6570".padEnd(cw.impressions),
1847
+ "\u8F6C\u5316\u6B21\u6570".padEnd(cw.conversions),
1848
+ "\u70B9\u51FB\u6B21\u6570".padEnd(cw.clicks),
1849
+ "\u70B9\u51FB\u5747\u4EF7".padEnd(cw.cpc),
1850
+ "\u4F59\u989D".padEnd(cw.balance),
1851
+ "\u6388\u6743\u72B6\u6001".padEnd(cw.authStatus),
1852
+ "\u6388\u6743/\u5F00\u6237\u65F6\u95F4".padEnd(cw.createdAt),
1853
+ "\u5206\u4EAB\u8D26\u6237".padEnd(cw.shared)
1854
+ ].join(sep);
1855
+ console.log(" " + header);
1856
+ console.log(" " + "-".repeat(header.length));
1857
+ for (const r of rows) {
1858
+ console.log(" " + [
1859
+ r.company.padEnd(cw.company),
1860
+ r.firstPwd.padEnd(cw.firstPwd),
1861
+ r.name.padEnd(cw.name),
1862
+ r.spend.padEnd(cw.spend),
1863
+ r.impressions.padEnd(cw.impressions),
1864
+ r.conversions.padEnd(cw.conversions),
1865
+ r.clicks.padEnd(cw.clicks),
1866
+ r.cpc.padEnd(cw.cpc),
1867
+ r.balance.padEnd(cw.balance),
1868
+ r.authStatus.padEnd(cw.authStatus),
1869
+ r.createdAt.padEnd(cw.createdAt),
1870
+ r.shared.padEnd(cw.shared)
1871
+ ].join(sep));
1872
+ }
1873
+ }
1874
+ function printTikTokTable(items) {
1875
+ const rows = items.map((item) => {
1876
+ const ma = item.ma;
1877
+ const authStatus = ma.invalidOAuthToken ? "\u26A0\uFE0F \u5931\u6548" : "\u2705 \u6B63\u5E38";
1878
+ const balance = ma.remainingAccountBudget != null ? `${ma.currencyCode ?? ""} ${ma.remainingAccountBudget.toFixed(2)}`.trim() : "-";
1879
+ const sharedCount = Array.isArray(ma.accountIds) ? String(Math.max(0, ma.accountIds.length - 1)) : "-";
1880
+ return {
1881
+ company: item.mag?.advertiserName ?? "-",
1882
+ id: ma.mediaCustomerId ?? "(\u672A\u5F00\u901A)",
1883
+ name: ma.mediaCustomerName ?? "-",
1884
+ spend: fmtNum(ma["spend"]),
1885
+ impressions: fmtNum(ma["impressions"], 0),
1886
+ conversions: fmtNum(ma["conversions"], 0),
1887
+ clicks: fmtNum(ma["clicks"], 0),
1888
+ cpc: fmtNum(ma["costPerClick"]),
1889
+ balance,
1890
+ bc: formatTikTokBcColumn(ma),
1891
+ authStatus,
1892
+ auditStatus: formatTikTokAuditStatus(ma),
1893
+ createdAt: fmtDate(ma.createdDateTime),
1894
+ shared: sharedCount
1895
+ };
1896
+ });
1897
+ const cw = {
1898
+ company: Math.max(8, ...rows.map((r) => r.company.length)),
1899
+ id: Math.max(10, ...rows.map((r) => r.id.length)),
1900
+ name: Math.max(8, ...rows.map((r) => r.name.length)),
1901
+ spend: Math.max(6, ...rows.map((r) => r.spend.length)),
1902
+ impressions: Math.max(8, ...rows.map((r) => r.impressions.length)),
1903
+ conversions: Math.max(8, ...rows.map((r) => r.conversions.length)),
1904
+ clicks: Math.max(8, ...rows.map((r) => r.clicks.length)),
1905
+ cpc: Math.max(8, ...rows.map((r) => r.cpc.length)),
1906
+ balance: Math.max(8, ...rows.map((r) => r.balance.length)),
1907
+ bc: Math.max(4, ...rows.map((r) => r.bc.length)),
1908
+ authStatus: Math.max(6, ...rows.map((r) => r.authStatus.length)),
1909
+ auditStatus: Math.max(8, ...rows.map((r) => r.auditStatus.length)),
1910
+ createdAt: 12,
1911
+ shared: 6
1912
+ };
1913
+ const sep = " ";
1914
+ const header = [
1915
+ "\u516C\u53F8\u540D\u79F0".padEnd(cw.company),
1916
+ "\u8D26\u6237ID".padEnd(cw.id),
1917
+ "\u8D26\u6237\u540D\u79F0".padEnd(cw.name),
1918
+ "\u6D88\u8017".padEnd(cw.spend),
1919
+ "\u5C55\u793A\u6B21\u6570".padEnd(cw.impressions),
1920
+ "\u8F6C\u5316\u6B21\u6570".padEnd(cw.conversions),
1921
+ "\u70B9\u51FB\u6B21\u6570".padEnd(cw.clicks),
1922
+ "\u70B9\u51FB\u5747\u4EF7".padEnd(cw.cpc),
1923
+ "\u4F59\u989D".padEnd(cw.balance),
1924
+ "BC".padEnd(cw.bc),
1925
+ "\u6388\u6743\u72B6\u6001".padEnd(cw.authStatus),
1926
+ "\u5BA1\u6838\u72B6\u6001".padEnd(cw.auditStatus),
1927
+ "\u6388\u6743/\u5F00\u6237\u65F6\u95F4".padEnd(cw.createdAt),
1928
+ "\u5206\u4EAB\u8D26\u6237".padEnd(cw.shared)
1929
+ ].join(sep);
1930
+ console.log(" " + header);
1931
+ console.log(" " + "-".repeat(header.length));
1932
+ for (const r of rows) {
1933
+ console.log(" " + [
1934
+ r.company.padEnd(cw.company),
1935
+ String(r.id).padEnd(cw.id),
1936
+ r.name.padEnd(cw.name),
1937
+ r.spend.padEnd(cw.spend),
1938
+ r.impressions.padEnd(cw.impressions),
1939
+ r.conversions.padEnd(cw.conversions),
1940
+ r.clicks.padEnd(cw.clicks),
1941
+ r.cpc.padEnd(cw.cpc),
1942
+ r.balance.padEnd(cw.balance),
1943
+ r.bc.padEnd(cw.bc),
1944
+ r.authStatus.padEnd(cw.authStatus),
1945
+ r.auditStatus.padEnd(cw.auditStatus),
1946
+ r.createdAt.padEnd(cw.createdAt),
1947
+ r.shared.padEnd(cw.shared)
1948
+ ].join(sep));
1949
+ }
1950
+ }
1951
+ function printGoogleTable(items) {
1952
+ const rows = items.map((item) => {
1953
+ const ma = item.ma;
1954
+ const authStatus = ma.invalidOAuthToken ? "\u26A0\uFE0F \u5931\u6548" : "\u2705 \u6B63\u5E38";
1955
+ const platformStatus = ma.platformStatus ?? "-";
1956
+ const balance = ma.remainingAccountBudget != null ? `${ma.currencyCode ?? ""} ${ma.remainingAccountBudget.toFixed(2)}`.trim() : "-";
1957
+ const sharedCount = Array.isArray(ma.accountIds) ? String(Math.max(0, ma.accountIds.length - 1)) : "-";
1958
+ return {
1959
+ company: item.mag?.advertiserName ?? "-",
1960
+ id: ma.mediaCustomerId ?? "(\u672A\u5F00\u901A)",
1961
+ name: ma.mediaCustomerName ?? "-",
1962
+ spend: fmtNum(ma["spend"]),
1963
+ impressions: fmtNum(ma["impressions"], 0),
1964
+ conversions: fmtNum(ma["conversions"], 0),
1965
+ clicks: fmtNum(ma["clicks"], 0),
1966
+ cpc: fmtNum(ma["costPerClick"]),
1967
+ balance,
1968
+ arit: String(ma.aritScore ?? "-"),
1969
+ platformStatus,
1970
+ authStatus,
1971
+ createdAt: fmtDate(ma.createdDateTime),
1972
+ shared: sharedCount
1973
+ };
1974
+ });
1975
+ const cw = {
1976
+ company: Math.max(8, ...rows.map((r) => r.company.length)),
1977
+ id: Math.max(10, ...rows.map((r) => r.id.length)),
1978
+ name: Math.max(8, ...rows.map((r) => r.name.length)),
1979
+ spend: Math.max(6, ...rows.map((r) => r.spend.length)),
1980
+ impressions: Math.max(6, ...rows.map((r) => r.impressions.length)),
1981
+ conversions: Math.max(6, ...rows.map((r) => r.conversions.length)),
1982
+ clicks: Math.max(6, ...rows.map((r) => r.clicks.length)),
1983
+ cpc: Math.max(6, ...rows.map((r) => r.cpc.length)),
1984
+ balance: Math.max(8, ...rows.map((r) => r.balance.length)),
1985
+ arit: Math.max(6, ...rows.map((r) => r.arit.length)),
1986
+ platformStatus: Math.max(6, ...rows.map((r) => r.platformStatus.length)),
1987
+ authStatus: Math.max(6, ...rows.map((r) => r.authStatus.length)),
1988
+ createdAt: 10,
1989
+ shared: 4
1990
+ };
1991
+ const sep = " ";
1992
+ const header = [
1993
+ "\u516C\u53F8\u540D\u79F0".padEnd(cw.company),
1994
+ "\u8D26\u6237ID".padEnd(cw.id),
1995
+ "\u8D26\u6237\u540D\u79F0".padEnd(cw.name),
1996
+ "\u6D88\u8017".padEnd(cw.spend),
1997
+ "\u5C55\u793A".padEnd(cw.impressions),
1998
+ "\u8F6C\u5316".padEnd(cw.conversions),
1999
+ "\u70B9\u51FB".padEnd(cw.clicks),
2000
+ "\u70B9\u51FB\u5747\u4EF7".padEnd(cw.cpc),
2001
+ "\u4F59\u989D".padEnd(cw.balance),
2002
+ "Arit".padEnd(cw.arit),
2003
+ "\u8D26\u6237\u72B6\u6001".padEnd(cw.platformStatus),
2004
+ "\u6388\u6743\u72B6\u6001".padEnd(cw.authStatus),
2005
+ "\u6388\u6743/\u5F00\u6237\u65F6\u95F4".padEnd(cw.createdAt),
2006
+ "\u5206\u4EAB"
2007
+ ].join(sep);
2008
+ console.log(" " + header);
2009
+ console.log(" " + "-".repeat(header.length));
2010
+ for (const r of rows) {
2011
+ const line = [
2012
+ r.company.padEnd(cw.company),
2013
+ r.id.padEnd(cw.id),
2014
+ r.name.padEnd(cw.name),
2015
+ r.spend.padEnd(cw.spend),
2016
+ r.impressions.padEnd(cw.impressions),
2017
+ r.conversions.padEnd(cw.conversions),
2018
+ r.clicks.padEnd(cw.clicks),
2019
+ r.cpc.padEnd(cw.cpc),
2020
+ r.balance.padEnd(cw.balance),
2021
+ r.arit.padEnd(cw.arit),
2022
+ r.platformStatus.padEnd(cw.platformStatus),
2023
+ r.authStatus.padEnd(cw.authStatus),
2024
+ r.createdAt.padEnd(cw.createdAt),
2025
+ r.shared
2026
+ ].join(sep);
2027
+ console.log(" " + line);
2028
+ }
2029
+ }
2030
+ function printDefaultTable(items, media) {
2031
+ const rows = items.map((item) => {
2032
+ const ma = item.ma;
2033
+ let statusLabel;
1342
2034
  if (ma.disabled) {
1343
- status = "\u{1F6AB} \u5DF2\u7981\u7528";
2035
+ statusLabel = "\u{1F6AB} \u5DF2\u7981\u7528";
1344
2036
  } else if (!ma.mediaCustomerId) {
1345
- status = "\u23F3 \u672A\u5F00\u901A";
2037
+ statusLabel = "\u23F3 \u672A\u5F00\u901A";
1346
2038
  } else if (ma.invalidOAuthToken) {
1347
- status = "\u26A0\uFE0F OAuth\u5931\u6548";
2039
+ statusLabel = "\u26A0\uFE0F OAuth\u5931\u6548";
1348
2040
  } else {
1349
- status = "\u2705 \u6B63\u5E38";
2041
+ statusLabel = "\u2705 \u6B63\u5E38";
1350
2042
  }
2043
+ const balance = ma.remainingAccountBudget != null ? `${ma.currencyCode ?? ""} ${ma.remainingAccountBudget.toFixed(2)}`.trim() : "-";
1351
2044
  return {
1352
2045
  mediaType: ma.mediaAccountType ?? "",
1353
2046
  mediaCustomerId: ma.mediaCustomerId ?? "(\u672A\u5F00\u901A)",
1354
2047
  advertiserName: item.mag?.advertiserName ?? "",
1355
2048
  mediaCustomerName: ma.mediaCustomerName ?? "",
1356
- status
2049
+ balance,
2050
+ status: statusLabel
1357
2051
  };
1358
2052
  });
1359
- const colWidths = {
2053
+ const cw = {
1360
2054
  mediaType: Math.max(8, ...rows.map((r) => r.mediaType.length)),
1361
2055
  mediaCustomerId: Math.max(10, ...rows.map((r) => r.mediaCustomerId.length)),
1362
2056
  advertiserName: Math.max(8, ...rows.map((r) => r.advertiserName.length)),
1363
- name: Math.max(8, ...rows.map((r) => r.mediaCustomerName.length))
2057
+ name: Math.max(8, ...rows.map((r) => r.mediaCustomerName.length)),
2058
+ balance: Math.max(8, ...rows.map((r) => r.balance.length))
1364
2059
  };
1365
2060
  const header = [
1366
- "\u5A92\u4F53\u7C7B\u578B".padEnd(colWidths.mediaType),
1367
- "\u8D26\u6237ID".padEnd(colWidths.mediaCustomerId),
1368
- "\u5E7F\u544A\u4E3B".padEnd(colWidths.advertiserName),
1369
- "\u8D26\u6237\u540D\u79F0".padEnd(colWidths.name),
2061
+ media ? "" : "\u5A92\u4F53\u7C7B\u578B".padEnd(cw.mediaType),
2062
+ "\u8D26\u6237ID".padEnd(cw.mediaCustomerId),
2063
+ "\u5E7F\u544A\u4E3B".padEnd(cw.advertiserName),
2064
+ "\u8D26\u6237\u540D\u79F0".padEnd(cw.name),
2065
+ "\u4F59\u989D".padEnd(cw.balance),
1370
2066
  "\u8D26\u6237\u72B6\u6001"
1371
- ].join(" ");
2067
+ ].filter(Boolean).join(" ");
1372
2068
  console.log(" " + header);
1373
2069
  console.log(" " + "-".repeat(header.length));
1374
- for (const row of rows) {
1375
- console.log(" " + [
1376
- row.mediaType.padEnd(colWidths.mediaType),
1377
- row.mediaCustomerId.padEnd(colWidths.mediaCustomerId),
1378
- row.advertiserName.padEnd(colWidths.advertiserName),
1379
- row.mediaCustomerName.padEnd(colWidths.name),
1380
- row.status
1381
- ].join(" "));
1382
- }
1383
- const hasMore = total !== void 0 ? page * pageSize < total : items.length >= pageSize;
1384
- if (hasMore) {
1385
- console.log(`
1386
- \u4F7F\u7528 --page <n> \u7FFB\u9875\uFF0C--page-size <n> \u8C03\u6574\u6BCF\u9875\u6570\u91CF\u3002`);
1387
- }
1388
- if (opts.media === "Google") {
1389
- console.log(`
1390
- \u2139\uFE0F \u6B64\u5217\u8868\u4E0D\u663E\u793A Google \u5C01\u53F7\uFF08Suspended\uFF09\u72B6\u6001\uFF0C\u5982\u9700\u67E5\u770B\u53EF\u63D0\u73B0\u7684\u88AB\u5C01\u8D26\u6237\u8BF7\u8FD0\u884C\uFF1A`);
1391
- console.log(` siluzan-tso account withdraw-list`);
2070
+ for (const r of rows) {
2071
+ const cells = [
2072
+ media ? "" : r.mediaType.padEnd(cw.mediaType),
2073
+ r.mediaCustomerId.padEnd(cw.mediaCustomerId),
2074
+ r.advertiserName.padEnd(cw.advertiserName),
2075
+ r.mediaCustomerName.padEnd(cw.name),
2076
+ r.balance.padEnd(cw.balance),
2077
+ r.status
2078
+ ].filter((_, i) => !media || i !== 0);
2079
+ console.log(" " + cells.join(" "));
1392
2080
  }
1393
- console.log();
1394
2081
  }
1395
2082
 
1396
2083
  // src/commands/balance.ts
@@ -1406,54 +2093,71 @@ async function runBalance(opts) {
1406
2093
  );
1407
2094
  process.exit(1);
1408
2095
  }
2096
+ const media = opts.media;
2097
+ if (!BALANCE_SUPPORTED_MEDIA.includes(media)) {
2098
+ console.error(`
2099
+ \u26A0\uFE0F ${media} \u6682\u4E0D\u652F\u6301\u4F59\u989D\u67E5\u8BE2
2100
+ `);
2101
+ process.exit(1);
2102
+ }
1409
2103
  const accountIds = opts.accounts.split(",").map((id) => id.trim()).filter(Boolean);
1410
2104
  if (accountIds.length === 0) {
1411
2105
  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");
1412
2106
  process.exit(1);
1413
2107
  }
1414
- const params = new URLSearchParams();
1415
- params.set("ids", accountIds.join("|"));
1416
- const url = `${config.apiBaseUrl}/query/media-account/${opts.media}/GetMediaAccountInfo?${params.toString()}`;
1417
- let raw;
2108
+ let balanceMap;
1418
2109
  try {
1419
- raw = await apiFetch2(url, config, {}, opts.verbose);
2110
+ balanceMap = await fetchBalanceMap(media, accountIds, config, opts.startDate, opts.endDate, opts.verbose);
1420
2111
  } catch (err) {
1421
2112
  console.error(`
1422
2113
  \u274C \u67E5\u8BE2\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
1423
2114
  `);
1424
2115
  process.exit(1);
1425
2116
  }
1426
- const items = Array.isArray(raw) ? raw : raw.items ?? [];
1427
2117
  if (opts.json) {
1428
- console.log(JSON.stringify(items, null, 2));
2118
+ const out = accountIds.map((id) => {
2119
+ const info = balanceMap.get(id);
2120
+ return {
2121
+ mediaCustomerId: id,
2122
+ remainingAccountBudget: info?.remainingAccountBudget ?? null,
2123
+ status: info?.status ?? null,
2124
+ currencyCode: info?.currencyCode ?? null,
2125
+ name: info?.name ?? null
2126
+ };
2127
+ });
2128
+ console.log(JSON.stringify(out, null, 2));
1429
2129
  return;
1430
2130
  }
1431
- if (items.length === 0) {
1432
- console.log("\n\u672A\u8FD4\u56DE\u4F59\u989D\u6570\u636E\uFF0C\u8BF7\u786E\u8BA4\u8D26\u6237 ID \u662F\u5426\u6B63\u786E\u3002\n");
2131
+ if (balanceMap.size === 0) {
2132
+ console.log("\n\u672A\u8FD4\u56DE\u4F59\u989D\u6570\u636E\uFF0C\u8BF7\u786E\u8BA4\u8D26\u6237 ID \u662F\u5426\u6B63\u786E\u4E14\u8D26\u6237\u72B6\u6001\u6709\u6548\u3002\n");
1433
2133
  return;
1434
2134
  }
1435
2135
  console.log(`
1436
- ${opts.media} \u8D26\u6237\u5B9E\u65F6\u4F59\u989D
2136
+ ${media} \u8D26\u6237\u4F59\u989D
1437
2137
  `);
1438
2138
  const colW = {
1439
- id: Math.max(10, ...items.map((i) => (i.mediaCustomerId ?? "").length)),
1440
- status: Math.max(8, ...items.map((i) => (i.accountStatus ?? "").length))
2139
+ id: Math.max(10, ...accountIds.map((id) => id.length)),
2140
+ name: Math.max(8, ...accountIds.map((id) => (balanceMap.get(id)?.name ?? "").length))
1441
2141
  };
1442
2142
  const header = [
1443
2143
  "\u8D26\u6237ID".padEnd(colW.id),
1444
- "\u4F59\u989D".padEnd(14),
2144
+ "\u8D26\u6237\u540D\u79F0".padEnd(colW.name),
2145
+ "\u4F59\u989D".padEnd(12),
1445
2146
  "\u5E01\u79CD".padEnd(6),
1446
- "\u8D26\u6237\u72B6\u6001"
2147
+ "\u72B6\u6001"
1447
2148
  ].join(" ");
1448
2149
  console.log(" " + header);
1449
2150
  console.log(" " + "-".repeat(header.length));
1450
- for (const item of items) {
1451
- const balance = item.remainingAccountBudget != null ? item.remainingAccountBudget.toFixed(2).padEnd(14) : "N/A".padEnd(14);
2151
+ for (const id of accountIds) {
2152
+ const info = balanceMap.get(id);
2153
+ const balance = info?.remainingAccountBudget != null ? info.remainingAccountBudget.toFixed(2).padEnd(12) : "N/A".padEnd(12);
2154
+ const status = info?.status ?? info?.accountStatus ?? "N/A";
1452
2155
  const row = [
1453
- (item.mediaCustomerId ?? "").padEnd(colW.id),
2156
+ id.padEnd(colW.id),
2157
+ (info?.name ?? "").padEnd(colW.name),
1454
2158
  balance,
1455
- (item.currency ?? "").padEnd(6),
1456
- item.accountStatus ?? "N/A"
2159
+ (info?.currencyCode ?? info?.currency ?? "").padEnd(6),
2160
+ status
1457
2161
  ].join(" ");
1458
2162
  console.log(" " + row);
1459
2163
  }
@@ -1462,13 +2166,13 @@ ${opts.media} \u8D26\u6237\u5B9E\u65F6\u4F59\u989D
1462
2166
 
1463
2167
  // src/commands/stats.ts
1464
2168
  var VALID_MEDIA_TYPES3 = ["Google", "TikTok", "Yandex", "MetaAd", "BingV2", "Kwai"];
1465
- function defaultDateRange() {
2169
+ function defaultDateRange2() {
1466
2170
  const end = /* @__PURE__ */ new Date();
1467
2171
  end.setDate(end.getDate() - 1);
1468
2172
  const start = new Date(end);
1469
2173
  start.setDate(start.getDate() - 6);
1470
- const fmt = (d) => d.toISOString().slice(0, 10);
1471
- return { startDate: fmt(start), endDate: fmt(end) };
2174
+ const fmt2 = (d) => d.toISOString().slice(0, 10);
2175
+ return { startDate: fmt2(start), endDate: fmt2(end) };
1472
2176
  }
1473
2177
  async function runStats(opts) {
1474
2178
  const config = loadConfig(opts.token);
@@ -1482,7 +2186,7 @@ async function runStats(opts) {
1482
2186
  process.exit(1);
1483
2187
  }
1484
2188
  const { startDate, endDate } = {
1485
- ...defaultDateRange(),
2189
+ ...defaultDateRange2(),
1486
2190
  ...opts.startDate ? { startDate: opts.startDate } : {},
1487
2191
  ...opts.endDate ? { endDate: opts.endDate } : {}
1488
2192
  };
@@ -2269,27 +2973,55 @@ ${label}\uFF08\u7B2C ${page}/${Math.ceil(total / pageSize)} \u9875\uFF0C\u5171 $
2269
2973
  console.log(" \u6682\u65E0\u6570\u636E\u3002\n");
2270
2974
  return;
2271
2975
  }
2976
+ const rows = items.map((item, idx) => ({
2977
+ idx: idx + 1 + (page - 1) * pageSize,
2978
+ tradeNo: String(item.tradeNo ?? item["orderNo"] ?? item["rechargeNo"] ?? "-").trim() || "-",
2979
+ entityId: String(item.entityId ?? "").trim() || "-",
2980
+ media: item.mediaAccountType ?? "",
2981
+ currency: item.currencyCode ?? "",
2982
+ amount: String(item.amounts ?? ""),
2983
+ state: item.invoiceState ?? item.rechargeStatus ?? "",
2984
+ name: item.mediaCustomerName ?? "",
2985
+ time: (item.createdDateTime ?? "").slice(0, 19)
2986
+ }));
2987
+ const cw = {
2988
+ no: Math.max(4, String(Math.max(...rows.map((r) => r.idx))).length),
2989
+ order: Math.max(10, ...rows.map((r) => r.tradeNo.length)),
2990
+ eid: Math.max(12, ...rows.map((r) => r.entityId.length)),
2991
+ media: Math.max(8, ...rows.map((r) => r.media.length)),
2992
+ cur: Math.max(6, ...rows.map((r) => r.currency.length)),
2993
+ amt: Math.max(8, ...rows.map((r) => r.amount.length)),
2994
+ st: Math.max(8, ...rows.map((r) => r.state.length)),
2995
+ name: Math.max(10, ...rows.map((r) => r.name.length))
2996
+ };
2997
+ const sep = " ";
2272
2998
  const header = [
2273
- "\u5A92\u4F53".padEnd(10),
2274
- "\u5E01\u79CD".padEnd(8),
2275
- "\u91D1\u989D".padEnd(12),
2276
- "\u72B6\u6001".padEnd(16),
2277
- "\u8D26\u6237\u540D\u79F0".padEnd(20),
2999
+ "#".padStart(cw.no),
3000
+ "\u8BA2\u5355\u53F7".padEnd(cw.order),
3001
+ "entityId(\u5F00\u7968\u7528)".padEnd(cw.eid),
3002
+ "\u5A92\u4F53".padEnd(cw.media),
3003
+ "\u5E01\u79CD".padEnd(cw.cur),
3004
+ "\u91D1\u989D".padEnd(cw.amt),
3005
+ "\u72B6\u6001".padEnd(cw.st),
3006
+ "\u8D26\u6237\u540D\u79F0".padEnd(cw.name),
2278
3007
  "\u521B\u5EFA\u65F6\u95F4"
2279
- ].join(" ");
3008
+ ].join(sep);
2280
3009
  console.log(" " + header);
2281
3010
  console.log(" " + "-".repeat(header.length));
2282
- for (const item of items) {
3011
+ for (const r of rows) {
2283
3012
  console.log(" " + [
2284
- (item.mediaAccountType ?? "").padEnd(10),
2285
- (item.currencyCode ?? "").padEnd(8),
2286
- String(item.amounts ?? "").padEnd(12),
2287
- (item.invoiceState ?? item.rechargeStatus ?? "").padEnd(16),
2288
- (item.mediaCustomerName ?? "").padEnd(20),
2289
- (item.createdDateTime ?? "").slice(0, 19)
2290
- ].join(" "));
2291
- }
2292
- console.log();
3013
+ String(r.idx).padStart(cw.no),
3014
+ r.tradeNo.padEnd(cw.order),
3015
+ r.entityId.padEnd(cw.eid),
3016
+ r.media.padEnd(cw.media),
3017
+ r.currency.padEnd(cw.cur),
3018
+ r.amount.padEnd(cw.amt),
3019
+ r.state.padEnd(cw.st),
3020
+ r.name.padEnd(cw.name),
3021
+ r.time
3022
+ ].join(sep));
3023
+ }
3024
+ console.log('\n \u5F00\u7968\u65F6\u8BF7\u628A\u6240\u9009\u884C\u7684 entityId \u4F20\u7ED9\uFF1Asiluzan-tso invoice apply --bill-ids "<entityId>" ...\n');
2293
3025
  }
2294
3026
  async function runInvoiceApply(opts) {
2295
3027
  const config = await ensureDataPermission(loadConfig(opts.token));
@@ -7760,11 +8492,13 @@ program.command("balance").description("\u67E5\u8BE2\u5E7F\u544A\u8D26\u6237\u5B
7760
8492
  ).requiredOption(
7761
8493
  "-a, --accounts <ids>",
7762
8494
  "\u8D26\u6237 ID\uFF0C\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF08\u6765\u81EA list-accounts \u7684 mediaCustomerId\uFF09"
7763
- ).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(async (opts) => {
8495
+ ).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--start-date <date>", "\u7EDF\u8BA1\u5F00\u59CB\u65E5\u671F yyyy-MM-dd\uFF08\u9ED8\u8BA4\uFF1A7 \u5929\u524D\uFF0CMetaAd \u4E0D\u9700\u8981\uFF09").option("--end-date <date>", "\u7EDF\u8BA1\u7ED3\u675F\u65E5\u671F yyyy-MM-dd\uFF08\u9ED8\u8BA4\uFF1A\u4ECA\u5929\uFF0CMetaAd \u4E0D\u9700\u8981\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(async (opts) => {
7764
8496
  await runBalance({
7765
8497
  token: opts.token,
7766
8498
  media: opts.media,
7767
8499
  accounts: opts.accounts,
8500
+ startDate: opts.startDate,
8501
+ endDate: opts.endDate,
7768
8502
  json: opts.json,
7769
8503
  verbose: opts.verbose
7770
8504
  });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.0.0-beta.26",
4
- "publishedAt": 1774428362970
3
+ "version": "1.0.0-beta.28",
4
+ "publishedAt": 1774436802305
5
5
  }
@@ -340,3 +340,35 @@ siluzan-tso invoice apply \
340
340
  --recipient-phone 13800138000 \
341
341
  --recipient-email zhang@example.com
342
342
  ```
343
+
344
+ ---
345
+
346
+ ### AI 助手:订单开票对话智能点
347
+
348
+ 当用户表达「给订单开发票」「申请开票」「充值要开票」等意图时,助手应**分步引导**,顺序如下。
349
+
350
+ #### 1. 先让用户选择订单
351
+
352
+ - 执行 `siluzan-tso invoice billable`(按需加 `-m`、`-c`、`--wallet`、日期范围等),把结果整理成**可读的订单列表**(建议标注:金额、币种、时间、媒体、`entityId`)。
353
+ - **必须**请用户明确要开哪一笔或哪几笔(可多选),得到确认的 `bill-ids`(`entityId` 逗号分隔)和 `bill-type`(`AmountAccount` / `WalletRecharge`)之后,再进入抬头环节。
354
+ - 不要替用户猜测订单;未确认订单前不要直接 `invoice apply`。
355
+
356
+ #### 2. 再让用户选择发票抬头
357
+
358
+ - 根据已选订单的**币种**确定允许的发票类型(与上文规则一致:人民币 → `VATI`/`VATSI`;外币 → `PI`)。
359
+ - 执行 `siluzan-tso invoice-info list --invoice-type <PI|VATI|VATSI>`(需要结构化数据时用 `--json`),向用户展示**已保存的发票抬头列表**。
360
+ - 请用户**从列表中选一条**(按 id 或公司名/抬头说明),或明确说要用**新的自定义抬头**。
361
+
362
+ #### 3. 自定义抬头:先匹配列表,没有再创建
363
+
364
+ - 用户给出自定义抬头(公司名、税号、抬头文案、地址、联系人等)时:
365
+ 1. 先用 `invoice-info list`(可加 `-k` 关键字、`--invoice-type`)在**已有抬头**中检索:公司名、`title`、税号等与用户输入是否**实质相同**(允许轻微空格/标点差异)。
366
+ 2. **若已存在等价记录**:不要重复 `invoice-info create`;直接沿用该条里的字段填 `invoice apply`(与 Web 一致:`apply` 传的是完整 `InvoiceInfomation`,不是只传抬头 id)。
367
+ 3. **若不存在**:先 `siluzan-tso invoice-info create ...` 创建成功,再 `siluzan-tso invoice apply ...`。避免未落库就假定「已保存」。
368
+
369
+ #### 4. 最后调用开票接口
370
+
371
+ - 组装 `invoice apply`:`--bill-ids`、`--bill-type`、`--invoice-type`、收件人信息,以及 PI 与 VATI/VATSI 各自必填的抬头字段(见上表)。
372
+ - 默认保留 CLI 的币种与发票类型校验;仅在用户理解风险时使用 `--skip-currency-check`。
373
+
374
+ **避免**:跳过订单选择;不展示 `invoice billable` 就让用户手写 id;对自定义抬头不做查重、反复 `create` 造成多条重复抬头。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.0.0-beta.26",
3
+ "version": "1.0.0-beta.28",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "type": "module",
6
6
  "bin": {