siluzan-tso-cli 1.1.33-beta.1 → 1.1.33-beta.3
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 +305 -97
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/references/accounts/accounts.md +39 -5
- package/dist/skill/references/core/agent-conventions.md +1 -1
- package/dist/skill/references/core/workflows.md +2 -1
- package/dist/skill/references/report-templates/bing-period-report.md +1 -0
- package/dist/skill/references/report-templates/google-ads-diagnosis.md +4 -0
- package/dist/skill/report-templates/GoogleAdsDiagnosisReport.html +133 -0
- package/dist/skill/report-templates/bing-period-report.md +1 -0
- package/dist/skill/report-templates/google-ads-diagnosis.md +4 -0
- 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.33-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.33-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -4329,6 +4329,44 @@ function groupAccountIdsByOverviewRoute(cliMedia, accountIds, accountMediaTypes)
|
|
|
4329
4329
|
}
|
|
4330
4330
|
return groups;
|
|
4331
4331
|
}
|
|
4332
|
+
function routeToBalanceMedia(route) {
|
|
4333
|
+
if (route === "FacebookAds") return "MetaAd";
|
|
4334
|
+
if (BALANCE_SUPPORTED_MEDIA.includes(route)) {
|
|
4335
|
+
return route;
|
|
4336
|
+
}
|
|
4337
|
+
return null;
|
|
4338
|
+
}
|
|
4339
|
+
function applyRealtimeBalanceIfNeeded(item, bal) {
|
|
4340
|
+
if (bal == null || typeof bal.remainingAccountBudget !== "number") return;
|
|
4341
|
+
if (bal.remainingAccountBudget > 0) {
|
|
4342
|
+
item.remainingAccountBudget = bal.remainingAccountBudget;
|
|
4343
|
+
}
|
|
4344
|
+
if (bal.currencyCode) item.currencyCode = bal.currencyCode;
|
|
4345
|
+
if (bal.status) item.status = bal.status;
|
|
4346
|
+
}
|
|
4347
|
+
async function overlayRealtimeBalanceOnOverviewMap(route, overviewMap, config, verbose) {
|
|
4348
|
+
const media = routeToBalanceMedia(route);
|
|
4349
|
+
if (!media || overviewMap.size === 0) return;
|
|
4350
|
+
const ids = [...overviewMap.keys()];
|
|
4351
|
+
const balanceMap = await fetchBalanceMap(media, ids, config, void 0, void 0, verbose);
|
|
4352
|
+
for (const [id, item] of overviewMap) {
|
|
4353
|
+
applyRealtimeBalanceIfNeeded(item, balanceMap.get(id));
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
async function enrichOverviewRecordWithRealtimeBalance(media, accountId, config, record, verbose) {
|
|
4357
|
+
if (!BALANCE_SUPPORTED_MEDIA.includes(media)) {
|
|
4358
|
+
return record;
|
|
4359
|
+
}
|
|
4360
|
+
const balanceMap = await fetchBalanceMap(media, [accountId], config, void 0, void 0, verbose);
|
|
4361
|
+
const bal = balanceMap.get(accountId);
|
|
4362
|
+
if (bal == null) return record;
|
|
4363
|
+
const out = { ...record };
|
|
4364
|
+
applyRealtimeBalanceIfNeeded(out, bal);
|
|
4365
|
+
if (bal.name && out["accountName"] == null && out["mediaCustomerName"] == null) {
|
|
4366
|
+
out["mediaCustomerName"] = bal.name;
|
|
4367
|
+
}
|
|
4368
|
+
return out;
|
|
4369
|
+
}
|
|
4332
4370
|
async function fetchBalanceMap(media, accountIds, config, startDate, endDate, verbose) {
|
|
4333
4371
|
const result = /* @__PURE__ */ new Map();
|
|
4334
4372
|
if (accountIds.length === 0) return result;
|
|
@@ -4462,70 +4500,73 @@ async function fetchOverviewMap(route, accountIds, config, startDate, endDate, v
|
|
|
4462
4500
|
for (const row of rows) {
|
|
4463
4501
|
result.set(row.mediaAccountId, row);
|
|
4464
4502
|
}
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4503
|
+
} else {
|
|
4504
|
+
const fetchLegacyBatch = async (ids) => {
|
|
4505
|
+
const batch = /* @__PURE__ */ new Map();
|
|
4506
|
+
const params = new URLSearchParams({
|
|
4507
|
+
period: "true",
|
|
4508
|
+
startDate: start,
|
|
4509
|
+
endDate: end,
|
|
4510
|
+
mediaCustomerIds: ids.join(",")
|
|
4511
|
+
});
|
|
4512
|
+
const url = `${config.apiBaseUrl}/report/media-account/${route}/accountsoverview?${params}`;
|
|
4513
|
+
const raw = await apiFetch2(url, config, {}, verbose);
|
|
4514
|
+
const items = Array.isArray(raw) ? raw : [];
|
|
4515
|
+
for (const item of items) {
|
|
4516
|
+
if (item.mediaAccountId != null) {
|
|
4517
|
+
batch.set(String(item.mediaAccountId), item);
|
|
4518
|
+
}
|
|
4481
4519
|
}
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
process.stderr.write(
|
|
4502
|
-
`[fetchOverviewMap] \u9010\u6237\u91CD\u8BD5\u5931\u8D25 ${id}\uFF1A${singleErr instanceof Error ? singleErr.message : String(singleErr)}
|
|
4520
|
+
return batch;
|
|
4521
|
+
};
|
|
4522
|
+
const mergeOverview = (batch) => {
|
|
4523
|
+
for (const [k, v] of batch) result.set(k, v);
|
|
4524
|
+
};
|
|
4525
|
+
if (accountIds.length === 1) {
|
|
4526
|
+
mergeOverview(await fetchLegacyBatch(accountIds));
|
|
4527
|
+
} else {
|
|
4528
|
+
try {
|
|
4529
|
+
const batch = await fetchLegacyBatch(accountIds);
|
|
4530
|
+
mergeOverview(batch);
|
|
4531
|
+
if (batch.size === 0) {
|
|
4532
|
+
for (const id of accountIds) {
|
|
4533
|
+
try {
|
|
4534
|
+
mergeOverview(await fetchLegacyBatch([id]));
|
|
4535
|
+
} catch (singleErr) {
|
|
4536
|
+
if (verbose) {
|
|
4537
|
+
process.stderr.write(
|
|
4538
|
+
`[fetchOverviewMap] \u9010\u6237\u91CD\u8BD5\u5931\u8D25 ${id}\uFF1A${singleErr instanceof Error ? singleErr.message : String(singleErr)}
|
|
4503
4539
|
`
|
|
4504
|
-
|
|
4540
|
+
);
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4505
4543
|
}
|
|
4506
4544
|
}
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4509
|
-
} catch (err) {
|
|
4510
|
-
if (verbose) {
|
|
4511
|
-
process.stderr.write(
|
|
4512
|
-
`[fetchOverviewMap] \u6279\u91CF\u5F02\u5E38\uFF0C\u9010\u6237\u91CD\u8BD5\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
4513
|
-
`
|
|
4514
|
-
);
|
|
4515
|
-
}
|
|
4516
|
-
for (const id of accountIds) {
|
|
4517
|
-
try {
|
|
4518
|
-
mergeOverview(await fetchLegacyBatch([id]));
|
|
4519
|
-
} catch (singleErr) {
|
|
4545
|
+
} catch (err) {
|
|
4520
4546
|
if (verbose) {
|
|
4521
4547
|
process.stderr.write(
|
|
4522
|
-
`[fetchOverviewMap] \u9010\u6237\u91CD\u8BD5\
|
|
4548
|
+
`[fetchOverviewMap] \u6279\u91CF\u5F02\u5E38\uFF0C\u9010\u6237\u91CD\u8BD5\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
4523
4549
|
`
|
|
4524
4550
|
);
|
|
4525
4551
|
}
|
|
4552
|
+
for (const id of accountIds) {
|
|
4553
|
+
try {
|
|
4554
|
+
mergeOverview(await fetchLegacyBatch([id]));
|
|
4555
|
+
} catch (singleErr) {
|
|
4556
|
+
if (verbose) {
|
|
4557
|
+
process.stderr.write(
|
|
4558
|
+
`[fetchOverviewMap] \u9010\u6237\u91CD\u8BD5\u5931\u8D25 ${id}\uFF1A${singleErr instanceof Error ? singleErr.message : String(singleErr)}
|
|
4559
|
+
`
|
|
4560
|
+
);
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
}
|
|
4526
4564
|
}
|
|
4527
4565
|
}
|
|
4528
4566
|
}
|
|
4567
|
+
if (result.size > 0) {
|
|
4568
|
+
await overlayRealtimeBalanceOnOverviewMap(route, result, config, verbose);
|
|
4569
|
+
}
|
|
4529
4570
|
} catch (err) {
|
|
4530
4571
|
if (verbose) {
|
|
4531
4572
|
process.stderr.write(
|
|
@@ -106210,11 +106251,15 @@ async function runStats(opts) {
|
|
|
106210
106251
|
}
|
|
106211
106252
|
}
|
|
106212
106253
|
} else {
|
|
106213
|
-
const
|
|
106214
|
-
|
|
106215
|
-
|
|
106216
|
-
|
|
106217
|
-
|
|
106254
|
+
const map = await fetchOverviewMap(
|
|
106255
|
+
opts.media,
|
|
106256
|
+
ids,
|
|
106257
|
+
config,
|
|
106258
|
+
startDate,
|
|
106259
|
+
endDate,
|
|
106260
|
+
opts.verbose
|
|
106261
|
+
);
|
|
106262
|
+
items = [...map.values()];
|
|
106218
106263
|
}
|
|
106219
106264
|
} catch (err) {
|
|
106220
106265
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -107748,6 +107793,7 @@ async function fetchBingJson(config, url, verbose) {
|
|
|
107748
107793
|
|
|
107749
107794
|
// src/commands/report-bing-analysis/run.ts
|
|
107750
107795
|
init_auth();
|
|
107796
|
+
init_balance();
|
|
107751
107797
|
async function runReportBingOverview(opts) {
|
|
107752
107798
|
const id = assertBingAccountId(opts.account);
|
|
107753
107799
|
const { startDate, endDate } = resolveBingDateRange(opts.start, opts.end);
|
|
@@ -107755,7 +107801,14 @@ async function runReportBingOverview(opts) {
|
|
|
107755
107801
|
const params = new URLSearchParams({ startDate, endDate });
|
|
107756
107802
|
const url = reportingUrl2(config, id, "OverviewSectionData", params.toString());
|
|
107757
107803
|
try {
|
|
107758
|
-
const
|
|
107804
|
+
const raw = await fetchBingJson(config, url, !!opts.verbose);
|
|
107805
|
+
const data = raw && typeof raw === "object" && !Array.isArray(raw) ? await enrichOverviewRecordWithRealtimeBalance(
|
|
107806
|
+
"BingV2",
|
|
107807
|
+
id,
|
|
107808
|
+
config,
|
|
107809
|
+
raw,
|
|
107810
|
+
opts.verbose
|
|
107811
|
+
) : raw;
|
|
107759
107812
|
if (opts.jsonOut) {
|
|
107760
107813
|
await emitBingSnapshot(opts, "bing-overview", id, startDate, endDate, data);
|
|
107761
107814
|
return;
|
|
@@ -121000,6 +121053,27 @@ async function runAccountAuth(opts) {
|
|
|
121000
121053
|
tryOpenBrowser: tryOpenHttpsUrlInBrowser
|
|
121001
121054
|
});
|
|
121002
121055
|
}
|
|
121056
|
+
async function runAccountReauth(opts) {
|
|
121057
|
+
if (!opts.id && (!opts.ids || opts.ids.length === 0)) {
|
|
121058
|
+
console.error(
|
|
121059
|
+
"\n\u274C \u91CD\u65B0\u6388\u6743\u987B\u6307\u5B9A\u8981\u89E3\u7ED1\u7684\u8D26\u6237 entityId\uFF1A--id <entityId> \u6216 --ids id1,id2\n entityId \u6765\u81EA list-accounts --json-out \u7684 ma.entityId\uFF08\u4E0D\u662F mediaCustomerId\uFF09\n"
|
|
121060
|
+
);
|
|
121061
|
+
process.exit(1);
|
|
121062
|
+
}
|
|
121063
|
+
console.log("\n\u6B65\u9AA4 1/2\uFF1A\u65AD\u5F00\u5173\u8054\uFF08\u91CD\u65B0\u6388\u6743\u524D\u5FC5\u987B\u5148\u89E3\u7ED1\uFF0C\u5BF9\u9F50\u7F51\u9875\u300C\u91CD\u65B0\u6388\u6743\u300D\uFF09\u2026\n");
|
|
121064
|
+
await runAccountDelink({
|
|
121065
|
+
token: opts.token,
|
|
121066
|
+
id: opts.id,
|
|
121067
|
+
ids: opts.ids,
|
|
121068
|
+
verbose: opts.verbose
|
|
121069
|
+
});
|
|
121070
|
+
console.log("\u6B65\u9AA4 2/2\uFF1A\u53D1\u8D77 OAuth \u6388\u6743\u2026\n");
|
|
121071
|
+
await runAccountAuth({
|
|
121072
|
+
token: opts.token,
|
|
121073
|
+
media: opts.media,
|
|
121074
|
+
verbose: opts.verbose
|
|
121075
|
+
});
|
|
121076
|
+
}
|
|
121003
121077
|
|
|
121004
121078
|
// src/commands/account-manage/google-mcc.ts
|
|
121005
121079
|
init_auth();
|
|
@@ -121959,12 +122033,30 @@ function register23(program2) {
|
|
|
121959
122033
|
});
|
|
121960
122034
|
}
|
|
121961
122035
|
);
|
|
121962
|
-
accountCmd.command("auth").description(
|
|
122036
|
+
accountCmd.command("auth").description(
|
|
122037
|
+
"\u6DFB\u52A0\u6388\u6743\uFF1A\u9996\u6B21\u7ED1\u5B9A\u5A92\u4F53\u8D26\u6237 OAuth\uFF08\u7F51\u9875 manageAccounts\u300C\u6DFB\u52A0\u6388\u6743\u300D\uFF09\uFF1BOAuth \u5931\u6548\u8BF7\u7528 reauth"
|
|
122038
|
+
).requiredOption(
|
|
121963
122039
|
"-m, --media <type>",
|
|
121964
122040
|
"\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Meta | Yandex | BingV2 | Kwai"
|
|
121965
122041
|
).option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
|
|
121966
122042
|
await runAccountAuth({ token: opts.token, media: opts.media, verbose: opts.verbose });
|
|
121967
122043
|
});
|
|
122044
|
+
accountCmd.command("reauth").description(
|
|
122045
|
+
"\u91CD\u65B0\u6388\u6743\uFF1A\u5148\u89E3\u7ED1\u518D OAuth\uFF08\u7F51\u9875 manageAccounts\u300C\u91CD\u65B0\u6388\u6743\u300D\uFF1BinvalidOAuthToken \u5931\u6548\u65F6\u5FC5\u8D70\u6B64\u547D\u4EE4\uFF09"
|
|
122046
|
+
).requiredOption(
|
|
122047
|
+
"-m, --media <type>",
|
|
122048
|
+
"\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Meta | Yandex | BingV2 | Kwai"
|
|
122049
|
+
).option("--id <entityId>", "\u5355\u4E2A\u8D26\u6237 entityId\uFF08\u6765\u81EA list-accounts \u7684 ma.entityId\uFF09").option("--ids <entityIds>", "\u6279\u91CF entityId\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u4E0E --id \u4E8C\u9009\u4E00\uFF0C\u53EF\u540C\u65F6\u4F20\uFF09").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
|
|
122050
|
+
async (opts) => {
|
|
122051
|
+
await runAccountReauth({
|
|
122052
|
+
token: opts.token,
|
|
122053
|
+
media: opts.media,
|
|
122054
|
+
id: opts.id,
|
|
122055
|
+
ids: opts.ids ? opts.ids.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
122056
|
+
verbose: opts.verbose
|
|
122057
|
+
});
|
|
122058
|
+
}
|
|
122059
|
+
);
|
|
121968
122060
|
accountCmd.command("mcc-bind").description("Google\uFF1A\u5C06\u5B50\u8D26\u6237\u7ED1\u5B9A\u5230\u7ECF\u7406\u8D26\u6237\uFF08MCC\uFF09\uFF0C\u9700\u5DF2\u914D\u7F6E googleApiUrl").requiredOption(
|
|
121969
122061
|
"--customers <ids>",
|
|
121970
122062
|
"\u5B50\u8D26\u6237 mediaCustomerId\uFF0C\u591A\u4E2A\u9017\u53F7\u5206\u9694\uFF08\u6765\u81EA list-accounts \u7684 ma.mediaCustomerId\uFF09"
|
|
@@ -124937,8 +125029,8 @@ function buildGoogleAdsDiagnosisCollectAgentHint(snapDir) {
|
|
|
124937
125029
|
"Google Ads \u8BCA\u65AD collect \u5DF2\u5B8C\u6210\uFF08\u4EC5\u4E8B\u5B9E\u6570\u636E\uFF0C\u4E0D\u542B\u5EFA\u8BAE\u6027\u6587\u6848\uFF09\u3002",
|
|
124938
125030
|
`1) Read ${dir}/${GOOGLE_ADS_DIAGNOSIS_COLLECT_BASENAME}\uFF08reportData + agentBrief\uFF09\uFF1B`,
|
|
124939
125031
|
"2) Read references/report-templates/google-ads-diagnosis.md\uFF0C\u7531 Agent \u64B0\u5199\u5168\u90E8 analysis/suggestions\u3001diagnosisOverview\u3001summary\u3001accountInfo.businessModel/industry\u3001budgetCompetitiveness[].strategy\u3001newFeatures[].optimizerRecommendation\u3001adCreativeOptimization.items[].suggestion \u7B49\uFF1B",
|
|
124940
|
-
`3) \u5C06 reportData \u4E0E\u53D9\u4E8B\u5B57\u6BB5\u5408\u5E76\u5199\u5165 ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME}\uFF1B`,
|
|
124941
|
-
`4) siluzan-tso google-ads-diagnosis render --data ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME} --out ${dir}/google-ads-diagnosis-report.html\u3002`,
|
|
125032
|
+
`3) \u5C06 reportData \u4E0E\u53D9\u4E8B\u5B57\u6BB5\u5408\u5E76\u5199\u5165 ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME}\uFF08**\u7981\u6B62\u6539\u5199** campaigns/geographic/keywords \u7684 items \u884C\uFF0C\u53EA\u586B analysis/suggestions\uFF09\uFF1B`,
|
|
125033
|
+
`4) siluzan-tso google-ads-diagnosis render --data ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME} --out ${dir}/google-ads-diagnosis-report.html\uFF08\u9ED8\u8BA4\u540C\u76EE\u5F55 collect \u81EA\u52A8\u6062\u590D\u88AB\u6539\u574F\u7684 items\uFF09\u3002`,
|
|
124942
125034
|
"\u7981\u6B62\u5728\u672A\u7ECF\u8FC7 Agent \u64B0\u5199\u7684\u60C5\u51B5\u4E0B\u76F4\u63A5 render collect \u4EA7\u7269\uFF1B\u7F3A\u4EFB\u4E00\u5EFA\u8BAE\u5B57\u6BB5 render \u4F1A\u5931\u8D25\u3002"
|
|
124943
125035
|
].join("\n");
|
|
124944
125036
|
}
|
|
@@ -126063,13 +126155,90 @@ function formatGoogleAdsDiagnosisContentErrors(check) {
|
|
|
126063
126155
|
return parts.join("\n");
|
|
126064
126156
|
}
|
|
126065
126157
|
|
|
126066
|
-
// src/commands/google-ads-diagnosis/
|
|
126158
|
+
// src/commands/google-ads-diagnosis/merge-collect-facts.ts
|
|
126159
|
+
var GOOGLE_ADS_DIAGNOSIS_COMPARISON_MODULES = [
|
|
126160
|
+
"campaigns",
|
|
126161
|
+
"geographic",
|
|
126162
|
+
"keywords"
|
|
126163
|
+
];
|
|
126067
126164
|
function asRecord6(value) {
|
|
126165
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
126166
|
+
return value;
|
|
126167
|
+
}
|
|
126168
|
+
return null;
|
|
126169
|
+
}
|
|
126170
|
+
function isValidRateChange(value) {
|
|
126171
|
+
if (value === null || value === void 0) return true;
|
|
126172
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
126173
|
+
}
|
|
126174
|
+
function isComparisonTableRowValid(row) {
|
|
126175
|
+
const rec = asRecord6(row);
|
|
126176
|
+
if (!rec) return false;
|
|
126177
|
+
if (!String(rec.title ?? "").trim()) return false;
|
|
126178
|
+
if (!Number.isFinite(Number(rec.currentCost))) return false;
|
|
126179
|
+
if (!Number.isFinite(Number(rec.currentClicks))) return false;
|
|
126180
|
+
if (!Number.isFinite(Number(rec.currentCvr))) return false;
|
|
126181
|
+
if (!isValidRateChange(rec.costRateChange)) return false;
|
|
126182
|
+
if (!isValidRateChange(rec.clicksRateChange)) return false;
|
|
126183
|
+
if (!isValidRateChange(rec.cvrRateChange)) return false;
|
|
126184
|
+
return true;
|
|
126185
|
+
}
|
|
126186
|
+
function moduleItemsNeedRestore(items) {
|
|
126187
|
+
if (!Array.isArray(items) || items.length === 0) return false;
|
|
126188
|
+
return items.some((row) => !isComparisonTableRowValid(row));
|
|
126189
|
+
}
|
|
126190
|
+
function getComparisonTableErrors(data) {
|
|
126191
|
+
const errors = [];
|
|
126192
|
+
for (const mod of GOOGLE_ADS_DIAGNOSIS_COMPARISON_MODULES) {
|
|
126193
|
+
const block = asRecord6(data[mod]);
|
|
126194
|
+
const items = block?.items;
|
|
126195
|
+
if (!Array.isArray(items) || items.length === 0) continue;
|
|
126196
|
+
for (let i = 0; i < items.length; i++) {
|
|
126197
|
+
if (!isComparisonTableRowValid(items[i])) {
|
|
126198
|
+
errors.push(
|
|
126199
|
+
`${mod}.items[${i}] \u7F3A\u5C11 title/currentCost/currentClicks \u6216\u73AF\u6BD4\u5B57\u6BB5\u88AB\u5199\u6210\u5B57\u7B26\u4E32\uFF08\u987B\u4FDD\u7559 collect \u4E8B\u5B9E\u884C\uFF0C\u4EC5\u7531 Agent \u586B\u5199 analysis/suggestions\uFF09`
|
|
126200
|
+
);
|
|
126201
|
+
}
|
|
126202
|
+
}
|
|
126203
|
+
}
|
|
126204
|
+
return errors;
|
|
126205
|
+
}
|
|
126206
|
+
function mergeComparisonItemsFromCollect(finalData, collectReportData) {
|
|
126207
|
+
const data = { ...finalData };
|
|
126208
|
+
const restoredModules = [];
|
|
126209
|
+
for (const mod of GOOGLE_ADS_DIAGNOSIS_COMPARISON_MODULES) {
|
|
126210
|
+
const finalBlock = asRecord6(data[mod]);
|
|
126211
|
+
const collectBlock = asRecord6(collectReportData[mod]);
|
|
126212
|
+
const finalItems = finalBlock?.items;
|
|
126213
|
+
const collectItems = collectBlock?.items;
|
|
126214
|
+
if (!moduleItemsNeedRestore(finalItems)) continue;
|
|
126215
|
+
if (!Array.isArray(collectItems) || collectItems.length === 0) continue;
|
|
126216
|
+
data[mod] = {
|
|
126217
|
+
...finalBlock ?? {},
|
|
126218
|
+
items: collectItems
|
|
126219
|
+
};
|
|
126220
|
+
restoredModules.push(mod);
|
|
126221
|
+
}
|
|
126222
|
+
return { data, restoredModules };
|
|
126223
|
+
}
|
|
126224
|
+
|
|
126225
|
+
// src/commands/google-ads-diagnosis/render-report.ts
|
|
126226
|
+
function asRecord7(value) {
|
|
126068
126227
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
126069
126228
|
return value;
|
|
126070
126229
|
}
|
|
126071
126230
|
throw new Error("\u8BCA\u65AD\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
|
|
126072
126231
|
}
|
|
126232
|
+
async function tryLoadCollectReportData(collectPath) {
|
|
126233
|
+
try {
|
|
126234
|
+
const raw = JSON.parse(await fs18.readFile(collectPath, "utf8"));
|
|
126235
|
+
const root = asRecord7(raw);
|
|
126236
|
+
const reportData = asRecord7(root?.reportData);
|
|
126237
|
+
return reportData ?? root;
|
|
126238
|
+
} catch {
|
|
126239
|
+
return null;
|
|
126240
|
+
}
|
|
126241
|
+
}
|
|
126073
126242
|
async function runGoogleAdsDiagnosisRender(opts) {
|
|
126074
126243
|
const dataPath = path23.resolve(opts.dataFile);
|
|
126075
126244
|
let dataRaw;
|
|
@@ -126081,7 +126250,39 @@ async function runGoogleAdsDiagnosisRender(opts) {
|
|
|
126081
126250
|
`);
|
|
126082
126251
|
process.exit(1);
|
|
126083
126252
|
}
|
|
126084
|
-
|
|
126253
|
+
let data = asRecord7(dataRaw);
|
|
126254
|
+
if (!opts.noMergeCollect) {
|
|
126255
|
+
const collectPath = path23.resolve(
|
|
126256
|
+
opts.collectFile ?? path23.join(path23.dirname(dataPath), GOOGLE_ADS_DIAGNOSIS_COLLECT_BASENAME)
|
|
126257
|
+
);
|
|
126258
|
+
const collectReportData = await tryLoadCollectReportData(collectPath);
|
|
126259
|
+
if (collectReportData) {
|
|
126260
|
+
const { data: merged, restoredModules } = mergeComparisonItemsFromCollect(
|
|
126261
|
+
data,
|
|
126262
|
+
collectReportData
|
|
126263
|
+
);
|
|
126264
|
+
data = merged;
|
|
126265
|
+
if (restoredModules.length > 0) {
|
|
126266
|
+
console.log(
|
|
126267
|
+
`
|
|
126268
|
+
\u2139\uFE0F \u5DF2\u4ECE collect \u6062\u590D \xA73.5 \u5BF9\u6BD4\u8868 items\uFF08${restoredModules.join("\u3001")}\uFF09\uFF1BAgent \u53EA\u5E94\u586B\u5199 analysis/suggestions\uFF0C\u52FF\u6539\u5199 items \u884C\u6570\u636E\u3002
|
|
126269
|
+
\u6765\u6E90\uFF1A${collectPath}
|
|
126270
|
+
`
|
|
126271
|
+
);
|
|
126272
|
+
}
|
|
126273
|
+
}
|
|
126274
|
+
}
|
|
126275
|
+
const comparisonErrors = getComparisonTableErrors(data);
|
|
126276
|
+
if (comparisonErrors.length > 0) {
|
|
126277
|
+
console.error("\n\u274C \u5BF9\u6BD4\u8868 items \u4E0D\u5B8C\u6574\uFF0CHTML \u4E2D\u7CFB\u5217/\u5730\u57DF/\u5173\u952E\u8BCD\u8868\u4F1A\u663E\u793A\u4E3A\u7A7A\uFF1A\n");
|
|
126278
|
+
for (const err of comparisonErrors) {
|
|
126279
|
+
console.error(` \xB7 ${err}`);
|
|
126280
|
+
}
|
|
126281
|
+
console.error(
|
|
126282
|
+
"\n \u4FEE\u590D\uFF1A\u540C\u76EE\u5F55\u4FDD\u7559 google-ads-diagnosis-collect.json \u540E\u91CD\u65B0 render\uFF08\u9ED8\u8BA4\u81EA\u52A8\u5408\u5E76\uFF09\uFF1B\u6216\u624B\u52A8\u4ECE collect.reportData \u590D\u5236 campaigns/geographic/keywords.items\u3002\n"
|
|
126283
|
+
);
|
|
126284
|
+
process.exit(1);
|
|
126285
|
+
}
|
|
126085
126286
|
const contentCheck = validateGoogleAdsDiagnosisContent(data);
|
|
126086
126287
|
if (!contentCheck.ok) {
|
|
126087
126288
|
console.error(`
|
|
@@ -126172,13 +126373,20 @@ function registerGoogleAdsDiagnosisCommands(program2) {
|
|
|
126172
126373
|
);
|
|
126173
126374
|
root.command("render").description(
|
|
126174
126375
|
"\u5C06 google-ads-diagnosis.json \u6CE8\u5165 GoogleAdsDiagnosisReport.html\uFF0C\u8F93\u51FA\u4E0E MarkAI \u4E00\u81F4\u7684 HTML \u7EC8\u7A3F"
|
|
126175
|
-
).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684\u8BCA\u65AD JSON\uFF08\u4E0E MarkAI sectionData \u540C\u7ED3\u6784\uFF09").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").option(
|
|
126176
|
-
|
|
126177
|
-
|
|
126178
|
-
|
|
126179
|
-
|
|
126180
|
-
|
|
126181
|
-
|
|
126376
|
+
).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684\u8BCA\u65AD JSON\uFF08\u4E0E MarkAI sectionData \u540C\u7ED3\u6784\uFF09").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").option(
|
|
126377
|
+
"--collect <file>",
|
|
126378
|
+
"collect JSON \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\u7684 google-ads-diagnosis-collect.json\uFF0C\u7528\u4E8E\u6062\u590D\u5BF9\u6BD4\u8868 items\uFF09"
|
|
126379
|
+
).option("--no-merge-collect", "\u4E0D\u4ECE collect \u81EA\u52A8\u6062\u590D campaigns/geographic/keywords.items", false).option("--lenient", "\u7ED3\u6784\u8B66\u544A\uFF08\u5982\u7F3A analysis\uFF09\u65F6\u4E0D\u963B\u65AD\u6E32\u67D3", false).action(
|
|
126380
|
+
async (opts) => {
|
|
126381
|
+
await runGoogleAdsDiagnosisRender({
|
|
126382
|
+
dataFile: opts.data,
|
|
126383
|
+
out: opts.out,
|
|
126384
|
+
collectFile: opts.collect,
|
|
126385
|
+
noMergeCollect: opts.noMergeCollect,
|
|
126386
|
+
lenient: opts.lenient
|
|
126387
|
+
});
|
|
126388
|
+
}
|
|
126389
|
+
);
|
|
126182
126390
|
}
|
|
126183
126391
|
|
|
126184
126392
|
// src/index.ts
|
|
@@ -126535,7 +126743,7 @@ function ensureScorecardFromSnapshot(healthDiagnosis, merged) {
|
|
|
126535
126743
|
}
|
|
126536
126744
|
|
|
126537
126745
|
// src/commands/facebook-analysis/merge-snapshot.ts
|
|
126538
|
-
function
|
|
126746
|
+
function asRecord8(value) {
|
|
126539
126747
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
126540
126748
|
return value;
|
|
126541
126749
|
}
|
|
@@ -126584,7 +126792,7 @@ async function readManifest(snapshotDir) {
|
|
|
126584
126792
|
async function loadSectionFile(snapshotDir, file) {
|
|
126585
126793
|
try {
|
|
126586
126794
|
const raw = await fs21.readFile(path28.join(snapshotDir, file), "utf8");
|
|
126587
|
-
return
|
|
126795
|
+
return asRecord8(JSON.parse(raw));
|
|
126588
126796
|
} catch {
|
|
126589
126797
|
return null;
|
|
126590
126798
|
}
|
|
@@ -126632,7 +126840,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
|
|
|
126632
126840
|
if (data) sectionMap.set(art.section, data);
|
|
126633
126841
|
}
|
|
126634
126842
|
const overview = sectionMap.get("overview");
|
|
126635
|
-
const current =
|
|
126843
|
+
const current = asRecord8(overview?.currentPeriod);
|
|
126636
126844
|
const accountName = typeof overview?.accountName === "string" ? overview.accountName : void 0;
|
|
126637
126845
|
const accountId = manifest.accountId ?? (typeof overview?.accountId === "string" ? overview.accountId : void 0);
|
|
126638
126846
|
const kpis = { ...payload.kpis ?? {} };
|
|
@@ -126754,7 +126962,7 @@ function isFiniteNumber(value) {
|
|
|
126754
126962
|
function asArray2(value) {
|
|
126755
126963
|
return Array.isArray(value) ? value : [];
|
|
126756
126964
|
}
|
|
126757
|
-
function
|
|
126965
|
+
function asRecord9(value) {
|
|
126758
126966
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
126759
126967
|
return value;
|
|
126760
126968
|
}
|
|
@@ -126766,26 +126974,26 @@ function pushMissing(missing, id, chapter, dimension, hint) {
|
|
|
126766
126974
|
function countAdSetsWithSpend(tables) {
|
|
126767
126975
|
const rows = asArray2(tables?.adSets);
|
|
126768
126976
|
return rows.filter((row) => {
|
|
126769
|
-
const r =
|
|
126977
|
+
const r = asRecord9(row);
|
|
126770
126978
|
const spend = r?.spend;
|
|
126771
126979
|
return isFiniteNumber(spend) && spend > 0;
|
|
126772
126980
|
}).length;
|
|
126773
126981
|
}
|
|
126774
126982
|
function chartLabels(chart) {
|
|
126775
|
-
const c =
|
|
126983
|
+
const c = asRecord9(chart);
|
|
126776
126984
|
return asArray2(c?.labels).length;
|
|
126777
126985
|
}
|
|
126778
126986
|
function validateMetaPeriodReportContent(data) {
|
|
126779
126987
|
const missing = [];
|
|
126780
|
-
const meta =
|
|
126781
|
-
const kpis =
|
|
126782
|
-
const narrative =
|
|
126783
|
-
const tables =
|
|
126784
|
-
const charts =
|
|
126785
|
-
const sections =
|
|
126786
|
-
const health =
|
|
126787
|
-
const priorityPlan =
|
|
126788
|
-
const actionChecklist =
|
|
126988
|
+
const meta = asRecord9(data.meta) ?? {};
|
|
126989
|
+
const kpis = asRecord9(data.kpis) ?? {};
|
|
126990
|
+
const narrative = asRecord9(data.narrative) ?? {};
|
|
126991
|
+
const tables = asRecord9(data.tables);
|
|
126992
|
+
const charts = asRecord9(data.charts);
|
|
126993
|
+
const sections = asRecord9(data.sections);
|
|
126994
|
+
const health = asRecord9(data.healthDiagnosis);
|
|
126995
|
+
const priorityPlan = asRecord9(data.priorityPlan);
|
|
126996
|
+
const actionChecklist = asRecord9(data.actionChecklist);
|
|
126789
126997
|
const adSetsWithSpend2 = countAdSetsWithSpend(tables ?? void 0);
|
|
126790
126998
|
const regionalNarratives = asArray2(narrative.regional).length;
|
|
126791
126999
|
const recommendations = asArray2(narrative.recommendations);
|
|
@@ -126857,7 +127065,7 @@ function validateMetaPeriodReportContent(data) {
|
|
|
126857
127065
|
"charts.audience.labels \u81F3\u5C11 1 \u9879"
|
|
126858
127066
|
);
|
|
126859
127067
|
}
|
|
126860
|
-
const funnel =
|
|
127068
|
+
const funnel = asRecord9(charts?.funnel);
|
|
126861
127069
|
if (!isFiniteNumber(funnel?.reach)) {
|
|
126862
127070
|
pushMissing(
|
|
126863
127071
|
missing,
|
|
@@ -126921,7 +127129,7 @@ function validateMetaPeriodReportContent(data) {
|
|
|
126921
127129
|
);
|
|
126922
127130
|
} else {
|
|
126923
127131
|
for (let i = 0; i < regionalNarratives; i++) {
|
|
126924
|
-
const row =
|
|
127132
|
+
const row = asRecord9(asArray2(narrative.regional)[i]);
|
|
126925
127133
|
if (textLen2(row?.text) < 80) {
|
|
126926
127134
|
pushMissing(
|
|
126927
127135
|
missing,
|
|
@@ -126951,7 +127159,7 @@ function validateMetaPeriodReportContent(data) {
|
|
|
126951
127159
|
"\u5FC5\u987B\u6070\u597D 4 \u6761\uFF08\u7B80\u5316\u8868\u5355/\u533A\u57DF\u8C03\u6574/\u9884\u7B97\u91CD\u6784/\u7D20\u6750\u5EFA\u8BAE\uFF09"
|
|
126952
127160
|
);
|
|
126953
127161
|
} else {
|
|
126954
|
-
const titles = recommendations.map((r) =>
|
|
127162
|
+
const titles = recommendations.map((r) => asRecord9(r)?.title);
|
|
126955
127163
|
for (const title of RECOMMENDATION_TITLES) {
|
|
126956
127164
|
if (!titles.includes(title)) {
|
|
126957
127165
|
pushMissing(
|
|
@@ -126964,7 +127172,7 @@ function validateMetaPeriodReportContent(data) {
|
|
|
126964
127172
|
}
|
|
126965
127173
|
}
|
|
126966
127174
|
for (let i = 0; i < recommendations.length; i++) {
|
|
126967
|
-
const rec =
|
|
127175
|
+
const rec = asRecord9(recommendations[i]);
|
|
126968
127176
|
if (textLen2(rec?.content) < 150) {
|
|
126969
127177
|
pushMissing(
|
|
126970
127178
|
missing,
|
|
@@ -126986,7 +127194,7 @@ function validateMetaPeriodReportContent(data) {
|
|
|
126986
127194
|
);
|
|
126987
127195
|
} else {
|
|
126988
127196
|
for (let i = 0; i < supplementary.length; i++) {
|
|
126989
|
-
const row =
|
|
127197
|
+
const row = asRecord9(supplementary[i]);
|
|
126990
127198
|
if (!textLen2(row?.dimension) || !textLen2(row?.issue)) {
|
|
126991
127199
|
pushMissing(
|
|
126992
127200
|
missing,
|
|
@@ -127068,7 +127276,7 @@ function validateMetaPeriodReportContent(data) {
|
|
|
127068
127276
|
);
|
|
127069
127277
|
} else {
|
|
127070
127278
|
for (let i = 0; i < fourQuestions.length; i++) {
|
|
127071
|
-
const q =
|
|
127279
|
+
const q = asRecord9(fourQuestions[i]);
|
|
127072
127280
|
const evidence = asArray2(q?.evidence).filter((e) => textLen2(e) > 0);
|
|
127073
127281
|
if (evidence.length < 2) {
|
|
127074
127282
|
pushMissing(
|
|
@@ -127092,7 +127300,7 @@ function validateMetaPeriodReportContent(data) {
|
|
|
127092
127300
|
}
|
|
127093
127301
|
let scorecardValidRows = 0;
|
|
127094
127302
|
for (let i = 0; i < scorecard.length; i++) {
|
|
127095
|
-
const row =
|
|
127303
|
+
const row = asRecord9(scorecard[i]);
|
|
127096
127304
|
const itemOk = textLen2(row?.item) > 0;
|
|
127097
127305
|
const dataOk = textLen2(row?.data) > 0;
|
|
127098
127306
|
const signalOk = SCORECARD_SIGNALS.includes(row?.signal);
|
|
@@ -127134,7 +127342,7 @@ function validateMetaPeriodReportContent(data) {
|
|
|
127134
127342
|
);
|
|
127135
127343
|
}
|
|
127136
127344
|
for (const key of ["platform", "country", "adSets"]) {
|
|
127137
|
-
const insight = textLen2(
|
|
127345
|
+
const insight = textLen2(asRecord9(sections?.[key])?.insight);
|
|
127138
127346
|
if (insight < 200) {
|
|
127139
127347
|
pushMissing(
|
|
127140
127348
|
missing,
|
|
@@ -127145,7 +127353,7 @@ function validateMetaPeriodReportContent(data) {
|
|
|
127145
127353
|
);
|
|
127146
127354
|
}
|
|
127147
127355
|
}
|
|
127148
|
-
const audience =
|
|
127356
|
+
const audience = asRecord9(sections?.audience);
|
|
127149
127357
|
if (asArray2(audience?.goldenProfile).filter((s) => textLen2(s) > 0).length < 3) {
|
|
127150
127358
|
pushMissing(
|
|
127151
127359
|
missing,
|
|
@@ -127173,7 +127381,7 @@ function validateMetaPeriodReportContent(data) {
|
|
|
127173
127381
|
"\u2265150 \u5B57"
|
|
127174
127382
|
);
|
|
127175
127383
|
}
|
|
127176
|
-
const landingRows = asArray2(
|
|
127384
|
+
const landingRows = asArray2(asRecord9(sections?.landingPage)?.rows);
|
|
127177
127385
|
if (landingRows.length < 3) {
|
|
127178
127386
|
pushMissing(
|
|
127179
127387
|
missing,
|
|
@@ -127258,7 +127466,7 @@ async function readJsonFile2(filePath) {
|
|
|
127258
127466
|
throw new Error(`\u65E0\u6CD5\u89E3\u6790 JSON\uFF1A${filePath}`);
|
|
127259
127467
|
}
|
|
127260
127468
|
}
|
|
127261
|
-
function
|
|
127469
|
+
function asRecord10(value) {
|
|
127262
127470
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
127263
127471
|
return value;
|
|
127264
127472
|
}
|
|
@@ -127273,7 +127481,7 @@ function injectReportData2(html, payload) {
|
|
|
127273
127481
|
}
|
|
127274
127482
|
async function runFacebookAnalysisRender(opts) {
|
|
127275
127483
|
const dataPath = path29.resolve(opts.dataFile);
|
|
127276
|
-
let data =
|
|
127484
|
+
let data = asRecord10(await readJsonFile2(dataPath));
|
|
127277
127485
|
if (opts.snapshotDir?.trim()) {
|
|
127278
127486
|
data = await mergeFacebookSnapshotIntoReport(data, path29.resolve(opts.snapshotDir));
|
|
127279
127487
|
}
|
package/dist/skill/_meta.json
CHANGED
|
@@ -307,9 +307,11 @@ siluzan-tso account me --check-phone 15130150466 --json-out ./snap-me
|
|
|
307
307
|
|
|
308
308
|
---
|
|
309
309
|
|
|
310
|
-
### auth — 添加媒体平台 OAuth
|
|
310
|
+
### auth — 添加媒体平台 OAuth 授权(首次绑定)
|
|
311
311
|
|
|
312
|
-
在浏览器中打开对应媒体的 OAuth
|
|
312
|
+
在浏览器中打开对应媒体的 OAuth 授权页面,授权后账户自动绑定到丝路赞。对应网页 **「添加授权」**。
|
|
313
|
+
|
|
314
|
+
> **OAuth 已失效**(`invalidOAuthToken=true`)时**不要**用本命令,须走下方 **`reauth`**(重新授权必须先解绑)。
|
|
313
315
|
|
|
314
316
|
```bash
|
|
315
317
|
siluzan-tso account auth -m <媒体类型>
|
|
@@ -322,13 +324,13 @@ siluzan-tso account auth -m <媒体类型>
|
|
|
322
324
|
**示例:**
|
|
323
325
|
|
|
324
326
|
```bash
|
|
325
|
-
#
|
|
327
|
+
# 首次授权 Google Ads 账户
|
|
326
328
|
siluzan-tso account auth -m Google
|
|
327
329
|
|
|
328
|
-
#
|
|
330
|
+
# 首次授权 TikTok Ads 账户
|
|
329
331
|
siluzan-tso account auth -m TikTok
|
|
330
332
|
|
|
331
|
-
#
|
|
333
|
+
# 首次授权 Meta(Facebook)Ads 账户
|
|
332
334
|
siluzan-tso account auth -m Meta
|
|
333
335
|
```
|
|
334
336
|
|
|
@@ -336,6 +338,38 @@ siluzan-tso account auth -m Meta
|
|
|
336
338
|
|
|
337
339
|
---
|
|
338
340
|
|
|
341
|
+
### reauth — 重新授权(先解绑再 OAuth)
|
|
342
|
+
|
|
343
|
+
OAuth 失效时恢复授权。对齐 TSO 网页 **「重新授权」**:程序会先 **delink** 断开关联,再跳转媒体 OAuth。**禁止**对失效账户跳过解绑直接用 `account auth`。
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
siluzan-tso account reauth -m <媒体类型> --id <entityId>
|
|
347
|
+
siluzan-tso account reauth -m Google --ids <id1,id2>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
| 选项 | 说明 |
|
|
351
|
+
| -------------------- | ------------------------------------------------------------------------ |
|
|
352
|
+
| `-m, --media <type>` | 媒体类型(必填) |
|
|
353
|
+
| `--id <entityId>` | 单个账户 `entityId`(来自 `list-accounts` 的 `ma.entityId`) |
|
|
354
|
+
| `--ids <id1,id2>` | 批量 `entityId`,逗号分隔(与 `--id` 二选一) |
|
|
355
|
+
|
|
356
|
+
**示例:**
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
# 1. 查失效账户的 entityId
|
|
360
|
+
siluzan-tso list-accounts -m Google --json-out ./snap
|
|
361
|
+
|
|
362
|
+
# 2. 重新授权(内置 delink → OAuth)
|
|
363
|
+
siluzan-tso account reauth -m Google --id abc123def456
|
|
364
|
+
|
|
365
|
+
# 3. 验证
|
|
366
|
+
siluzan-tso list-accounts -m Google
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
> 手动两步等价于 `reauth`:`account delink --id …` → `account auth -m …`(须用户确认解绑风险)。
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
339
373
|
### delink — 解除授权 / 断开账户关联
|
|
340
374
|
|
|
341
375
|
从当前丝路赞账号下移除媒体账户绑定。
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
## 十一、常见 HTTP 状态码
|
|
220
220
|
|
|
221
221
|
- **400**:参数错误,查看对应 reference 或 `-h`
|
|
222
|
-
- **401
|
|
222
|
+
- **401**:媒体 OAuth 失效 → `account reauth -m <媒体> --id <entityId>`(须先 delink,见 W9);**丝路赞登录凭据失效** → `send-login-code` + `login --phone --code`,见 `references/core/setup.md`
|
|
223
223
|
- **500**:服务可能正在部署/升级,建议反馈 Siluzan 相关人员
|
|
224
224
|
|
|
225
225
|
---
|
|
@@ -152,7 +152,8 @@
|
|
|
152
152
|
- **步骤(按场景)**:
|
|
153
153
|
- **分享**:`list-accounts --json-out` 取 `entityId` → `account share --id <entityId> --phone <手机号>`;查 `account share-detail --customer-id <mediaCustomerId>`;取消 `account unshare --id <entityId> --account-id <userId>`。
|
|
154
154
|
- **解绑**:`account delink --id <entityId>` / `--ids id1,id2`。
|
|
155
|
-
- **OAuth 重授权**:`invalidOAuthToken=true` → `account
|
|
155
|
+
- **OAuth 重授权**:`invalidOAuthToken=true` → `list-accounts --json-out` 取 `ma.entityId` → **`account reauth -m <媒体> --id <entityId>`**(内置先 delink 再 OAuth,对齐网页「重新授权」)→ `list-accounts` 验证。**禁止**对失效账户直接用 `account auth`(首次「添加授权」专用)。
|
|
156
|
+
- **首次 OAuth 添加授权**:`account auth -m <媒体>`(无需先 delink)。
|
|
156
157
|
- **MCC**:`account mcc-bind --customers <mediaCustomerId,...> --mcc <MCC客户ID>` / `mcc-unbind`(走 `googleApiUrl`,先 `config show`)。
|
|
157
158
|
- **BC(TikTok)**:`account bc-bind --customers <id> --bc-ids <id>` / `bc-unbind --bc-id <id>`(解绑一次一个)。
|
|
158
159
|
- **BM(Meta)**:`account bm-bind --account-id <mediaCustomerId> --bm-id <bmId>`。
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
## 1. 执行摘要(总览)
|
|
19
19
|
|
|
20
20
|
- **CLI**:`siluzan-tso report bing-overview -a <mediaCustomerId> [--start … --end …] --json-out <dir>`
|
|
21
|
+
- **余额**:若 `OverviewSectionData` 误返 `remainingAccountBudget: 0`,CLI 会自动用 `GetMediaAccountInfo` 实时余额回填(与 Web 账户列表一致);**仍建议**单查余额用 `balance -m BingV2`。
|
|
21
22
|
|
|
22
23
|
## 2. 设备与地域
|
|
23
24
|
|
|
@@ -33,6 +33,9 @@ siluzan-tso google-ads-diagnosis render \
|
|
|
33
33
|
| `collect` | 拉 google-analysis + 可选 Lighthouse;输出 `google-ads-diagnosis-collect.json` |
|
|
34
34
|
| `render` | 读取 Agent 产出的 `google-ads-diagnosis.json`,注入 `GoogleAdsDiagnosisReport.html` |
|
|
35
35
|
|
|
36
|
+
- **render 自动合并(默认开启)**:若同目录存在 `google-ads-diagnosis-collect.json`,且 Agent 改坏了 `campaigns` / `geographic` / `keywords` 的 `items`(缺 `title`/`currentCost` 或环比写成字符串),CLI **自动从 collect.reportData 恢复 items**,保留 Agent 的 `analysis`/`suggestions`。可用 `--no-merge-collect` 关闭;`--collect <file>` 指定 collect 路径。
|
|
37
|
+
- **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空)。
|
|
38
|
+
|
|
36
39
|
- **禁止**在 `collect` 阶段生成或写入 `analysis` / `suggestions` / `diagnosisOverview` / `summary` 等建议性文案。
|
|
37
40
|
- **禁止**跳过 Agent 直接 `render` collect 产物(`render` 会校验全部叙事 / 建议字段;调试可加 `--lenient`)。
|
|
38
41
|
- JSON 顶层结构须与 MarkAI `sectionData.js` / `fetchData` 输出一致(见下文各 `section-*` 数据对象)。
|
|
@@ -204,6 +207,7 @@ siluzan-tso google-ads-diagnosis render \
|
|
|
204
207
|
- 指标列:花费(上期/本期/环比)、点击(上期/本期/环比)、转化率(上期/本期/环比)
|
|
205
208
|
- **数据**:`campaigns.items`(`title`、`previousCost`、`currentCost`、`costRateChange`、`previousClicks`、`currentClicks`、`clicksRateChange`、`previousCvr`、`currentCvr`、`cvrRateChange`)
|
|
206
209
|
- **分析 / 建议**:`campaigns.analysis`、`campaigns.suggestions`
|
|
210
|
+
- **Agent 禁止改写 `items` 行**(须原样保留 collect `reportData`);`render` 会从同目录 collect 自动恢复被改坏的 items
|
|
207
211
|
|
|
208
212
|
#### 国家地区分析
|
|
209
213
|
|
|
@@ -4296,6 +4296,75 @@
|
|
|
4296
4296
|
const labels = items.map((item, index) =>
|
|
4297
4297
|
formatDateLabel(item?.date, index),
|
|
4298
4298
|
);
|
|
4299
|
+
const totalConversions = items.reduce(
|
|
4300
|
+
(sum, item) => sum + (Number(item?.conversions) || 0),
|
|
4301
|
+
0,
|
|
4302
|
+
);
|
|
4303
|
+
// 零转化账户:CPA/转化折线全为 0 视觉上像空图,改展示消耗 + 点击按日趋势
|
|
4304
|
+
const useCostClickTrend = totalConversions === 0;
|
|
4305
|
+
|
|
4306
|
+
if (useCostClickTrend) {
|
|
4307
|
+
const costData = items.map((item) => Number(item?.cost) || 0);
|
|
4308
|
+
const clicksData = items.map((item) => Number(item?.clicks) || 0);
|
|
4309
|
+
return {
|
|
4310
|
+
type: "line",
|
|
4311
|
+
data: {
|
|
4312
|
+
labels,
|
|
4313
|
+
datasets: [
|
|
4314
|
+
{
|
|
4315
|
+
label: "消耗",
|
|
4316
|
+
data: costData,
|
|
4317
|
+
borderColor: "#3b82f6",
|
|
4318
|
+
backgroundColor: "rgba(59, 130, 246, 0.12)",
|
|
4319
|
+
tension: 0.3,
|
|
4320
|
+
yAxisID: "y1",
|
|
4321
|
+
pointRadius: 3,
|
|
4322
|
+
},
|
|
4323
|
+
{
|
|
4324
|
+
label: "点击次数",
|
|
4325
|
+
data: clicksData,
|
|
4326
|
+
borderColor: "#10b981",
|
|
4327
|
+
backgroundColor: "rgba(16, 185, 129, 0.12)",
|
|
4328
|
+
tension: 0.3,
|
|
4329
|
+
yAxisID: "y2",
|
|
4330
|
+
pointRadius: 3,
|
|
4331
|
+
},
|
|
4332
|
+
],
|
|
4333
|
+
},
|
|
4334
|
+
options: {
|
|
4335
|
+
responsive: true,
|
|
4336
|
+
maintainAspectRatio: false,
|
|
4337
|
+
interaction: { mode: "index", intersect: false },
|
|
4338
|
+
stacked: false,
|
|
4339
|
+
plugins: {
|
|
4340
|
+
legend: { labels: { font: { size: 12 } } },
|
|
4341
|
+
},
|
|
4342
|
+
scales: {
|
|
4343
|
+
y1: {
|
|
4344
|
+
type: "linear",
|
|
4345
|
+
position: "left",
|
|
4346
|
+
beginAtZero: true,
|
|
4347
|
+
ticks: { font: { size: 11 } },
|
|
4348
|
+
title: { display: true, text: "消耗", font: { size: 12 } },
|
|
4349
|
+
},
|
|
4350
|
+
y2: {
|
|
4351
|
+
type: "linear",
|
|
4352
|
+
position: "right",
|
|
4353
|
+
beginAtZero: true,
|
|
4354
|
+
ticks: { font: { size: 11 } },
|
|
4355
|
+
grid: { drawOnChartArea: false },
|
|
4356
|
+
title: {
|
|
4357
|
+
display: true,
|
|
4358
|
+
text: "点击次数",
|
|
4359
|
+
font: { size: 12 },
|
|
4360
|
+
},
|
|
4361
|
+
},
|
|
4362
|
+
x: { ticks: { font: { size: 11 } } },
|
|
4363
|
+
},
|
|
4364
|
+
},
|
|
4365
|
+
};
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4299
4368
|
const cpaData = items.map((item) => Number(item?.cpa) || 0);
|
|
4300
4369
|
const conversionsData = items.map((item) =>
|
|
4301
4370
|
Number(item?.conversions ?? 0),
|
|
@@ -4401,6 +4470,40 @@
|
|
|
4401
4470
|
};
|
|
4402
4471
|
};
|
|
4403
4472
|
|
|
4473
|
+
const buildConversionCostDailyTableHtml = (items) => {
|
|
4474
|
+
const rows = Array.isArray(items) ? items : [];
|
|
4475
|
+
if (!rows.length) {
|
|
4476
|
+
return '<tr><td colspan="5" class="empty-row">暂无按日趋势数据</td></tr>';
|
|
4477
|
+
}
|
|
4478
|
+
return rows
|
|
4479
|
+
.map((item) => {
|
|
4480
|
+
const rawDate = item?.date;
|
|
4481
|
+
let dateLabel = rawDate;
|
|
4482
|
+
const parsed = new Date(rawDate);
|
|
4483
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
4484
|
+
dateLabel = parsed.toISOString().slice(0, 10);
|
|
4485
|
+
}
|
|
4486
|
+
const cost = Number(item?.cost);
|
|
4487
|
+
const clicks = Number(item?.clicks);
|
|
4488
|
+
const conversions = Number(item?.conversions ?? 0);
|
|
4489
|
+
const cpa = Number(item?.cpa);
|
|
4490
|
+
const cpaDisplay =
|
|
4491
|
+
conversions > 0 && Number.isFinite(cpa)
|
|
4492
|
+
? formatNumber(cpa, 2)
|
|
4493
|
+
: "—";
|
|
4494
|
+
return `
|
|
4495
|
+
<tr>
|
|
4496
|
+
<td data-label="日期">${escapeHtml(String(dateLabel || "—"))}</td>
|
|
4497
|
+
<td data-label="消耗">${escapeHtml(formatNumber(cost, 2))}</td>
|
|
4498
|
+
<td data-label="点击">${escapeHtml(formatNumber(clicks, 0))}</td>
|
|
4499
|
+
<td data-label="转化">${escapeHtml(formatNumber(conversions, 0))}</td>
|
|
4500
|
+
<td data-label="CPA">${escapeHtml(cpaDisplay)}</td>
|
|
4501
|
+
</tr>
|
|
4502
|
+
`;
|
|
4503
|
+
})
|
|
4504
|
+
.join("");
|
|
4505
|
+
};
|
|
4506
|
+
|
|
4404
4507
|
const createAccountInfoSection = () => {
|
|
4405
4508
|
const reportMain = document.getElementById("report-main");
|
|
4406
4509
|
if (!reportMain) return null;
|
|
@@ -4883,17 +4986,47 @@
|
|
|
4883
4986
|
const conversionTrendSuggestionsList = toArray(
|
|
4884
4987
|
conversionCost.suggestions,
|
|
4885
4988
|
).map((item) => `<li class="insight-item">${escapeHtml(item)}</li>`);
|
|
4989
|
+
const conversionCostDailyItems = Array.isArray(conversionCost.items)
|
|
4990
|
+
? conversionCost.items
|
|
4991
|
+
: [];
|
|
4992
|
+
const conversionDailyTableHtml = buildConversionCostDailyTableHtml(
|
|
4993
|
+
conversionCostDailyItems,
|
|
4994
|
+
);
|
|
4886
4995
|
const conversionTrendContent =
|
|
4887
4996
|
(chartConfig ||
|
|
4997
|
+
conversionCostDailyItems.length ||
|
|
4888
4998
|
conversionTrendAnalysisList.length ||
|
|
4889
4999
|
conversionTrendSuggestionsList.length) &&
|
|
4890
5000
|
`
|
|
4891
5001
|
<div class="analysis-dimension conversion-trend-section">
|
|
4892
5002
|
<h4 class="dimension-title">转化成本与转化趋势优化</h4>
|
|
4893
5003
|
<div class="dimension-content">
|
|
5004
|
+
<div class="table-block" data-table-key="conversion-cost-daily">
|
|
5005
|
+
<table class="data-table">
|
|
5006
|
+
<thead>
|
|
5007
|
+
<tr>
|
|
5008
|
+
<th>日期</th>
|
|
5009
|
+
<th>消耗</th>
|
|
5010
|
+
<th>点击</th>
|
|
5011
|
+
<th>转化</th>
|
|
5012
|
+
<th>CPA</th>
|
|
5013
|
+
</tr>
|
|
5014
|
+
</thead>
|
|
5015
|
+
<tbody>
|
|
5016
|
+
${conversionDailyTableHtml}
|
|
5017
|
+
</tbody>
|
|
5018
|
+
</table>
|
|
5019
|
+
</div>
|
|
4894
5020
|
${
|
|
4895
5021
|
chartConfig
|
|
4896
5022
|
? `
|
|
5023
|
+
<p class="conversion-trend-chart-note" style="margin:12px 0 8px;font-size:13px;color:#64748b;">${
|
|
5024
|
+
conversionCostDailyItems.every(
|
|
5025
|
+
(item) => !(Number(item?.conversions) > 0),
|
|
5026
|
+
) && conversionCostDailyItems.length
|
|
5027
|
+
? "本期无转化记录,下图展示消耗与点击按日趋势。"
|
|
5028
|
+
: "下图展示 CPA 与转化次数按日趋势。"
|
|
5029
|
+
}</p>
|
|
4897
5030
|
<div class="chart-card conversion-trend-chart">
|
|
4898
5031
|
<div style="height: 280px;">
|
|
4899
5032
|
<canvas id="${chartId}" height="260"></canvas>
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
## 1. 执行摘要(总览)
|
|
19
19
|
|
|
20
20
|
- **CLI**:`siluzan-tso report bing-overview -a <mediaCustomerId> [--start … --end …] --json-out <dir>`
|
|
21
|
+
- **余额**:若 `OverviewSectionData` 误返 `remainingAccountBudget: 0`,CLI 会自动用 `GetMediaAccountInfo` 实时余额回填(与 Web 账户列表一致);**仍建议**单查余额用 `balance -m BingV2`。
|
|
21
22
|
|
|
22
23
|
## 2. 设备与地域
|
|
23
24
|
|
|
@@ -33,6 +33,9 @@ siluzan-tso google-ads-diagnosis render \
|
|
|
33
33
|
| `collect` | 拉 google-analysis + 可选 Lighthouse;输出 `google-ads-diagnosis-collect.json` |
|
|
34
34
|
| `render` | 读取 Agent 产出的 `google-ads-diagnosis.json`,注入 `GoogleAdsDiagnosisReport.html` |
|
|
35
35
|
|
|
36
|
+
- **render 自动合并(默认开启)**:若同目录存在 `google-ads-diagnosis-collect.json`,且 Agent 改坏了 `campaigns` / `geographic` / `keywords` 的 `items`(缺 `title`/`currentCost` 或环比写成字符串),CLI **自动从 collect.reportData 恢复 items**,保留 Agent 的 `analysis`/`suggestions`。可用 `--no-merge-collect` 关闭;`--collect <file>` 指定 collect 路径。
|
|
37
|
+
- **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空)。
|
|
38
|
+
|
|
36
39
|
- **禁止**在 `collect` 阶段生成或写入 `analysis` / `suggestions` / `diagnosisOverview` / `summary` 等建议性文案。
|
|
37
40
|
- **禁止**跳过 Agent 直接 `render` collect 产物(`render` 会校验全部叙事 / 建议字段;调试可加 `--lenient`)。
|
|
38
41
|
- JSON 顶层结构须与 MarkAI `sectionData.js` / `fetchData` 输出一致(见下文各 `section-*` 数据对象)。
|
|
@@ -204,6 +207,7 @@ siluzan-tso google-ads-diagnosis render \
|
|
|
204
207
|
- 指标列:花费(上期/本期/环比)、点击(上期/本期/环比)、转化率(上期/本期/环比)
|
|
205
208
|
- **数据**:`campaigns.items`(`title`、`previousCost`、`currentCost`、`costRateChange`、`previousClicks`、`currentClicks`、`clicksRateChange`、`previousCvr`、`currentCvr`、`cvrRateChange`)
|
|
206
209
|
- **分析 / 建议**:`campaigns.analysis`、`campaigns.suggestions`
|
|
210
|
+
- **Agent 禁止改写 `items` 行**(须原样保留 collect `reportData`);`render` 会从同目录 collect 自动恢复被改坏的 items
|
|
207
211
|
|
|
208
212
|
#### 国家地区分析
|
|
209
213
|
|
|
@@ -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.33-beta.
|
|
12
|
+
$PKG_VERSION = '1.1.33-beta.3'
|
|
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.33-beta.
|
|
12
|
+
readonly PKG_VERSION="1.1.33-beta.3"
|
|
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"
|