siluzan-tso-cli 1.1.20-beta.21 → 1.1.20-beta.22
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 +201 -179
- package/dist/skill/SKILL.md +1 -1
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/assets/campaign-create-template.md +3 -3
- package/dist/skill/references/account-analytics.md +1 -1
- package/dist/skill/references/currency.md +1 -1
- package/dist/skill/references/google-ads-campaign-plan.md +14 -2
- package/dist/skill/references/google-ads-rules/google-ads-keyword-taxonomy.md +1 -1
- package/dist/skill/references/google-ads.md +4 -3
- package/dist/skill/references/keyword-planner-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.20-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.20-beta.22),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -2418,12 +2418,12 @@ function createVersionNotifier(opts) {
|
|
|
2418
2418
|
const HOURS_24 = 24 * 60 * 60 * 1e3;
|
|
2419
2419
|
const TTL_MAIN_TAG_MS = 60 * 60 * 1e3;
|
|
2420
2420
|
const TTL_MIN_REQUIRED_MS = HOURS_24;
|
|
2421
|
-
async function fetchVersionByTag(tag,
|
|
2421
|
+
async function fetchVersionByTag(tag, cacheKey, fetchAtKey, cfg, maxAgeMs) {
|
|
2422
2422
|
const lastAt = cfg[fetchAtKey];
|
|
2423
|
-
if (typeof lastAt === "string" &&
|
|
2423
|
+
if (typeof lastAt === "string" && cacheKey in cfg) {
|
|
2424
2424
|
const lastMs = new Date(lastAt).getTime();
|
|
2425
2425
|
if (Date.now() - lastMs < maxAgeMs) {
|
|
2426
|
-
const v = cfg[
|
|
2426
|
+
const v = cfg[cacheKey];
|
|
2427
2427
|
const sv = typeof v === "string" && v ? v : null;
|
|
2428
2428
|
return { version: sv, hitNetwork: false };
|
|
2429
2429
|
}
|
|
@@ -109480,6 +109480,31 @@ init_cli_json_snapshot();
|
|
|
109480
109480
|
init_strip_legacy_google_fields();
|
|
109481
109481
|
init_cli_table();
|
|
109482
109482
|
|
|
109483
|
+
// src/commands/ad/campaign-length-violations.ts
|
|
109484
|
+
function pushLengthViolation(violations, item) {
|
|
109485
|
+
violations.push({ ...item, excess: item.actual - item.limit });
|
|
109486
|
+
}
|
|
109487
|
+
function formatLengthViolationsReport(violations) {
|
|
109488
|
+
if (violations.length === 0) return "";
|
|
109489
|
+
const lines = [
|
|
109490
|
+
"",
|
|
109491
|
+
`\u{1F4CF} \u8D85\u957F\u5185\u5BB9\u6E05\u5355\uFF08\u5171 ${violations.length} \u9879\uFF09`,
|
|
109492
|
+
" \u8BF7\u52FF\u5728 JSON \u4E2D\u81EA\u52A8\u622A\u65AD\uFF1B\u8BF7\u5C06\u5168\u90E8\u6761\u76EE\u4E0E\u4FEE\u6539\u65B9\u6848\u5217\u7ED9\u7528\u6237\uFF0C\u786E\u8BA4\u540E\u518D\u6539 JSON \u5E76\u91CD\u65B0 campaign-validate\u3002",
|
|
109493
|
+
""
|
|
109494
|
+
];
|
|
109495
|
+
for (let i = 0; i < violations.length; i++) {
|
|
109496
|
+
const v = violations[i];
|
|
109497
|
+
const mode = v.countMode === "google" ? "Google \u5B57\u7B26\u5BBD\uFF08CJK\xD72\uFF09" : "\u5B57\u7B26\u6570";
|
|
109498
|
+
lines.push(
|
|
109499
|
+
` ${i + 1}. ${v.path}`,
|
|
109500
|
+
` \u5B57\u6BB5 ${v.field} \xB7 \u4E0A\u9650 ${v.limit} \xB7 \u5F53\u524D ${v.actual}\uFF08${mode}\uFF0C\u8D85\u51FA ${v.excess}\uFF09`,
|
|
109501
|
+
` \u539F\u6587\uFF1A${JSON.stringify(v.text)}`
|
|
109502
|
+
);
|
|
109503
|
+
}
|
|
109504
|
+
lines.push("");
|
|
109505
|
+
return lines.join("\n");
|
|
109506
|
+
}
|
|
109507
|
+
|
|
109483
109508
|
// src/commands/ad/campaign-extensions.ts
|
|
109484
109509
|
var SITELINK_DESCRIPTION_MAX_LEN = 25;
|
|
109485
109510
|
function pushErr(errors, msg) {
|
|
@@ -109506,12 +109531,11 @@ function coercePropertiesFromRaw(raw) {
|
|
|
109506
109531
|
return props;
|
|
109507
109532
|
}
|
|
109508
109533
|
function normalizeSitelinkProperties(props) {
|
|
109509
|
-
const trimSitelinkDesc = (s) => s.length <= SITELINK_DESCRIPTION_MAX_LEN ? s : `${s.slice(0, SITELINK_DESCRIPTION_MAX_LEN - 1)}\u2026`;
|
|
109510
109534
|
if (props.LinkText && !props.Text) props.Text = props.LinkText;
|
|
109511
109535
|
const d1 = props.Description1 ?? props.Line2 ?? props.Text ?? props.LinkText;
|
|
109512
109536
|
const d2 = props.Description2 ?? props.Line3;
|
|
109513
|
-
props.Line2 =
|
|
109514
|
-
props.Line3 =
|
|
109537
|
+
props.Line2 = (d1 ?? "").trim() || props.Text || " ";
|
|
109538
|
+
props.Line3 = (d2 ?? "").trim() || props.Line2;
|
|
109515
109539
|
delete props.LinkText;
|
|
109516
109540
|
delete props.Description1;
|
|
109517
109541
|
delete props.Description2;
|
|
@@ -109530,7 +109554,7 @@ function normalizeExtensionsForBatchJob(extensions) {
|
|
|
109530
109554
|
return out;
|
|
109531
109555
|
});
|
|
109532
109556
|
}
|
|
109533
|
-
function validateSitelinkProperties(prefix, raw, errors, warnings) {
|
|
109557
|
+
function validateSitelinkProperties(prefix, raw, errors, warnings, lengthViolations) {
|
|
109534
109558
|
if (Array.isArray(raw["FinalUrls"])) {
|
|
109535
109559
|
pushErr(
|
|
109536
109560
|
errors,
|
|
@@ -109559,15 +109583,33 @@ function validateSitelinkProperties(prefix, raw, errors, warnings) {
|
|
|
109559
109583
|
const line2Raw = props.Description2 ?? props.Line3;
|
|
109560
109584
|
const line2 = typeof line2Raw === "string" ? line2Raw.trim() : "";
|
|
109561
109585
|
if (line1.length > SITELINK_DESCRIPTION_MAX_LEN) {
|
|
109586
|
+
pushLengthViolation(lengthViolations, {
|
|
109587
|
+
path: `${prefix}.Properties`,
|
|
109588
|
+
field: "Description1/Line2",
|
|
109589
|
+
kind: "sitelink_description",
|
|
109590
|
+
limit: SITELINK_DESCRIPTION_MAX_LEN,
|
|
109591
|
+
actual: line1.length,
|
|
109592
|
+
countMode: "ascii",
|
|
109593
|
+
text: line1
|
|
109594
|
+
});
|
|
109562
109595
|
pushErr(
|
|
109563
109596
|
errors,
|
|
109564
|
-
`${prefix}.Properties \u63CF\u8FF0\u884C 1 \u8D85\u8FC7 ${SITELINK_DESCRIPTION_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${line1.length}\uFF09\uFF1A"${line1
|
|
109597
|
+
`${prefix}.Properties \u63CF\u8FF0\u884C 1 \u8D85\u8FC7 ${SITELINK_DESCRIPTION_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${line1.length}\uFF09\uFF1A"${line1}"`
|
|
109565
109598
|
);
|
|
109566
109599
|
}
|
|
109567
109600
|
if (line2.length > SITELINK_DESCRIPTION_MAX_LEN) {
|
|
109601
|
+
pushLengthViolation(lengthViolations, {
|
|
109602
|
+
path: `${prefix}.Properties`,
|
|
109603
|
+
field: "Description2/Line3",
|
|
109604
|
+
kind: "sitelink_description",
|
|
109605
|
+
limit: SITELINK_DESCRIPTION_MAX_LEN,
|
|
109606
|
+
actual: line2.length,
|
|
109607
|
+
countMode: "ascii",
|
|
109608
|
+
text: line2
|
|
109609
|
+
});
|
|
109568
109610
|
pushErr(
|
|
109569
109611
|
errors,
|
|
109570
|
-
`${prefix}.Properties \u63CF\u8FF0\u884C 2 \u8D85\u8FC7 ${SITELINK_DESCRIPTION_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${line2.length}\uFF09\uFF1A"${line2
|
|
109612
|
+
`${prefix}.Properties \u63CF\u8FF0\u884C 2 \u8D85\u8FC7 ${SITELINK_DESCRIPTION_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${line2.length}\uFF09\uFF1A"${line2}"`
|
|
109571
109613
|
);
|
|
109572
109614
|
}
|
|
109573
109615
|
const rawDesc2 = raw["Description2"];
|
|
@@ -109581,7 +109623,7 @@ function validateSitelinkProperties(prefix, raw, errors, warnings) {
|
|
|
109581
109623
|
);
|
|
109582
109624
|
}
|
|
109583
109625
|
}
|
|
109584
|
-
function validateCampaignExtensionsForBatchJob(campaign, errors, warnings) {
|
|
109626
|
+
function validateCampaignExtensionsForBatchJob(campaign, errors, warnings, lengthViolations = []) {
|
|
109585
109627
|
const extensions = campaign["ExtensionsForBatchJob"];
|
|
109586
109628
|
if (extensions === void 0) return;
|
|
109587
109629
|
if (!Array.isArray(extensions)) {
|
|
@@ -109603,7 +109645,13 @@ function validateCampaignExtensionsForBatchJob(campaign, errors, warnings) {
|
|
|
109603
109645
|
continue;
|
|
109604
109646
|
}
|
|
109605
109647
|
if (type === "SITELINK" && props && typeof props === "object") {
|
|
109606
|
-
validateSitelinkProperties(
|
|
109648
|
+
validateSitelinkProperties(
|
|
109649
|
+
prefix,
|
|
109650
|
+
props,
|
|
109651
|
+
errors,
|
|
109652
|
+
warnings,
|
|
109653
|
+
lengthViolations
|
|
109654
|
+
);
|
|
109607
109655
|
} else if (type === "SITELINK") {
|
|
109608
109656
|
pushErr(errors, `${prefix}\uFF08SITELINK\uFF09\u7F3A\u5C11 Properties`);
|
|
109609
109657
|
}
|
|
@@ -109651,6 +109699,11 @@ function firstKeywordTextFromRecord(k) {
|
|
|
109651
109699
|
}
|
|
109652
109700
|
return "";
|
|
109653
109701
|
}
|
|
109702
|
+
function keywordRowToYuan(item) {
|
|
109703
|
+
const { maxCPC: rawMaxCpc, ...rest } = item;
|
|
109704
|
+
if (rawMaxCpc == null) return item;
|
|
109705
|
+
return { ...rest, maxCPCYuan: toDisplayMoney(rawMaxCpc) };
|
|
109706
|
+
}
|
|
109654
109707
|
async function runAdKeywords(opts) {
|
|
109655
109708
|
const config = loadConfig(opts.token);
|
|
109656
109709
|
const googleApiUrl = requireGoogleApi(config);
|
|
@@ -109678,8 +109731,9 @@ async function runAdKeywords(opts) {
|
|
|
109678
109731
|
});
|
|
109679
109732
|
const label = opts.negative ? "\u5426\u5B9A\u5173\u952E\u8BCD" : "\u5173\u952E\u8BCD";
|
|
109680
109733
|
const n = items.length;
|
|
109734
|
+
const displayItems = opts.negative ? items : items.map((row) => keywordRowToYuan(row));
|
|
109681
109735
|
const kwPayload = stripLegacyGoogleFieldsIfV2Present(
|
|
109682
|
-
wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
|
|
109736
|
+
wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items: displayItems })
|
|
109683
109737
|
);
|
|
109684
109738
|
if (await emitCliJsonOrSnapshot(opts, {
|
|
109685
109739
|
section: `ad-keywords-${opts.account}-${opts.negative ? "negative" : "positive"}`,
|
|
@@ -109870,7 +109924,7 @@ async function runAdKeywordEdit(opts) {
|
|
|
109870
109924
|
} else if (opts.text !== void 0) {
|
|
109871
109925
|
body["keywordText"] = [opts.text];
|
|
109872
109926
|
}
|
|
109873
|
-
if (opts.maxCpc !== void 0) body["maxCPC"] = opts.maxCpc;
|
|
109927
|
+
if (opts.maxCpc !== void 0) body["maxCPC"] = toCentAmount(opts.maxCpc);
|
|
109874
109928
|
if (opts.finalUrl !== void 0) body["finalURL"] = opts.finalUrl;
|
|
109875
109929
|
if (opts.status !== void 0) body["userStatusV2"] = opts.status;
|
|
109876
109930
|
const url = `${googleApiUrl}/keywordmanagement/Keyword/${opts.account}/batch`;
|
|
@@ -110040,15 +110094,26 @@ function inferMatchTypeFromDisplayText(text) {
|
|
|
110040
110094
|
if (t.length >= 2 && t.startsWith("[") && t.endsWith("]")) return "Exact";
|
|
110041
110095
|
return "Broad";
|
|
110042
110096
|
}
|
|
110043
|
-
function validateKeywordCore(core, path22, errors) {
|
|
110097
|
+
function validateKeywordCore(core, path22, errors, lengthViolations) {
|
|
110044
110098
|
const trimmed = core.trim();
|
|
110045
110099
|
if (!trimmed) {
|
|
110046
110100
|
errors.push(`${path22} \u5173\u952E\u8BCD\u8BCD\u5E72\u4E0D\u80FD\u4E3A\u7A7A`);
|
|
110047
110101
|
return false;
|
|
110048
110102
|
}
|
|
110049
110103
|
if (trimmed.length > GOOGLE_KEYWORD_MAX_CORE_LENGTH) {
|
|
110104
|
+
if (lengthViolations) {
|
|
110105
|
+
pushLengthViolation(lengthViolations, {
|
|
110106
|
+
path: path22,
|
|
110107
|
+
field: "KeywordText",
|
|
110108
|
+
kind: "keyword_core",
|
|
110109
|
+
limit: GOOGLE_KEYWORD_MAX_CORE_LENGTH,
|
|
110110
|
+
actual: trimmed.length,
|
|
110111
|
+
countMode: "ascii",
|
|
110112
|
+
text: trimmed
|
|
110113
|
+
});
|
|
110114
|
+
}
|
|
110050
110115
|
errors.push(
|
|
110051
|
-
`${path22} \u5173\u952E\u8BCD\u8BCD\u5E72\u8D85\u8FC7 ${GOOGLE_KEYWORD_MAX_CORE_LENGTH} \u5B57\u7B26\uFF08\u5F53\u524D ${trimmed.length}\uFF09\uFF1A"${trimmed
|
|
110116
|
+
`${path22} \u5173\u952E\u8BCD\u8BCD\u5E72\u8D85\u8FC7 ${GOOGLE_KEYWORD_MAX_CORE_LENGTH} \u5B57\u7B26\uFF08\u5F53\u524D ${trimmed.length}\uFF09\uFF1A"${trimmed}"`
|
|
110052
110117
|
);
|
|
110053
110118
|
return false;
|
|
110054
110119
|
}
|
|
@@ -110066,7 +110131,7 @@ function normalizeKeywordSurface(raw, matchTypeV2) {
|
|
|
110066
110131
|
const formatted = formatKeywordTextForMatchType(raw, matchType);
|
|
110067
110132
|
return { formatted, matchType, inferredMatchType: fromField === null };
|
|
110068
110133
|
}
|
|
110069
|
-
function normalizeKeywordsForBatchJobBlock(block, path22, errors, warnings) {
|
|
110134
|
+
function normalizeKeywordsForBatchJobBlock(block, path22, errors, warnings, lengthViolations) {
|
|
110070
110135
|
const texts = block["KeywordText"];
|
|
110071
110136
|
if (!Array.isArray(texts)) return;
|
|
110072
110137
|
const declaredUi = matchTypeV2ToUi(block["MatchTypeV2"]);
|
|
@@ -110100,7 +110165,8 @@ function normalizeKeywordsForBatchJobBlock(block, path22, errors, warnings) {
|
|
|
110100
110165
|
warnings.push(`${path22}.KeywordText[${k}] \u8BCD\u9762\u5DF2\u89C4\u8303\u5316\uFF1A${trimmedRaw} \u2192 ${formatted}`);
|
|
110101
110166
|
}
|
|
110102
110167
|
const core = unwrapKeywordDisplayTextForEdit(formatted);
|
|
110103
|
-
if (!validateKeywordCore(core, `${path22}.KeywordText[${k}]`, errors))
|
|
110168
|
+
if (!validateKeywordCore(core, `${path22}.KeywordText[${k}]`, errors, lengthViolations))
|
|
110169
|
+
continue;
|
|
110104
110170
|
const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
|
|
110105
110171
|
if (seen.has(dedupeKey)) {
|
|
110106
110172
|
warnings.push(`${path22}.KeywordText[${k}] \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
|
|
@@ -110118,7 +110184,7 @@ function normalizeKeywordsForBatchJobBlock(block, path22, errors, warnings) {
|
|
|
110118
110184
|
block["MatchTypeV2"] = matchTypeUiToV2(resolvedUi);
|
|
110119
110185
|
}
|
|
110120
110186
|
}
|
|
110121
|
-
function normalizeCampaignKeywordTrees(campaign, errors, warnings) {
|
|
110187
|
+
function normalizeCampaignKeywordTrees(campaign, errors, warnings, lengthViolations) {
|
|
110122
110188
|
const neg = campaign["NegativeKeywordsForBatchJob"];
|
|
110123
110189
|
if (Array.isArray(neg)) {
|
|
110124
110190
|
for (let i = 0; i < neg.length; i++) {
|
|
@@ -110129,7 +110195,8 @@ function normalizeCampaignKeywordTrees(campaign, errors, warnings) {
|
|
|
110129
110195
|
block,
|
|
110130
110196
|
`campaign.NegativeKeywordsForBatchJob[${i}]`,
|
|
110131
110197
|
errors,
|
|
110132
|
-
warnings
|
|
110198
|
+
warnings,
|
|
110199
|
+
lengthViolations
|
|
110133
110200
|
);
|
|
110134
110201
|
}
|
|
110135
110202
|
}
|
|
@@ -110147,7 +110214,8 @@ function normalizeCampaignKeywordTrees(campaign, errors, warnings) {
|
|
|
110147
110214
|
block,
|
|
110148
110215
|
`campaign.AdGroupsForBatchJob[${i}].KeywordsForBatchJob[${j}]`,
|
|
110149
110216
|
errors,
|
|
110150
|
-
warnings
|
|
110217
|
+
warnings,
|
|
110218
|
+
lengthViolations
|
|
110151
110219
|
);
|
|
110152
110220
|
}
|
|
110153
110221
|
}
|
|
@@ -110265,7 +110333,7 @@ function calcGoogleCharLength(text) {
|
|
|
110265
110333
|
}
|
|
110266
110334
|
return len;
|
|
110267
110335
|
}
|
|
110268
|
-
function validateRsaDisplayPath(prefix, ad, errors) {
|
|
110336
|
+
function validateRsaDisplayPath(prefix, ad, errors, lengthViolations) {
|
|
110269
110337
|
const jsonHint = `\u5728 ${prefix} \u589E\u52A0 "Path1"\u3001"Path2"\uFF08PascalCase\uFF0C\u52FF\u7528 path1/path2\uFF1B\u6A21\u677F\u89C1 assets/campaign-create-template.json\uFF09`;
|
|
110270
110338
|
if (ad["path1"] !== void 0 && ad["Path1"] === void 0) {
|
|
110271
110339
|
pushErr2(
|
|
@@ -110307,6 +110375,15 @@ function validateRsaDisplayPath(prefix, ad, errors) {
|
|
|
110307
110375
|
}
|
|
110308
110376
|
const len = calcGoogleCharLength(raw);
|
|
110309
110377
|
if (len > 15) {
|
|
110378
|
+
pushLengthViolation(lengthViolations, {
|
|
110379
|
+
path: `${prefix}.${field}`,
|
|
110380
|
+
field,
|
|
110381
|
+
kind: "rsa_path",
|
|
110382
|
+
limit: 15,
|
|
110383
|
+
actual: len,
|
|
110384
|
+
countMode: "google",
|
|
110385
|
+
text: raw
|
|
110386
|
+
});
|
|
110310
110387
|
pushErr2(
|
|
110311
110388
|
errors,
|
|
110312
110389
|
`${prefix}.${field} \u8D85\u8FC7 15 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u6309 2 \u8BA1\uFF09\uFF1A"${raw}"`
|
|
@@ -110320,7 +110397,7 @@ function validateRsaDisplayPath(prefix, ad, errors) {
|
|
|
110320
110397
|
}
|
|
110321
110398
|
}
|
|
110322
110399
|
}
|
|
110323
|
-
function validateRsaAd(prefix, ad, errors, warnings) {
|
|
110400
|
+
function validateRsaAd(prefix, ad, errors, warnings, lengthViolations) {
|
|
110324
110401
|
const h1 = ad["headlinePart1"];
|
|
110325
110402
|
const h2 = ad["headlinePart2"];
|
|
110326
110403
|
const h3 = ad["headlinePart3"];
|
|
@@ -110341,12 +110418,28 @@ function validateRsaAd(prefix, ad, errors, warnings) {
|
|
|
110341
110418
|
} else if (headlines.length < 12) {
|
|
110342
110419
|
pushWarn2(warnings, `${prefix} \u6807\u9898\u63A8\u8350 12\u201315 \u6761\uFF08\u5F53\u524D ${headlines.length} \u6761\uFF09\uFF0C\u66F4\u591A\u7EC4\u5408\u53EF\u63D0\u5347\u6295\u653E\u8868\u73B0`);
|
|
110343
110420
|
}
|
|
110421
|
+
const headlineFields = [
|
|
110422
|
+
"headlinePart1",
|
|
110423
|
+
"headlinePart2",
|
|
110424
|
+
"headlinePart3",
|
|
110425
|
+
...moreH.map((_, idx) => `AddtionalHeadlines[${idx}]`)
|
|
110426
|
+
];
|
|
110344
110427
|
for (let i = 0; i < headlines.length; i++) {
|
|
110345
110428
|
const len = calcGoogleCharLength(headlines[i]);
|
|
110346
110429
|
if (len > 30) {
|
|
110430
|
+
const field = headlineFields[i] ?? `headline[${i}]`;
|
|
110431
|
+
pushLengthViolation(lengthViolations, {
|
|
110432
|
+
path: `${prefix}.${field}`,
|
|
110433
|
+
field,
|
|
110434
|
+
kind: "rsa_headline",
|
|
110435
|
+
limit: 30,
|
|
110436
|
+
actual: len,
|
|
110437
|
+
countMode: "google",
|
|
110438
|
+
text: headlines[i]
|
|
110439
|
+
});
|
|
110347
110440
|
pushErr2(
|
|
110348
110441
|
errors,
|
|
110349
|
-
`${prefix} \u6807\u9898[${i}] \u8D85\u8FC7 30 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u8BA1 2\uFF09\uFF1A"${headlines[i]
|
|
110442
|
+
`${prefix} \u6807\u9898[${i}] \u8D85\u8FC7 30 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u8BA1 2\uFF09\uFF1A"${headlines[i]}"`
|
|
110350
110443
|
);
|
|
110351
110444
|
}
|
|
110352
110445
|
}
|
|
@@ -110360,13 +110453,28 @@ function validateRsaAd(prefix, ad, errors, warnings) {
|
|
|
110360
110453
|
} else if (descriptions.length < 4) {
|
|
110361
110454
|
pushWarn2(warnings, `${prefix} \u63CF\u8FF0\u63A8\u8350 4 \u6761\uFF08\u5F53\u524D ${descriptions.length} \u6761\uFF09`);
|
|
110362
110455
|
}
|
|
110456
|
+
const descriptionFields = [
|
|
110457
|
+
"adDescription",
|
|
110458
|
+
"adDescription2",
|
|
110459
|
+
...moreD.map((_, idx) => `AddtionalAdDescriptions[${idx}]`)
|
|
110460
|
+
];
|
|
110363
110461
|
for (let i = 0; i < descriptions.length; i++) {
|
|
110364
110462
|
const len = calcGoogleCharLength(descriptions[i]);
|
|
110365
110463
|
if (len > 90) {
|
|
110366
|
-
|
|
110464
|
+
const field = descriptionFields[i] ?? `description[${i}]`;
|
|
110465
|
+
pushLengthViolation(lengthViolations, {
|
|
110466
|
+
path: `${prefix}.${field}`,
|
|
110467
|
+
field,
|
|
110468
|
+
kind: "rsa_description",
|
|
110469
|
+
limit: 90,
|
|
110470
|
+
actual: len,
|
|
110471
|
+
countMode: "google",
|
|
110472
|
+
text: descriptions[i]
|
|
110473
|
+
});
|
|
110474
|
+
pushErr2(errors, `${prefix} \u63CF\u8FF0[${i}] \u8D85\u8FC7 90 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF09\uFF1A"${descriptions[i]}"`);
|
|
110367
110475
|
}
|
|
110368
110476
|
}
|
|
110369
|
-
validateRsaDisplayPath(prefix, ad, errors);
|
|
110477
|
+
validateRsaDisplayPath(prefix, ad, errors, lengthViolations);
|
|
110370
110478
|
const finalUrl = ad["Finalurl"] ?? ad["DestinationUrl"];
|
|
110371
110479
|
if (typeof finalUrl === "string" && finalUrl.length > 0 && !URL_REGEX.test(finalUrl)) {
|
|
110372
110480
|
pushErr2(errors, `${prefix} Finalurl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08\u5E94\u4EE5 http(s):// \u5F00\u5934\uFF09\uFF1A${finalUrl}`);
|
|
@@ -110376,8 +110484,14 @@ function validateRsaAd(prefix, ad, errors, warnings) {
|
|
|
110376
110484
|
function validateCampaignCreateConfigCore(cfg) {
|
|
110377
110485
|
const errors = [];
|
|
110378
110486
|
const warnings = [];
|
|
110487
|
+
const lengthViolations = [];
|
|
110379
110488
|
if (cfg.campaign && typeof cfg.campaign === "object" && !Array.isArray(cfg.campaign)) {
|
|
110380
|
-
normalizeCampaignKeywordTrees(
|
|
110489
|
+
normalizeCampaignKeywordTrees(
|
|
110490
|
+
cfg.campaign,
|
|
110491
|
+
errors,
|
|
110492
|
+
warnings,
|
|
110493
|
+
lengthViolations
|
|
110494
|
+
);
|
|
110381
110495
|
}
|
|
110382
110496
|
if (!cfg.account?.toString().trim()) {
|
|
110383
110497
|
pushErr2(errors, "account\uFF08\u9876\u5C42 customerId\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
|
|
@@ -110391,7 +110505,7 @@ function validateCampaignCreateConfigCore(cfg) {
|
|
|
110391
110505
|
}
|
|
110392
110506
|
if (!cfg.campaign || typeof cfg.campaign !== "object" || Array.isArray(cfg.campaign)) {
|
|
110393
110507
|
pushErr2(errors, "campaign \u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A\uFF08\u540E\u7AEF\uFF1Acampaign is null\uFF09");
|
|
110394
|
-
return { errors, warnings };
|
|
110508
|
+
return { errors, warnings, lengthViolations };
|
|
110395
110509
|
}
|
|
110396
110510
|
const campaign = cfg.campaign;
|
|
110397
110511
|
if (campaign["TargetPartnerSearchNetwork"] === true) {
|
|
@@ -110554,7 +110668,7 @@ function validateCampaignCreateConfigCore(cfg) {
|
|
|
110554
110668
|
for (let j = 0; j < ads.length; j++) {
|
|
110555
110669
|
const ad = ads[j];
|
|
110556
110670
|
if (ad && typeof ad === "object") {
|
|
110557
|
-
validateRsaAd(`${gPrefix}.AdsForBatchJob[${j}]`, ad, errors, warnings);
|
|
110671
|
+
validateRsaAd(`${gPrefix}.AdsForBatchJob[${j}]`, ad, errors, warnings, lengthViolations);
|
|
110558
110672
|
}
|
|
110559
110673
|
}
|
|
110560
110674
|
}
|
|
@@ -110571,8 +110685,8 @@ function validateCampaignCreateConfigCore(cfg) {
|
|
|
110571
110685
|
}
|
|
110572
110686
|
validateCampaignRsaCrossGroupHeadlines(campaign, errors);
|
|
110573
110687
|
}
|
|
110574
|
-
validateCampaignExtensionsForBatchJob(campaign, errors, warnings);
|
|
110575
|
-
return { errors, warnings };
|
|
110688
|
+
validateCampaignExtensionsForBatchJob(campaign, errors, warnings, lengthViolations);
|
|
110689
|
+
return { errors, warnings, lengthViolations };
|
|
110576
110690
|
}
|
|
110577
110691
|
function runCampaignCreateValidation(cfg) {
|
|
110578
110692
|
return validateCampaignCreateConfigCore(cfg);
|
|
@@ -113146,46 +113260,53 @@ async function runAiCreationUpdate(opts) {
|
|
|
113146
113260
|
|
|
113147
113261
|
// src/commands/ad/campaign-validate.ts
|
|
113148
113262
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
113263
|
+
init_cli_json_snapshot();
|
|
113264
|
+
var LENGTH_VIOLATION_AGENT_HINT = "\u52FF\u81EA\u52A8\u622A\u65AD JSON\u3002\u8BFB\u53D6\u843D\u76D8 JSON \u7684 lengthViolations\uFF0C\u5C06\u5168\u90E8\u6761\u76EE\u4E0E\u4FEE\u6539\u65B9\u6848\u5217\u7ED9\u7528\u6237\uFF0C\u786E\u8BA4\u540E\u5199\u5165 campaign.json \u5E76\u91CD\u65B0 campaign-validate\u3002";
|
|
113149
113265
|
async function runAdCampaignValidate(opts) {
|
|
113150
113266
|
const cfg = loadCampaignCreateConfig(opts.configFile);
|
|
113151
|
-
const { errors, warnings } = runCampaignCreateValidation(cfg);
|
|
113267
|
+
const { errors, warnings, lengthViolations } = runCampaignCreateValidation(cfg);
|
|
113152
113268
|
if (opts.writeNormalized) {
|
|
113153
113269
|
const toWrite = stripMetaKeysForExport(cfg);
|
|
113154
113270
|
writeFileSync3(opts.writeNormalized, `${JSON.stringify(toWrite, null, 2)}
|
|
113155
113271
|
`, "utf8");
|
|
113156
113272
|
}
|
|
113157
|
-
|
|
113158
|
-
|
|
113159
|
-
|
|
113160
|
-
|
|
113161
|
-
|
|
113162
|
-
|
|
113163
|
-
|
|
113164
|
-
|
|
113165
|
-
|
|
113166
|
-
|
|
113167
|
-
|
|
113168
|
-
|
|
113169
|
-
|
|
113170
|
-
|
|
113171
|
-
|
|
113172
|
-
|
|
113173
|
-
|
|
113174
|
-
if (
|
|
113175
|
-
|
|
113176
|
-
|
|
113177
|
-
|
|
113178
|
-
|
|
113179
|
-
}
|
|
113180
|
-
console.log("\n\u2705 \u6295\u653E\u914D\u7F6E\u6821\u9A8C\u901A\u8FC7");
|
|
113181
|
-
if (opts.writeNormalized) {
|
|
113182
|
-
console.log(` \u5DF2\u5199\u5165\u89C4\u8303\u5316 JSON\uFF1A${opts.writeNormalized}`);
|
|
113183
|
-
}
|
|
113184
|
-
console.log();
|
|
113273
|
+
const payload = {
|
|
113274
|
+
ok: errors.length === 0,
|
|
113275
|
+
configFile: opts.configFile,
|
|
113276
|
+
account: cfg.account?.toString().trim() || void 0,
|
|
113277
|
+
errors,
|
|
113278
|
+
warnings,
|
|
113279
|
+
lengthViolations,
|
|
113280
|
+
agentHint: lengthViolations.length > 0 ? LENGTH_VIOLATION_AGENT_HINT : void 0
|
|
113281
|
+
};
|
|
113282
|
+
const accountSuffix = payload.account || void 0;
|
|
113283
|
+
if (await emitCliJsonOrSnapshot(opts, {
|
|
113284
|
+
section: "ad-campaign-validate",
|
|
113285
|
+
commandLabel: "ad campaign-validate",
|
|
113286
|
+
commandHint: opts.configFile,
|
|
113287
|
+
payload,
|
|
113288
|
+
idSuffix: accountSuffix
|
|
113289
|
+
})) {
|
|
113290
|
+
if (!payload.ok) process.exit(1);
|
|
113291
|
+
return;
|
|
113292
|
+
}
|
|
113293
|
+
if (warnings.length > 0) {
|
|
113294
|
+
console.warn("\n\u26A0\uFE0F \u6295\u653E\u914D\u7F6E\u8B66\u544A\uFF1A");
|
|
113295
|
+
for (const w of warnings) console.warn(` \u2022 ${w}`);
|
|
113185
113296
|
}
|
|
113186
113297
|
if (errors.length > 0) {
|
|
113298
|
+
console.error("\n\u274C \u6295\u653E\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A");
|
|
113299
|
+
for (const e of errors) console.error(` \u2022 ${e}`);
|
|
113300
|
+
const lengthReport = formatLengthViolationsReport(lengthViolations);
|
|
113301
|
+
if (lengthReport) console.error(lengthReport);
|
|
113302
|
+
console.error();
|
|
113187
113303
|
process.exit(1);
|
|
113188
113304
|
}
|
|
113305
|
+
console.log("\n\u2705 \u6295\u653E\u914D\u7F6E\u6821\u9A8C\u901A\u8FC7");
|
|
113306
|
+
if (opts.writeNormalized) {
|
|
113307
|
+
console.log(` \u5DF2\u5199\u5165\u89C4\u8303\u5316 JSON\uFF1A${opts.writeNormalized}`);
|
|
113308
|
+
}
|
|
113309
|
+
console.log();
|
|
113189
113310
|
}
|
|
113190
113311
|
|
|
113191
113312
|
// src/commands/ad/_register.ts
|
|
@@ -113535,16 +113656,20 @@ function register20(program2) {
|
|
|
113535
113656
|
}
|
|
113536
113657
|
);
|
|
113537
113658
|
adCmd.command("campaign-validate").description(
|
|
113538
|
-
"\u6821\u9A8C campaign-create JSON\uFF08\u8BCD\u9762\u89C4\u8303\u5316 + \u540E\u7AEF\u786C\u7EA6\u675F\uFF1B\u4E0D\u8C03\u7528 API\uFF09\n\n \u7528\u6CD5\uFF1A\n siluzan-tso ad campaign-validate --config-file ./campaign.json\n siluzan-tso ad campaign-validate --config-file ./campaign.json --write-normalized ./campaign.normalized.json"
|
|
113659
|
+
"\u6821\u9A8C campaign-create JSON\uFF08\u8BCD\u9762\u89C4\u8303\u5316 + \u540E\u7AEF\u786C\u7EA6\u675F\uFF1B\u4E0D\u8C03\u7528 API\uFF09\n\n \u7528\u6CD5\uFF1A\n siluzan-tso ad campaign-validate --config-file ./campaign.json --json-out ./snap-campaign\n siluzan-tso ad campaign-validate --config-file ./campaign.json --write-normalized ./campaign.normalized.json"
|
|
113539
113660
|
).requiredOption("--config-file <path>", "campaign-create JSON \u8DEF\u5F84").option(
|
|
113540
113661
|
"--write-normalized <path>",
|
|
113541
113662
|
"\u5C06\u89C4\u8303\u5316\u540E\u7684 JSON \u5199\u5165\u8BE5\u8DEF\u5F84\uFF08\u5173\u952E\u8BCD\u8BCD\u9762\u5DF2\u4FEE\u6B63\uFF09"
|
|
113542
|
-
).option("--json", "\u8F93\u51FA { ok, errors, warnings }
|
|
113663
|
+
).option("--json", "\u8F93\u51FA { ok, errors, warnings, lengthViolations }\uFF08\u4E0E --json-out \u4E92\u65A5\uFF09", false).option(
|
|
113664
|
+
"--json-out <path>",
|
|
113665
|
+
"\u843D\u76D8\u6821\u9A8C\u7ED3\u679C\uFF08\u542B lengthViolations\uFF09\u5E76\u66F4\u65B0 cli-manifest\uFF1B\u4E0E --json \u4E92\u65A5"
|
|
113666
|
+
).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
|
|
113543
113667
|
async (opts) => {
|
|
113544
113668
|
await runAdCampaignValidate({
|
|
113545
113669
|
configFile: opts.configFile,
|
|
113546
113670
|
writeNormalized: opts.writeNormalized,
|
|
113547
113671
|
json: opts.json,
|
|
113672
|
+
jsonOut: opts.jsonOut,
|
|
113548
113673
|
verbose: opts.verbose
|
|
113549
113674
|
});
|
|
113550
113675
|
}
|
|
@@ -113705,7 +113830,7 @@ function register20(program2) {
|
|
|
113705
113830
|
'\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact\uFF08\u5199 matchTypeV2\uFF0C\u5E76\u9ED8\u8BA4\u540C\u6B65\u6539\u5199 keywordText \u4E3A\u8BCD\u5E72/"\u8BCD"/[\u8BCD] \u4EE5\u7B26\u5408\u7F51\u5173\u63A8\u65AD\uFF09'
|
|
113706
113831
|
).option(
|
|
113707
113832
|
"--max-cpc <n>",
|
|
113708
|
-
"\u6700\u9AD8\u6BCF\u6B21\u70B9\u51FB\u8D39\u7528 maxCPC\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982
|
|
113833
|
+
"\u6700\u9AD8\u6BCF\u6B21\u70B9\u51FB\u8D39\u7528 maxCPC\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 10 \u8868\u793A \xA510\uFF1BCLI \u5185\u90E8 \xD7100 \u5199\u5165\u300C\u5206\u300D\u5B57\u6BB5\uFF0C\u4E0E adgroup-edit --max-cpc \u540C\u53E3\u5F84\uFF1B0 \u8868\u793A\u6309\u5E73\u53F0/\u8BA1\u5212\u9ED8\u8BA4\uFF09"
|
|
113709
113834
|
).option("--final-url <url>", "\u5173\u952E\u8BCD\u7EA7\u6700\u7EC8\u5230\u8FBE\u7F51\u5740 finalURL").option("--status <Enabled|Paused>", "\u7528\u6237\u72B6\u6001\uFF08\u5199\u5165 userStatusV2\uFF0C\u4E0E Web \u5173\u952E\u8BCD\u5F00\u5173\u4E00\u81F4\uFF09").option("--start <date>", "\u5217\u8868\u67E5\u8BE2\u8D77\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u5217\u8868\u67E5\u8BE2\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
|
|
113710
113835
|
async (opts) => {
|
|
113711
113836
|
if (opts.matchType && !["Broad", "Phrase", "Exact"].includes(opts.matchType)) {
|
|
@@ -114022,59 +114147,6 @@ function register20(program2) {
|
|
|
114022
114147
|
|
|
114023
114148
|
// src/commands/keyword.ts
|
|
114024
114149
|
init_auth();
|
|
114025
|
-
|
|
114026
|
-
// src/utils/usd-cny-rate.ts
|
|
114027
|
-
init_auth();
|
|
114028
|
-
var RATE_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
114029
|
-
var rateCache = null;
|
|
114030
|
-
function cacheKey(config) {
|
|
114031
|
-
return `${config.apiBaseUrl.replace(/\/$/, "")}|USD|CNY`;
|
|
114032
|
-
}
|
|
114033
|
-
function parseExrate(data) {
|
|
114034
|
-
if (data == null || typeof data !== "object") return null;
|
|
114035
|
-
const raw = data.exrate;
|
|
114036
|
-
const n = typeof raw === "string" ? Number(raw) : Number(raw);
|
|
114037
|
-
if (!Number.isFinite(n) || n <= 0) return null;
|
|
114038
|
-
return n;
|
|
114039
|
-
}
|
|
114040
|
-
async function getUsdToCnyExchangeRate(config, verbose) {
|
|
114041
|
-
const key = cacheKey(config);
|
|
114042
|
-
const now = Date.now();
|
|
114043
|
-
if (rateCache && rateCache.key === key && now - rateCache.fetchedAt < RATE_CACHE_TTL_MS) {
|
|
114044
|
-
if (verbose) {
|
|
114045
|
-
console.error(` [\u6C47\u7387] \u4F7F\u7528\u7F13\u5B58 USD\u2192CNY=${rateCache.rate}\uFF08${Math.round((now - rateCache.fetchedAt) / 1e3)}s \u524D\uFF09`);
|
|
114046
|
-
}
|
|
114047
|
-
return rateCache.rate;
|
|
114048
|
-
}
|
|
114049
|
-
const base = config.apiBaseUrl.replace(/\/$/, "");
|
|
114050
|
-
const url = `${base}/allinpay/GetRate?srcccy=USD&dstccy=CNY`;
|
|
114051
|
-
try {
|
|
114052
|
-
const data = await apiFetch2(url, config, { method: "GET" }, verbose);
|
|
114053
|
-
const rate = parseExrate(data);
|
|
114054
|
-
if (rate == null) {
|
|
114055
|
-
console.error("\n\u26A0 \u83B7\u53D6\u6C47\u7387\u5931\u8D25\uFF1AGetRate \u672A\u8FD4\u56DE\u6709\u6548 exrate\uFF0C\u51FA\u4EF7 CNY \u5B57\u6BB5\u5C06\u7701\u7565\u3002\n");
|
|
114056
|
-
return null;
|
|
114057
|
-
}
|
|
114058
|
-
rateCache = { key, rate, fetchedAt: now };
|
|
114059
|
-
if (verbose) {
|
|
114060
|
-
console.error(` [\u6C47\u7387] USD\u2192CNY=${rate}\uFF08\u5DF2\u7F13\u5B58 ${RATE_CACHE_TTL_MS / 6e4} \u5206\u949F\uFF09`);
|
|
114061
|
-
}
|
|
114062
|
-
return rate;
|
|
114063
|
-
} catch (err) {
|
|
114064
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
114065
|
-
console.error(`
|
|
114066
|
-
\u26A0 \u83B7\u53D6 USD\u2192CNY \u6C47\u7387\u5931\u8D25\uFF08\u51FA\u4EF7 CNY \u5B57\u6BB5\u5C06\u7701\u7565\uFF09\uFF1A${msg}
|
|
114067
|
-
`);
|
|
114068
|
-
return null;
|
|
114069
|
-
}
|
|
114070
|
-
}
|
|
114071
|
-
function convertUsdToCny(amountUsd, rate) {
|
|
114072
|
-
if (amountUsd == null || !Number.isFinite(amountUsd)) return null;
|
|
114073
|
-
if (!Number.isFinite(rate) || rate <= 0) return null;
|
|
114074
|
-
return Math.round(amountUsd * rate * 100) / 100;
|
|
114075
|
-
}
|
|
114076
|
-
|
|
114077
|
-
// src/commands/keyword.ts
|
|
114078
114150
|
init_cli_json_snapshot();
|
|
114079
114151
|
init_cli_table();
|
|
114080
114152
|
|
|
@@ -114145,40 +114217,16 @@ function requireGoogleApi2(config) {
|
|
|
114145
114217
|
return config.googleApiUrl;
|
|
114146
114218
|
}
|
|
114147
114219
|
var KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY = "USD";
|
|
114148
|
-
function applyUsdBidAmountsInCny(item, usdToCnyRate) {
|
|
114149
|
-
return {
|
|
114150
|
-
...item,
|
|
114151
|
-
averageCpcCNY: convertUsdToCny(item.averageCpc, usdToCnyRate),
|
|
114152
|
-
lowTopOfPageBidCNY: convertUsdToCny(item.lowTopOfPageBid, usdToCnyRate),
|
|
114153
|
-
highTopOfPageBidCNY: convertUsdToCny(item.highTopOfPageBid, usdToCnyRate)
|
|
114154
|
-
};
|
|
114155
|
-
}
|
|
114156
|
-
function withUsdFieldAliases(item) {
|
|
114157
|
-
return {
|
|
114158
|
-
...item,
|
|
114159
|
-
averageCpcUSD: item.averageCpc,
|
|
114160
|
-
lowTopOfPageBidUSD: item.lowTopOfPageBid,
|
|
114161
|
-
highTopOfPageBidUSD: item.highTopOfPageBid
|
|
114162
|
-
};
|
|
114163
|
-
}
|
|
114164
114220
|
function buildKeywordSuggestJsonPayload(items, opts) {
|
|
114165
114221
|
const currency = opts.bidAmountCurrency.toUpperCase();
|
|
114166
|
-
|
|
114222
|
+
const normalized = items.map((row) => ({
|
|
114167
114223
|
...microsItemToBidAmounts(row),
|
|
114168
114224
|
bidAmountCurrency: currency
|
|
114169
114225
|
}));
|
|
114170
|
-
const rate = opts.usdToCnyRate;
|
|
114171
|
-
const isUsdPlanner = currency === KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY;
|
|
114172
|
-
if (isUsdPlanner && rate != null && Number.isFinite(rate) && rate > 0) {
|
|
114173
|
-
normalized = normalized.map((row) => withUsdFieldAliases(applyUsdBidAmountsInCny(row, rate)));
|
|
114174
|
-
} else if (isUsdPlanner) {
|
|
114175
|
-
normalized = normalized.map(withUsdFieldAliases);
|
|
114176
|
-
}
|
|
114177
114226
|
const n = normalized.length;
|
|
114178
114227
|
return {
|
|
114179
114228
|
...wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items: normalized }),
|
|
114180
|
-
bidAmountCurrency: currency
|
|
114181
|
-
...isUsdPlanner && rate != null && Number.isFinite(rate) && rate > 0 ? { usdToCnyExchangeRate: rate } : {}
|
|
114229
|
+
bidAmountCurrency: currency
|
|
114182
114230
|
};
|
|
114183
114231
|
}
|
|
114184
114232
|
async function fetchUrlKeywords(url, keywords, verbose) {
|
|
@@ -114367,12 +114415,7 @@ async function runKeywordSuggest(opts) {
|
|
|
114367
114415
|
});
|
|
114368
114416
|
}
|
|
114369
114417
|
const n = items.length;
|
|
114370
|
-
const
|
|
114371
|
-
const usdToCnyRate = isUsdPlanner ? await getUsdToCnyExchangeRate(config, opts.verbose) : null;
|
|
114372
|
-
const kwPayload = buildKeywordSuggestJsonPayload(items, {
|
|
114373
|
-
bidAmountCurrency,
|
|
114374
|
-
usdToCnyRate
|
|
114375
|
-
});
|
|
114418
|
+
const kwPayload = buildKeywordSuggestJsonPayload(items, { bidAmountCurrency });
|
|
114376
114419
|
const displayItems = kwPayload.items;
|
|
114377
114420
|
if (await emitCliJsonOrSnapshot(opts, {
|
|
114378
114421
|
section: "keyword-suggest",
|
|
@@ -114390,16 +114433,7 @@ async function runKeywordSuggest(opts) {
|
|
|
114390
114433
|
}
|
|
114391
114434
|
const fmtAmt = (v) => v != null && Number.isFinite(v) ? v.toFixed(2) : "\u2014";
|
|
114392
114435
|
const cur = bidAmountCurrency;
|
|
114393
|
-
const
|
|
114394
|
-
const cols = hasCnyRef ? [
|
|
114395
|
-
{ key: "keyword", header: "\u5173\u952E\u8BCD" },
|
|
114396
|
-
{ key: "montlySearch", header: "\u6708\u5747\u641C\u7D22" },
|
|
114397
|
-
{ key: "cpcMain", header: `\u5E73\u5747CPC(${cur})` },
|
|
114398
|
-
{ key: "cpcCny", header: "\u5E73\u5747CPC(CNY\u53C2\u8003)" },
|
|
114399
|
-
{ key: "bidRangeMain", header: `\u9875\u9996\u51FA\u4EF7(${cur})` },
|
|
114400
|
-
{ key: "bidRangeCny", header: "\u9875\u9996\u51FA\u4EF7(CNY\u53C2\u8003)" },
|
|
114401
|
-
{ key: "competition", header: "\u7ADE\u4E89\u5EA6" }
|
|
114402
|
-
] : [
|
|
114436
|
+
const cols = [
|
|
114403
114437
|
{ key: "keyword", header: "\u5173\u952E\u8BCD" },
|
|
114404
114438
|
{ key: "montlySearch", header: "\u6708\u5747\u641C\u7D22" },
|
|
114405
114439
|
{ key: "cpcMain", header: `\u5E73\u5747CPC(${cur})` },
|
|
@@ -114412,33 +114446,21 @@ async function runKeywordSuggest(opts) {
|
|
|
114412
114446
|
const highMain = fmtAmt(item.highTopOfPageBid);
|
|
114413
114447
|
const bidRangeMain = lowMain === "\u2014" && highMain === "\u2014" ? "\u2014" : `${lowMain} ~ ${highMain}`;
|
|
114414
114448
|
const competitionDisplay = item.competitionV2 ?? (item.competition != null ? item.competition.toFixed(2) : "\u2014");
|
|
114415
|
-
|
|
114449
|
+
return {
|
|
114416
114450
|
keyword: item.keyword ?? "",
|
|
114417
114451
|
montlySearch: String(item.montlySearch ?? "\u2014"),
|
|
114418
114452
|
cpcMain,
|
|
114419
114453
|
bidRangeMain,
|
|
114420
114454
|
competition: competitionDisplay
|
|
114421
114455
|
};
|
|
114422
|
-
if (hasCnyRef) {
|
|
114423
|
-
const cpcCny = fmtAmt(item.averageCpcCNY);
|
|
114424
|
-
const lowCny = fmtAmt(item.lowTopOfPageBidCNY);
|
|
114425
|
-
const highCny = fmtAmt(item.highTopOfPageBidCNY);
|
|
114426
|
-
row.cpcCny = cpcCny;
|
|
114427
|
-
row.bidRangeCny = lowCny === "\u2014" && highCny === "\u2014" ? "\u2014" : `${lowCny} ~ ${highCny}`;
|
|
114428
|
-
}
|
|
114429
|
-
return row;
|
|
114430
114456
|
});
|
|
114431
114457
|
printCliTable(rows, cols);
|
|
114432
|
-
if (
|
|
114433
|
-
console.log(
|
|
114434
|
-
` \uFF08\u51FA\u4EF7\u5E01\u79CD ${cur}\uFF1BCNY \u4E3A\u6C47\u7387 ${usdToCnyRate} \u6362\u7B97\u53C2\u8003\uFF0C\u975E Google \u8D26\u6237\u5E01\u79CD\u53E3\u5F84\uFF09
|
|
114435
|
-
`
|
|
114436
|
-
);
|
|
114437
|
-
} else if (accountId) {
|
|
114458
|
+
if (accountId) {
|
|
114438
114459
|
console.log(` \uFF08\u51FA\u4EF7\u5E01\u79CD\uFF1A${cur}\uFF0C\u4E0E\u8D26\u6237 currencyCode \u4E00\u81F4\uFF09
|
|
114439
114460
|
`);
|
|
114440
114461
|
} else {
|
|
114441
|
-
console.log(
|
|
114462
|
+
console.log(` \uFF08\u51FA\u4EF7\u5E01\u79CD\uFF1A${cur}\uFF09
|
|
114463
|
+
`);
|
|
114442
114464
|
}
|
|
114443
114465
|
}
|
|
114444
114466
|
function microsToAmount(v) {
|
|
@@ -115686,8 +115708,8 @@ async function runAccountWithdrawSubmit(opts) {
|
|
|
115686
115708
|
});
|
|
115687
115709
|
const feeRateCache = /* @__PURE__ */ new Map();
|
|
115688
115710
|
for (const { currency, availableAmount } of accountDetails) {
|
|
115689
|
-
const
|
|
115690
|
-
if (feeRateCache.has(
|
|
115711
|
+
const cacheKey = `${currency}-${availableAmount.toFixed(2)}`;
|
|
115712
|
+
if (feeRateCache.has(cacheKey)) continue;
|
|
115691
115713
|
try {
|
|
115692
115714
|
const feeParams = new URLSearchParams({
|
|
115693
115715
|
mediaType: "Google",
|
|
@@ -115700,16 +115722,16 @@ async function runAccountWithdrawSubmit(opts) {
|
|
|
115700
115722
|
{},
|
|
115701
115723
|
opts.verbose
|
|
115702
115724
|
);
|
|
115703
|
-
feeRateCache.set(
|
|
115725
|
+
feeRateCache.set(cacheKey, feeData?.feeRate ?? 0);
|
|
115704
115726
|
} catch {
|
|
115705
115727
|
console.warn(` \u26A0\uFE0F ${currency} \u7BA1\u7406\u8D39\u67E5\u8BE2\u5931\u8D25\uFF0C\u5C06\u4EE5 0 \u8D39\u7387\u63D0\u4EA4\u3002`);
|
|
115706
|
-
feeRateCache.set(
|
|
115728
|
+
feeRateCache.set(cacheKey, 0);
|
|
115707
115729
|
}
|
|
115708
115730
|
}
|
|
115709
115731
|
const body = accountDetails.map(
|
|
115710
115732
|
({ ma, mai, currency, balance, adjustments, availableAmount }) => {
|
|
115711
|
-
const
|
|
115712
|
-
const feeRate = feeRateCache.get(
|
|
115733
|
+
const cacheKey = `${currency}-${availableAmount.toFixed(2)}`;
|
|
115734
|
+
const feeRate = feeRateCache.get(cacheKey) ?? 0;
|
|
115713
115735
|
const taxRate = currency === "CNY" ? 0.06 : 0;
|
|
115714
115736
|
const totalAmounts = availableAmount * (1 + feeRate) * (1 + taxRate);
|
|
115715
115737
|
return {
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -152,7 +152,7 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
|
|
|
152
152
|
- `ad campaigns --json/--json-out` → `budget`(元,与 `campaign-edit --budget` 同口径)
|
|
153
153
|
- `ad groups --json` → `maxCPCAmountYuan` / `targetCpaAmountYuan`(元)
|
|
154
154
|
- `google-analysis campaigns-*.json` → `budgetAmountYuan` / `campaignTargetCpaYuan` / `maximizeConversionsTargetCpaYuan` / `spend` / `averageCpc` / `costPerConversion`(均元)
|
|
155
|
-
- `keyword --json` → `averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid` +
|
|
155
|
+
- `keyword --json` → `averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid` + 根级与每条 `bidAmountCurrency`(无 `-a` 为 USD;有 `-a` 为账户 `currencyCode`);`-a <mediaCustomerId>` 走账户级推荐接口;限定市场用 `keyword geo-list` + `--geo <id>`(**多 id = 汇总指标**;分市场须多次调用、每次一个 `--geo`,见 `references/keyword-planner-workflows.md`)
|
|
156
156
|
- **品牌名优先级**:(1) 用户明确提供 → (2) `list-accounts.mag.advertiserName` → (3) 用户提供网址 → 域名占位并标注 `[待确认品牌名]`。**严禁**把英文域名翻译为虚构中文品牌。
|
|
157
157
|
- 完整字段表见 `references/currency.md`。
|
|
158
158
|
|
package/dist/skill/_meta.json
CHANGED
|
@@ -23,11 +23,11 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
|
|
|
23
23
|
|
|
24
24
|
| 命令 | 支持 `--json-out`? | 推荐用法 |
|
|
25
25
|
| ---- | :-----------------: | -------- |
|
|
26
|
-
| `ad campaign-validate` |
|
|
27
|
-
| `ad campaign-create` |
|
|
26
|
+
| `ad campaign-validate` | **是** | **推荐** `--json-out ./snap-campaign`(落盘 `ad-campaign-validate-<account>.json`,含 `lengthViolations`);与 `--json` 互斥。人读:`--config-file` 即可;词面规范化:`--write-normalized`。超长勿自动截断,见 `google-ads-campaign-plan.md` § 超长人工确认 |
|
|
27
|
+
| `ad campaign-create` | **是** | 落盘任务响应:`--json-out ./snap-campaign`;与 `--json` 互斥 |
|
|
28
28
|
| `ad batch get` / `ad batch diff` | **是** | 轮询与 diff 结果落盘,见 `references/tips.md` |
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
**Agent**:校验与 create/batch 共用同一 `--json-out` 目录时,按 stdout 摘要里的 `outlineFile` → 再读对应 JSON(勿把 outline 当数据)。
|
|
31
31
|
|
|
32
32
|
### RSA 落地页:`Finalurl` 必填
|
|
33
33
|
|
|
@@ -83,7 +83,7 @@ CLI 出口的所有 JSON / 表格金额已统一为**元**,关键字段:
|
|
|
83
83
|
| `ad campaigns --json` | `budget`(元,与 `--budget` 写参同口径) |
|
|
84
84
|
| `ad groups --json` | `maxCPCAmountYuan`、`targetCpaAmountYuan` |
|
|
85
85
|
| `google-analysis campaigns` 落盘 `campaigns-*.json` | `budgetAmountYuan`、`campaignTargetCpaYuan`、`maximizeConversionsTargetCpaYuan`;同行 `spend` / `averageCpc` / `costPerConversion` 也是元 |
|
|
86
|
-
| `keyword suggest --json` | `averageCpc`、`lowTopOfPageBid`、`highTopOfPageBid
|
|
86
|
+
| `keyword suggest --json` | `averageCpc`、`lowTopOfPageBid`、`highTopOfPageBid`;根级与每条 `bidAmountCurrency`(有 `-a` 为账户币;无 `-a` 为 USD) |
|
|
87
87
|
| `balance` 等账户余额接口 | `remainingAccountBudget`(元) |
|
|
88
88
|
|
|
89
89
|
旧字段 `budgetAmount`(分)、`maxCPCAmountDisplay`、`*Micros`(微元)**已不再落盘**,下游脚本无需做单位换算。金额保留 2 位小数,带货币代码(如 `¥50.00 CNY`、`$50.00 USD`),`currencyCode` 从响应读取,跨币种账户分表;细则见 `references/currency.md`。
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
|
|
47
47
|
## 金额单位约定
|
|
48
48
|
|
|
49
|
-
- **CLI 出口的大多数 JSON / 表格金额以「元」为单位**:`budget`、`*Yuan` 后缀(`budgetAmountYuan`、`maxCPCAmountYuan` 等)、`spend` / `averageCpc` / `costPerConversion` 等。**`keyword suggest`**:`averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid` 的币种见根级与每条 **`bidAmountCurrency`**(传 `-a` 时为账户 `currencyCode`;无 `-a` 时为 **USD
|
|
49
|
+
- **CLI 出口的大多数 JSON / 表格金额以「元」为单位**:`budget`、`*Yuan` 后缀(`budgetAmountYuan`、`maxCPCAmountYuan` 等)、`spend` / `averageCpc` / `costPerConversion` 等。**`keyword suggest`**:`averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid` 的币种见根级与每条 **`bidAmountCurrency`**(传 `-a` 时为账户 `currencyCode`;无 `-a` 时为 **USD**)。
|
|
50
50
|
- **写 CLI 参数**(`--budget`、`--max-cpc`、`--target-cpa`、`--amount` 等):同样传**主币种元**,与账户 `currencyCode` 一致;CLI 内部按需 ×100 / ×1_000_000 写后端。
|
|
51
51
|
- 旧版网关字段(`budgetAmount` 分、`*Micros` 微元、`maxCPCAmountDisplay` 等)**已不再落盘到 CLI 输出**,下游脚本无需做单位换算。
|
|
52
52
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
| 2 | 可选 `rag query`;`keyword` / `keyword geo-list` 拓词 | `references/keyword-planner-workflows.md` |
|
|
27
27
|
| 3 | 按分层规则写入 `KeywordsForBatchJob`(Exact/Phrase/Broad) | `google-ads-rules/google-ads-keyword-taxonomy.md`(参考,非 CLI 强制) |
|
|
28
28
|
| 4 | 填 `campaign`(预算/出价/地域/否词≥20/RSA/附加信息) | `assets/campaign-create-template.md` |
|
|
29
|
-
| 5 | **`ad campaign-validate --config-file <json>`**(失败只改 JSON
|
|
29
|
+
| 5 | **`ad campaign-validate --config-file <json>`**(失败只改 JSON;超长见下文「超长人工确认」) | 下文「校验」 |
|
|
30
30
|
| 6 | 输出:**JSON 代码块** → **Markdown**(`google-ads-launch-plan-template.md` 正文)→ 待确认 | — |
|
|
31
31
|
| 7 | 用户确认后 **`ad campaign-create`** | `google-ads.md`|
|
|
32
32
|
| 8 | 每隔5s 获取创建结果| `ad batch get --id <taskId> --config-file ./campaign.json` |
|
|
@@ -72,7 +72,8 @@
|
|
|
72
72
|
## 校验与创建(命令速查)
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
siluzan-tso ad campaign-validate --config-file ./campaign.json
|
|
75
|
+
siluzan-tso ad campaign-validate --config-file ./campaign.json --json-out ./snap-campaign
|
|
76
|
+
siluzan-tso ad campaign-validate --config-file ./campaign.json [--json] [--write-normalized <path>]
|
|
76
77
|
siluzan-tso ad campaign-create --config-file ./campaign.json
|
|
77
78
|
siluzan-tso ad batch get --id <taskId> --config-file ./campaign.json
|
|
78
79
|
siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json
|
|
@@ -81,6 +82,17 @@ siluzan-tso ad geo search
|
|
|
81
82
|
|
|
82
83
|
validate 与 create **共用** `runCampaignCreateValidation`:词面规范化 + 后端/Google 硬约束(预算、RSA、匹配符号与 `MatchTypeV2` 对齐、搜索网络等)。**不含**关键词分层数量、匹配占比、否词条数下限。
|
|
83
84
|
|
|
85
|
+
### 超长内容:禁止 Agent 自动截断
|
|
86
|
+
|
|
87
|
+
标题/描述/Path/关键词/Sitelink 超限时 CLI **报错阻断**,不会在 JSON 里静默改短。
|
|
88
|
+
|
|
89
|
+
1. 使用 **`ad campaign-validate --config-file <json> --json-out <dir>`**(与 create/batch 同一落盘目录),读落盘文件中的 `lengthViolations`(每项含 `path`、`limit`、`actual`、**完整** `text`)。小文件可用 `--json` 代替。
|
|
90
|
+
2. Agent 将 **全部** 超长条目整理成表(路径、原文、上限、超出量),并为每条给出 **1–2 个改写方案**(保留卖点、符合字符计数;CJK 按 2 计见 `google-ads-compliance.md` §3.2.1)。
|
|
91
|
+
3. **用户确认**选用方案后,Agent **只改 JSON 对应字段**,再执行 `campaign-validate`;通过后再 `campaign-create`。
|
|
92
|
+
4. **禁止**:未确认前 `slice`/省略号截断、仅改 `--write-normalized` 而不经用户确认。
|
|
93
|
+
|
|
94
|
+
人读模式失败时 CLI 会额外打印「📏 超长内容清单」;`--json-out` / `--json` 时见 `lengthViolations` + `agentHint`。
|
|
95
|
+
|
|
84
96
|
---
|
|
85
97
|
|
|
86
98
|
## 已上线后的修改
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
| 技术词 | api/sdk/integration | payment sdk integration |
|
|
55
55
|
| 问题词 | how to + 问题 | how to reduce failed payments |
|
|
56
56
|
|
|
57
|
-
拓词编排见 `references/keyword-planner-workflows.md`;Planner
|
|
57
|
+
拓词编排见 `references/keyword-planner-workflows.md`;Planner 出价见 `averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid` 与根级、每条 `bidAmountCurrency`。
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 金额单位(全局重要)
|
|
8
8
|
|
|
9
|
-
> **所有 CLI 金额参数均按「主币种金额」传入**(如 `1.5` = ¥1.50 / $1.50
|
|
10
|
-
> **唯一例外**:`ad keyword-edit --max-cpc` 单位为「主币种元」(直接透传,不做 ×100 转换)。
|
|
9
|
+
> **所有 CLI 金额参数均按「主币种金额」传入**(如 `1.5` = ¥1.50 / $1.50);CLI 写入网关前对「分」字段 ×100(含 `ad keyword-edit --max-cpc` → `maxCPC`)。
|
|
11
10
|
> **禁止** 按 Google micros(×1,000,000)填写任何金额参数。
|
|
12
11
|
|
|
13
12
|
---
|
|
@@ -277,6 +276,8 @@ siluzan-tso keyword geo-list [--country-code <US,CN,...>] [--name-contains <text
|
|
|
277
276
|
|
|
278
277
|
不提交 API;创建系列前**建议**跑。命令、选项、与 create 共用校验逻辑见 **`references/google-ads-campaign-plan.md`** § 校验与创建(后端/Google 硬约束,不含关键词分层占比)。
|
|
279
278
|
|
|
279
|
+
**超长内容**:加 **`--json-out <dir>`**(推荐,与 create/batch 共用目录)或 `--json` 时响应含 `lengthViolations`(完整 `text` + JSON `path`)。Agent **勿自动截断**;须列出全部超长项与改写方案,用户确认后再改 JSON 并重跑 validate(流程见 `google-ads-campaign-plan.md` § 超长人工确认)。
|
|
280
|
+
|
|
280
281
|
---
|
|
281
282
|
|
|
282
283
|
## ad campaign-create — 广告系列创建
|
|
@@ -442,7 +443,7 @@ siluzan-tso ad keyword-edit \
|
|
|
442
443
|
[--max-cpc <n>] [--final-url <url>] [--status Enabled|Paused]
|
|
443
444
|
```
|
|
444
445
|
|
|
445
|
-
传 `--match-type` 时 CLI 自动规范 `keywordText` 括号/引号格式。至少传一项。`--max-cpc`
|
|
446
|
+
传 `--match-type` 时 CLI 自动规范 `keywordText` 括号/引号格式。至少传一项。`--max-cpc` 为主币种元(CLI ×100 写入 `maxCPC`「分」字段,与 `adgroup-edit --max-cpc` 同口径)。`ad keywords --json` 出价见 `maxCPCYuan`。`--status` 写入 `userStatusV2`(与 Web 关键词开关一致,非系列的 `statusV2`)。
|
|
446
447
|
|
|
447
448
|
---
|
|
448
449
|
|
|
@@ -205,6 +205,6 @@ siluzan-tso keyword geo-list [--country-code <codes>] [--name-contains <text>] [
|
|
|
205
205
|
|
|
206
206
|
`--google-only`:只调 Google 推荐主接口(有 `-a` 为账户接口,无 `-a` 为 `keywordidea/google`),不叠加 `--url` 的网址拓词;**分支 B(仅 Google)必加**。
|
|
207
207
|
|
|
208
|
-
**返回字段**(与后端 `Samm.Core.Service.KeywordRecommendation` 对齐):根级与每条 **`bidAmountCurrency`**(无账户=`USD`;有账户=`list-accounts` 的 `currencyCode`);`averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid`(微元 ÷1,000,000,**与 `bidAmountCurrency`
|
|
208
|
+
**返回字段**(与后端 `Samm.Core.Service.KeywordRecommendation` 对齐):根级与每条 **`bidAmountCurrency`**(无账户=`USD`;有账户=`list-accounts` 的 `currencyCode`);`averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid`(微元 ÷1,000,000,**与 `bidAmountCurrency` 一致**)。另有 `keyword` / `montlySearch` / `competition` / `competitionV2` / `source` 等。
|
|
209
209
|
|
|
210
210
|
与只读账户关键词列表、否词 CRUD 的对照仍归 **`references/google-ads.md`** 中 `ad keywords` / `ad keyword-*` 各节。
|
|
@@ -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.20-beta.
|
|
12
|
+
$PKG_VERSION = '1.1.20-beta.22'
|
|
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.20-beta.
|
|
12
|
+
readonly PKG_VERSION="1.1.20-beta.22"
|
|
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"
|