siluzan-tso-cli 1.1.56-beta.4 → 1.1.57-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +207 -5
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/references/accounts/accounts-balance-stats.md +23 -0
- package/dist/skill/references/core/agent-conventions.md +1 -0
- package/dist/skill/references/core/playbooks.md +3 -1
- package/dist/skill/references/core/workflows.md +1 -1
- 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.
|
|
54
|
+
> **注意**:当前为测试版(1.1.57-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -5376,6 +5376,119 @@ var init_brief = __esm({
|
|
|
5376
5376
|
}
|
|
5377
5377
|
});
|
|
5378
5378
|
|
|
5379
|
+
// src/commands/accounts-digest/campaign-types.ts
|
|
5380
|
+
function presenceCount(value) {
|
|
5381
|
+
if (value == null || value === false || value === "") return 0;
|
|
5382
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
|
|
5383
|
+
if (typeof value === "string") {
|
|
5384
|
+
const n = Number(value);
|
|
5385
|
+
if (value.trim() !== "" && Number.isFinite(n)) return n;
|
|
5386
|
+
return 0;
|
|
5387
|
+
}
|
|
5388
|
+
if (Array.isArray(value)) return value.length;
|
|
5389
|
+
if (typeof value === "object") {
|
|
5390
|
+
const obj = value;
|
|
5391
|
+
for (const key of COUNT_KEYS) {
|
|
5392
|
+
if (key in obj) return presenceCount(obj[key]);
|
|
5393
|
+
}
|
|
5394
|
+
return Object.keys(obj).length > 0 ? 1 : 0;
|
|
5395
|
+
}
|
|
5396
|
+
return 0;
|
|
5397
|
+
}
|
|
5398
|
+
function summarizeCampaignTypes(record2, depth = 0) {
|
|
5399
|
+
const empty = {
|
|
5400
|
+
campaignTypesStatus: "ok",
|
|
5401
|
+
campaignTypes: [],
|
|
5402
|
+
hasDisplay: false,
|
|
5403
|
+
hasPmax: false,
|
|
5404
|
+
campaignTypesError: null
|
|
5405
|
+
};
|
|
5406
|
+
if (record2 == null || typeof record2 !== "object" || Array.isArray(record2)) return empty;
|
|
5407
|
+
const bag = record2;
|
|
5408
|
+
const types = [];
|
|
5409
|
+
let hasDisplay = false;
|
|
5410
|
+
let hasPmax = false;
|
|
5411
|
+
for (const [key, value] of Object.entries(bag)) {
|
|
5412
|
+
if (key === "ok" || key === "meta" || key.startsWith("_")) continue;
|
|
5413
|
+
if (NESTED_KEYS.includes(key)) continue;
|
|
5414
|
+
if (presenceCount(value) <= 0) continue;
|
|
5415
|
+
types.push(key);
|
|
5416
|
+
if (DISPLAY_KEY.test(key)) hasDisplay = true;
|
|
5417
|
+
if (PMAX_KEY.test(key)) hasPmax = true;
|
|
5418
|
+
}
|
|
5419
|
+
if (types.length === 0 && depth === 0) {
|
|
5420
|
+
for (const nest of NESTED_KEYS) {
|
|
5421
|
+
const inner = bag[nest];
|
|
5422
|
+
if (inner && typeof inner === "object") return summarizeCampaignTypes(inner, 1);
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
return {
|
|
5426
|
+
campaignTypesStatus: "ok",
|
|
5427
|
+
campaignTypes: types,
|
|
5428
|
+
hasDisplay,
|
|
5429
|
+
hasPmax,
|
|
5430
|
+
campaignTypesError: null
|
|
5431
|
+
};
|
|
5432
|
+
}
|
|
5433
|
+
function skippedNoSpendCampaignTypes() {
|
|
5434
|
+
return {
|
|
5435
|
+
campaignTypesStatus: "skipped-no-spend",
|
|
5436
|
+
campaignTypes: null,
|
|
5437
|
+
hasDisplay: null,
|
|
5438
|
+
hasPmax: null,
|
|
5439
|
+
campaignTypesError: null
|
|
5440
|
+
};
|
|
5441
|
+
}
|
|
5442
|
+
function errorCampaignTypes(message) {
|
|
5443
|
+
const text2 = message.replace(/\s+/g, " ").trim().slice(0, 180);
|
|
5444
|
+
return {
|
|
5445
|
+
campaignTypesStatus: "error",
|
|
5446
|
+
campaignTypes: null,
|
|
5447
|
+
hasDisplay: null,
|
|
5448
|
+
hasPmax: null,
|
|
5449
|
+
campaignTypesError: text2 || "unknown error"
|
|
5450
|
+
};
|
|
5451
|
+
}
|
|
5452
|
+
function errorMessage(err) {
|
|
5453
|
+
if (err instanceof Error) return err.message;
|
|
5454
|
+
return String(err);
|
|
5455
|
+
}
|
|
5456
|
+
async function fetchCampaignTypeSummaries(accountIds, fetchOne, opts) {
|
|
5457
|
+
const ids = [...new Set(accountIds.filter((id) => id.trim() !== ""))];
|
|
5458
|
+
const out = /* @__PURE__ */ new Map();
|
|
5459
|
+
if (ids.length === 0) return out;
|
|
5460
|
+
const concurrency = Math.max(1, Math.min(opts?.concurrency ?? 8, 16, ids.length));
|
|
5461
|
+
let cursor = 0;
|
|
5462
|
+
let done = 0;
|
|
5463
|
+
const workers = new Array(concurrency).fill(0).map(async () => {
|
|
5464
|
+
while (true) {
|
|
5465
|
+
const index = cursor++;
|
|
5466
|
+
if (index >= ids.length) return;
|
|
5467
|
+
const id = ids[index];
|
|
5468
|
+
try {
|
|
5469
|
+
const record2 = await fetchOne(id);
|
|
5470
|
+
out.set(id, summarizeCampaignTypes(record2));
|
|
5471
|
+
} catch (err) {
|
|
5472
|
+
out.set(id, errorCampaignTypes(errorMessage(err)));
|
|
5473
|
+
}
|
|
5474
|
+
done += 1;
|
|
5475
|
+
opts?.onProgress?.(done, ids.length);
|
|
5476
|
+
}
|
|
5477
|
+
});
|
|
5478
|
+
await Promise.all(workers);
|
|
5479
|
+
return out;
|
|
5480
|
+
}
|
|
5481
|
+
var DISPLAY_KEY, PMAX_KEY, NESTED_KEYS, COUNT_KEYS;
|
|
5482
|
+
var init_campaign_types = __esm({
|
|
5483
|
+
"src/commands/accounts-digest/campaign-types.ts"() {
|
|
5484
|
+
"use strict";
|
|
5485
|
+
DISPLAY_KEY = /^(display|responsive_display|display_network)$/i;
|
|
5486
|
+
PMAX_KEY = /^(performance_?max|performancemax)$/i;
|
|
5487
|
+
NESTED_KEYS = ["data", "record", "types", "typesSummary"];
|
|
5488
|
+
COUNT_KEYS = ["count", "total", "campaignCount", "value", "campaigns"];
|
|
5489
|
+
}
|
|
5490
|
+
});
|
|
5491
|
+
|
|
5379
5492
|
// src/commands/accounts-digest/filters.ts
|
|
5380
5493
|
function keepDigestRow(row, opts) {
|
|
5381
5494
|
const { maxCpa, zeroConversions } = opts;
|
|
@@ -5404,6 +5517,18 @@ var init_filters = __esm({
|
|
|
5404
5517
|
});
|
|
5405
5518
|
|
|
5406
5519
|
// src/commands/accounts-digest/run.ts
|
|
5520
|
+
function formatCampaignTypesCell(row) {
|
|
5521
|
+
if (row.campaignTypesStatus === "ok") {
|
|
5522
|
+
const flags = [
|
|
5523
|
+
row.hasDisplay ? "Display" : "\u65E0Display",
|
|
5524
|
+
row.hasPmax ? "PMax" : "\u65E0PMax"
|
|
5525
|
+
];
|
|
5526
|
+
return flags.join(" ");
|
|
5527
|
+
}
|
|
5528
|
+
if (row.campaignTypesStatus === "skipped-no-spend") return "\u672A\u67E5(\u65E0\u6D88\u8017)";
|
|
5529
|
+
if (row.campaignTypesStatus === "error") return "\u67E5\u8BE2\u5931\u8D25";
|
|
5530
|
+
return "-";
|
|
5531
|
+
}
|
|
5407
5532
|
function round2(n) {
|
|
5408
5533
|
if (n == null) return null;
|
|
5409
5534
|
const v = Number(n);
|
|
@@ -5422,6 +5547,16 @@ async function runAccountsDigest(opts) {
|
|
|
5422
5547
|
process.exit(1);
|
|
5423
5548
|
}
|
|
5424
5549
|
const media = opts.media;
|
|
5550
|
+
if (opts.withCampaignTypes && media !== "Google") {
|
|
5551
|
+
console.error(
|
|
5552
|
+
"\n\u274C --with-campaign-types \u53EA\u652F\u6301 -m Google\uFF08\u7CFB\u5217\u7C7B\u578B\u6765\u81EA Google campaigns/types-summary\uFF09\u3002\n"
|
|
5553
|
+
);
|
|
5554
|
+
process.exit(1);
|
|
5555
|
+
}
|
|
5556
|
+
if (opts.withCampaignTypes && !config.googleApiUrl) {
|
|
5557
|
+
console.error("\n\u274C \u5F53\u524D\u914D\u7F6E\u6CA1\u6709 googleApiUrl\uFF0C\u65E0\u6CD5\u62C9\u53D6\u5E7F\u544A\u7CFB\u5217\u7C7B\u578B\u3002\n");
|
|
5558
|
+
process.exit(1);
|
|
5559
|
+
}
|
|
5425
5560
|
const filterIds = (opts.accounts ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
5426
5561
|
const filterSet = new Set(filterIds);
|
|
5427
5562
|
const minSpend = opts.minSpend ?? 0;
|
|
@@ -5565,6 +5700,56 @@ async function runAccountsDigest(opts) {
|
|
|
5565
5700
|
rows.push(row);
|
|
5566
5701
|
}
|
|
5567
5702
|
rows.sort((a, b) => (b.spend ?? 0) - (a.spend ?? 0));
|
|
5703
|
+
let campaignTypeMeta = null;
|
|
5704
|
+
if (opts.withCampaignTypes) {
|
|
5705
|
+
const concurrency = Math.max(1, Math.min(opts.campaignTypeConcurrency ?? 8, 16));
|
|
5706
|
+
const spendIds = rows.filter((row) => (row.spend ?? 0) > 0).map((row) => row.mediaCustomerId);
|
|
5707
|
+
process.stderr.write(
|
|
5708
|
+
`\u23F3 [accounts-digest] \u7CFB\u5217\u7C7B\u578B\uFF1A${spendIds.length} \u4E2A\u6709\u6D88\u8017\u8D26\u6237\uFF0C\u5E76\u53D1 ${concurrency}\uFF08\u6D88\u8017\u4E3A 0 \u7684\u4E0D\u8BF7\u6C42\uFF09\u3002
|
|
5709
|
+
`
|
|
5710
|
+
);
|
|
5711
|
+
const summaries = await fetchCampaignTypeSummaries(
|
|
5712
|
+
spendIds,
|
|
5713
|
+
(accountId) => {
|
|
5714
|
+
const url = `${config.googleApiUrl}/reporting/media-account/${encodeURIComponent(accountId)}/campaigns/types-summary`;
|
|
5715
|
+
return apiFetch2(url, config, {}, !!opts.verbose);
|
|
5716
|
+
},
|
|
5717
|
+
{
|
|
5718
|
+
concurrency,
|
|
5719
|
+
onProgress: (done, total) => {
|
|
5720
|
+
if (done === total || done % 50 === 0) {
|
|
5721
|
+
process.stderr.write(` \u2713 [\u7CFB\u5217\u7C7B\u578B] ${done}/${total}
|
|
5722
|
+
`);
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
);
|
|
5727
|
+
let ok = 0;
|
|
5728
|
+
let failed = 0;
|
|
5729
|
+
let skippedNoSpend = 0;
|
|
5730
|
+
for (const row of rows) {
|
|
5731
|
+
const fields = (row.spend ?? 0) > 0 ? summaries.get(row.mediaCustomerId) ?? errorCampaignTypes("\u672A\u8FD4\u56DE\u8BE5\u8D26\u6237\u7684\u7CFB\u5217\u7C7B\u578B") : skippedNoSpendCampaignTypes();
|
|
5732
|
+
row.campaignTypesStatus = fields.campaignTypesStatus;
|
|
5733
|
+
row.campaignTypes = fields.campaignTypes;
|
|
5734
|
+
row.hasDisplay = fields.hasDisplay;
|
|
5735
|
+
row.hasPmax = fields.hasPmax;
|
|
5736
|
+
row.campaignTypesError = fields.campaignTypesError;
|
|
5737
|
+
if (fields.campaignTypesStatus === "ok") ok += 1;
|
|
5738
|
+
else if (fields.campaignTypesStatus === "error") failed += 1;
|
|
5739
|
+
else skippedNoSpend += 1;
|
|
5740
|
+
}
|
|
5741
|
+
campaignTypeMeta = {
|
|
5742
|
+
requested: spendIds.length,
|
|
5743
|
+
ok,
|
|
5744
|
+
failed,
|
|
5745
|
+
skippedNoSpend,
|
|
5746
|
+
concurrency
|
|
5747
|
+
};
|
|
5748
|
+
process.stderr.write(
|
|
5749
|
+
` \u2713 [\u7CFB\u5217\u7C7B\u578B] \u5B8C\u6210\uFF1A\u6210\u529F ${ok}\uFF0C\u5931\u8D25 ${failed}\uFF0C\u672A\u67E5\uFF08\u65E0\u6D88\u8017\uFF09${skippedNoSpend}
|
|
5750
|
+
`
|
|
5751
|
+
);
|
|
5752
|
+
}
|
|
5568
5753
|
const totals = rows.reduce(
|
|
5569
5754
|
(acc, r) => {
|
|
5570
5755
|
acc.spend += r.spend ?? 0;
|
|
@@ -5606,7 +5791,9 @@ async function runAccountsDigest(opts) {
|
|
|
5606
5791
|
},
|
|
5607
5792
|
totalsScope: "returned",
|
|
5608
5793
|
currencyNote: "\u5404\u8D26\u6237\u4E3B\u5E01\u79CD\u53EF\u80FD\u4E0D\u540C\uFF1Btotals \u53EA\u5408\u8BA1 returned\uFF08\u547D\u4E2D\uFF09\u884C\uFF0C\u7981\u6B62\u5199\u6210\u5168\u90E8\u8D26\u6237\u3002\u8DE8\u5E01\u79CD\u8BF7\u6309 currencyCode \u5206\u8868",
|
|
5609
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5794
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5795
|
+
/** 未传 --with-campaign-types 时为 null。失败户的 hasDisplay 是 null,不能当成「无展示」。 */
|
|
5796
|
+
campaignTypes: campaignTypeMeta
|
|
5610
5797
|
};
|
|
5611
5798
|
const agentBrief = buildDigestAgentBrief({
|
|
5612
5799
|
rows,
|
|
@@ -5649,6 +5836,9 @@ ${media} \u8D26\u6237\u6295\u653E\u753B\u50CF\uFF1A\u8FD4\u56DE ${rows.length} \
|
|
|
5649
5836
|
{ key: "cpa", header: "CPA" },
|
|
5650
5837
|
{ key: "balance", header: "\u4F59\u989D" }
|
|
5651
5838
|
];
|
|
5839
|
+
if (opts.withCampaignTypes) {
|
|
5840
|
+
cols.push({ key: "types", header: "\u7CFB\u5217\u7C7B\u578B" });
|
|
5841
|
+
}
|
|
5652
5842
|
const tableRows = rows.map((r) => ({
|
|
5653
5843
|
id: r.mediaCustomerId,
|
|
5654
5844
|
company: r.advertiserName ?? "-",
|
|
@@ -5660,7 +5850,8 @@ ${media} \u8D26\u6237\u6295\u653E\u753B\u50CF\uFF1A\u8FD4\u56DE ${rows.length} \
|
|
|
5660
5850
|
ctr: r.ctr != null ? r.ctr.toFixed(2) : "-",
|
|
5661
5851
|
cpc: formatMoneyDisplay(r.cpc, r.currencyCode),
|
|
5662
5852
|
cpa: formatMoneyDisplay(r.cpa, r.currencyCode),
|
|
5663
|
-
balance: formatMoneyDisplay(r.balance, r.currencyCode)
|
|
5853
|
+
balance: formatMoneyDisplay(r.balance, r.currencyCode),
|
|
5854
|
+
types: formatCampaignTypesCell(r)
|
|
5664
5855
|
}));
|
|
5665
5856
|
printCliTable(tableRows, cols);
|
|
5666
5857
|
if (meta.nullConversionCount > 0) {
|
|
@@ -5693,6 +5884,7 @@ var init_run = __esm({
|
|
|
5693
5884
|
init_list_fetch();
|
|
5694
5885
|
init_login_customer_id();
|
|
5695
5886
|
init_brief();
|
|
5887
|
+
init_campaign_types();
|
|
5696
5888
|
init_filters();
|
|
5697
5889
|
}
|
|
5698
5890
|
});
|
|
@@ -5716,7 +5908,15 @@ function register10(program2) {
|
|
|
5716
5908
|
"--skip-balance",
|
|
5717
5909
|
"\u53EA\u62C9\u6D88\u8017\u3001\u4E0D\u62C9\u4F59\u989D\uFF08\u8F6C\u5316\u6210\u672C/CPA \u5DE1\u68C0\u63A8\u8350\uFF1B\u53EF\u5C11\u4E00\u534A\u8BF7\u6C42\u65F6\u95F4\uFF09",
|
|
5718
5910
|
false
|
|
5719
|
-
).option("--page-size <n>", "\u8D26\u6237\u6E05\u5355\u5206\u9875\u5927\u5C0F\uFF08\u9ED8\u8BA4 200\uFF0C\u4E0A\u9650 500\uFF09", (v) => parseInt(v, 10)).option("--login-customer-id <id>", LOGIN_CUSTOMER_ID_OPTION_HELP).option(
|
|
5911
|
+
).option("--page-size <n>", "\u8D26\u6237\u6E05\u5355\u5206\u9875\u5927\u5C0F\uFF08\u9ED8\u8BA4 200\uFF0C\u4E0A\u9650 500\uFF09", (v) => parseInt(v, 10)).option("--login-customer-id <id>", LOGIN_CUSTOMER_ID_OPTION_HELP).option(
|
|
5912
|
+
"--with-campaign-types",
|
|
5913
|
+
"\u4EC5 Google\uFF1A\u5BF9\u533A\u95F4\u6D88\u8017 > 0 \u7684\u8D26\u6237\u5E76\u53D1\u62C9\u53D6 campaigns/types-summary\uFF0C\u5728\u6BCF\u884C\u5199\u5165 hasDisplay / hasPmax\u3002\u7528\u6765\u505A\u300C\u65E0\u5C55\u793A\u6216\u65E0 PMax\u300D\u5DE1\u68C0\uFF0C\u4E0D\u8981\u518D\u9010\u6237\u8C03 ad campaigns\u3002",
|
|
5914
|
+
false
|
|
5915
|
+
).option(
|
|
5916
|
+
"--campaign-type-concurrency <n>",
|
|
5917
|
+
"\u7CFB\u5217\u7C7B\u578B\u8BF7\u6C42\u5E76\u53D1\uFF081\u201316\uFF0C\u9ED8\u8BA4 8\uFF09",
|
|
5918
|
+
(v) => parseInt(v, 10)
|
|
5919
|
+
).option("--max-pages <n>", "\u6700\u591A\u626B\u63CF\u9875\u6570\uFF08\u9ED8\u8BA4 20\uFF0C\u4E0A\u9650 200\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option(
|
|
5720
5920
|
"--json-out <path>",
|
|
5721
5921
|
"\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json\uFF1B\u76EE\u5F55\u6A21\u5F0F\u6587\u4EF6\u540D\u4E3A `<section>[-<\u67E5\u8BE2id>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09",
|
|
5722
5922
|
void 0
|
|
@@ -5737,7 +5937,9 @@ function register10(program2) {
|
|
|
5737
5937
|
jsonOut: opts.jsonOut,
|
|
5738
5938
|
refreshDp: opts.refreshDp,
|
|
5739
5939
|
verbose: opts.verbose,
|
|
5740
|
-
loginCustomerId: opts.loginCustomerId
|
|
5940
|
+
loginCustomerId: opts.loginCustomerId,
|
|
5941
|
+
withCampaignTypes: opts.withCampaignTypes,
|
|
5942
|
+
campaignTypeConcurrency: opts.campaignTypeConcurrency
|
|
5741
5943
|
});
|
|
5742
5944
|
}
|
|
5743
5945
|
);
|
|
@@ -144781,7 +144983,7 @@ function playbookCodeFatal(cmd) {
|
|
|
144781
144983
|
const known = {
|
|
144782
144984
|
W1: "siluzan-tso list-accounts [-m Google] --json-out <dir>",
|
|
144783
144985
|
P2: "siluzan-tso balance-scan -m Google --json-out <dir>",
|
|
144784
|
-
P3: "siluzan-tso accounts-digest -m Google --start <S> --end <D> --json-out <dir
|
|
144986
|
+
P3: "siluzan-tso accounts-digest -m Google --start <S> --end <D> --json-out <dir>\uFF08\u8981\u5224\u65AD Display/PMax \u518D\u52A0 --with-campaign-types\uFF0C\u4E0D\u8981\u9010\u6237 ad campaigns\uFF09"
|
|
144785
144987
|
};
|
|
144786
144988
|
const replacement = known[key];
|
|
144787
144989
|
if (replacement) {
|
package/dist/skill/_meta.json
CHANGED
|
@@ -98,6 +98,7 @@ siluzan-tso balance -m Google -a 6326027735 --json-out ./snap
|
|
|
98
98
|
> - 口语「零询盘 / 转化成本」且**无 CRM 询盘附件** → 本命令(P3),**不是** P7。
|
|
99
99
|
> - `conversions` / `cpa` 可能为 **null**(overview 未返回转化)→ 交付写「转化未返回」;脚本勿对 null 做数值比较。过滤用 `--max-cpa` / `--zero-conversions`,巡检交付用 `agentBrief.userDelivery`,勿手写脆弱 Python 重算 CTR/合计。
|
|
100
100
|
> - **没有** `cpa-risk-scan`。`--max-cpa` 的零转化仅当消耗已超过阈值才命中。`meta.totals` **只合计 returned**,禁止写成全部账户。`ctr` 已是百分数。
|
|
101
|
+
> - **没有**按户循环 `ad campaigns` 来判断 Display / PMax 的做法。要系列类型就加 `--with-campaign-types`(仅 Google,只查消耗 > 0 的户)。
|
|
101
102
|
|
|
102
103
|
```bash
|
|
103
104
|
siluzan-tso accounts-digest -m <媒体类型> [选项]
|
|
@@ -116,6 +117,9 @@ siluzan-tso accounts-digest -m <媒体类型> [选项]
|
|
|
116
117
|
| `--skip-balance` | 只拉消耗、不拉余额(转化成本巡检推荐) | off |
|
|
117
118
|
| `--page-size <n>` | 全量扫描时清单分页大小(上限 500) | `200` |
|
|
118
119
|
| `--max-pages <n>` | 全量扫描时最多页数(上限 200) | `20` |
|
|
120
|
+
| `--login-customer-id <id>` | 只保留该 MCC(`ma.loginCustomerId`)下的账户。可带横杠。先拉清单再过滤,**不要**先 `list-accounts` 把 ID 拼进 `-a` | — |
|
|
121
|
+
| `--with-campaign-types` | **仅 Google**。对区间消耗 > 0 的账户并发请求 `campaigns/types-summary`,每行写入系列类型。四项巡检用这个,禁止逐户 `ad campaigns` | off |
|
|
122
|
+
| `--campaign-type-concurrency <n>` | 上面这个请求的并发(1–16) | `8` |
|
|
119
123
|
| `--json-out <path>` | **Agent 推荐**:落盘目录或 `*.json` 文件;stdout 一行摘要(含 `outlineFile`、`writtenFiles`、`manifestFile`) | — |
|
|
120
124
|
|
|
121
125
|
**`--json-out` 落盘**:
|
|
@@ -152,10 +156,29 @@ siluzan-tso accounts-digest -m Google --start 2026-07-14 --end 2026-07-20 \
|
|
|
152
156
|
# 零转化 / 口语「零询盘」巡检(无 CRM 附件时用本命令,不是 P7)
|
|
153
157
|
siluzan-tso accounts-digest -m Google --start 2026-07-20 --end 2026-07-20 \
|
|
154
158
|
--zero-conversions --json-out ./snap-zero-conv
|
|
159
|
+
|
|
160
|
+
# Google 四项巡检:CPA、余额、零转化、有没有 Display/PMax。一条命令,不要再逐户拉系列
|
|
161
|
+
siluzan-tso accounts-digest -m Google --login-customer-id 397-889-3117 \
|
|
162
|
+
--with-campaign-types --start 2026-10-16 --end 2026-10-22 \
|
|
163
|
+
--json-out ./snap-inspect
|
|
155
164
|
```
|
|
156
165
|
|
|
166
|
+
四项都要时不要加 `--max-cpa` / `--zero-conversions`(会丢掉其它行)。外层命令必须直接是 `siluzan-tso`,不要写进 Python 再调用。
|
|
167
|
+
|
|
157
168
|
**`data.items[]` 主要字段**:`mediaCustomerId`、`name`、`advertiserName`、`currencyCode`、`balance`、`spend`(**区间合计**,同 `stats`)、`impressions`、`clicks`、`conversions`(可为 null)、`ctr`(%)、`cpc`、`cpa`(可为 null)。跨币种汇总见 `references/accounts/currency.md`(**禁止**对 `meta.totals` 跨币种直接当最终结论)。
|
|
158
169
|
|
|
170
|
+
**`--with-campaign-types` 额外字段**(只在打开开关时出现):
|
|
171
|
+
|
|
172
|
+
| 字段 | 含义 |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `campaignTypesStatus` | `ok` 查到了;`skipped-no-spend` 区间消耗为 0,没请求;`error` 这一户失败 |
|
|
175
|
+
| `campaignTypes` | `ok` 时是计数 > 0 的类型名(如 `Search`、`Display`、`PerformanceMax`);否则 `null` |
|
|
176
|
+
| `hasDisplay` | `ok` 时才是布尔。`Display` 计数 > 0 为 true |
|
|
177
|
+
| `hasPmax` | `ok` 时才是布尔。`PerformanceMax` / `PERFORMANCE_MAX` 计数 > 0 为 true |
|
|
178
|
+
| `campaignTypesError` | `error` 时的短原因 |
|
|
179
|
+
|
|
180
|
+
脚本规则:`campaignTypesStatus === "ok" && hasDisplay === false && hasPmax === false` 才算「无展示且无 PMax」。`error` 和 `skipped-no-spend` 单独列出,**禁止**当成「无」。`meta.campaignTypes` 有 `requested` / `ok` / `failed` / `skippedNoSpend`。
|
|
181
|
+
|
|
159
182
|
---
|
|
160
183
|
|
|
161
184
|
## stats — 查询投放消耗数据
|
|
@@ -249,6 +249,7 @@
|
|
|
249
249
|
| -------------------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
250
250
|
| 多账户余额 / 预算不足预警 | `balance-scan -m <媒体> --threshold-days 7`(用 **`dailySpend`** / `remainingDays`;交付 `agentBrief.userDelivery`) | 逐账户 `balance --accounts ...`;`account balance`;把 `stats` 多日 **`spend` 当「日消耗」** |
|
|
251
251
|
| 多账户投放画像 / 转化成本巡检 | `accounts-digest -m <媒体> [-a ...] --start --end --json-out`(CPA 加 `--max-cpa --skip-balance`;**`spend`=区间合计**;交付 `agentBrief.userDelivery`) | 发明 `cpa-risk-scan`;逐账户 `stats`;把 `meta.totals` 说成全部账户;把 `ctr` 再乘 100 |
|
|
252
|
+
| Google 巡检要「有没有 Display / PMax」 | 同一条 `accounts-digest -m Google --with-campaign-types [--login-customer-id <MCC>]`。只对消耗 > 0 的户查类型 | 逐户 `ad campaigns` / `google-analysis --sections campaign-types`;把 `siluzan-tso` 写进 Python 再 subprocess;把 `campaignTypesStatus≠ok` 当成「无展示」 |
|
|
252
253
|
| 单户按日 Excel | `stats … --by-day --json-out`(P4-DAILY) | 默认 `stats` 当按日 |
|
|
253
254
|
| 多账户 × 多维度 Google 数据 | 全量:`google-analysis-batch run`(省略 `-a`);2~10:`google-analysis -a id1,id2,...` | 外层 for-loop;先 list 再拼 `-a` |
|
|
254
255
|
| 多系列诊断 | `ad campaigns --json-out` + node 读文件过滤 | 逐系列 `ad campaign-get` |
|
|
@@ -59,8 +59,10 @@
|
|
|
59
59
|
- `conversions`/`cpa` 为 null 时写「转化未返回」,**禁止**对 null 做 `>` 比较导致脚本崩溃。
|
|
60
60
|
- **禁止**把 `meta.totals` 写成「全部账户」合计(`totalsScope=returned`,只合计命中行)。
|
|
61
61
|
- **禁止**把 `ctr` 再 `.2%` / `*100`(已是百分数)。
|
|
62
|
+
- **Google 四项巡检**(CPA、余额、零转化、无 Display/PMax,常带 MCC):只跑下面这一条,**禁止**再逐户 `ad campaigns` / `google-analysis --sections campaign-types`,**禁止**把 `siluzan-tso` 写进脚本再 subprocess。
|
|
62
63
|
- **步骤**:
|
|
63
|
-
1. 确认时间范围后执行 `accounts-digest -m <媒体> -a id1,id2,... --start <S> --end <D> --json-out ./snap-p3`(全量则省略 `-a
|
|
64
|
+
1. 确认时间范围后执行 `accounts-digest -m <媒体> -a id1,id2,... --start <S> --end <D> --json-out ./snap-p3`(全量则省略 `-a`)。指定 MCC 时加 `--login-customer-id <id>`,不要先 `list-accounts` 再把全部 ID 拼进 `-a`。
|
|
65
|
+
1b. **要系列类型时**(仅 Google):同一条命令加 `--with-campaign-types`。四项都要时**不要**再加 `--max-cpa` / `--zero-conversions`(那两个开关会丢掉其它行)。脚本读 `data.items`:`cpa`、`balance`、`spend`+`conversions`,以及 `hasDisplay` / `hasPmax`。只有 `campaignTypesStatus=ok` 且两项都为 false 才写入「无展示且无 PMax」;`error` / `skipped-no-spend` 另列。
|
|
64
66
|
2. **高 CPA / 转化成本巡检**:`--max-cpa <n> --skip-balance`(CPA > n,或零转化且消耗已 > n)。读盘后**只把 `agentBrief.userDelivery` 原样发给用户**(无命中即「全部正常」句;有命中只贴表)。
|
|
65
67
|
3. **零转化 / 零询盘巡检**(要全部空耗户,含小额):加 `--zero-conversions`。
|
|
66
68
|
4. 指定子集对比表:基于 `data.items`;**不要**再逐账户 `stats`。
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
- **必读**:`references/accounts/accounts-balance-stats.md`;首页看板口径见 `references/misc/tso-home.md`。Google 超预算/空耗熔断另读 `references/operations/guard.md`。Bing / Yandex / TikTok / Facebook 只读巡检读 `hosted-automation-bing.md` / `hosted-automation-yandex.md` / `hosted-automation-tiktok.md` / `hosted-automation-facebook.md`。
|
|
214
214
|
- **步骤**:
|
|
215
215
|
1. 余额:`list-accounts -m <媒体> --json-out ./snap` → `balance -m <媒体> -a <mediaCustomerId,...>`(多户续航预警走 **P2**)。
|
|
216
|
-
2.
|
|
216
|
+
2. 消耗:多户汇总走 **P3** `accounts-digest`(Bing/Yandex **今天**不要用 `stats`)。Google 还要判断有没有 Display / PMax 时,同一条命令加 `--with-campaign-types`(可加 `--login-customer-id`),**禁止**逐户 `ad campaigns` 或把 CLI 塞进 Python。
|
|
217
217
|
3. Google 当日超预算/空耗熔断(若用户要求):`guard budget-circuit` / `guard zero-conv -m Google --json-out`(**禁止**逐户 for-loop)。
|
|
218
218
|
4. Bing 巡检(若用户要求):按 `hosted-automation-bing.md` 跑封禁/拒审/当日超预算或空耗预警(只告警,不暂停)。
|
|
219
219
|
5. Yandex 巡检(若用户要求):按 `hosted-automation-yandex.md` 跑归档/拒审/当日超预算或空耗预警(只告警,不暂停)。
|
|
@@ -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.
|
|
12
|
+
$PKG_VERSION = '1.1.57-beta.1'
|
|
13
13
|
$CLI_BIN = 'siluzan-tso'
|
|
14
14
|
$SKILL_LABEL = 'Siluzan TSO'
|
|
15
15
|
$INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
|
|
@@ -9,7 +9,7 @@ set -euo pipefail
|
|
|
9
9
|
# -- Package info (injected at build time) ------------------------------------
|
|
10
10
|
readonly PKG_NAME="siluzan-tso-cli"
|
|
11
11
|
# PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
|
|
12
|
-
readonly PKG_VERSION="1.1.
|
|
12
|
+
readonly PKG_VERSION="1.1.57-beta.1"
|
|
13
13
|
readonly CLI_BIN="siluzan-tso"
|
|
14
14
|
readonly SKILL_LABEL="Siluzan TSO"
|
|
15
15
|
readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
|