siluzan-tso-cli 1.1.30-beta.6 → 1.1.30-beta.8
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 +1 -1
- package/dist/index.js +309 -80
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/references/accounts/accounts.md +11 -4
- package/dist/skill/references/analytics/account-analytics.md +2 -2
- package/dist/skill/scripts/install.ps1 +1 -1
- package/dist/skill/scripts/install.sh +1 -1
- package/package.json +1 -1
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.30-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.30-beta.8),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -4170,11 +4170,27 @@ function defaultDateRange() {
|
|
|
4170
4170
|
start.setDate(start.getDate() - 6);
|
|
4171
4171
|
return { startDate: fmt(start), endDate: fmt(end) };
|
|
4172
4172
|
}
|
|
4173
|
+
function resolveAccountOverviewMediaRoute(cliMedia, accountMediaAccountType) {
|
|
4174
|
+
if (cliMedia === "MetaAd" && accountMediaAccountType === "FacebookAds") {
|
|
4175
|
+
return "FacebookAds";
|
|
4176
|
+
}
|
|
4177
|
+
return cliMedia;
|
|
4178
|
+
}
|
|
4179
|
+
function groupAccountIdsByOverviewRoute(cliMedia, accountIds, accountMediaTypes) {
|
|
4180
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4181
|
+
for (const id of accountIds) {
|
|
4182
|
+
const route = resolveAccountOverviewMediaRoute(cliMedia, accountMediaTypes?.get(id));
|
|
4183
|
+
const list = groups.get(route) ?? [];
|
|
4184
|
+
list.push(id);
|
|
4185
|
+
groups.set(route, list);
|
|
4186
|
+
}
|
|
4187
|
+
return groups;
|
|
4188
|
+
}
|
|
4173
4189
|
async function fetchBalanceMap(media, accountIds, config, startDate, endDate, verbose) {
|
|
4174
4190
|
const result = /* @__PURE__ */ new Map();
|
|
4175
|
-
if (accountIds.length === 0
|
|
4191
|
+
if (accountIds.length === 0) return result;
|
|
4176
4192
|
try {
|
|
4177
|
-
if (["Google", "Yandex", "TikTok", "BingV2"].includes(media)) {
|
|
4193
|
+
if (["Google", "Yandex", "TikTok", "BingV2", "MetaAd"].includes(media)) {
|
|
4178
4194
|
const params = new URLSearchParams({ ids: accountIds.join("|") });
|
|
4179
4195
|
const url = `${config.apiBaseUrl}/query/media-account/${media}/GetMediaAccountInfo?${params}`;
|
|
4180
4196
|
const raw = await apiFetch2(
|
|
@@ -4284,39 +4300,87 @@ function parseGoogleAccountSpendOverviewRows(raw) {
|
|
|
4284
4300
|
const items = Array.isArray(r.items) ? r.items : [];
|
|
4285
4301
|
return items.map(fromDbItem).filter((row) => row !== null);
|
|
4286
4302
|
}
|
|
4287
|
-
async function fetchOverviewMap(
|
|
4303
|
+
async function fetchOverviewMap(route, accountIds, config, startDate, endDate, verbose) {
|
|
4288
4304
|
const result = /* @__PURE__ */ new Map();
|
|
4289
|
-
if (accountIds.length === 0
|
|
4305
|
+
if (accountIds.length === 0) return result;
|
|
4290
4306
|
const range = defaultDateRange();
|
|
4291
4307
|
const start = startDate ?? range.startDate;
|
|
4292
4308
|
const end = endDate ?? range.endDate;
|
|
4293
4309
|
try {
|
|
4294
|
-
if (
|
|
4295
|
-
const
|
|
4310
|
+
if (route === "Google") {
|
|
4311
|
+
const params = new URLSearchParams({
|
|
4296
4312
|
startDate: start,
|
|
4297
4313
|
endDate: end,
|
|
4298
4314
|
mediaCustomerIds: accountIds.join("|")
|
|
4299
4315
|
});
|
|
4300
|
-
const
|
|
4301
|
-
const
|
|
4302
|
-
const rows = parseGoogleAccountSpendOverviewRows(
|
|
4316
|
+
const url = `${config.apiBaseUrl}/report/media-account/google/account-spend-overview?${params}`;
|
|
4317
|
+
const raw = await apiFetch2(url, config, {}, verbose);
|
|
4318
|
+
const rows = parseGoogleAccountSpendOverviewRows(raw);
|
|
4303
4319
|
for (const row of rows) {
|
|
4304
4320
|
result.set(row.mediaAccountId, row);
|
|
4305
4321
|
}
|
|
4306
4322
|
return result;
|
|
4307
4323
|
}
|
|
4308
|
-
const
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4324
|
+
const fetchLegacyBatch = async (ids) => {
|
|
4325
|
+
const batch = /* @__PURE__ */ new Map();
|
|
4326
|
+
const params = new URLSearchParams({
|
|
4327
|
+
period: "true",
|
|
4328
|
+
startDate: start,
|
|
4329
|
+
endDate: end,
|
|
4330
|
+
mediaCustomerIds: ids.join(",")
|
|
4331
|
+
});
|
|
4332
|
+
const url = `${config.apiBaseUrl}/report/media-account/${route}/accountsoverview?${params}`;
|
|
4333
|
+
const raw = await apiFetch2(url, config, {}, verbose);
|
|
4334
|
+
const items = Array.isArray(raw) ? raw : [];
|
|
4335
|
+
for (const item of items) {
|
|
4336
|
+
if (item.mediaAccountId != null) {
|
|
4337
|
+
batch.set(String(item.mediaAccountId), item);
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
return batch;
|
|
4341
|
+
};
|
|
4342
|
+
const mergeOverview = (batch) => {
|
|
4343
|
+
for (const [k, v] of batch) result.set(k, v);
|
|
4344
|
+
};
|
|
4345
|
+
if (accountIds.length === 1) {
|
|
4346
|
+
mergeOverview(await fetchLegacyBatch(accountIds));
|
|
4347
|
+
return result;
|
|
4348
|
+
}
|
|
4349
|
+
try {
|
|
4350
|
+
const batch = await fetchLegacyBatch(accountIds);
|
|
4351
|
+
mergeOverview(batch);
|
|
4352
|
+
if (batch.size === 0) {
|
|
4353
|
+
for (const id of accountIds) {
|
|
4354
|
+
try {
|
|
4355
|
+
mergeOverview(await fetchLegacyBatch([id]));
|
|
4356
|
+
} catch (singleErr) {
|
|
4357
|
+
if (verbose) {
|
|
4358
|
+
process.stderr.write(
|
|
4359
|
+
`[fetchOverviewMap] \u9010\u6237\u91CD\u8BD5\u5931\u8D25 ${id}\uFF1A${singleErr instanceof Error ? singleErr.message : String(singleErr)}
|
|
4360
|
+
`
|
|
4361
|
+
);
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
} catch (err) {
|
|
4367
|
+
if (verbose) {
|
|
4368
|
+
process.stderr.write(
|
|
4369
|
+
`[fetchOverviewMap] \u6279\u91CF\u5F02\u5E38\uFF0C\u9010\u6237\u91CD\u8BD5\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
4370
|
+
`
|
|
4371
|
+
);
|
|
4372
|
+
}
|
|
4373
|
+
for (const id of accountIds) {
|
|
4374
|
+
try {
|
|
4375
|
+
mergeOverview(await fetchLegacyBatch([id]));
|
|
4376
|
+
} catch (singleErr) {
|
|
4377
|
+
if (verbose) {
|
|
4378
|
+
process.stderr.write(
|
|
4379
|
+
`[fetchOverviewMap] \u9010\u6237\u91CD\u8BD5\u5931\u8D25 ${id}\uFF1A${singleErr instanceof Error ? singleErr.message : String(singleErr)}
|
|
4380
|
+
`
|
|
4381
|
+
);
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4320
4384
|
}
|
|
4321
4385
|
}
|
|
4322
4386
|
} catch (err) {
|
|
@@ -4339,7 +4403,8 @@ var init_balance = __esm({
|
|
|
4339
4403
|
"Yandex",
|
|
4340
4404
|
"TikTok",
|
|
4341
4405
|
"BingV2",
|
|
4342
|
-
"Kwai"
|
|
4406
|
+
"Kwai",
|
|
4407
|
+
"MetaAd"
|
|
4343
4408
|
];
|
|
4344
4409
|
}
|
|
4345
4410
|
});
|
|
@@ -4373,16 +4438,25 @@ async function fetchBalanceAndOverviewSequential(opts) {
|
|
|
4373
4438
|
const ids = chunks[chunkIdx];
|
|
4374
4439
|
const oneBased = chunkIdx + 1;
|
|
4375
4440
|
opts.onChunkStart?.(oneBased, chunks.length, ids);
|
|
4376
|
-
const
|
|
4441
|
+
const routeGroups = groupAccountIdsByOverviewRoute(
|
|
4377
4442
|
media,
|
|
4378
4443
|
ids,
|
|
4379
|
-
|
|
4380
|
-
startDate,
|
|
4381
|
-
endDate,
|
|
4382
|
-
verbose
|
|
4444
|
+
opts.accountMediaTypes
|
|
4383
4445
|
);
|
|
4384
|
-
|
|
4385
|
-
for (const [
|
|
4446
|
+
let chunkOverviewSize = 0;
|
|
4447
|
+
for (const [route, routeIds] of routeGroups) {
|
|
4448
|
+
const overviewChunk = await fetchOverviewMap(
|
|
4449
|
+
route,
|
|
4450
|
+
routeIds,
|
|
4451
|
+
config,
|
|
4452
|
+
startDate,
|
|
4453
|
+
endDate,
|
|
4454
|
+
verbose
|
|
4455
|
+
);
|
|
4456
|
+
chunkOverviewSize += overviewChunk.size;
|
|
4457
|
+
for (const [k, v] of overviewChunk) overviewMap.set(k, v);
|
|
4458
|
+
}
|
|
4459
|
+
opts.onOverviewDone?.(oneBased, chunks.length, ids, chunkOverviewSize);
|
|
4386
4460
|
const balanceChunk = await fetchBalanceMap(
|
|
4387
4461
|
media,
|
|
4388
4462
|
ids,
|
|
@@ -4403,6 +4477,143 @@ var init_media_account_batch = __esm({
|
|
|
4403
4477
|
}
|
|
4404
4478
|
});
|
|
4405
4479
|
|
|
4480
|
+
// src/utils/media-account-lookup.ts
|
|
4481
|
+
function pickMediaCustomerName(items, mediaCustomerId) {
|
|
4482
|
+
const list = Array.isArray(items) ? items : [];
|
|
4483
|
+
const target = mediaCustomerId.trim();
|
|
4484
|
+
for (const item of list) {
|
|
4485
|
+
const cid = String(item.ma?.mediaCustomerId ?? "");
|
|
4486
|
+
if (cid === target) {
|
|
4487
|
+
const name2 = String(item.ma?.mediaCustomerName ?? "").trim();
|
|
4488
|
+
return name2 || null;
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
return null;
|
|
4492
|
+
}
|
|
4493
|
+
function metaAccountIdDigits(raw) {
|
|
4494
|
+
const t = raw.trim();
|
|
4495
|
+
if (t.startsWith("act_")) return t.slice(4);
|
|
4496
|
+
return t;
|
|
4497
|
+
}
|
|
4498
|
+
function idsMatch(a, b) {
|
|
4499
|
+
return metaAccountIdDigits(a) === metaAccountIdDigits(b);
|
|
4500
|
+
}
|
|
4501
|
+
async function lookupFromMediaAccountQuery(apiBaseUrl, config, mediaType, mediaCustomerId, verbose) {
|
|
4502
|
+
const params = new URLSearchParams({
|
|
4503
|
+
MediaType: mediaType,
|
|
4504
|
+
mediaAccountState: "Approved,Linked",
|
|
4505
|
+
pageNo: "1",
|
|
4506
|
+
pageSize: "20",
|
|
4507
|
+
mediaCustomerId: mediaCustomerId.trim()
|
|
4508
|
+
});
|
|
4509
|
+
const data = await apiFetch2(
|
|
4510
|
+
`${apiBaseUrl}/query/media-account/?${params}`,
|
|
4511
|
+
config,
|
|
4512
|
+
{},
|
|
4513
|
+
verbose
|
|
4514
|
+
);
|
|
4515
|
+
return pickMediaCustomerName(data, mediaCustomerId);
|
|
4516
|
+
}
|
|
4517
|
+
async function lookupMediaCustomerName(apiBaseUrl, config, mediaType, mediaCustomerId, verbose = false) {
|
|
4518
|
+
const id = mediaCustomerId.trim();
|
|
4519
|
+
if (!id) return null;
|
|
4520
|
+
if (mediaType === "Google" || mediaType === "Yandex") {
|
|
4521
|
+
return lookupFromMediaAccountQuery(apiBaseUrl, config, mediaType, id, verbose);
|
|
4522
|
+
}
|
|
4523
|
+
return null;
|
|
4524
|
+
}
|
|
4525
|
+
async function lookupMetaMediaAccountTypes(config, accountIds, verbose = false) {
|
|
4526
|
+
const result = /* @__PURE__ */ new Map();
|
|
4527
|
+
const pending = new Set(accountIds.map((id) => id.trim()).filter(Boolean));
|
|
4528
|
+
if (pending.size === 0) return result;
|
|
4529
|
+
const CHUNK = 50;
|
|
4530
|
+
const ids = [...pending];
|
|
4531
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
4532
|
+
const chunk = ids.slice(i, i + CHUNK);
|
|
4533
|
+
const params = new URLSearchParams({
|
|
4534
|
+
MediaTypes: "MetaAd|FacebookAds",
|
|
4535
|
+
advStatus: "",
|
|
4536
|
+
mediaAccountState: "Approved,Linked",
|
|
4537
|
+
isForce: "false",
|
|
4538
|
+
pageNum: "1",
|
|
4539
|
+
pageSize: String(Math.max(20, chunk.length)),
|
|
4540
|
+
mediaCustomerIds: chunk.join(",")
|
|
4541
|
+
});
|
|
4542
|
+
try {
|
|
4543
|
+
const data = await apiFetch2(
|
|
4544
|
+
`${config.apiBaseUrl}/query/media-account/SearchMediaAcountByCriteria?${params}`,
|
|
4545
|
+
config,
|
|
4546
|
+
{},
|
|
4547
|
+
verbose
|
|
4548
|
+
);
|
|
4549
|
+
for (const item of data?.mas ?? []) {
|
|
4550
|
+
const ma = item.ma ?? {};
|
|
4551
|
+
const cid = String(ma.mediaCustomerId ?? "");
|
|
4552
|
+
const typ = String(ma.mediaAccountType ?? "").trim();
|
|
4553
|
+
if (!cid || !typ) continue;
|
|
4554
|
+
for (const inputId of chunk) {
|
|
4555
|
+
if (idsMatch(inputId, cid)) {
|
|
4556
|
+
result.set(inputId, typ);
|
|
4557
|
+
pending.delete(inputId);
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
} catch {
|
|
4562
|
+
}
|
|
4563
|
+
}
|
|
4564
|
+
for (const inputId of [...pending]) {
|
|
4565
|
+
for (const idCandidate of [inputId, `act_${metaAccountIdDigits(inputId)}`]) {
|
|
4566
|
+
const params = new URLSearchParams({
|
|
4567
|
+
MediaTypes: "MetaAd|FacebookAds",
|
|
4568
|
+
advStatus: "",
|
|
4569
|
+
mediaAccountState: "Approved,Linked",
|
|
4570
|
+
isForce: "false",
|
|
4571
|
+
pageNum: "1",
|
|
4572
|
+
pageSize: "20",
|
|
4573
|
+
mediaCustomerIds: idCandidate
|
|
4574
|
+
});
|
|
4575
|
+
try {
|
|
4576
|
+
const data = await apiFetch2(
|
|
4577
|
+
`${config.apiBaseUrl}/query/media-account/SearchMediaAcountByCriteria?${params}`,
|
|
4578
|
+
config,
|
|
4579
|
+
{},
|
|
4580
|
+
verbose
|
|
4581
|
+
);
|
|
4582
|
+
for (const item of data?.mas ?? []) {
|
|
4583
|
+
const ma = item.ma ?? {};
|
|
4584
|
+
const cid = String(ma.mediaCustomerId ?? "");
|
|
4585
|
+
if (idsMatch(inputId, cid)) {
|
|
4586
|
+
result.set(inputId, String(ma.mediaAccountType ?? "FacebookAds"));
|
|
4587
|
+
pending.delete(inputId);
|
|
4588
|
+
break;
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
} catch {
|
|
4592
|
+
}
|
|
4593
|
+
if (!pending.has(inputId)) break;
|
|
4594
|
+
}
|
|
4595
|
+
if (!result.has(inputId)) {
|
|
4596
|
+
result.set(inputId, "FacebookAds");
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
return result;
|
|
4600
|
+
}
|
|
4601
|
+
function buildAccountMediaTypesMap(items) {
|
|
4602
|
+
const map = /* @__PURE__ */ new Map();
|
|
4603
|
+
for (const item of items) {
|
|
4604
|
+
const id = item.ma?.mediaCustomerId;
|
|
4605
|
+
const typ = item.ma?.mediaAccountType;
|
|
4606
|
+
if (id && typ) map.set(String(id), String(typ));
|
|
4607
|
+
}
|
|
4608
|
+
return map;
|
|
4609
|
+
}
|
|
4610
|
+
var init_media_account_lookup = __esm({
|
|
4611
|
+
"src/utils/media-account-lookup.ts"() {
|
|
4612
|
+
"use strict";
|
|
4613
|
+
init_auth();
|
|
4614
|
+
}
|
|
4615
|
+
});
|
|
4616
|
+
|
|
4406
4617
|
// src/commands/accounts-digest/list-fetch.ts
|
|
4407
4618
|
async function fetchAccountsByMedia(media, config, opts) {
|
|
4408
4619
|
const cfg = DIGEST_PLATFORM_CONFIG[media];
|
|
@@ -4491,6 +4702,17 @@ var init_list_fetch = __esm({
|
|
|
4491
4702
|
isForce: false
|
|
4492
4703
|
},
|
|
4493
4704
|
responseType: "mas"
|
|
4705
|
+
},
|
|
4706
|
+
MetaAd: {
|
|
4707
|
+
path: "/query/media-account/SearchMediaAcountByCriteria",
|
|
4708
|
+
pageParam: "pageNum",
|
|
4709
|
+
fixedParams: {
|
|
4710
|
+
MediaTypes: "MetaAd|FacebookAds",
|
|
4711
|
+
advStatus: "",
|
|
4712
|
+
mediaAccountState: "Approved,Linked",
|
|
4713
|
+
isForce: false
|
|
4714
|
+
},
|
|
4715
|
+
responseType: "mas"
|
|
4494
4716
|
}
|
|
4495
4717
|
};
|
|
4496
4718
|
}
|
|
@@ -4509,7 +4731,7 @@ async function runAccountsDigest(opts) {
|
|
|
4509
4731
|
console.error(
|
|
4510
4732
|
`
|
|
4511
4733
|
\u274C accounts-digest \u6682\u4E0D\u652F\u6301\u5A92\u4F53\uFF1A${opts.media}
|
|
4512
|
-
\u53EF\u9009\uFF1A${BALANCE_SUPPORTED_MEDIA.join(" | ")}
|
|
4734
|
+
\u53EF\u9009\uFF1A${BALANCE_SUPPORTED_MEDIA.join(" | ")}
|
|
4513
4735
|
`
|
|
4514
4736
|
);
|
|
4515
4737
|
process.exit(1);
|
|
@@ -4523,12 +4745,16 @@ async function runAccountsDigest(opts) {
|
|
|
4523
4745
|
let accountsList;
|
|
4524
4746
|
let scannedFromList = false;
|
|
4525
4747
|
if (filterSet.size > 0) {
|
|
4748
|
+
let mediaTypes = /* @__PURE__ */ new Map();
|
|
4749
|
+
if (media === "MetaAd") {
|
|
4750
|
+
mediaTypes = await lookupMetaMediaAccountTypes(config, filterIds, opts.verbose);
|
|
4751
|
+
}
|
|
4526
4752
|
accountsList = filterIds.map((id) => ({
|
|
4527
4753
|
ma: {
|
|
4528
4754
|
entityId: "",
|
|
4529
4755
|
mediaCustomerId: id,
|
|
4530
4756
|
mediaCustomerName: null,
|
|
4531
|
-
mediaAccountType: media,
|
|
4757
|
+
mediaAccountType: mediaTypes.get(id) ?? media,
|
|
4532
4758
|
invalidOAuthToken: false
|
|
4533
4759
|
},
|
|
4534
4760
|
accepted: true,
|
|
@@ -4566,6 +4792,7 @@ async function runAccountsDigest(opts) {
|
|
|
4566
4792
|
startDate: opts.startDate,
|
|
4567
4793
|
endDate: opts.endDate,
|
|
4568
4794
|
verbose: opts.verbose,
|
|
4795
|
+
accountMediaTypes: buildAccountMediaTypesMap(accountsList),
|
|
4569
4796
|
onChunkStart: (chunkIdx, totalChunks, ids) => {
|
|
4570
4797
|
process.stderr.write(
|
|
4571
4798
|
` \u2192 [\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\uFF08${ids.length} \u6237\uFF09\u2026
|
|
@@ -4725,6 +4952,7 @@ var init_run = __esm({
|
|
|
4725
4952
|
init_cli_table();
|
|
4726
4953
|
init_currency_display();
|
|
4727
4954
|
init_media_account_batch();
|
|
4955
|
+
init_media_account_lookup();
|
|
4728
4956
|
init_list_fetch();
|
|
4729
4957
|
}
|
|
4730
4958
|
});
|
|
@@ -4735,7 +4963,7 @@ function register10(program2) {
|
|
|
4735
4963
|
"\u591A\u8D26\u6237\u6295\u653E\u753B\u50CF\uFF1A\u4E00\u6761\u547D\u4EE4\u62FF\u9F50\u6D88\u8017/\u70B9\u51FB/\u8F6C\u5316/\u4F59\u989D/\u6D3E\u751F\u6307\u6807\uFF08CTR/CPC/CPA\uFF09\uFF0C\u66FF\u4EE3 AI \u9010\u8D26\u6237 `list-accounts` + `stats` \u7684\u7F16\u6392\u3002\u5178\u578B\u573A\u666F\uFF1A\u300C\u8FD9 10 \u4E2A\u8D26\u6237\uFF08\u6216\u67D0\u5A92\u4F53\u5168\u90E8\u8D26\u6237\uFF09\u5E2E\u6211\u6574\u7406\u4E00\u4E0B\u300D\u3002"
|
|
4736
4964
|
).requiredOption(
|
|
4737
4965
|
"-m, --media <type>",
|
|
4738
|
-
"\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Yandex | BingV2 | Kwai
|
|
4966
|
+
"\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Yandex | MetaAd | BingV2 | Kwai"
|
|
4739
4967
|
).option(
|
|
4740
4968
|
"-a, --accounts <ids>",
|
|
4741
4969
|
"\u6307\u5B9A\u8D26\u6237 mediaCustomerId\uFF0C\u9017\u53F7\u5206\u9694\uFF1B\u7559\u7A7A\u5219\u626B\u63CF\u8BE5\u5A92\u4F53\u4E0B\u5168\u90E8\u8D26\u6237"
|
|
@@ -105320,6 +105548,7 @@ init_balance();
|
|
|
105320
105548
|
init_cli_table();
|
|
105321
105549
|
init_currency_display();
|
|
105322
105550
|
init_media_account_batch();
|
|
105551
|
+
init_media_account_lookup();
|
|
105323
105552
|
|
|
105324
105553
|
// src/commands/balance-scan/list-fetch.ts
|
|
105325
105554
|
init_auth();
|
|
@@ -105369,6 +105598,17 @@ var SCAN_PLATFORM_CONFIG = {
|
|
|
105369
105598
|
isForce: false
|
|
105370
105599
|
},
|
|
105371
105600
|
responseType: "mas"
|
|
105601
|
+
},
|
|
105602
|
+
MetaAd: {
|
|
105603
|
+
path: "/query/media-account/SearchMediaAcountByCriteria",
|
|
105604
|
+
pageParam: "pageNum",
|
|
105605
|
+
fixedParams: {
|
|
105606
|
+
MediaTypes: "MetaAd|FacebookAds",
|
|
105607
|
+
advStatus: "",
|
|
105608
|
+
mediaAccountState: "Approved,Linked",
|
|
105609
|
+
isForce: false
|
|
105610
|
+
},
|
|
105611
|
+
responseType: "mas"
|
|
105372
105612
|
}
|
|
105373
105613
|
};
|
|
105374
105614
|
async function fetchOnePage(media, page, pageSize, config, verbose) {
|
|
@@ -105472,7 +105712,7 @@ async function runBalanceScan(opts) {
|
|
|
105472
105712
|
console.error(
|
|
105473
105713
|
`
|
|
105474
105714
|
\u274C balance scan \u6682\u4E0D\u652F\u6301\u5A92\u4F53\uFF1A${opts.media}
|
|
105475
|
-
\u53EF\u9009\uFF1A${BALANCE_SUPPORTED_MEDIA.join(" | ")}
|
|
105715
|
+
\u53EF\u9009\uFF1A${BALANCE_SUPPORTED_MEDIA.join(" | ")}
|
|
105476
105716
|
`
|
|
105477
105717
|
);
|
|
105478
105718
|
process.exit(1);
|
|
@@ -105489,12 +105729,16 @@ async function runBalanceScan(opts) {
|
|
|
105489
105729
|
let allItems;
|
|
105490
105730
|
let total;
|
|
105491
105731
|
if (isSubset) {
|
|
105732
|
+
let mediaTypes = /* @__PURE__ */ new Map();
|
|
105733
|
+
if (media === "MetaAd") {
|
|
105734
|
+
mediaTypes = await lookupMetaMediaAccountTypes(config, filterIds, opts.verbose);
|
|
105735
|
+
}
|
|
105492
105736
|
allItems = filterIds.map((id) => ({
|
|
105493
105737
|
ma: {
|
|
105494
105738
|
entityId: "",
|
|
105495
105739
|
mediaCustomerId: id,
|
|
105496
105740
|
mediaCustomerName: null,
|
|
105497
|
-
mediaAccountType: media,
|
|
105741
|
+
mediaAccountType: mediaTypes.get(id) ?? media,
|
|
105498
105742
|
invalidOAuthToken: false
|
|
105499
105743
|
},
|
|
105500
105744
|
accepted: true,
|
|
@@ -105532,6 +105776,7 @@ async function runBalanceScan(opts) {
|
|
|
105532
105776
|
accountIds: validIds,
|
|
105533
105777
|
config,
|
|
105534
105778
|
verbose: opts.verbose,
|
|
105779
|
+
accountMediaTypes: buildAccountMediaTypesMap(allItems),
|
|
105535
105780
|
onChunkStart: (chunkIdx, totalChunks, ids) => {
|
|
105536
105781
|
process.stderr.write(
|
|
105537
105782
|
` \u2192 [\u8FD17\u65E5\u6D88\u8017] \u7B2C ${chunkIdx}/${totalChunks} \u6279\u8BF7\u6C42\u4E2D\uFF08${ids.length} \u6237\uFF09\u2026
|
|
@@ -105691,7 +105936,7 @@ function register9(program2) {
|
|
|
105691
105936
|
"\u4E00\u952E\u626B\u63CF\u67D0\u5A92\u4F53\u4E0B\u6240\u6709\u8D26\u6237\u7684\u4F59\u989D\uFF0C\u7B5B\u51FA\u7EED\u822A\u5929\u6570\u4E0D\u8DB3\u7684\u8D26\u6237\u3002\u66FF\u4EE3\u5FAA\u73AF\u8C03\u7528 balance \u7684\u4F20\u7EDF\u505A\u6CD5\uFF0C\u5178\u578B\u573A\u666F\uFF1A\u300C\u5E2E\u6211\u627E\u51FA 117 \u4E2A Bing \u8D26\u6237\u91CC\u4F59\u989D\u4E0D\u8DB3 7 \u5929\u7684\u300D\u3002"
|
|
105692
105937
|
).requiredOption(
|
|
105693
105938
|
"-m, --media <type>",
|
|
105694
|
-
"\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Yandex | BingV2 | Kwai
|
|
105939
|
+
"\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Yandex | MetaAd | BingV2 | Kwai"
|
|
105695
105940
|
).option(
|
|
105696
105941
|
"-a, --accounts <ids>",
|
|
105697
105942
|
"\u6307\u5B9A mediaCustomerId\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF1B\u8DF3\u8FC7\u6E05\u5355\u7FFB\u9875\uFF0C\u62C9\u53D6\u540E\u5168\u90E8\u8F93\u51FA\uFF08\u4E0D\u6309\u9608\u503C\u8FC7\u6EE4\uFF09"
|
|
@@ -105743,6 +105988,7 @@ init_auth();
|
|
|
105743
105988
|
init_cli_json_snapshot();
|
|
105744
105989
|
init_cli_table();
|
|
105745
105990
|
init_balance();
|
|
105991
|
+
init_media_account_lookup();
|
|
105746
105992
|
var VALID_MEDIA_TYPES3 = ["Google", "TikTok", "Yandex", "MetaAd", "BingV2", "Kwai"];
|
|
105747
105993
|
function defaultDateRange2() {
|
|
105748
105994
|
const end = /* @__PURE__ */ new Date();
|
|
@@ -105780,15 +106026,12 @@ async function runStats(opts) {
|
|
|
105780
106026
|
}
|
|
105781
106027
|
const ids = opts.accounts.split(",").map((id) => id.trim()).filter(Boolean);
|
|
105782
106028
|
const isGoogle = opts.media === "Google";
|
|
105783
|
-
const
|
|
105784
|
-
const p = new URLSearchParams({ startDate, endDate, period: "true" });
|
|
105785
|
-
p.set("mediaCustomerIds", ids.join(","));
|
|
105786
|
-
return p;
|
|
105787
|
-
})();
|
|
105788
|
-
const url = isGoogle ? `${config.apiBaseUrl}/report/media-account/google/account-spend-overview?${params.toString()}` : `${config.apiBaseUrl}/report/media-account/${opts.media}/accountsoverview?${params.toString()}`;
|
|
106029
|
+
const isMetaAd = opts.media === "MetaAd";
|
|
105789
106030
|
let items;
|
|
105790
106031
|
try {
|
|
105791
106032
|
if (isGoogle) {
|
|
106033
|
+
const params = new URLSearchParams({ startDate, endDate, mediaCustomerIds: ids.join("|") });
|
|
106034
|
+
const url = `${config.apiBaseUrl}/report/media-account/google/account-spend-overview?${params.toString()}`;
|
|
105792
106035
|
const raw = await apiFetch2(url, config, {}, opts.verbose);
|
|
105793
106036
|
items = parseGoogleAccountSpendOverviewRows(raw).map((row) => ({
|
|
105794
106037
|
mediaAccountId: row.mediaAccountId,
|
|
@@ -105802,7 +106045,30 @@ async function runStats(opts) {
|
|
|
105802
106045
|
remainingAccountBudget: row.remainingAccountBudget,
|
|
105803
106046
|
status: row.status
|
|
105804
106047
|
}));
|
|
106048
|
+
} else if (isMetaAd) {
|
|
106049
|
+
const accountMediaTypes = await lookupMetaMediaAccountTypes(config, ids, opts.verbose);
|
|
106050
|
+
items = [];
|
|
106051
|
+
for (const [route, routeIds] of groupAccountIdsByOverviewRoute(
|
|
106052
|
+
"MetaAd",
|
|
106053
|
+
ids,
|
|
106054
|
+
accountMediaTypes
|
|
106055
|
+
)) {
|
|
106056
|
+
const map = await fetchOverviewMap(
|
|
106057
|
+
route,
|
|
106058
|
+
routeIds,
|
|
106059
|
+
config,
|
|
106060
|
+
startDate,
|
|
106061
|
+
endDate,
|
|
106062
|
+
opts.verbose
|
|
106063
|
+
);
|
|
106064
|
+
for (const item of map.values()) {
|
|
106065
|
+
items.push(item);
|
|
106066
|
+
}
|
|
106067
|
+
}
|
|
105805
106068
|
} else {
|
|
106069
|
+
const params = new URLSearchParams({ startDate, endDate, period: "true" });
|
|
106070
|
+
params.set("mediaCustomerIds", ids.join(","));
|
|
106071
|
+
const url = `${config.apiBaseUrl}/report/media-account/${opts.media}/accountsoverview?${params.toString()}`;
|
|
105806
106072
|
const raw = await apiFetch2(url, config, {}, opts.verbose);
|
|
105807
106073
|
items = Array.isArray(raw) ? raw : raw.items ?? [];
|
|
105808
106074
|
}
|
|
@@ -111664,46 +111930,8 @@ function tryLoadCampaignCreateConfig(configFile) {
|
|
|
111664
111930
|
return stripMetaKeys(raw);
|
|
111665
111931
|
}
|
|
111666
111932
|
|
|
111667
|
-
// src/utils/media-account-lookup.ts
|
|
111668
|
-
init_auth();
|
|
111669
|
-
function pickMediaCustomerName(items, mediaCustomerId) {
|
|
111670
|
-
const list = Array.isArray(items) ? items : [];
|
|
111671
|
-
const target = mediaCustomerId.trim();
|
|
111672
|
-
for (const item of list) {
|
|
111673
|
-
const cid = String(item.ma?.mediaCustomerId ?? "");
|
|
111674
|
-
if (cid === target) {
|
|
111675
|
-
const name2 = String(item.ma?.mediaCustomerName ?? "").trim();
|
|
111676
|
-
return name2 || null;
|
|
111677
|
-
}
|
|
111678
|
-
}
|
|
111679
|
-
return null;
|
|
111680
|
-
}
|
|
111681
|
-
async function lookupFromMediaAccountQuery(apiBaseUrl, config, mediaType, mediaCustomerId, verbose) {
|
|
111682
|
-
const params = new URLSearchParams({
|
|
111683
|
-
MediaType: mediaType,
|
|
111684
|
-
mediaAccountState: "Approved,Linked",
|
|
111685
|
-
pageNo: "1",
|
|
111686
|
-
pageSize: "20",
|
|
111687
|
-
mediaCustomerId: mediaCustomerId.trim()
|
|
111688
|
-
});
|
|
111689
|
-
const data = await apiFetch2(
|
|
111690
|
-
`${apiBaseUrl}/query/media-account/?${params}`,
|
|
111691
|
-
config,
|
|
111692
|
-
{},
|
|
111693
|
-
verbose
|
|
111694
|
-
);
|
|
111695
|
-
return pickMediaCustomerName(data, mediaCustomerId);
|
|
111696
|
-
}
|
|
111697
|
-
async function lookupMediaCustomerName(apiBaseUrl, config, mediaType, mediaCustomerId, verbose = false) {
|
|
111698
|
-
const id = mediaCustomerId.trim();
|
|
111699
|
-
if (!id) return null;
|
|
111700
|
-
if (mediaType === "Google" || mediaType === "Yandex") {
|
|
111701
|
-
return lookupFromMediaAccountQuery(apiBaseUrl, config, mediaType, id, verbose);
|
|
111702
|
-
}
|
|
111703
|
-
return null;
|
|
111704
|
-
}
|
|
111705
|
-
|
|
111706
111933
|
// src/commands/ad/campaign-customer-name.ts
|
|
111934
|
+
init_media_account_lookup();
|
|
111707
111935
|
var CampaignCustomerNameResolveError = class extends Error {
|
|
111708
111936
|
constructor(accountId) {
|
|
111709
111937
|
super(
|
|
@@ -113932,6 +114160,7 @@ async function runAdGeoSetBid(opts) {
|
|
|
113932
114160
|
|
|
113933
114161
|
// src/commands/ad/ai-creation.ts
|
|
113934
114162
|
init_auth();
|
|
114163
|
+
init_media_account_lookup();
|
|
113935
114164
|
init_cli_json_snapshot();
|
|
113936
114165
|
init_cli_table();
|
|
113937
114166
|
|
package/dist/skill/_meta.json
CHANGED
|
@@ -31,6 +31,10 @@ siluzan-tso list-accounts [选项]
|
|
|
31
31
|
| 列出全部某媒体账户 | `list-accounts -m <媒体> --page-size 999 --json-out <dir>` | `items[]`(`ma.mediaCustomerId`、`ma.mediaCustomerName`、`ma.currencyCode`、`ma.mediaAccountState` 等) |
|
|
32
32
|
| 有多少个账户 | 同上 | **`total`**(无需翻页;`itemCount < total` 时说明 page-size 不够大) |
|
|
33
33
|
| 只查某一个户 | `list-accounts -m <媒体> -k <id或名称> --json-out <dir>` | 无需大 page-size |
|
|
34
|
+
| **Meta 全部账户 + 余额/消耗** | **`accounts-digest -m MetaAd --json-out <dir>`**(一步;内部翻页+分批) | `accounts-digest-metaad.json` → `data.items[]`(含 `balance`、`spend`) |
|
|
35
|
+
| Meta 余额续航预警 | `balance-scan -m MetaAd --json-out <dir>` | `balance-scan-metaad.json` |
|
|
36
|
+
|
|
37
|
+
> **MetaAd ID 格式**:OAuth 授权户(`list-accounts` 里 `mediaAccountType=FacebookAds`)的 `mediaCustomerId` **须带 `act_` 前缀**(与列表返回值一致)。**禁止**把 70+ 个 ID 拼成一条 `balance -a …`;全量用 `accounts-digest`。
|
|
34
38
|
|
|
35
39
|
仅当读盘后 `total > itemCount` 且已用 `--page-size 999` 时,再 `--page 2` 等同参数补拉;**禁止**对 stdout 写翻页循环(stdout 摘要无 `total` / `items`,读盘协议见 `references/core/agent-conventions.md` §三)。列账户 / 数个数**不需要** `accounts-digest`、`balance-scan`。
|
|
36
40
|
|
|
@@ -116,7 +120,7 @@ siluzan-tso account-active-bills -m Google --id 18176820-6204-43c2-9a1f-0d0f5e9e
|
|
|
116
120
|
siluzan-tso account-active-bills -m Google --id 18176820-6204-43c2-9a1f-0d0f5e9eb957 --json-out ./snap
|
|
117
121
|
```
|
|
118
122
|
|
|
119
|
-
> **勿在文档或聊天中粘贴真实 JWT
|
|
123
|
+
> **勿在文档或聊天中粘贴真实 JWT。
|
|
120
124
|
|
|
121
125
|
---
|
|
122
126
|
|
|
@@ -128,7 +132,7 @@ siluzan-tso balance -m <媒体类型> -a <账户ID列表>
|
|
|
128
132
|
|
|
129
133
|
| 选项 | 说明 |
|
|
130
134
|
| ---------------------- | ------------------------------------------------------------------------------------ |
|
|
131
|
-
| `-m, --media <type>` |
|
|
135
|
+
| `-m, --media <type>` | 媒体类型(必填):`Google \| TikTok \| Yandex \| MetaAd \| BingV2 \| Kwai`(MetaAd 走 `GetMediaAccountInfo`,余额字段为 `spend_cap`) |
|
|
132
136
|
| `-a, --accounts <ids>` | 账户 `mediaCustomerId`(数字 ID),多个用逗号分隔(必填)。**注意:不是 `entityId`** |
|
|
133
137
|
| `--json-out` | 输出原始 JSON;不支持或查询失败时 stdout 为 `{"ok":false,"error":"..."}` |
|
|
134
138
|
|
|
@@ -141,6 +145,9 @@ siluzan-tso balance -m Google -a 6326027735
|
|
|
141
145
|
# 查询多个 TikTok 账户余额
|
|
142
146
|
siluzan-tso balance -m TikTok -a 1234567890,9876543210
|
|
143
147
|
|
|
148
|
+
# 查询 Meta 广告账户余额
|
|
149
|
+
siluzan-tso balance -m MetaAd -a <mediaCustomerId>
|
|
150
|
+
|
|
144
151
|
# JSON 输出,供脚本使用
|
|
145
152
|
siluzan-tso balance -m Google -a 6326027735 --json-out ./snap
|
|
146
153
|
```
|
|
@@ -153,7 +160,7 @@ siluzan-tso balance -m Google -a 6326027735 --json-out ./snap
|
|
|
153
160
|
|
|
154
161
|
一条命令替代 AI 对每个账户循环 `list-accounts -k` + `stats`。**多账户汇总表、对比消耗、跨账户巡检** 应优先本命令,禁止外层 for-loop 逐户 `stats`。
|
|
155
162
|
|
|
156
|
-
> **数据时效性**:与 `stats` / `balance-scan` 相同(Google `account-spend-overview` 分流;TikTok/Yandex/BingV2/Kwai 为截至昨天的 `accountsoverview`)。完整表见 `references/analytics/account-analytics.md` 顶部。
|
|
163
|
+
> **数据时效性**:与 `stats` / `balance-scan` 相同(Google `account-spend-overview` 分流;TikTok/Yandex/BingV2/Kwai/**MetaAd** 为截至昨天的 `accountsoverview`)。完整表见 `references/analytics/account-analytics.md` 顶部。
|
|
157
164
|
|
|
158
165
|
```bash
|
|
159
166
|
siluzan-tso accounts-digest -m <媒体类型> [选项]
|
|
@@ -161,7 +168,7 @@ siluzan-tso accounts-digest -m <媒体类型> [选项]
|
|
|
161
168
|
|
|
162
169
|
| 选项 | 说明 | 默认 |
|
|
163
170
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------- | ------- |
|
|
164
|
-
| `-m, --media <type>` | 媒体类型(必填):`Google \| TikTok \| Yandex \| BingV2 \| Kwai
|
|
171
|
+
| `-m, --media <type>` | 媒体类型(必填):`Google \| TikTok \| Yandex \| MetaAd \| BingV2 \| Kwai` | — |
|
|
165
172
|
| `-a, --accounts <ids>` | 指定 `mediaCustomerId`,逗号分隔;**留空**则翻页拉该媒体全部账户 | — |
|
|
166
173
|
| `--start <YYYY-MM-DD>` | 统计开始日期(SKILL 要求 AI 先与用户确认区间) | 近 7 天 |
|
|
167
174
|
| `--end <YYYY-MM-DD>` | 统计结束日期 | 昨天 |
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
| `google-analysis --sections campaign-types`(`types-summary`) | **实时** | ✅ 可查当天 | 当天系列类型分布 |
|
|
15
15
|
| `google-analysis` 其他维度(`campaigns` / `keywords` / `devices` / ...) | 实时(受 Google Ads API 同步延迟影响) | 可查当天,但当天可能尚未结算 | 周期分析、报告 |
|
|
16
16
|
| `stats -m Google` / `balance-scan -m Google` / `accounts-digest -m Google` / `list-accounts -m Google` 合并消耗(TSO `account-spend-overview`,2026-05 起) | **后端自动分流**:窗口完全在历史 → `database` 模式(含余额/状态/币种/账户名 + 当期消耗);窗口含今天 → `googleCombined` 模式(仅实时消耗,无余额/状态/币种/账户名) | ✅ 可查当天(含今天时切实时聚合) | 历史回溯、巡检、余额续航估算;含今天时也能给出实时消耗 |
|
|
17
|
-
| `stats` / `balance-scan` / `accounts-digest` / `list-accounts` 的 **TikTok / Yandex / BingV2 / Kwai** 合并消耗(TSO `accountsoverview
|
|
17
|
+
| `stats` / `balance-scan` / `accounts-digest` / `list-accounts` 的 **TikTok / Yandex / BingV2 / Kwai / MetaAd** 合并消耗(TSO `accountsoverview`;MetaAd OAuth 户走 `FacebookAds` 段) | **每日同步昨天** | ❌ 查今天会全为 0 | 历史回溯、巡检、余额续航估算(口径为"截至昨天") |
|
|
18
18
|
| `balance`(`GetMediaAccountInfo`) | 实时 | — | 仅当前余额,不反映消耗 |
|
|
19
19
|
|
|
20
20
|
**选用规则**:
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
- 「今天/当天/今日消耗」「实时消耗排行」 → 优先 `google-analysis(-batch) --sections overview`,`--start` / `--end` 都设为今天;
|
|
23
23
|
- Google 单账户/批量取数也可直接 `stats -m Google` 把 `--end-date` 设为今天,后端会切到 `googleCombined` 模式给实时消耗(但**不会**返回余额/币种/账户名)。
|
|
24
24
|
- 「最近 N 天消耗 / 周报 / 月报 / 余额续航」 → `stats` / `balance-scan` / `accounts-digest`,默认窗口截至昨天即可(Google 此时走 `database`,包含完整字段)。
|
|
25
|
-
- **禁止**用 TikTok / Yandex / BingV2 / Kwai 的 `accountsoverview` 接口判断当天消耗(仍是每日同步)。
|
|
25
|
+
- **禁止**用 TikTok / Yandex / BingV2 / Kwai / MetaAd 的 `accountsoverview` 接口判断当天消耗(仍是每日同步)。
|
|
26
26
|
- **禁止**给非 Google 媒体的当天高消耗场景加 `--min-spend`:其预筛选来自非实时 `accountsoverview`,会把今天有消耗的账号当成 0 给筛掉。Google 媒体当 `--end-date` 设为今天时,预筛选走的是 `googleCombined` 实时数据,可以使用 `--min-spend`。
|
|
27
27
|
|
|
28
28
|
---
|
|
@@ -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.30-beta.
|
|
12
|
+
$PKG_VERSION = '1.1.30-beta.8'
|
|
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.30-beta.
|
|
12
|
+
readonly PKG_VERSION="1.1.30-beta.8"
|
|
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"
|