siluzan-tso-cli 1.1.20-beta.19 → 1.1.20-beta.21
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 +191 -54
- 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/keyword-planner-workflows.md +9 -7
- 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.21),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -103992,6 +103992,22 @@ async function fetchTikTokAccountByMediaCustomerId(config, mediaCustomerId, verb
|
|
|
103992
103992
|
const match = items.find((it) => String(it.ma?.mediaCustomerId ?? "") === id);
|
|
103993
103993
|
return match ?? null;
|
|
103994
103994
|
}
|
|
103995
|
+
async function fetchGoogleAccountByMediaCustomerId(config, mediaCustomerId, verbose) {
|
|
103996
|
+
const cfg = PLATFORM_CONFIG.Google;
|
|
103997
|
+
const params = new URLSearchParams();
|
|
103998
|
+
for (const [k, v] of Object.entries(cfg.fixedParams)) {
|
|
103999
|
+
params.set(k, String(v));
|
|
104000
|
+
}
|
|
104001
|
+
params.set(cfg.pageParam, "1");
|
|
104002
|
+
params.set("pageSize", "50");
|
|
104003
|
+
params.set(cfg.idSearchParam, mediaCustomerId.trim());
|
|
104004
|
+
const url = `${config.apiBaseUrl}${cfg.path}?${params}`;
|
|
104005
|
+
const res = await apiFetchWithHeaders2(url, config, {}, verbose);
|
|
104006
|
+
const items = Array.isArray(res.data) ? res.data : [];
|
|
104007
|
+
const id = mediaCustomerId.trim();
|
|
104008
|
+
const match = items.find((it) => String(it.ma?.mediaCustomerId ?? "") === id);
|
|
104009
|
+
return match ?? null;
|
|
104010
|
+
}
|
|
103995
104011
|
|
|
103996
104012
|
// src/commands/list-accounts/printers.ts
|
|
103997
104013
|
init_cli_table();
|
|
@@ -110229,6 +110245,8 @@ var VALID_STATUS_V2 = ["Enabled", "Paused"];
|
|
|
110229
110245
|
var VALID_CHANNEL_V2 = ["SEARCH", "DISPLAY", "VIDEO", "SHOPPING", "MULTI_CHANNEL"];
|
|
110230
110246
|
var URL_REGEX = /^https?:\/\/.+/;
|
|
110231
110247
|
var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
110248
|
+
var PATH_SEGMENT_REGEX = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
110249
|
+
var RSA_PATH_BACKEND_HINT = "\u540E\u7AEF GoogleAdsAcctMgmtServiceProviderV2.BuildResponsiveSearchAd \u4F1A\u5C06 Path1 \u5199\u5165 Google Ads SDK\uFF1BJSON \u7F3A\u5B57\u6BB5\u6216\u4E3A null \u65F6\u53CD\u5E8F\u5217\u5316\u4E3A null\uFF0C\u89E6\u53D1 ArgumentNullException\uFF08Parameter 'value'\uFF09\uFF0C\u6574\u5305 BatchJob \u5931\u8D25";
|
|
110232
110250
|
function pushErr2(errors, msg) {
|
|
110233
110251
|
errors.push(msg);
|
|
110234
110252
|
}
|
|
@@ -110247,6 +110265,61 @@ function calcGoogleCharLength(text) {
|
|
|
110247
110265
|
}
|
|
110248
110266
|
return len;
|
|
110249
110267
|
}
|
|
110268
|
+
function validateRsaDisplayPath(prefix, ad, errors) {
|
|
110269
|
+
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
|
+
if (ad["path1"] !== void 0 && ad["Path1"] === void 0) {
|
|
110271
|
+
pushErr2(
|
|
110272
|
+
errors,
|
|
110273
|
+
`${prefix} \u68C0\u6D4B\u5230\u5C0F\u5199 path1="${String(ad["path1"])}"\uFF0C\u540E\u7AEF\u5951\u7EA6\u5B57\u6BB5\u4E3A Path1\uFF08PascalCase\uFF09\uFF0C\u5F53\u524D\u63D0\u4EA4\u4E0D\u4F1A\u751F\u6548`
|
|
110274
|
+
);
|
|
110275
|
+
}
|
|
110276
|
+
if (ad["path2"] !== void 0 && ad["Path2"] === void 0) {
|
|
110277
|
+
pushErr2(
|
|
110278
|
+
errors,
|
|
110279
|
+
`${prefix} \u68C0\u6D4B\u5230\u5C0F\u5199 path2="${String(ad["path2"])}"\uFF0C\u540E\u7AEF\u5951\u7EA6\u5B57\u6BB5\u4E3A Path2\uFF08PascalCase\uFF09\uFF0C\u5F53\u524D\u63D0\u4EA4\u4E0D\u4F1A\u751F\u6548`
|
|
110280
|
+
);
|
|
110281
|
+
}
|
|
110282
|
+
for (const [field, label, example] of [
|
|
110283
|
+
["Path1", "\u663E\u793A\u8DEF\u5F84\u7B2C 1 \u6BB5", '"Path1": "products"'],
|
|
110284
|
+
["Path2", "\u663E\u793A\u8DEF\u5F84\u7B2C 2 \u6BB5", '"Path2": "steel"']
|
|
110285
|
+
]) {
|
|
110286
|
+
const raw = ad[field];
|
|
110287
|
+
if (raw === void 0) {
|
|
110288
|
+
pushErr2(
|
|
110289
|
+
errors,
|
|
110290
|
+
`${prefix}.${field} \u5B57\u6BB5\u7F3A\u5931\uFF08RSA \u5FC5\u586B\uFF09\u3002${label}\uFF0C\u5C55\u793A\u4E3A finalUrl \u57DF\u540D\u540E\u7684\u8DEF\u5F84\u6BB5\uFF08\u226415 \u5B57\u7B26\uFF0CCJK \u6309 2 \u8BA1\uFF09\u3002${RSA_PATH_BACKEND_HINT}\u3002${jsonHint}\u3002\u793A\u4F8B\uFF1A${example}`
|
|
110291
|
+
);
|
|
110292
|
+
continue;
|
|
110293
|
+
}
|
|
110294
|
+
if (raw === null) {
|
|
110295
|
+
pushErr2(
|
|
110296
|
+
errors,
|
|
110297
|
+
`${prefix}.${field} \u4E3A null\uFF08RSA \u5FC5\u586B\uFF0C\u987B\u4E3A\u5B57\u7B26\u4E32\uFF0C\u53EF\u586B ""\uFF09\u3002${RSA_PATH_BACKEND_HINT}\u3002${jsonHint}\u3002\u793A\u4F8B\uFF1A${example}`
|
|
110298
|
+
);
|
|
110299
|
+
continue;
|
|
110300
|
+
}
|
|
110301
|
+
if (typeof raw !== "string") {
|
|
110302
|
+
pushErr2(
|
|
110303
|
+
errors,
|
|
110304
|
+
`${prefix}.${field} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u5F53\u524D\u7C7B\u578B\u4E3A ${typeof raw}\u3002${jsonHint}`
|
|
110305
|
+
);
|
|
110306
|
+
continue;
|
|
110307
|
+
}
|
|
110308
|
+
const len = calcGoogleCharLength(raw);
|
|
110309
|
+
if (len > 15) {
|
|
110310
|
+
pushErr2(
|
|
110311
|
+
errors,
|
|
110312
|
+
`${prefix}.${field} \u8D85\u8FC7 15 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF0CCJK \u6309 2 \u8BA1\uFF09\uFF1A"${raw}"`
|
|
110313
|
+
);
|
|
110314
|
+
}
|
|
110315
|
+
if (raw.length > 0 && !PATH_SEGMENT_REGEX.test(raw)) {
|
|
110316
|
+
pushErr2(
|
|
110317
|
+
errors,
|
|
110318
|
+
`${prefix}.${field} \u542B\u975E\u6CD5\u5B57\u7B26\u6216\u683C\u5F0F\uFF08\u4EC5\u5141\u8BB8\u5C0F\u5199 a-z\u30010-9\u3001\u8FDE\u5B57\u7B26\uFF0C\u4E14\u4E0D\u80FD\u4EE5\u8FDE\u5B57\u7B26\u5F00\u5934/\u7ED3\u5C3E\uFF09\uFF1A"${raw}"`
|
|
110319
|
+
);
|
|
110320
|
+
}
|
|
110321
|
+
}
|
|
110322
|
+
}
|
|
110250
110323
|
function validateRsaAd(prefix, ad, errors, warnings) {
|
|
110251
110324
|
const h1 = ad["headlinePart1"];
|
|
110252
110325
|
const h2 = ad["headlinePart2"];
|
|
@@ -110293,14 +110366,7 @@ function validateRsaAd(prefix, ad, errors, warnings) {
|
|
|
110293
110366
|
pushErr2(errors, `${prefix} \u63CF\u8FF0[${i}] \u8D85\u8FC7 90 \u5B57\u7B26\uFF08\u5F53\u524D ${len}\uFF09\uFF1A"${descriptions[i].slice(0, 40)}"`);
|
|
110294
110367
|
}
|
|
110295
110368
|
}
|
|
110296
|
-
|
|
110297
|
-
const p2 = ad["Path2"];
|
|
110298
|
-
if (typeof p1 === "string" && calcGoogleCharLength(p1) > 15) {
|
|
110299
|
-
pushErr2(errors, `${prefix} Path1 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p1}"`);
|
|
110300
|
-
}
|
|
110301
|
-
if (typeof p2 === "string" && calcGoogleCharLength(p2) > 15) {
|
|
110302
|
-
pushErr2(errors, `${prefix} Path2 \u8D85\u8FC7 15 \u5B57\u7B26\uFF08CJK \u8BA1 2\uFF09\uFF1A"${p2}"`);
|
|
110303
|
-
}
|
|
110369
|
+
validateRsaDisplayPath(prefix, ad, errors);
|
|
110304
110370
|
const finalUrl = ad["Finalurl"] ?? ad["DestinationUrl"];
|
|
110305
110371
|
if (typeof finalUrl === "string" && finalUrl.length > 0 && !URL_REGEX.test(finalUrl)) {
|
|
110306
110372
|
pushErr2(errors, `${prefix} Finalurl \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08\u5E94\u4EE5 http(s):// \u5F00\u5934\uFF09\uFF1A${finalUrl}`);
|
|
@@ -114027,11 +114093,19 @@ function parseGeoTargetConstantIds(raw) {
|
|
|
114027
114093
|
}
|
|
114028
114094
|
return [...new Set(ids)];
|
|
114029
114095
|
}
|
|
114096
|
+
function appendGeoTargetConstantIdsQuery(baseUrl, geoTargetConstantIds) {
|
|
114097
|
+
if (geoTargetConstantIds.length === 0) return baseUrl;
|
|
114098
|
+
const qs = geoTargetConstantIds.map((id) => `geoTargetConstantIds=${encodeURIComponent(String(id))}`).join("&");
|
|
114099
|
+
return `${baseUrl}?${qs}`;
|
|
114100
|
+
}
|
|
114030
114101
|
function buildKeywordIdeaGoogleUrl(googleApiUrl, geoTargetConstantIds) {
|
|
114031
114102
|
const base = `${googleApiUrl.replace(/\/$/, "")}/keywordidea/google`;
|
|
114032
|
-
|
|
114033
|
-
|
|
114034
|
-
|
|
114103
|
+
return appendGeoTargetConstantIdsQuery(base, geoTargetConstantIds);
|
|
114104
|
+
}
|
|
114105
|
+
function buildKeywordRecommendationGoogleUrl(googleApiUrl, mediaCustomerId, geoTargetConstantIds) {
|
|
114106
|
+
const id = encodeURIComponent(mediaCustomerId.trim());
|
|
114107
|
+
const base = `${googleApiUrl.replace(/\/$/, "")}/keywordrecommendation/recommend/${id}/google`;
|
|
114108
|
+
return appendGeoTargetConstantIdsQuery(base, geoTargetConstantIds);
|
|
114035
114109
|
}
|
|
114036
114110
|
function geoTargetConstantsListPath(googleApiUrl) {
|
|
114037
114111
|
return `${googleApiUrl.replace(/\/$/, "")}/geotargetmanagement/GeoTargetConstans`;
|
|
@@ -114074,22 +114148,37 @@ var KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY = "USD";
|
|
|
114074
114148
|
function applyUsdBidAmountsInCny(item, usdToCnyRate) {
|
|
114075
114149
|
return {
|
|
114076
114150
|
...item,
|
|
114077
|
-
averageCpcCNY: convertUsdToCny(item.
|
|
114078
|
-
lowTopOfPageBidCNY: convertUsdToCny(item.
|
|
114079
|
-
highTopOfPageBidCNY: convertUsdToCny(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
|
|
114080
114162
|
};
|
|
114081
114163
|
}
|
|
114082
114164
|
function buildKeywordSuggestJsonPayload(items, opts) {
|
|
114083
|
-
|
|
114084
|
-
|
|
114085
|
-
|
|
114086
|
-
|
|
114087
|
-
}
|
|
114088
|
-
const
|
|
114165
|
+
const currency = opts.bidAmountCurrency.toUpperCase();
|
|
114166
|
+
let normalized = items.map((row) => ({
|
|
114167
|
+
...microsItemToBidAmounts(row),
|
|
114168
|
+
bidAmountCurrency: currency
|
|
114169
|
+
}));
|
|
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
|
+
const n = normalized.length;
|
|
114089
114178
|
return {
|
|
114090
|
-
...wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items:
|
|
114091
|
-
bidAmountCurrency:
|
|
114092
|
-
...rate != null && Number.isFinite(rate) && rate > 0 ? { usdToCnyExchangeRate: rate } : {}
|
|
114179
|
+
...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 } : {}
|
|
114093
114182
|
};
|
|
114094
114183
|
}
|
|
114095
114184
|
async function fetchUrlKeywords(url, keywords, verbose) {
|
|
@@ -114204,7 +114293,41 @@ async function runKeywordSuggest(opts) {
|
|
|
114204
114293
|
`);
|
|
114205
114294
|
process.exit(1);
|
|
114206
114295
|
}
|
|
114207
|
-
const
|
|
114296
|
+
const accountId = opts.account?.trim() ?? "";
|
|
114297
|
+
let bidAmountCurrency = KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY;
|
|
114298
|
+
let apiUrl;
|
|
114299
|
+
if (accountId) {
|
|
114300
|
+
const acct = await fetchGoogleAccountByMediaCustomerId(config, accountId, opts.verbose);
|
|
114301
|
+
if (!acct) {
|
|
114302
|
+
console.error(
|
|
114303
|
+
`
|
|
114304
|
+
\u274C \u672A\u627E\u5230 Google \u8D26\u6237 mediaCustomerId=${accountId}\u3002
|
|
114305
|
+
\u8BF7\u5148\u6267\u884C\uFF1Asiluzan-tso list-accounts -m Google -k ` + accountId + "\n"
|
|
114306
|
+
);
|
|
114307
|
+
process.exit(1);
|
|
114308
|
+
}
|
|
114309
|
+
const code = acct.ma?.currencyCode?.trim();
|
|
114310
|
+
if (!code) {
|
|
114311
|
+
console.error(
|
|
114312
|
+
`
|
|
114313
|
+
\u274C \u8D26\u6237 ${accountId} \u7F3A\u5C11 currencyCode\uFF0C\u65E0\u6CD5\u6807\u6CE8\u51FA\u4EF7\u5E01\u79CD\u3002
|
|
114314
|
+
\u8BF7\u7528 list-accounts -m Google -k ` + accountId + " --json \u786E\u8BA4\u8D26\u6237\u6570\u636E\u3002\n"
|
|
114315
|
+
);
|
|
114316
|
+
process.exit(1);
|
|
114317
|
+
}
|
|
114318
|
+
bidAmountCurrency = code.toUpperCase();
|
|
114319
|
+
apiUrl = buildKeywordRecommendationGoogleUrl(googleApiUrl, accountId, geoTargetConstantIds);
|
|
114320
|
+
if (opts.verbose) {
|
|
114321
|
+
console.error(
|
|
114322
|
+
` [keyword] account=${accountId} bidAmountCurrency=${bidAmountCurrency} api=keywordrecommendation/recommend/.../google`
|
|
114323
|
+
);
|
|
114324
|
+
}
|
|
114325
|
+
} else {
|
|
114326
|
+
apiUrl = buildKeywordIdeaGoogleUrl(googleApiUrl, geoTargetConstantIds);
|
|
114327
|
+
if (opts.verbose) {
|
|
114328
|
+
console.error(` [keyword] api=keywordidea/google (shared MCC, USD)`);
|
|
114329
|
+
}
|
|
114330
|
+
}
|
|
114208
114331
|
if (opts.verbose && geoTargetConstantIds.length > 0) {
|
|
114209
114332
|
console.error(` [keyword] geoTargetConstantIds=${geoTargetConstantIds.join(",")}`);
|
|
114210
114333
|
}
|
|
@@ -114244,9 +114367,13 @@ async function runKeywordSuggest(opts) {
|
|
|
114244
114367
|
});
|
|
114245
114368
|
}
|
|
114246
114369
|
const n = items.length;
|
|
114247
|
-
const
|
|
114248
|
-
const
|
|
114249
|
-
const
|
|
114370
|
+
const isUsdPlanner = bidAmountCurrency === KEYWORD_SUGGEST_BID_AMOUNT_CURRENCY;
|
|
114371
|
+
const usdToCnyRate = isUsdPlanner ? await getUsdToCnyExchangeRate(config, opts.verbose) : null;
|
|
114372
|
+
const kwPayload = buildKeywordSuggestJsonPayload(items, {
|
|
114373
|
+
bidAmountCurrency,
|
|
114374
|
+
usdToCnyRate
|
|
114375
|
+
});
|
|
114376
|
+
const displayItems = kwPayload.items;
|
|
114250
114377
|
if (await emitCliJsonOrSnapshot(opts, {
|
|
114251
114378
|
section: "keyword-suggest",
|
|
114252
114379
|
commandLabel: "keyword suggest",
|
|
@@ -114262,36 +114389,37 @@ async function runKeywordSuggest(opts) {
|
|
|
114262
114389
|
return;
|
|
114263
114390
|
}
|
|
114264
114391
|
const fmtAmt = (v) => v != null && Number.isFinite(v) ? v.toFixed(2) : "\u2014";
|
|
114265
|
-
const
|
|
114266
|
-
const
|
|
114392
|
+
const cur = bidAmountCurrency;
|
|
114393
|
+
const hasCnyRef = isUsdPlanner && usdToCnyRate != null && usdToCnyRate > 0;
|
|
114394
|
+
const cols = hasCnyRef ? [
|
|
114267
114395
|
{ key: "keyword", header: "\u5173\u952E\u8BCD" },
|
|
114268
114396
|
{ key: "montlySearch", header: "\u6708\u5747\u641C\u7D22" },
|
|
114269
|
-
{ key: "
|
|
114270
|
-
{ key: "cpcCny", header: "\u5E73\u5747CPC(CNY)" },
|
|
114271
|
-
{ key: "
|
|
114272
|
-
{ key: "bidRangeCny", header: "\u9875\u9996\u51FA\u4EF7(CNY)" },
|
|
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)" },
|
|
114273
114401
|
{ key: "competition", header: "\u7ADE\u4E89\u5EA6" }
|
|
114274
114402
|
] : [
|
|
114275
114403
|
{ key: "keyword", header: "\u5173\u952E\u8BCD" },
|
|
114276
114404
|
{ key: "montlySearch", header: "\u6708\u5747\u641C\u7D22" },
|
|
114277
|
-
{ key: "
|
|
114278
|
-
{ key: "
|
|
114405
|
+
{ key: "cpcMain", header: `\u5E73\u5747CPC(${cur})` },
|
|
114406
|
+
{ key: "bidRangeMain", header: `\u9875\u9996\u51FA\u4EF7(${cur})` },
|
|
114279
114407
|
{ key: "competition", header: "\u7ADE\u4E89\u5EA6" }
|
|
114280
114408
|
];
|
|
114281
|
-
const rows =
|
|
114282
|
-
const
|
|
114283
|
-
const
|
|
114284
|
-
const
|
|
114285
|
-
const
|
|
114409
|
+
const rows = displayItems.map((item) => {
|
|
114410
|
+
const cpcMain = fmtAmt(item.averageCpc);
|
|
114411
|
+
const lowMain = fmtAmt(item.lowTopOfPageBid);
|
|
114412
|
+
const highMain = fmtAmt(item.highTopOfPageBid);
|
|
114413
|
+
const bidRangeMain = lowMain === "\u2014" && highMain === "\u2014" ? "\u2014" : `${lowMain} ~ ${highMain}`;
|
|
114286
114414
|
const competitionDisplay = item.competitionV2 ?? (item.competition != null ? item.competition.toFixed(2) : "\u2014");
|
|
114287
114415
|
const row = {
|
|
114288
114416
|
keyword: item.keyword ?? "",
|
|
114289
114417
|
montlySearch: String(item.montlySearch ?? "\u2014"),
|
|
114290
|
-
|
|
114291
|
-
|
|
114418
|
+
cpcMain,
|
|
114419
|
+
bidRangeMain,
|
|
114292
114420
|
competition: competitionDisplay
|
|
114293
114421
|
};
|
|
114294
|
-
if (
|
|
114422
|
+
if (hasCnyRef) {
|
|
114295
114423
|
const cpcCny = fmtAmt(item.averageCpcCNY);
|
|
114296
114424
|
const lowCny = fmtAmt(item.lowTopOfPageBidCNY);
|
|
114297
114425
|
const highCny = fmtAmt(item.highTopOfPageBidCNY);
|
|
@@ -114301,14 +114429,19 @@ async function runKeywordSuggest(opts) {
|
|
|
114301
114429
|
return row;
|
|
114302
114430
|
});
|
|
114303
114431
|
printCliTable(rows, cols);
|
|
114304
|
-
if (
|
|
114305
|
-
console.log(
|
|
114432
|
+
if (hasCnyRef) {
|
|
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) {
|
|
114438
|
+
console.log(` \uFF08\u51FA\u4EF7\u5E01\u79CD\uFF1A${cur}\uFF0C\u4E0E\u8D26\u6237 currencyCode \u4E00\u81F4\uFF09
|
|
114306
114439
|
`);
|
|
114307
114440
|
} else {
|
|
114308
114441
|
console.log();
|
|
114309
114442
|
}
|
|
114310
114443
|
}
|
|
114311
|
-
function
|
|
114444
|
+
function microsToAmount(v) {
|
|
114312
114445
|
if (v == null) return null;
|
|
114313
114446
|
const n = Number(v);
|
|
114314
114447
|
if (!Number.isFinite(n)) return null;
|
|
@@ -114320,7 +114453,7 @@ function legacyUsdAmount(...candidates) {
|
|
|
114320
114453
|
}
|
|
114321
114454
|
return null;
|
|
114322
114455
|
}
|
|
114323
|
-
function
|
|
114456
|
+
function microsItemToBidAmounts(item) {
|
|
114324
114457
|
const {
|
|
114325
114458
|
averageCpc: _legacyCpc,
|
|
114326
114459
|
lowTopOfPageBidMicros: _legacyLow,
|
|
@@ -114344,20 +114477,20 @@ function microsItemToUSD(item) {
|
|
|
114344
114477
|
} = item;
|
|
114345
114478
|
return {
|
|
114346
114479
|
...rest,
|
|
114347
|
-
|
|
114348
|
-
|
|
114480
|
+
averageCpc: legacyUsdAmount(
|
|
114481
|
+
microsToAmount(_legacyCpc),
|
|
114349
114482
|
_existingCpcUsd,
|
|
114350
114483
|
_legacyCpcDollar,
|
|
114351
114484
|
_legacyCpcYuan
|
|
114352
114485
|
),
|
|
114353
|
-
|
|
114354
|
-
|
|
114486
|
+
lowTopOfPageBid: legacyUsdAmount(
|
|
114487
|
+
microsToAmount(_legacyLow),
|
|
114355
114488
|
_existingLowUsd,
|
|
114356
114489
|
_legacyLowDollar,
|
|
114357
114490
|
_legacyLowYuan
|
|
114358
114491
|
),
|
|
114359
|
-
|
|
114360
|
-
|
|
114492
|
+
highTopOfPageBid: legacyUsdAmount(
|
|
114493
|
+
microsToAmount(_legacyHigh),
|
|
114361
114494
|
_existingHighUsd,
|
|
114362
114495
|
_legacyHighDollar,
|
|
114363
114496
|
_legacyHighYuan
|
|
@@ -114372,15 +114505,19 @@ function splitKeywordSeeds(raw) {
|
|
|
114372
114505
|
}
|
|
114373
114506
|
function register21(program2) {
|
|
114374
114507
|
const keywordCmd = program2.command("keyword").description(
|
|
114375
|
-
"Google \u5173\u952E\u5B57\u63A8\u8350\uFF08\
|
|
114508
|
+
"Google \u5173\u952E\u5B57\u63A8\u8350\uFF08\u53EF\u9009 -a \u8D26\u6237 ID \u6309\u8D26\u6237\u5E01\u79CD\u51FA\u4EF7\uFF1B\u5426\u5219\u5171\u4EAB MCC/USD\uFF1B\u53EF --geo \u9650\u5B9A\u5E02\u573A\uFF09"
|
|
114376
114509
|
);
|
|
114377
114510
|
attachBaseCliOptions(keywordCmd);
|
|
114378
114511
|
keywordCmd.requiredOption("-k, --keyword <words>", "\u641C\u7D22\u8BCD\uFF0C\u591A\u4E2A\u9017\u53F7\u5206\u9694").option(
|
|
114512
|
+
"-a, --account <id>",
|
|
114513
|
+
"Google mediaCustomerId\uFF1B\u4F20\u5165\u5219\u8D70\u8D26\u6237\u7EA7\u63A8\u8350\u5E76\u6309 list-accounts \u7684 currencyCode \u5C55\u793A\u51FA\u4EF7"
|
|
114514
|
+
).option(
|
|
114379
114515
|
"--geo <ids>",
|
|
114380
114516
|
"geoTargetConstant ID\uFF08\u5148 keyword geo-list\uFF1B\u4F8B 2840=\u7F8E\u56FD\uFF09\u3002\u591A\u4E2A ID \u4E3A\u6C47\u603B\u6307\u6807\uFF1B\u5206\u5E02\u573A\u987B\u591A\u6B21\u8C03\u7528\u3001\u6BCF\u6B21\u53EA\u4F20\u4E00\u4E2A"
|
|
114381
114517
|
).option("--url <url>", "\u516C\u53F8/\u4EA7\u54C1\u7F51\u5740\uFF08\u586B\u5199\u540E\u89E6\u53D1\u7F51\u5740\u62D3\u8BCD\uFF1B\u4E0E --google-only \u4E92\u65A5\uFF09").option("--google-only", "\u4EC5 Google Keyword Planner\uFF0C\u4E0D\u53E0\u52A0\u7F51\u5740\u62D3\u8BCD").option("--include <words>", "\u7ED3\u679C\u5FC5\u987B\u5305\u542B\u7684\u8BCD\uFF0C\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694").option("--exclude <words>", "\u7ED3\u679C\u4E0D\u5305\u542B\u7684\u8BCD\uFF0C\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694").action(async (opts) => {
|
|
114382
114518
|
await runKeywordSuggest({
|
|
114383
114519
|
token: opts.token,
|
|
114520
|
+
account: opts.account,
|
|
114384
114521
|
keywords: splitKeywordSeeds(opts.keyword),
|
|
114385
114522
|
geo: opts.geo,
|
|
114386
114523
|
url: opts.url,
|
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` →
|
|
155
|
+
- `keyword --json` → `averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid` + 每条 `bidAmountCurrency`(无 `-a` 为 USD,可选 `averageCpcCNY` 汇率参考;有 `-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
|
@@ -65,7 +65,7 @@ for ag in camp.get("AdGroupsForBatchJob", []):
|
|
|
65
65
|
```bash
|
|
66
66
|
siluzan-tso ad campaign-validate --config-file ./campaign.json
|
|
67
67
|
# 用户确认方案后:
|
|
68
|
-
siluzan-tso ad campaign-create --config-file ./campaign.json --
|
|
68
|
+
siluzan-tso ad campaign-create --config-file ./campaign.json --commit '<campaign create description>'
|
|
69
69
|
siluzan-tso ad batch get --id <taskId> --config-file ./campaign.json --json-out ./snap-campaign
|
|
70
70
|
siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --json-out ./snap-campaign
|
|
71
71
|
```
|
|
@@ -218,7 +218,7 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --js
|
|
|
218
218
|
| `DestinationUrl` | string | ✅ | 展示/编辑用落地页 URL |
|
|
219
219
|
| `Finalurl` | string | ✅ | **后端 BatchJob 必填**;与 `DestinationUrl` 填**相同** URL。勿只写前者——validate 可能仍通过,create 会失败 |
|
|
220
220
|
| `AdTitle` | null \| string | | 可选;无标题时写 `null`(与 Web 智投一致) |
|
|
221
|
-
| `Path1` / `Path2` | string |
|
|
221
|
+
| `Path1` / `Path2` | string | ✅ | 显示路径(**必填**,缺/null 会导致后端 BatchJob `ArgumentNullException`);**≤ 15 字符**(CJK 按 2 计);小写 a-z、数字、连字符 |
|
|
222
222
|
| `headlinePart1/2/3` | string | ✅ | 前 3 条标题;**每条 ≤ 30 字符**(CJK 按 2 计) |
|
|
223
223
|
| `AddtionalHeadlines` | string[] | | 第 4–15 条标题(合计 ≤ 15) |
|
|
224
224
|
| `adDescription` / `adDescription2` | string | ✅ | 前 2 条描述;**每条 ≤ 90 字符** |
|
|
@@ -232,7 +232,7 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --js
|
|
|
232
232
|
| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
|
|
233
233
|
| `CampaignCommandController.CreateCampaignAsync` | `customerName` 非空 / `campaign` 非空 |
|
|
234
234
|
| `CampaignCommandController` 行 94–106 | `campaign.TargetPartnerSearchNetwork` 必须 false;`!TargetGoogleSearch && TargetSearchNetwork` 拒绝 |
|
|
235
|
-
| Google Ads BatchJob | RSA
|
|
235
|
+
| Google Ads BatchJob | RSA `Path1`/`Path2` 必填(缺/null → 后端 ArgumentNullException);字段数与字符上限;关键词词面非空;SITELINK `Line2`/`Line3` ≤25 字且不可 null |
|
|
236
236
|
| CLI 实务 | `Budget > 0`、地理/语言至少 1 项、日期格式与先后、出价策略与配套字段 |
|
|
237
237
|
|
|
238
238
|
`ad campaign-validate` 通过不保证 BatchJob 成功(例如仅写 `DestinationUrl` 未写 `Finalurl` 时 validate 仍可能 ✅)。异步结果用 `ad batch get` 轮询;`HasFailed` / 部分失败时用 `ad batch diff` 对照 JSON 补缺,系列级失败时改 JSON 重提,勿在半成品上反复整包创建。写操作须 `--commit`,见 `references/google-ads.md` § ad campaign-create。
|
|
@@ -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` | `
|
|
86
|
+
| `keyword suggest --json` | `averageCpc`、`lowTopOfPageBid`、`highTopOfPageBid`;`bidAmountCurrency`(有 `-a` 为账户币;无 `-a` 为 USD,可选 `averageCpcCNY` 参考) |
|
|
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`
|
|
49
|
+
- **CLI 出口的大多数 JSON / 表格金额以「元」为单位**:`budget`、`*Yuan` 后缀(`budgetAmountYuan`、`maxCPCAmountYuan` 等)、`spend` / `averageCpc` / `costPerConversion` 等。**`keyword suggest`**:`averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid` 的币种见根级与每条 **`bidAmountCurrency`**(传 `-a` 时为账户 `currencyCode`;无 `-a` 时为 **USD**,可选 `averageCpcCNY` 为汇率参考、非 Google 账户口径)。
|
|
50
50
|
- **写 CLI 参数**(`--budget`、`--max-cpc`、`--target-cpa`、`--amount` 等):同样传**主币种元**,与账户 `currencyCode` 一致;CLI 内部按需 ×100 / ×1_000_000 写后端。
|
|
51
51
|
- 旧版网关字段(`budgetAmount` 分、`*Micros` 微元、`maxCPCAmountDisplay` 等)**已不再落盘到 CLI 输出**,下游脚本无需做单位换算。
|
|
52
52
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **拓词命令**读本文;**建户 JSON + validate + create** 读 `references/google-ads-campaign-plan.md`。关键词数量规则:`google-ads-rules/google-ads-keyword-taxonomy.md`。
|
|
4
4
|
>
|
|
5
|
-
> **数据口径**:`siluzan-tso keyword`
|
|
5
|
+
> **数据口径**:`siluzan-tso keyword` 默认走 `keywordidea/google`(共享 MCC,出价 **USD**);传 **`-a <mediaCustomerId>`** 时走 `keywordrecommendation/recommend/{id}/google`,出价币种为 `list-accounts` 的 **`currencyCode`**(如 CNY)。可选 `--url` 叠加 **网址拓词**(`websitereco`)。与 **`google-analysis` 投放表现**不是同一套数据。
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
| 来源 | 典型做法 | 是否含 `montlySearch` / CPC 等 Google 指标 |
|
|
15
15
|
| ------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
16
16
|
| **A. 宿主联网搜索** | 助手用 WebSearch / 公开网页归纳行业词、竞品词、长尾变体,再写入词包 | ❌ 无,须再调 `keyword` 补指标或单独标注「无市场数据」 |
|
|
17
|
-
| **B. Google API** | `siluzan-tso keyword -k "种子,..."
|
|
17
|
+
| **B. Google API** | `siluzan-tso keyword -k "种子,..."`(无 `-a`:`keywordidea/google`/USD;有 `-a`:账户接口/账户币;可选 `--geo`) | ✅ 有 |
|
|
18
18
|
| **C. 网址拓词** | `keyword` 带 `--url`(`websitereco` 轮询,与 Web `/tool/keyword` 填网址分支一致) | 通常仅词面,**无**完整 Planner 指标 |
|
|
19
19
|
|
|
20
20
|
### 分支 A — 混合拓词
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
1. (可选)宿主 **联网搜索** 或 `rag query` 归纳 **2–8 个英文种子词**(RAG 见 §0,联网搜索勿与 Google 指标混写在同一列)。
|
|
25
25
|
2. `siluzan-tso keyword -k "种子1,种子2,..." [--url "<落地页或竞品站>"] --json-out ./snap-kw`
|
|
26
|
-
3. 对落盘 `items`:以 **`keyword` 返回行为准**(有 `montlySearch` / `
|
|
26
|
+
3. 对落盘 `items`:以 **`keyword` 返回行为准**(有 `montlySearch` / `averageCpc` 等,币种见 `bidAmountCurrency`);联网搜索得到的词若无对应行,可并入词包但须标注 **「无 Google 市场数据」**,或丢弃。
|
|
27
27
|
|
|
28
28
|
### 分支 B — 仅 Google 市场数据(默认)
|
|
29
29
|
|
|
@@ -179,7 +179,7 @@ siluzan-tso keyword -k "structural adhesive,SG-200,curtain wall bonding" --url "
|
|
|
179
179
|
### 5)高商业意图粗筛(出价 + 竞争度 + 搜索量)
|
|
180
180
|
|
|
181
181
|
1. `siluzan-tso keyword -k "..." --json-out ./snap-kw`
|
|
182
|
-
2. 对落盘 `items` 按 `
|
|
182
|
+
2. 对落盘 `items` 按 `averageCpc`(CLI 出口已 ÷1,000,000,货币见 **`bidAmountCurrency`**)、`competition` / `competitionV2`、`montlySearch` 综合排序截断;此外 `lowTopOfPageBid` / `highTopOfPageBid`(页首出价 20/80 分位,与 `bidAmountCurrency` 一致)可用于评估合理出价区间。
|
|
183
183
|
|
|
184
184
|
### 6)词包 → campaign-create JSON
|
|
185
185
|
|
|
@@ -197,12 +197,14 @@ siluzan-tso keyword -k "structural adhesive,SG-200,curtain wall bonding" --url "
|
|
|
197
197
|
## 单命令速查
|
|
198
198
|
|
|
199
199
|
```bash
|
|
200
|
-
siluzan-tso keyword -k "<必填,逗号分隔多词>" [--geo <id[,id...]>] [--url <url>] [--google-only] [--include <words>] [--exclude <words>] [--json] [--json-out <dir>] [--verbose]
|
|
200
|
+
siluzan-tso keyword -k "<必填,逗号分隔多词>" [-a <mediaCustomerId>] [--geo <id[,id...]>] [--url <url>] [--google-only] [--include <words>] [--exclude <words>] [--json] [--json-out <dir>] [--verbose]
|
|
201
201
|
siluzan-tso keyword geo-list [--country-code <codes>] [--name-contains <text>] [--json] [--json-out <dir>]
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
`-a`:走 `keywordrecommendation/recommend/{id}/google`;先 `list-accounts -m Google -k <id>` 确认账户与 **`currencyCode`**;出价金额字段为 `averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid`(**账户币种「元」**,非汇率换算)。
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
`--google-only`:只调 Google 推荐主接口(有 `-a` 为账户接口,无 `-a` 为 `keywordidea/google`),不叠加 `--url` 的网址拓词;**分支 B(仅 Google)必加**。
|
|
207
|
+
|
|
208
|
+
**返回字段**(与后端 `Samm.Core.Service.KeywordRecommendation` 对齐):根级与每条 **`bidAmountCurrency`**(无账户=`USD`;有账户=`list-accounts` 的 `currencyCode`);`averageCpc` / `lowTopOfPageBid` / `highTopOfPageBid`(微元 ÷1,000,000,**与 `bidAmountCurrency` 一致**)。无账户 USD 时可选 `usdToCnyExchangeRate` 与 `averageCpcCNY` 等**参考**字段(非 Google 账户币种口径)。另有 `keyword` / `montlySearch` / `competition` / `competitionV2` / `source` 等。
|
|
207
209
|
|
|
208
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.21'
|
|
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.21"
|
|
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"
|