siluzan-tso-cli 1.1.37-beta.3 → 1.1.37-beta.4
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 +150 -32
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/assets/campaign-create-template.json +19 -1
- package/dist/skill/assets/campaign-create-template.md +3 -1
- package/dist/skill/references/core/user-communication-guide.md +7 -4
- package/dist/skill/references/google-ads/google-ads.md +18 -4
- package/dist/skill/references/google-ads/rules/google-ads-keyword-strategy.md +2 -2
- package/dist/skill/scripts/install.ps1 +1 -1
- package/dist/skill/scripts/install.sh +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
|
|
|
51
51
|
siluzan-tso init --force # 强制覆盖已存在文件
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
> **注意**:当前为测试版(1.1.37-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.37-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -110511,6 +110511,7 @@ function validateCampaignExtensionsForBatchJob(campaign, errors, warnings, lengt
|
|
|
110511
110511
|
|
|
110512
110512
|
// src/commands/ad/keyword.ts
|
|
110513
110513
|
init_auth();
|
|
110514
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
110514
110515
|
init_cli_json_snapshot();
|
|
110515
110516
|
init_strip_legacy_google_fields();
|
|
110516
110517
|
init_cli_table();
|
|
@@ -110539,6 +110540,68 @@ function formatKeywordTextForMatchType(rawCoreOrDisplay, matchType) {
|
|
|
110539
110540
|
}
|
|
110540
110541
|
}
|
|
110541
110542
|
}
|
|
110543
|
+
function parseKeywordsCsv(raw) {
|
|
110544
|
+
const out = [];
|
|
110545
|
+
let cur = "";
|
|
110546
|
+
let inDouble = false;
|
|
110547
|
+
let inBracket = false;
|
|
110548
|
+
for (let i = 0; i < raw.length; i++) {
|
|
110549
|
+
const ch = raw[i];
|
|
110550
|
+
if (ch === '"' && !inBracket) {
|
|
110551
|
+
inDouble = !inDouble;
|
|
110552
|
+
cur += ch;
|
|
110553
|
+
continue;
|
|
110554
|
+
}
|
|
110555
|
+
if (ch === "[" && !inDouble && !inBracket) {
|
|
110556
|
+
inBracket = true;
|
|
110557
|
+
cur += ch;
|
|
110558
|
+
continue;
|
|
110559
|
+
}
|
|
110560
|
+
if (ch === "]" && inBracket) {
|
|
110561
|
+
inBracket = false;
|
|
110562
|
+
cur += ch;
|
|
110563
|
+
continue;
|
|
110564
|
+
}
|
|
110565
|
+
if (ch === "," && !inDouble && !inBracket) {
|
|
110566
|
+
const t = cur.trim();
|
|
110567
|
+
if (t) out.push(t);
|
|
110568
|
+
cur = "";
|
|
110569
|
+
continue;
|
|
110570
|
+
}
|
|
110571
|
+
cur += ch;
|
|
110572
|
+
}
|
|
110573
|
+
const last = cur.trim();
|
|
110574
|
+
if (last) out.push(last);
|
|
110575
|
+
return out;
|
|
110576
|
+
}
|
|
110577
|
+
function loadKeywordsFromFile(filePath) {
|
|
110578
|
+
let raw;
|
|
110579
|
+
try {
|
|
110580
|
+
raw = JSON.parse(readFileSync4(filePath, "utf8"));
|
|
110581
|
+
} catch (err) {
|
|
110582
|
+
throw new Error(
|
|
110583
|
+
`\u8BFB\u53D6\u5173\u952E\u8BCD\u6587\u4EF6\u5931\u8D25\uFF08${filePath}\uFF09\uFF1A${err instanceof Error ? err.message : String(err)}`
|
|
110584
|
+
);
|
|
110585
|
+
}
|
|
110586
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
110587
|
+
throw new Error(`\u5173\u952E\u8BCD\u6587\u4EF6\u987B\u4E3A\u975E\u7A7A JSON \u5B57\u7B26\u4E32\u6570\u7EC4\uFF1A${filePath}`);
|
|
110588
|
+
}
|
|
110589
|
+
const list = raw.map((x) => typeof x === "string" ? x.trim() : "").filter((s) => s.length > 0);
|
|
110590
|
+
if (list.length === 0) {
|
|
110591
|
+
throw new Error(`\u5173\u952E\u8BCD\u6587\u4EF6\u65E0\u6709\u6548\u5B57\u7B26\u4E32\uFF1A${filePath}`);
|
|
110592
|
+
}
|
|
110593
|
+
return list;
|
|
110594
|
+
}
|
|
110595
|
+
function resolveKeywordTexts(opts) {
|
|
110596
|
+
const fromCsv = opts.keywords?.length ? opts.keywords : [];
|
|
110597
|
+
const fromFile = opts.keywordsFile ? loadKeywordsFromFile(opts.keywordsFile) : [];
|
|
110598
|
+
const merged = [...fromCsv, ...fromFile];
|
|
110599
|
+
if (merged.length === 0) {
|
|
110600
|
+
throw new Error("\u8BF7\u4F20 --keywords \u548C/\u6216 --keywords-file\uFF08\u591A\u8BCD\u77ED\u8BED\u76F4\u63A5\u5199\u7A7A\u683C\u5373\u53EF\uFF0C\u5982 how to make\uFF09");
|
|
110601
|
+
}
|
|
110602
|
+
if (!opts.matchType) return merged;
|
|
110603
|
+
return merged.map((k) => formatKeywordTextForMatchType(k, opts.matchType));
|
|
110604
|
+
}
|
|
110542
110605
|
function firstKeywordTextFromRecord(k) {
|
|
110543
110606
|
const kt = k["keywordText"];
|
|
110544
110607
|
if (Array.isArray(kt) && kt.length > 0 && typeof kt[0] === "string") {
|
|
@@ -110661,8 +110724,21 @@ ${label}\uFF08\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672C
|
|
|
110661
110724
|
async function runAdKeywordCreate(opts) {
|
|
110662
110725
|
const config = loadConfig(opts.token);
|
|
110663
110726
|
const googleApiUrl = requireGoogleApi(config);
|
|
110727
|
+
let keywords;
|
|
110728
|
+
try {
|
|
110729
|
+
keywords = resolveKeywordTexts({
|
|
110730
|
+
keywords: opts.keywords,
|
|
110731
|
+
keywordsFile: opts.keywordsFile,
|
|
110732
|
+
matchType: opts.matchType
|
|
110733
|
+
});
|
|
110734
|
+
} catch (err) {
|
|
110735
|
+
console.error(`
|
|
110736
|
+
\u274C ${err instanceof Error ? err.message : String(err)}
|
|
110737
|
+
`);
|
|
110738
|
+
process.exit(1);
|
|
110739
|
+
}
|
|
110664
110740
|
const body = {
|
|
110665
|
-
KeywordText:
|
|
110741
|
+
KeywordText: keywords,
|
|
110666
110742
|
FinalUrl: opts.finalUrl ?? "",
|
|
110667
110743
|
AdGroup: opts.adgroupName,
|
|
110668
110744
|
AdGroupId: opts.adgroupId,
|
|
@@ -110688,20 +110764,33 @@ async function runAdKeywordCreate(opts) {
|
|
|
110688
110764
|
section: `ad-keyword-create-${opts.account}-${opts.adgroupId}`,
|
|
110689
110765
|
commandLabel: "ad keyword-create",
|
|
110690
110766
|
commandHint: `${opts.account}:${opts.adgroupId}`,
|
|
110691
|
-
payload: { request: { adgroupId: opts.adgroupId, count:
|
|
110767
|
+
payload: { request: { adgroupId: opts.adgroupId, count: keywords.length }, response },
|
|
110692
110768
|
idSuffix: `${opts.account}-${opts.adgroupId}`
|
|
110693
110769
|
})) {
|
|
110694
110770
|
return;
|
|
110695
110771
|
}
|
|
110696
110772
|
console.log(`
|
|
110697
|
-
\u2705 \u5DF2\u6DFB\u52A0 ${
|
|
110773
|
+
\u2705 \u5DF2\u6DFB\u52A0 ${keywords.length} \u4E2A\u5173\u952E\u8BCD\u5230\u5E7F\u544A\u7EC4\u300C${opts.adgroupName}\u300D
|
|
110698
110774
|
`);
|
|
110699
110775
|
}
|
|
110700
110776
|
async function runAdNegativeKeywordCreate(opts) {
|
|
110701
110777
|
const config = loadConfig(opts.token);
|
|
110702
110778
|
const googleApiUrl = requireGoogleApi(config);
|
|
110779
|
+
let keywords;
|
|
110780
|
+
try {
|
|
110781
|
+
keywords = resolveKeywordTexts({
|
|
110782
|
+
keywords: opts.keywords,
|
|
110783
|
+
keywordsFile: opts.keywordsFile,
|
|
110784
|
+
matchType: opts.matchType
|
|
110785
|
+
});
|
|
110786
|
+
} catch (err) {
|
|
110787
|
+
console.error(`
|
|
110788
|
+
\u274C ${err instanceof Error ? err.message : String(err)}
|
|
110789
|
+
`);
|
|
110790
|
+
process.exit(1);
|
|
110791
|
+
}
|
|
110703
110792
|
const body = {
|
|
110704
|
-
KeywordText:
|
|
110793
|
+
KeywordText: keywords,
|
|
110705
110794
|
CampaignId: opts.campaignId,
|
|
110706
110795
|
Campaign: opts.campaignName
|
|
110707
110796
|
};
|
|
@@ -110720,7 +110809,7 @@ async function runAdNegativeKeywordCreate(opts) {
|
|
|
110720
110809
|
process.exit(1);
|
|
110721
110810
|
}
|
|
110722
110811
|
console.log(`
|
|
110723
|
-
\u2705 \u5DF2\u5728${level}\u5C42\u7EA7\u6DFB\u52A0 ${
|
|
110812
|
+
\u2705 \u5DF2\u5728${level}\u5C42\u7EA7\u6DFB\u52A0 ${keywords.length} \u4E2A\u5426\u5B9A\u5173\u952E\u8BCD
|
|
110724
110813
|
`);
|
|
110725
110814
|
}
|
|
110726
110815
|
async function runAdKeywordDelete(opts) {
|
|
@@ -111957,7 +112046,7 @@ function runCampaignCreateValidation(cfg) {
|
|
|
111957
112046
|
}
|
|
111958
112047
|
|
|
111959
112048
|
// src/commands/ad/campaign-load.ts
|
|
111960
|
-
import { readFileSync as
|
|
112049
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
111961
112050
|
function stripMetaKeys(value) {
|
|
111962
112051
|
if (Array.isArray(value)) {
|
|
111963
112052
|
return value.map((item) => stripMetaKeys(item));
|
|
@@ -111988,7 +112077,7 @@ function loadCampaignCreateConfig(configFile) {
|
|
|
111988
112077
|
function tryLoadCampaignCreateConfig(configFile) {
|
|
111989
112078
|
let raw;
|
|
111990
112079
|
try {
|
|
111991
|
-
raw = JSON.parse(
|
|
112080
|
+
raw = JSON.parse(readFileSync5(configFile, "utf8"));
|
|
111992
112081
|
} catch {
|
|
111993
112082
|
return null;
|
|
111994
112083
|
}
|
|
@@ -113017,12 +113106,12 @@ async function runAdEdit(opts) {
|
|
|
113017
113106
|
|
|
113018
113107
|
// src/commands/ad/extension.ts
|
|
113019
113108
|
init_auth();
|
|
113020
|
-
import { readFileSync as
|
|
113109
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
113021
113110
|
init_cli_json_snapshot();
|
|
113022
113111
|
init_strip_legacy_google_fields();
|
|
113023
113112
|
|
|
113024
113113
|
// src/commands/ad/extension-lead-form.ts
|
|
113025
|
-
import { readFileSync as
|
|
113114
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
113026
113115
|
var VALID_LEAD_FORM_INPUT_TYPES = [
|
|
113027
113116
|
"FULL_NAME",
|
|
113028
113117
|
"FIRST_NAME",
|
|
@@ -113046,7 +113135,7 @@ var LEAD_FORM_CTA_DESCRIPTION_MAX_LEN = 30;
|
|
|
113046
113135
|
function loadLeadFormExtensionConfig(configFile) {
|
|
113047
113136
|
let raw;
|
|
113048
113137
|
try {
|
|
113049
|
-
raw = JSON.parse(
|
|
113138
|
+
raw = JSON.parse(readFileSync6(configFile, "utf8"));
|
|
113050
113139
|
} catch {
|
|
113051
113140
|
console.error(`
|
|
113052
113141
|
\u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${configFile}\uFF09
|
|
@@ -113158,7 +113247,7 @@ function buildLeadFormExtensionBody(cfg, extensionId) {
|
|
|
113158
113247
|
}
|
|
113159
113248
|
|
|
113160
113249
|
// src/commands/ad/extension-whatsapp.ts
|
|
113161
|
-
import { readFileSync as
|
|
113250
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
113162
113251
|
var VALID_WHATSAPP_CTA_SELECTIONS = [
|
|
113163
113252
|
"CONTACT_US",
|
|
113164
113253
|
"LEARN_MORE",
|
|
@@ -113170,7 +113259,7 @@ var VALID_WHATSAPP_CTA_SELECTIONS = [
|
|
|
113170
113259
|
function loadWhatsappExtensionConfig(configFile) {
|
|
113171
113260
|
let raw;
|
|
113172
113261
|
try {
|
|
113173
|
-
raw = JSON.parse(
|
|
113262
|
+
raw = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
113174
113263
|
} catch {
|
|
113175
113264
|
console.error(`
|
|
113176
113265
|
\u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${configFile}\uFF09
|
|
@@ -113632,7 +113721,7 @@ async function runAdExtensionUpdate(opts) {
|
|
|
113632
113721
|
const googleApiUrl = requireGoogleApi(config);
|
|
113633
113722
|
let raw;
|
|
113634
113723
|
try {
|
|
113635
|
-
raw = stripMetaKeys(JSON.parse(
|
|
113724
|
+
raw = stripMetaKeys(JSON.parse(readFileSync8(opts.configFile, "utf8")));
|
|
113636
113725
|
} catch {
|
|
113637
113726
|
console.error(`
|
|
113638
113727
|
\u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${opts.configFile}\uFF09
|
|
@@ -115826,7 +115915,7 @@ async function runPmaxImageValidation(configFile, cfg) {
|
|
|
115826
115915
|
// src/commands/ad/pmax-image-resolve.ts
|
|
115827
115916
|
init_auth();
|
|
115828
115917
|
import { basename as basename4, dirname as dirname8, isAbsolute as isAbsolute3, resolve as resolve7 } from "path";
|
|
115829
|
-
import { readFileSync as
|
|
115918
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
115830
115919
|
|
|
115831
115920
|
// src/commands/ad/pmax-urls.ts
|
|
115832
115921
|
function pmaxChannelTypesUrl(googleApiUrl) {
|
|
@@ -115989,7 +116078,7 @@ async function uploadPmaxImageFileBase64(config, googleApiUrl, accountId, absIma
|
|
|
115989
116078
|
async function uploadPmaxImageFile(config, googleApiUrl, accountId, absImagePath, name2, verbose) {
|
|
115990
116079
|
const fileName = basename4(absImagePath);
|
|
115991
116080
|
const assetName = (name2 ?? fileName).trim() || fileName;
|
|
115992
|
-
const fileBuffer =
|
|
116081
|
+
const fileBuffer = readFileSync9(absImagePath);
|
|
115993
116082
|
if (fileBuffer.length <= MAX_BASE64_UPLOAD_BYTES) {
|
|
115994
116083
|
return uploadPmaxImageFileBase64(
|
|
115995
116084
|
config,
|
|
@@ -116075,7 +116164,7 @@ function assertPmaxImageSlotsResolved(slots, requiredKinds = ["marketing", "squa
|
|
|
116075
116164
|
}
|
|
116076
116165
|
|
|
116077
116166
|
// src/commands/ad/pmax-load.ts
|
|
116078
|
-
import { readFileSync as
|
|
116167
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
116079
116168
|
|
|
116080
116169
|
// src/commands/ad/pmax-campaign-extensions.ts
|
|
116081
116170
|
init_auth();
|
|
@@ -116705,7 +116794,7 @@ function detectDeprecatedPmaxVideoKeys(raw) {
|
|
|
116705
116794
|
function tryLoadPmaxCreateConfig(configFile) {
|
|
116706
116795
|
let raw;
|
|
116707
116796
|
try {
|
|
116708
|
-
raw = JSON.parse(
|
|
116797
|
+
raw = JSON.parse(readFileSync10(configFile, "utf8"));
|
|
116709
116798
|
} catch {
|
|
116710
116799
|
return null;
|
|
116711
116800
|
}
|
|
@@ -116713,7 +116802,7 @@ function tryLoadPmaxCreateConfig(configFile) {
|
|
|
116713
116802
|
}
|
|
116714
116803
|
function loadPmaxCreateConfigRaw(configFile) {
|
|
116715
116804
|
try {
|
|
116716
|
-
const raw = JSON.parse(
|
|
116805
|
+
const raw = JSON.parse(readFileSync10(configFile, "utf8"));
|
|
116717
116806
|
return stripMetaKeys(raw);
|
|
116718
116807
|
} catch {
|
|
116719
116808
|
return null;
|
|
@@ -116764,19 +116853,19 @@ function buildPmaxCreateUrl(googleApiUrl, accountId) {
|
|
|
116764
116853
|
}
|
|
116765
116854
|
|
|
116766
116855
|
// src/commands/ad/pmax-video-upload.ts
|
|
116767
|
-
import { existsSync as existsSync3, readFileSync as
|
|
116856
|
+
import { existsSync as existsSync3, readFileSync as readFileSync12 } from "fs";
|
|
116768
116857
|
import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
|
|
116769
116858
|
|
|
116770
116859
|
// src/commands/ad/pmax-shared.ts
|
|
116771
116860
|
init_auth();
|
|
116772
116861
|
init_cli_json_snapshot();
|
|
116773
|
-
import { readFileSync as
|
|
116862
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
116774
116863
|
import { dirname as dirname9, isAbsolute as isAbsolute4, resolve as resolve8 } from "path";
|
|
116775
116864
|
var PMAX_MONEY_KEYS = /* @__PURE__ */ new Set(["budget", "targetCpa_BidingAmount"]);
|
|
116776
116865
|
function loadPmaxJsonFile(configFile) {
|
|
116777
116866
|
let raw;
|
|
116778
116867
|
try {
|
|
116779
|
-
raw = JSON.parse(
|
|
116868
|
+
raw = JSON.parse(readFileSync11(configFile, "utf8"));
|
|
116780
116869
|
} catch (e) {
|
|
116781
116870
|
const msg = e instanceof Error ? e.message : String(e);
|
|
116782
116871
|
console.error(`
|
|
@@ -116976,7 +117065,7 @@ function runPmaxVideoValidation(configFile, cfg) {
|
|
|
116976
117065
|
return { errors, warnings };
|
|
116977
117066
|
}
|
|
116978
117067
|
try {
|
|
116979
|
-
const buf =
|
|
117068
|
+
const buf = readFileSync12(abs);
|
|
116980
117069
|
if (buf.length === 0) errors.push(`videoPath \u4E3A\u7A7A\u6587\u4EF6\uFF1A${abs}`);
|
|
116981
117070
|
} catch (e) {
|
|
116982
117071
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -117039,7 +117128,7 @@ async function uploadPmaxLocalVideo(opts) {
|
|
|
117039
117128
|
const fileName = basename5(opts.absVideoPath);
|
|
117040
117129
|
const title = (opts.title ?? fileName).trim() || fileName;
|
|
117041
117130
|
const description = (opts.description ?? "Uploaded via siluzan-tso PMax video upload").trim() || "Uploaded via siluzan-tso";
|
|
117042
|
-
const fileBuffer =
|
|
117131
|
+
const fileBuffer = readFileSync12(opts.absVideoPath);
|
|
117043
117132
|
const form = new FormData();
|
|
117044
117133
|
form.append("media_account_id", opts.accountId.replace(/-/g, ""));
|
|
117045
117134
|
form.append("title", title);
|
|
@@ -117097,7 +117186,7 @@ function validatePmaxVideoPathQuick(absPath) {
|
|
|
117097
117186
|
return `\u89C6\u9891\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${absPath}`;
|
|
117098
117187
|
}
|
|
117099
117188
|
try {
|
|
117100
|
-
if (
|
|
117189
|
+
if (readFileSync12(absPath).length === 0) return `\u89C6\u9891\u6587\u4EF6\u4E3A\u7A7A\uFF1A${absPath}`;
|
|
117101
117190
|
} catch (e) {
|
|
117102
117191
|
const msg = e instanceof Error ? e.message : String(e);
|
|
117103
117192
|
return `\u65E0\u6CD5\u8BFB\u53D6\u89C6\u9891\u6587\u4EF6\uFF08${absPath}\uFF09\uFF1A${msg}`;
|
|
@@ -118500,7 +118589,7 @@ async function runAdCampaignValidate(opts) {
|
|
|
118500
118589
|
}
|
|
118501
118590
|
|
|
118502
118591
|
// src/commands/ad/pmax-image-convert.ts
|
|
118503
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as
|
|
118592
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as readFileSync13, existsSync as existsSync4 } from "fs";
|
|
118504
118593
|
import { resolve as resolve10, dirname as dirname11, basename as basename7, extname } from "path";
|
|
118505
118594
|
var SPECS = [
|
|
118506
118595
|
{ kind: "marketing", width: 1200, height: 628, fit: "cover", suffix: "_marketing" },
|
|
@@ -118589,7 +118678,7 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
118589
118678
|
if (opts.updateConfig) {
|
|
118590
118679
|
const configPath = resolve10(opts.updateConfig);
|
|
118591
118680
|
try {
|
|
118592
|
-
const raw = JSON.parse(
|
|
118681
|
+
const raw = JSON.parse(readFileSync13(configPath, "utf8"));
|
|
118593
118682
|
const imgPaths = raw["imagePaths"] ?? {};
|
|
118594
118683
|
if (outputPaths.marketing) imgPaths["marketing"] = outputPaths.marketing;
|
|
118595
118684
|
if (outputPaths.square) imgPaths["square"] = outputPaths.square;
|
|
@@ -118745,6 +118834,17 @@ async function runAdPmaxBrandGuidelinesEnable(opts) {
|
|
|
118745
118834
|
}
|
|
118746
118835
|
|
|
118747
118836
|
// src/commands/ad/_register.ts
|
|
118837
|
+
function parseOptionalMatchType(raw) {
|
|
118838
|
+
if (!raw?.trim()) return void 0;
|
|
118839
|
+
const t = raw.trim().toLowerCase();
|
|
118840
|
+
if (t === "broad") return "Broad";
|
|
118841
|
+
if (t === "phrase") return "Phrase";
|
|
118842
|
+
if (t === "exact") return "Exact";
|
|
118843
|
+
console.error(`
|
|
118844
|
+
\u274C --match-type \u65E0\u6548\uFF1A${raw}\uFF1B\u5408\u6CD5\u503C Broad | Phrase | Exact
|
|
118845
|
+
`);
|
|
118846
|
+
process.exit(1);
|
|
118847
|
+
}
|
|
118748
118848
|
function register20(program2) {
|
|
118749
118849
|
const adCmd = program2.command("ad").description(
|
|
118750
118850
|
"Google \u5E7F\u544A\u7BA1\u7406\uFF08Google API \u5730\u5740\u4ECE TSO API \u81EA\u52A8\u63A8\u5BFC\uFF0C\u53EF\u901A\u8FC7 SILUZAN_GOOGLE_API \u73AF\u5883\u53D8\u91CF\u8986\u76D6\uFF09\n \u542B\uFF1A\u7CFB\u5217/\u7EC4/\u521B\u610F/\u5173\u952E\u8BCD\u7B49\u5E38\u89C4\u5E7F\u544A\u6295\u653E\u4E0E\u7BA1\u7406\u80FD\u529B"
|
|
@@ -119029,10 +119129,16 @@ function register20(program2) {
|
|
|
119029
119129
|
});
|
|
119030
119130
|
}
|
|
119031
119131
|
);
|
|
119032
|
-
adCmd.command("keyword-create").description("\u5411\u5E7F\u544A\u7EC4\u6DFB\u52A0\u641C\u7D22\u5173\u952E\u8BCD").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--adgroup-id <id>", "\u76EE\u6807\u5E7F\u544A\u7EC4 ID").requiredOption("--adgroup-name <name>", "\u76EE\u6807\u5E7F\u544A\u7EC4\u540D\u79F0").requiredOption("--campaign-id <id>", "\u6240\u5C5E\u5E7F\u544A\u7CFB\u5217 ID").requiredOption("--campaign-name <name>", "\u6240\u5C5E\u5E7F\u544A\u7CFB\u5217\u540D\u79F0").
|
|
119132
|
+
adCmd.command("keyword-create").description("\u5411\u5E7F\u544A\u7EC4\u6DFB\u52A0\u641C\u7D22\u5173\u952E\u8BCD\uFF08\u652F\u6301\u7A7A\u683C\u591A\u8BCD\uFF1B\u53EF\u7528 --keywords-file\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--adgroup-id <id>", "\u76EE\u6807\u5E7F\u544A\u7EC4 ID").requiredOption("--adgroup-name <name>", "\u76EE\u6807\u5E7F\u544A\u7EC4\u540D\u79F0").requiredOption("--campaign-id <id>", "\u6240\u5C5E\u5E7F\u544A\u7CFB\u5217 ID").requiredOption("--campaign-name <name>", "\u6240\u5C5E\u5E7F\u544A\u7CFB\u5217\u540D\u79F0").option(
|
|
119033
119133
|
"--keywords <kw1,kw2,...>",
|
|
119034
|
-
"\u5173\u952E\u8BCD\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694",
|
|
119035
|
-
(v) => v
|
|
119134
|
+
"\u5173\u952E\u8BCD\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u7A7A\u683C\u591A\u8BCD\u5408\u6CD5\uFF0C\u5982 how to make\uFF09\uFF1B\u53EF\u4E0E --keywords-file \u5E76\u7528",
|
|
119135
|
+
(v) => parseKeywordsCsv(v)
|
|
119136
|
+
).option(
|
|
119137
|
+
"--keywords-file <path>",
|
|
119138
|
+
'JSON \u5B57\u7B26\u4E32\u6570\u7EC4\u6587\u4EF6\uFF08\u63A8\u8350\u591A\u8BCD/\u5E26\u5F15\u53F7\u573A\u666F\uFF09\uFF0C\u5982 ["how to make","home cooking"]'
|
|
119139
|
+
).option(
|
|
119140
|
+
"--match-type <Broad|Phrase|Exact>",
|
|
119141
|
+
'\u7EDF\u4E00\u5339\u914D\u7C7B\u578B\uFF08\u5199\u5165\u65F6\u7528\u6587\u6848 "\u2026" / [\u2026] \u5305\u88C5\uFF1B\u7701\u7565\u5219\u6309\u8BCD\u9762\u539F\u6837\u63D0\u4EA4\uFF09'
|
|
119036
119142
|
).option("--final-url <url>", "\u5173\u952E\u8BCD\u6700\u7EC8\u5230\u8FBE\u7F51\u5740\uFF08\u53EF\u9009\uFF09").option("-t, --token <token>", "Auth Token").option(
|
|
119037
119143
|
"--json-out <path>",
|
|
119038
119144
|
"\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
|
|
@@ -119047,16 +119153,26 @@ function register20(program2) {
|
|
|
119047
119153
|
campaignId: opts.campaignId,
|
|
119048
119154
|
campaignName: opts.campaignName,
|
|
119049
119155
|
keywords: opts.keywords,
|
|
119156
|
+
keywordsFile: opts.keywordsFile,
|
|
119157
|
+
matchType: parseOptionalMatchType(opts.matchType),
|
|
119050
119158
|
finalUrl: opts.finalUrl,
|
|
119051
119159
|
jsonOut: opts.jsonOut,
|
|
119052
119160
|
verbose: opts.verbose
|
|
119053
119161
|
});
|
|
119054
119162
|
}
|
|
119055
119163
|
);
|
|
119056
|
-
adCmd.command("keyword-negative-create").description(
|
|
119164
|
+
adCmd.command("keyword-negative-create").description(
|
|
119165
|
+
"\u6DFB\u52A0\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u7CFB\u5217/\u7EC4\u7EA7\uFF1B\u652F\u6301\u7A7A\u683C\u591A\u8BCD\u77ED\u8BED\uFF0C\u5982 how to make\uFF1B\u53EF\u7528 --keywords-file\uFF09"
|
|
119166
|
+
).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--campaign-id <id>", "\u5E7F\u544A\u7CFB\u5217 ID").requiredOption("--campaign-name <name>", "\u5E7F\u544A\u7CFB\u5217\u540D\u79F0").option(
|
|
119057
119167
|
"--keywords <kw1,kw2,...>",
|
|
119058
|
-
"\u5426\u5B9A\
|
|
119059
|
-
(v) => v
|
|
119168
|
+
"\u5426\u5B9A\u8BCD\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u7A7A\u683C\u591A\u8BCD\u5408\u6CD5\uFF09\uFF1B\u53EF\u4E0E --keywords-file \u5E76\u7528",
|
|
119169
|
+
(v) => parseKeywordsCsv(v)
|
|
119170
|
+
).option(
|
|
119171
|
+
"--keywords-file <path>",
|
|
119172
|
+
'JSON \u5B57\u7B26\u4E32\u6570\u7EC4\u6587\u4EF6\uFF08\u63A8\u8350\u591A\u8BCD\u77ED\u8BED\uFF09\uFF0C\u5982 ["how to make","home cooking","second hand"]'
|
|
119173
|
+
).option(
|
|
119174
|
+
"--match-type <Broad|Phrase|Exact>",
|
|
119175
|
+
"\u7EDF\u4E00\u5339\u914D\u7C7B\u578B\uFF08Broad \u65E0\u5305\u88C5 / Phrase \u52A0\u53CC\u5F15\u53F7 / Exact \u52A0\u65B9\u62EC\u53F7\uFF09"
|
|
119060
119176
|
).option("--adgroup-id <id>", "\u5E7F\u544A\u7EC4 ID\uFF08\u6307\u5B9A\u540E\u4E3A\u5E7F\u544A\u7EC4\u5C42\u7EA7\u5426\u5B9A\uFF09").option("--adgroup-name <name>", "\u5E7F\u544A\u7EC4\u540D\u79F0").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
|
|
119061
119177
|
async (opts) => {
|
|
119062
119178
|
await runAdNegativeKeywordCreate({
|
|
@@ -119067,6 +119183,8 @@ function register20(program2) {
|
|
|
119067
119183
|
adgroupId: opts.adgroupId,
|
|
119068
119184
|
adgroupName: opts.adgroupName,
|
|
119069
119185
|
keywords: opts.keywords,
|
|
119186
|
+
keywordsFile: opts.keywordsFile,
|
|
119187
|
+
matchType: parseOptionalMatchType(opts.matchType),
|
|
119070
119188
|
verbose: opts.verbose
|
|
119071
119189
|
});
|
|
119072
119190
|
}
|
package/dist/skill/_meta.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"agentPitfalls": [
|
|
8
8
|
"campaign-validate 推荐 --json-out ",
|
|
9
9
|
"否词只写 campaign.NegativeKeywordsForBatchJob,禁止把 free/jobs/tutorial 等否词抄进 AdGroupsForBatchJob[].KeywordsForBatchJob",
|
|
10
|
+
"KeywordText 是 JSON 字符串数组,空格多词合法(如 how to make);禁止因多词省略否词或让用户去后台手补",
|
|
10
11
|
"RSA 须同时写 DestinationUrl 与 Finalurl(后端必填 Finalurl;仅有 DestinationUrl 时 validate 可能仍通过)",
|
|
11
12
|
"SITELINK 须写 AssetFieldType: SITELINK(与 typeV2 一致)",
|
|
12
13
|
"提交前:ad campaign-validate → 用户确认 → ad campaign-create(勿对 validate 加 --json-out)"
|
|
@@ -65,7 +66,24 @@
|
|
|
65
66
|
],
|
|
66
67
|
|
|
67
68
|
"NegativeKeywordsForBatchJob": [
|
|
68
|
-
{
|
|
69
|
+
{
|
|
70
|
+
"KeywordText": [
|
|
71
|
+
"free",
|
|
72
|
+
"cheap",
|
|
73
|
+
"download",
|
|
74
|
+
"how to make",
|
|
75
|
+
"home cooking",
|
|
76
|
+
"second hand",
|
|
77
|
+
"near me"
|
|
78
|
+
],
|
|
79
|
+
"MatchTypeV2": "BROAD",
|
|
80
|
+
"FinalURL": ""
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"KeywordText": ["\"vegan recipe\"", "\"stock price\""],
|
|
84
|
+
"MatchTypeV2": "PHRASE",
|
|
85
|
+
"FinalURL": ""
|
|
86
|
+
}
|
|
69
87
|
],
|
|
70
88
|
|
|
71
89
|
"ExtensionsForBatchJob": [
|
|
@@ -31,6 +31,8 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
|
|
|
31
31
|
**禁止**:把方案 Markdown「否定词表」里的 `free` / `jobs` / `tutorial` 等抄进 `KeywordsForBatchJob`——那会**主动购买**这些流量。
|
|
32
32
|
**正确**:否词只写 `NegativeKeywordsForBatchJob`;`ad campaign-validate` 会对误填给出 **warnings**(常见否词词根 / 与否词块重复)。
|
|
33
33
|
|
|
34
|
+
**多词短语(与 `keyword-negative-create` 同类误判)**:`KeywordText` 是 **JSON 字符串数组**,**空格多词完全合法**(如 `"how to make"`、`"home cooking"`)。PHRASE 写 `"\"vegan recipe\""` 或裸词 + `MatchTypeV2: "PHRASE"`(validate 会补引号)。**禁止**因「多词」就省略否词或让用户去 Google Ads 后台手补——那是 Agent 误判,不是 `campaign-create` 限制。
|
|
35
|
+
|
|
34
36
|
投放方案 Markdown 中 §3.3 关键词矩阵 → `KeywordsForBatchJob`;§3.3 系列级否定词 / §3.4 账户级否定词 → `NegativeKeywordsForBatchJob`(账户级列表仍须在 Google 后台单独应用)。
|
|
35
37
|
|
|
36
38
|
### CLI 参数(勿混用)
|
|
@@ -184,7 +186,7 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --js
|
|
|
184
186
|
| 字段 | 类型 | 说明 |
|
|
185
187
|
| ----------------------------- | -------- | --------------------------------------------------------------------------------- |
|
|
186
188
|
| `AdGroupsForBatchJob` | object[] | **至少 1 组**;见下 |
|
|
187
|
-
| `NegativeKeywordsForBatchJob` | object[] | 系列级否词;元素:`{ KeywordText: string[], MatchTypeV2: "BROAD", FinalURL: "" }
|
|
189
|
+
| `NegativeKeywordsForBatchJob` | object[] | 系列级否词;元素:`{ KeywordText: string[], MatchTypeV2: "BROAD"\|"PHRASE"\|"EXACT", FinalURL: "" }`;**KeywordText 可含空格多词**(见模板示例) |
|
|
188
190
|
| `ExtensionsForBatchJob` | object[] | 附加信息;`Properties` 须 **string→string**(勿用数组值)。SITELINK 见下表 |
|
|
189
191
|
|
|
190
192
|
#### SITELINK(`ExtensionsForBatchJob[i]`,`typeV2` / `AssetFieldType` = `SITELINK`)
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
2. **结论先行**:第一句给出可操作的判断(正常 / 有问题 / 原因是什么),不要让用户从表格里自己猜。
|
|
28
28
|
3. **数据支撑**:引用**当次 CLI/脚本**的真实数字(消耗、CPA、余额、状态等);禁止编造、禁止示例 ID。
|
|
29
29
|
4. **行动建议**:1~3 条可执行项,点名对象(系列名、账户、关键词)并引用数据。
|
|
30
|
-
5. **下一步**:一句预测或邀请(「要我帮您调整预算吗?」「还想看关键词明细吗?」)。
|
|
30
|
+
5. **下一步**:一句预测或邀请(「要我帮您调整预算吗?」「还想看关键词明细吗?」)。
|
|
31
|
+
**注意**:客户端会另行展示可点击的 suggestion chips(完整用户话术)。本文件与各任务 reference **只要求回复里出现一句邀请**,**不要**在 skill 各任务文档里维护「可点 chips 全文清单」——那份清单由产品侧 chips 规则维护,skill 更新不应与之逐条对齐。
|
|
31
32
|
|
|
32
33
|
长任务(预计 >30s):中间用**进度式**短句(「正在拉取系列数据…」「正在检查余额…」),不要长时间沉默后突然扔一大段表。
|
|
33
34
|
|
|
@@ -59,7 +60,8 @@
|
|
|
59
60
|
| 帮我开个户 / 建广告 | W2 / W3 | 「没问题,我先告诉您需要准备哪些资料。」 |
|
|
60
61
|
| 网站行不行 | P8 | 「我来给这个网站做个体检,看是否符合投放要求。」 |
|
|
61
62
|
|
|
62
|
-
完整话术矩阵见产品文档第十章;Skill 内记住**结构**即可,勿背诵全文。
|
|
63
|
+
完整话术矩阵见产品文档第十章;Skill 内记住**结构**即可,勿背诵全文。
|
|
64
|
+
可点击的「下一步」完整句子由客户端 suggestion chips 提供,**禁止**在本 skill 的 playbooks/workflows 里复制维护 chips 文案表(避免 skill 更新后与产品侧脱节)。
|
|
63
65
|
|
|
64
66
|
---
|
|
65
67
|
|
|
@@ -106,7 +108,8 @@
|
|
|
106
108
|
| ---- | ---- |
|
|
107
109
|
| `agent-conventions.md` | 怎么拉数、怎么读 outline、怎么自检、硬规范 |
|
|
108
110
|
| `intent-routing.md` | 报告/分析消歧、选哪个 P/W |
|
|
109
|
-
| `playbooks.md` / `workflows.md` |
|
|
110
|
-
| **本文件** |
|
|
111
|
+
| `playbooks.md` / `workflows.md` | 步骤清单(执行),不维护 chips 全文 |
|
|
112
|
+
| **本文件** | **对用户怎么说**(结构 + 一句下一步邀请) |
|
|
113
|
+
| 客户端 suggestion chips | **可点的下一步完整话术**(产品侧维护,与本 skill 解耦) |
|
|
111
114
|
|
|
112
115
|
数值纪律不变:所有金额、状态、ID 仍只来自当次 CLI/`--json-out` 脚本;本文件只约束**表述方式**,不放宽数据真实性要求。
|
|
@@ -268,26 +268,38 @@ siluzan-tso ad keyword-create \
|
|
|
268
268
|
-a <accountId> \
|
|
269
269
|
--adgroup-id <adGroupId> --adgroup-name <adGroupName> \
|
|
270
270
|
--campaign-id <campaignId> --campaign-name <campaignName> \
|
|
271
|
-
--keywords "词1,词2
|
|
271
|
+
--keywords "词1,词2,多词短语" [--final-url <url>] \
|
|
272
|
+
[--match-type Broad|Phrase|Exact] [--keywords-file ./kws.json] \
|
|
272
273
|
[--json-out <path>]
|
|
273
274
|
```
|
|
274
275
|
|
|
275
|
-
`--json-out` 落盘 `{ request: { adgroupId, count }, response: ... }`,批量脚本可直接据此核对成功量。
|
|
276
|
+
`--keywords` **支持空格多词**(逗号才是分隔符)。`--json-out` 落盘 `{ request: { adgroupId, count }, response: ... }`,批量脚本可直接据此核对成功量。
|
|
276
277
|
|
|
277
278
|
### 否定关键词
|
|
278
279
|
|
|
280
|
+
**支持空格多词短语**(如 `how to make`、`home cooking`)。匹配类型与 Web 一致:裸词=广泛,`"短语"`=词组,`[词]`=完全;也可用 `--match-type`。多词/带引号时推荐 `--keywords-file`,避免 shell 转义。
|
|
281
|
+
|
|
279
282
|
```bash
|
|
280
283
|
# 添加(默认系列层级;传 --adgroup-id 则为组层级)
|
|
284
|
+
# 多词短语直接写空格,逗号分隔多条——CLI 支持,无需改后台手工补
|
|
281
285
|
siluzan-tso ad keyword-negative-create \
|
|
282
286
|
-a <accountId> \
|
|
283
287
|
--campaign-id <campaignId> --campaign-name <campaignName> \
|
|
284
|
-
--keywords "
|
|
285
|
-
|
|
288
|
+
--keywords "how to make,home cooking,second hand,near me"
|
|
289
|
+
|
|
290
|
+
# 统一词组匹配 + JSON 文件(推荐)
|
|
291
|
+
# negatives.json → ["how to make","home cooking","vegan recipe"]
|
|
292
|
+
siluzan-tso ad keyword-negative-create \
|
|
293
|
+
-a <accountId> \
|
|
294
|
+
--campaign-id <campaignId> --campaign-name <campaignName> \
|
|
295
|
+
--keywords-file ./negatives.json --match-type Phrase
|
|
286
296
|
|
|
287
297
|
# 删除(先用 ad keywords --negative --json-out ./snap 获取 id)
|
|
288
298
|
siluzan-tso ad keyword-negative-delete -a <accountId> --id <negativeKeywordId>
|
|
289
299
|
```
|
|
290
300
|
|
|
301
|
+
> **禁止**因「多词」就让用户去 Google Ads 后台手工补否词——那是 Agent 误判;用上面命令即可。
|
|
302
|
+
|
|
291
303
|
---
|
|
292
304
|
|
|
293
305
|
## ad batch — 异步批量创建记录
|
|
@@ -405,6 +417,8 @@ BatchJob 中 `ExtensionsForBatchJob`(尤其 **Sitelink**)失败率较高,
|
|
|
405
417
|
|
|
406
418
|
**关键词匹配:** 写在 `KeywordsForBatchJob` 块内;同一块同匹配类型,`MatchTypeV2` 与 `KeywordText` 词面格式必须对齐(`PHRASE` 用 `"词"`、`EXACT` 用 `[词]`、`BROAD` 直写)。
|
|
407
419
|
|
|
420
|
+
**多词短语:** `KeywordText` / 否词 `NegativeKeywordsForBatchJob[].KeywordText` 是 **JSON 字符串数组**,**空格多词合法**(如 `"how to make"`)。与 `keyword-negative-create` 不同,这里**没有**逗号 CLI 解析问题——**禁止**因「多词」省略否词或让用户去后台手补。
|
|
421
|
+
|
|
408
422
|
广告组/关键词/创意的分步创建仍用 `adgroup-create`、`keyword-create`、`ad-create`。
|
|
409
423
|
|
|
410
424
|
---
|
|
@@ -409,8 +409,8 @@ Broad Match + 受众信号 = 伪装成广泛定向的精准投放。
|
|
|
409
409
|
| ------------ | ------------------------------------------------------------ | ----------------------------------------------------------------------- |
|
|
410
410
|
| 关键词调研 | `keyword -k "词" [--url]` | 结果按意图分层后再添加 |
|
|
411
411
|
| 添加关键词 | `ad keyword-create --keywords "词1,词2"` | 格式:`词→BROAD`、`"词"→PHRASE`、`[词]→EXACT` |
|
|
412
|
-
| 添加否定词 | `ad keyword-negative-create` |
|
|
413
|
-
| 创建广告系列 | `ad campaign-create --config-file`(先 `campaign-validate`) |
|
|
412
|
+
| 添加否定词 | `ad keyword-negative-create` | **支持空格多词**(`how to make`);多词用 `--keywords` 或 `--keywords-file`,**禁止**因多词改让用户后台手补 |
|
|
413
|
+
| 创建广告系列 | `ad campaign-create --config-file`(先 `campaign-validate`) | `KeywordText` 为 JSON 数组,**空格多词合法**;否词写 `NegativeKeywordsForBatchJob`,勿因多词省略 |
|
|
414
414
|
| 搜索词分析 | `ad search-terms` | 每周导出,按第八章流程处理 |
|
|
415
415
|
| 竞品监控 | `ad auction-insights`(如有) | 按第九章分析竞争态势 |
|
|
416
416
|
| 出价调整 | 广告系列级 Smart Bidding 设置 | 新账户用 Manual/ECPC → 有数据后切 tCPA/tROAS |
|
|
@@ -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.37-beta.
|
|
12
|
+
$PKG_VERSION = '1.1.37-beta.4'
|
|
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.37-beta.
|
|
12
|
+
readonly PKG_VERSION="1.1.37-beta.4"
|
|
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"
|