siluzan-tso-cli 1.1.37-beta.2 → 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 +178 -33
- 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/assets/pmax-lead-form-template.md +5 -5
- package/dist/skill/references/accounts/accounts.md +3 -1
- package/dist/skill/references/core/agent-conventions.md +5 -2
- package/dist/skill/references/core/tips.md +4 -3
- package/dist/skill/references/core/user-communication-guide.md +7 -4
- package/dist/skill/references/google-ads/google-ads.md +21 -5
- package/dist/skill/references/google-ads/rules/google-ads-keyword-strategy.md +2 -2
- package/dist/skill/references/operations/hosted-automation-self-control.md +14 -9
- package/dist/skill/references/operations/hosted-automation-user-catalog.md +2 -0
- package/dist/skill/scripts/install.ps1 +1 -1
- package/dist/skill/scripts/install.sh +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
|
|
|
51
51
|
siluzan-tso init --force # 强制覆盖已存在文件
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
> **注意**:当前为测试版(1.1.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",
|
|
@@ -113039,10 +113128,14 @@ var VALID_LEAD_FORM_INPUT_TYPES = [
|
|
|
113039
113128
|
"POSTAL_CODE",
|
|
113040
113129
|
"STREET_ADDRESS"
|
|
113041
113130
|
];
|
|
113131
|
+
var LEAD_FORM_HEADLINE_MAX_LEN = 30;
|
|
113132
|
+
var LEAD_FORM_DESCRIPTION_MAX_LEN = 200;
|
|
113133
|
+
var LEAD_FORM_BUSINESS_NAME_MAX_LEN = 25;
|
|
113134
|
+
var LEAD_FORM_CTA_DESCRIPTION_MAX_LEN = 30;
|
|
113042
113135
|
function loadLeadFormExtensionConfig(configFile) {
|
|
113043
113136
|
let raw;
|
|
113044
113137
|
try {
|
|
113045
|
-
raw = JSON.parse(
|
|
113138
|
+
raw = JSON.parse(readFileSync6(configFile, "utf8"));
|
|
113046
113139
|
} catch {
|
|
113047
113140
|
console.error(`
|
|
113048
113141
|
\u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${configFile}\uFF09
|
|
@@ -113066,10 +113159,30 @@ function validateLeadFormPayload(lf, prefix = "leadForm") {
|
|
|
113066
113159
|
return errors;
|
|
113067
113160
|
}
|
|
113068
113161
|
if (!lf.businessName?.trim()) errors.push(`${prefix}.businessName \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
113162
|
+
else if (lf.businessName.trim().length > LEAD_FORM_BUSINESS_NAME_MAX_LEN) {
|
|
113163
|
+
errors.push(
|
|
113164
|
+
`${prefix}.businessName \u8D85\u957F\uFF08${lf.businessName.trim().length}/${LEAD_FORM_BUSINESS_NAME_MAX_LEN}\uFF09\uFF1A\u300C${lf.businessName.trim()}\u300D`
|
|
113165
|
+
);
|
|
113166
|
+
}
|
|
113069
113167
|
if (!lf.headline?.trim()) errors.push(`${prefix}.headline \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
113168
|
+
else if (lf.headline.trim().length > LEAD_FORM_HEADLINE_MAX_LEN) {
|
|
113169
|
+
errors.push(
|
|
113170
|
+
`${prefix}.headline \u8D85\u957F\uFF08${lf.headline.trim().length}/${LEAD_FORM_HEADLINE_MAX_LEN}\uFF09\uFF1A\u300C${lf.headline.trim()}\u300D`
|
|
113171
|
+
);
|
|
113172
|
+
}
|
|
113070
113173
|
if (!lf.description?.trim()) errors.push(`${prefix}.description \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
113174
|
+
else if (lf.description.trim().length > LEAD_FORM_DESCRIPTION_MAX_LEN) {
|
|
113175
|
+
errors.push(
|
|
113176
|
+
`${prefix}.description \u8D85\u957F\uFF08${lf.description.trim().length}/${LEAD_FORM_DESCRIPTION_MAX_LEN}\uFF09`
|
|
113177
|
+
);
|
|
113178
|
+
}
|
|
113071
113179
|
if (!lf.privacyPolicyUrl?.trim()) errors.push(`${prefix}.privacyPolicyUrl \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
113072
113180
|
if (!lf.finalUrl?.trim()) errors.push(`${prefix}.finalUrl \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
113181
|
+
if (lf.callToActionDescription?.trim() && lf.callToActionDescription.trim().length > LEAD_FORM_CTA_DESCRIPTION_MAX_LEN) {
|
|
113182
|
+
errors.push(
|
|
113183
|
+
`${prefix}.callToActionDescription \u8D85\u957F\uFF08${lf.callToActionDescription.trim().length}/${LEAD_FORM_CTA_DESCRIPTION_MAX_LEN}\uFF09`
|
|
113184
|
+
);
|
|
113185
|
+
}
|
|
113073
113186
|
if (!Array.isArray(lf.fields) || lf.fields.length === 0) {
|
|
113074
113187
|
errors.push(`${prefix}.fields \u81F3\u5C11 1 \u9879`);
|
|
113075
113188
|
} else {
|
|
@@ -113134,7 +113247,7 @@ function buildLeadFormExtensionBody(cfg, extensionId) {
|
|
|
113134
113247
|
}
|
|
113135
113248
|
|
|
113136
113249
|
// src/commands/ad/extension-whatsapp.ts
|
|
113137
|
-
import { readFileSync as
|
|
113250
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
113138
113251
|
var VALID_WHATSAPP_CTA_SELECTIONS = [
|
|
113139
113252
|
"CONTACT_US",
|
|
113140
113253
|
"LEARN_MORE",
|
|
@@ -113146,7 +113259,7 @@ var VALID_WHATSAPP_CTA_SELECTIONS = [
|
|
|
113146
113259
|
function loadWhatsappExtensionConfig(configFile) {
|
|
113147
113260
|
let raw;
|
|
113148
113261
|
try {
|
|
113149
|
-
raw = JSON.parse(
|
|
113262
|
+
raw = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
113150
113263
|
} catch {
|
|
113151
113264
|
console.error(`
|
|
113152
113265
|
\u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${configFile}\uFF09
|
|
@@ -113608,7 +113721,7 @@ async function runAdExtensionUpdate(opts) {
|
|
|
113608
113721
|
const googleApiUrl = requireGoogleApi(config);
|
|
113609
113722
|
let raw;
|
|
113610
113723
|
try {
|
|
113611
|
-
raw = stripMetaKeys(JSON.parse(
|
|
113724
|
+
raw = stripMetaKeys(JSON.parse(readFileSync8(opts.configFile, "utf8")));
|
|
113612
113725
|
} catch {
|
|
113613
113726
|
console.error(`
|
|
113614
113727
|
\u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${opts.configFile}\uFF09
|
|
@@ -115802,7 +115915,7 @@ async function runPmaxImageValidation(configFile, cfg) {
|
|
|
115802
115915
|
// src/commands/ad/pmax-image-resolve.ts
|
|
115803
115916
|
init_auth();
|
|
115804
115917
|
import { basename as basename4, dirname as dirname8, isAbsolute as isAbsolute3, resolve as resolve7 } from "path";
|
|
115805
|
-
import { readFileSync as
|
|
115918
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
115806
115919
|
|
|
115807
115920
|
// src/commands/ad/pmax-urls.ts
|
|
115808
115921
|
function pmaxChannelTypesUrl(googleApiUrl) {
|
|
@@ -115965,7 +116078,7 @@ async function uploadPmaxImageFileBase64(config, googleApiUrl, accountId, absIma
|
|
|
115965
116078
|
async function uploadPmaxImageFile(config, googleApiUrl, accountId, absImagePath, name2, verbose) {
|
|
115966
116079
|
const fileName = basename4(absImagePath);
|
|
115967
116080
|
const assetName = (name2 ?? fileName).trim() || fileName;
|
|
115968
|
-
const fileBuffer =
|
|
116081
|
+
const fileBuffer = readFileSync9(absImagePath);
|
|
115969
116082
|
if (fileBuffer.length <= MAX_BASE64_UPLOAD_BYTES) {
|
|
115970
116083
|
return uploadPmaxImageFileBase64(
|
|
115971
116084
|
config,
|
|
@@ -116051,7 +116164,7 @@ function assertPmaxImageSlotsResolved(slots, requiredKinds = ["marketing", "squa
|
|
|
116051
116164
|
}
|
|
116052
116165
|
|
|
116053
116166
|
// src/commands/ad/pmax-load.ts
|
|
116054
|
-
import { readFileSync as
|
|
116167
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
116055
116168
|
|
|
116056
116169
|
// src/commands/ad/pmax-campaign-extensions.ts
|
|
116057
116170
|
init_auth();
|
|
@@ -116555,7 +116668,10 @@ function classifyExtensionAttachError(error) {
|
|
|
116555
116668
|
return `${raw}\uFF08\u591A\u4E3A header \u4E0D\u5728\u5408\u6CD5\u679A\u4E3E\uFF1B\u8FD0\u884C ad extension snippet-headers \u6838\u5BF9\uFF09`;
|
|
116556
116669
|
}
|
|
116557
116670
|
if (/lead form|lead_form|LEAD_FORM/i.test(raw)) {
|
|
116558
|
-
return `${raw}\uFF08\u68C0\u67E5
|
|
116671
|
+
return `${raw}\uFF08\u68C0\u67E5\uFF1Aheadline\u226430\u3001description\u2264200\u3001businessName\u226425\uFF1Bfields \u52FF\u7528 MESSAGE\uFF1BGoogle Ads UI \u662F\u5426\u5DF2\u63A5\u53D7 Lead Form ToS\uFF09`;
|
|
116672
|
+
}
|
|
116673
|
+
if (/^HTTP 400\b/i.test(raw)) {
|
|
116674
|
+
return `${raw}\uFF08\u6269\u5C55\u5B57\u6BB5\u8FDD\u89C4\u5E38\u89C1\uFF1Asnippet header \u975E\u679A\u4E3E\u3001Lead Form \u6587\u6848\u8D85\u957F\u3001URL \u65E0\u6548\uFF1B\u52A0 --verbose \u770B\u7F51\u5173\u539F\u6587\uFF09`;
|
|
116559
116675
|
}
|
|
116560
116676
|
return raw;
|
|
116561
116677
|
}
|
|
@@ -116678,7 +116794,7 @@ function detectDeprecatedPmaxVideoKeys(raw) {
|
|
|
116678
116794
|
function tryLoadPmaxCreateConfig(configFile) {
|
|
116679
116795
|
let raw;
|
|
116680
116796
|
try {
|
|
116681
|
-
raw = JSON.parse(
|
|
116797
|
+
raw = JSON.parse(readFileSync10(configFile, "utf8"));
|
|
116682
116798
|
} catch {
|
|
116683
116799
|
return null;
|
|
116684
116800
|
}
|
|
@@ -116686,7 +116802,7 @@ function tryLoadPmaxCreateConfig(configFile) {
|
|
|
116686
116802
|
}
|
|
116687
116803
|
function loadPmaxCreateConfigRaw(configFile) {
|
|
116688
116804
|
try {
|
|
116689
|
-
const raw = JSON.parse(
|
|
116805
|
+
const raw = JSON.parse(readFileSync10(configFile, "utf8"));
|
|
116690
116806
|
return stripMetaKeys(raw);
|
|
116691
116807
|
} catch {
|
|
116692
116808
|
return null;
|
|
@@ -116737,19 +116853,19 @@ function buildPmaxCreateUrl(googleApiUrl, accountId) {
|
|
|
116737
116853
|
}
|
|
116738
116854
|
|
|
116739
116855
|
// src/commands/ad/pmax-video-upload.ts
|
|
116740
|
-
import { existsSync as existsSync3, readFileSync as
|
|
116856
|
+
import { existsSync as existsSync3, readFileSync as readFileSync12 } from "fs";
|
|
116741
116857
|
import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
|
|
116742
116858
|
|
|
116743
116859
|
// src/commands/ad/pmax-shared.ts
|
|
116744
116860
|
init_auth();
|
|
116745
116861
|
init_cli_json_snapshot();
|
|
116746
|
-
import { readFileSync as
|
|
116862
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
116747
116863
|
import { dirname as dirname9, isAbsolute as isAbsolute4, resolve as resolve8 } from "path";
|
|
116748
116864
|
var PMAX_MONEY_KEYS = /* @__PURE__ */ new Set(["budget", "targetCpa_BidingAmount"]);
|
|
116749
116865
|
function loadPmaxJsonFile(configFile) {
|
|
116750
116866
|
let raw;
|
|
116751
116867
|
try {
|
|
116752
|
-
raw = JSON.parse(
|
|
116868
|
+
raw = JSON.parse(readFileSync11(configFile, "utf8"));
|
|
116753
116869
|
} catch (e) {
|
|
116754
116870
|
const msg = e instanceof Error ? e.message : String(e);
|
|
116755
116871
|
console.error(`
|
|
@@ -116949,7 +117065,7 @@ function runPmaxVideoValidation(configFile, cfg) {
|
|
|
116949
117065
|
return { errors, warnings };
|
|
116950
117066
|
}
|
|
116951
117067
|
try {
|
|
116952
|
-
const buf =
|
|
117068
|
+
const buf = readFileSync12(abs);
|
|
116953
117069
|
if (buf.length === 0) errors.push(`videoPath \u4E3A\u7A7A\u6587\u4EF6\uFF1A${abs}`);
|
|
116954
117070
|
} catch (e) {
|
|
116955
117071
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -117012,7 +117128,7 @@ async function uploadPmaxLocalVideo(opts) {
|
|
|
117012
117128
|
const fileName = basename5(opts.absVideoPath);
|
|
117013
117129
|
const title = (opts.title ?? fileName).trim() || fileName;
|
|
117014
117130
|
const description = (opts.description ?? "Uploaded via siluzan-tso PMax video upload").trim() || "Uploaded via siluzan-tso";
|
|
117015
|
-
const fileBuffer =
|
|
117131
|
+
const fileBuffer = readFileSync12(opts.absVideoPath);
|
|
117016
117132
|
const form = new FormData();
|
|
117017
117133
|
form.append("media_account_id", opts.accountId.replace(/-/g, ""));
|
|
117018
117134
|
form.append("title", title);
|
|
@@ -117070,7 +117186,7 @@ function validatePmaxVideoPathQuick(absPath) {
|
|
|
117070
117186
|
return `\u89C6\u9891\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${absPath}`;
|
|
117071
117187
|
}
|
|
117072
117188
|
try {
|
|
117073
|
-
if (
|
|
117189
|
+
if (readFileSync12(absPath).length === 0) return `\u89C6\u9891\u6587\u4EF6\u4E3A\u7A7A\uFF1A${absPath}`;
|
|
117074
117190
|
} catch (e) {
|
|
117075
117191
|
const msg = e instanceof Error ? e.message : String(e);
|
|
117076
117192
|
return `\u65E0\u6CD5\u8BFB\u53D6\u89C6\u9891\u6587\u4EF6\uFF08${absPath}\uFF09\uFF1A${msg}`;
|
|
@@ -118473,7 +118589,7 @@ async function runAdCampaignValidate(opts) {
|
|
|
118473
118589
|
}
|
|
118474
118590
|
|
|
118475
118591
|
// src/commands/ad/pmax-image-convert.ts
|
|
118476
|
-
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";
|
|
118477
118593
|
import { resolve as resolve10, dirname as dirname11, basename as basename7, extname } from "path";
|
|
118478
118594
|
var SPECS = [
|
|
118479
118595
|
{ kind: "marketing", width: 1200, height: 628, fit: "cover", suffix: "_marketing" },
|
|
@@ -118562,7 +118678,7 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
118562
118678
|
if (opts.updateConfig) {
|
|
118563
118679
|
const configPath = resolve10(opts.updateConfig);
|
|
118564
118680
|
try {
|
|
118565
|
-
const raw = JSON.parse(
|
|
118681
|
+
const raw = JSON.parse(readFileSync13(configPath, "utf8"));
|
|
118566
118682
|
const imgPaths = raw["imagePaths"] ?? {};
|
|
118567
118683
|
if (outputPaths.marketing) imgPaths["marketing"] = outputPaths.marketing;
|
|
118568
118684
|
if (outputPaths.square) imgPaths["square"] = outputPaths.square;
|
|
@@ -118718,6 +118834,17 @@ async function runAdPmaxBrandGuidelinesEnable(opts) {
|
|
|
118718
118834
|
}
|
|
118719
118835
|
|
|
118720
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
|
+
}
|
|
118721
118848
|
function register20(program2) {
|
|
118722
118849
|
const adCmd = program2.command("ad").description(
|
|
118723
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"
|
|
@@ -119002,10 +119129,16 @@ function register20(program2) {
|
|
|
119002
119129
|
});
|
|
119003
119130
|
}
|
|
119004
119131
|
);
|
|
119005
|
-
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(
|
|
119006
119133
|
"--keywords <kw1,kw2,...>",
|
|
119007
|
-
"\u5173\u952E\u8BCD\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694",
|
|
119008
|
-
(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'
|
|
119009
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(
|
|
119010
119143
|
"--json-out <path>",
|
|
119011
119144
|
"\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
|
|
@@ -119020,16 +119153,26 @@ function register20(program2) {
|
|
|
119020
119153
|
campaignId: opts.campaignId,
|
|
119021
119154
|
campaignName: opts.campaignName,
|
|
119022
119155
|
keywords: opts.keywords,
|
|
119156
|
+
keywordsFile: opts.keywordsFile,
|
|
119157
|
+
matchType: parseOptionalMatchType(opts.matchType),
|
|
119023
119158
|
finalUrl: opts.finalUrl,
|
|
119024
119159
|
jsonOut: opts.jsonOut,
|
|
119025
119160
|
verbose: opts.verbose
|
|
119026
119161
|
});
|
|
119027
119162
|
}
|
|
119028
119163
|
);
|
|
119029
|
-
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(
|
|
119030
119167
|
"--keywords <kw1,kw2,...>",
|
|
119031
|
-
"\u5426\u5B9A\
|
|
119032
|
-
(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"
|
|
119033
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(
|
|
119034
119177
|
async (opts) => {
|
|
119035
119178
|
await runAdNegativeKeywordCreate({
|
|
@@ -119040,6 +119183,8 @@ function register20(program2) {
|
|
|
119040
119183
|
adgroupId: opts.adgroupId,
|
|
119041
119184
|
adgroupName: opts.adgroupName,
|
|
119042
119185
|
keywords: opts.keywords,
|
|
119186
|
+
keywordsFile: opts.keywordsFile,
|
|
119187
|
+
matchType: parseOptionalMatchType(opts.matchType),
|
|
119043
119188
|
verbose: opts.verbose
|
|
119044
119189
|
});
|
|
119045
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`)
|
|
@@ -40,14 +40,14 @@ siluzan-tso ad extension update -a <accountId> --id <extensionId> --config-file
|
|
|
40
40
|
| ---------------------------------- | -------- | :--: | -------------------------------------------------------------- |
|
|
41
41
|
| `account` | string | ✅ | Google 媒体客户 ID |
|
|
42
42
|
| `campaignId` | string | ✅ | PMax 活动 ID(`channelTypeV2=PERFORMANCE_MAX`) |
|
|
43
|
-
| `leadForm.businessName` | string | ✅ |
|
|
44
|
-
| `leadForm.headline` | string | ✅ |
|
|
45
|
-
| `leadForm.description` | string | ✅ |
|
|
43
|
+
| `leadForm.businessName` | string | ✅ | 商家名称(≤25 字符) |
|
|
44
|
+
| `leadForm.headline` | string | ✅ | 表单标题(**≤30 字符**,超长网关常直接 HTTP 400) |
|
|
45
|
+
| `leadForm.description` | string | ✅ | 表单描述(≤200 字符) |
|
|
46
46
|
| `leadForm.privacyPolicyUrl` | string | ✅ | 隐私政策 URL |
|
|
47
47
|
| `leadForm.finalUrl` | string | ✅ | 落地页 URL |
|
|
48
48
|
| `leadForm.callToActionType` | string | | 默认 `LEARN_MORE` |
|
|
49
|
-
| `leadForm.callToActionDescription` | string | | 默认 `Contact us
|
|
50
|
-
| `leadForm.fields` | object[] | ✅ | 至少 1 项;`inputType` 如 `FULL_NAME`、`EMAIL`、`PHONE_NUMBER` |
|
|
49
|
+
| `leadForm.callToActionDescription` | string | | 默认 `Contact us`(≤30 字符) |
|
|
50
|
+
| `leadForm.fields` | object[] | ✅ | 至少 1 项;`inputType` 如 `FULL_NAME`、`EMAIL`、`PHONE_NUMBER`(**无** `MESSAGE`) |
|
|
51
51
|
| `leadForm.webhook` | object | | 写入 Google 的 Webhook(非平台接收端) |
|
|
52
52
|
|
|
53
53
|
---
|
|
@@ -203,7 +203,7 @@ siluzan-tso accounts-digest -m Google -a id1,id2 --min-spend 10 \
|
|
|
203
203
|
--start 2026-04-01 --end 2026-04-15 --json-out ./snap-p3
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
**`data.items[]` 主要字段**:`mediaCustomerId`、`name`、`advertiserName`、`currencyCode`、`balance`、`spend
|
|
206
|
+
**`data.items[]` 主要字段**:`mediaCustomerId`、`name`、`advertiserName`、`currencyCode`、`balance`、`spend`(**区间合计**,同 `stats`)、`impressions`、`clicks`、`conversions`、`ctr`(%)、`cpc`、`cpa`。跨币种汇总见 `references/accounts/currency.md`(**禁止**对 `meta.totals` 跨币种直接当最终结论)。
|
|
207
207
|
|
|
208
208
|
---
|
|
209
209
|
|
|
@@ -230,6 +230,8 @@ siluzan-tso stats -m <媒体类型> [选项]
|
|
|
230
230
|
| `--start-date` / `--end-date` | 与 `--start` / `--end` 同义(CLI 别名,与 SKILL Playbook 一致) | — |
|
|
231
231
|
| `--json-out` | 输出原始 JSON;**失败时 stdout 仍为 JSON**(`{"ok":false,"error":"..."}`) | — |
|
|
232
232
|
|
|
233
|
+
**口径(易错)**:返回的 **`spend` = `--start`~`--end` 闭区间合计消耗**,默认近 7 天即约 **7 日之和**,**不是**「一天的消耗」。要日消耗须 `start=end`;要日均用 `spend/天数` 或 `balance-scan` 的 `dailySpend`。**禁止**把默认窗口的 `spend` 当成日消耗去比日预算或写「今天花了 X」。
|
|
234
|
+
|
|
233
235
|
**示例:**
|
|
234
236
|
|
|
235
237
|
```bash
|
|
@@ -194,10 +194,13 @@
|
|
|
194
194
|
|
|
195
195
|
| 任务 | 推荐命令 | 禁止 |
|
|
196
196
|
| --------------------------- | -------------------------------------------------------------------------------------- | -------------------------------- |
|
|
197
|
-
| 多账户余额 / 预算不足预警 | `balance-scan -m <媒体> --threshold-days 7`
|
|
198
|
-
| 多账户投放画像 | `accounts-digest -m <媒体> [-a ...] --start --end --json-out
|
|
197
|
+
| 多账户余额 / 预算不足预警 | `balance-scan -m <媒体> --threshold-days 7`(用 **`dailySpend`** / `remainingDays`) | 逐账户 `balance --accounts ...`;把 `stats` 多日 **`spend` 当「日消耗」** |
|
|
198
|
+
| 多账户投放画像 | `accounts-digest -m <媒体> [-a ...] --start --end --json-out`(**`spend`=区间合计**) | 逐账户 `stats`;把合计 `spend` 说成「每天花了」 |
|
|
199
199
|
| 多账户 × 多维度 Google 数据 | 全量:`google-analysis-batch run`(省略 `-a`);2~10:`google-analysis -a id1,id2,...` | 外层 for-loop;先 list 再拼 `-a` |
|
|
200
200
|
| 多系列诊断 | `ad campaigns --json-out` + node 读文件过滤 | 逐系列 `ad campaign-get` |
|
|
201
|
+
| 单日预算熔断 / 当日花费比日预算 | `ad campaigns --start <当日> --end <当日>`(**start=end**)后再比 `spend` 与 `budget` | 用默认近 7 天窗口的 `spend` 对比日预算 |
|
|
202
|
+
|
|
203
|
+
**消耗口径(自动化必读)**:凡 `--start`/`--end` 读命令的 `spend` = **闭区间合计**,默认近 7 天 ≠ 一天。日均用 `balance-scan.dailySpend` 或 `合计÷天数`;详见 `references/operations/hosted-automation-self-control.md`「检查项常用 JSON 字段」。
|
|
201
204
|
|
|
202
205
|
**`google-analysis-batch` 纪律**(详见 `references/analytics/google-analysis-batch.md`):拉全量时省略 `-a`;中断后**必须** `resume --run-id <id>`,**禁止**重新 `run`;stdout 始终单行 JSON(`kind=siluzan-tso-batch-summary`);退出码 `0` 全成功 / `2` 部分成功 / `3` 全失败或 Token 失效 / `4` 用法错误;401 → 整批终止,按 `references/core/setup.md` 重登录后 `resume`。
|
|
203
206
|
|
|
@@ -93,16 +93,17 @@ low.forEach((a) => console.log(' ', a.mediaCustomerId, a.name, '余额:', a.rema
|
|
|
93
93
|
|
|
94
94
|
### 3. 汇总消耗数据
|
|
95
95
|
|
|
96
|
-
`stats` 的 `AccountOverviewItem` 使用 **`spend`**(非 `cost
|
|
96
|
+
`stats` 的 `AccountOverviewItem` 使用 **`spend`**(非 `cost`)。**`spend` = `--start`~`--end` 区间合计**(默认近 7 天 ≈ 7 日之和,**不是**「一天的消耗」):
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
|
-
siluzan-tso stats -m Google --
|
|
99
|
+
siluzan-tso stats -m Google -a <id> --start 2026-07-06 --end 2026-07-12 --json-out ./snap-cli
|
|
100
100
|
node -e "
|
|
101
101
|
const d = require('./snap-cli/stats-google.json');
|
|
102
102
|
const list = d.items || [];
|
|
103
|
+
// 「总消耗」= 区间合计;日均请 total/天数,或 balance-scan.dailySpend;勿说成每天花了 total
|
|
103
104
|
const total = list.reduce((s, a) => s + Number(a.spend ?? 0), 0);
|
|
104
105
|
const clicks = list.reduce((s, a) => s + Number(a.clicks ?? 0), 0);
|
|
105
|
-
console.log('
|
|
106
|
+
console.log('区间总消耗:', total.toFixed(2), ' 总点击:', clicks);
|
|
106
107
|
list
|
|
107
108
|
.slice()
|
|
108
109
|
.sort((a, b) => Number(b.spend) - Number(a.spend))
|
|
@@ -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` 脚本;本文件只约束**表述方式**,不放宽数据真实性要求。
|
|
@@ -97,7 +97,9 @@
|
|
|
97
97
|
siluzan-tso ad campaigns -a <accountId> [--start <YYYY-MM-DD>] [--end <YYYY-MM-DD>] [--json-out ./snap]
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
落盘 JSON 中
|
|
100
|
+
落盘 JSON 中 **`budget` 为日预算**(主币种「元」,CLI 已 ÷100),另有 `statusDisplay`(状态文案)。
|
|
101
|
+
带 `--start` / `--end` 时,同行的 **`spend` / `impressions` / `clicks` / `conversions` 为该闭区间合计**(**不是**日消耗)。
|
|
102
|
+
与日预算比较「是否超预算」时,**必须** `start=end=统计日`;多日窗口须先 ÷ 天数或改用 `balance-scan.dailySpend` / `overview.averageDailyCost` 再谈「日均」。
|
|
101
103
|
|
|
102
104
|
### 启停
|
|
103
105
|
|
|
@@ -266,26 +268,38 @@ siluzan-tso ad keyword-create \
|
|
|
266
268
|
-a <accountId> \
|
|
267
269
|
--adgroup-id <adGroupId> --adgroup-name <adGroupName> \
|
|
268
270
|
--campaign-id <campaignId> --campaign-name <campaignName> \
|
|
269
|
-
--keywords "词1,词2
|
|
271
|
+
--keywords "词1,词2,多词短语" [--final-url <url>] \
|
|
272
|
+
[--match-type Broad|Phrase|Exact] [--keywords-file ./kws.json] \
|
|
270
273
|
[--json-out <path>]
|
|
271
274
|
```
|
|
272
275
|
|
|
273
|
-
`--json-out` 落盘 `{ request: { adgroupId, count }, response: ... }`,批量脚本可直接据此核对成功量。
|
|
276
|
+
`--keywords` **支持空格多词**(逗号才是分隔符)。`--json-out` 落盘 `{ request: { adgroupId, count }, response: ... }`,批量脚本可直接据此核对成功量。
|
|
274
277
|
|
|
275
278
|
### 否定关键词
|
|
276
279
|
|
|
280
|
+
**支持空格多词短语**(如 `how to make`、`home cooking`)。匹配类型与 Web 一致:裸词=广泛,`"短语"`=词组,`[词]`=完全;也可用 `--match-type`。多词/带引号时推荐 `--keywords-file`,避免 shell 转义。
|
|
281
|
+
|
|
277
282
|
```bash
|
|
278
283
|
# 添加(默认系列层级;传 --adgroup-id 则为组层级)
|
|
284
|
+
# 多词短语直接写空格,逗号分隔多条——CLI 支持,无需改后台手工补
|
|
279
285
|
siluzan-tso ad keyword-negative-create \
|
|
280
286
|
-a <accountId> \
|
|
281
287
|
--campaign-id <campaignId> --campaign-name <campaignName> \
|
|
282
|
-
--keywords "
|
|
283
|
-
|
|
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
|
|
284
296
|
|
|
285
297
|
# 删除(先用 ad keywords --negative --json-out ./snap 获取 id)
|
|
286
298
|
siluzan-tso ad keyword-negative-delete -a <accountId> --id <negativeKeywordId>
|
|
287
299
|
```
|
|
288
300
|
|
|
301
|
+
> **禁止**因「多词」就让用户去 Google Ads 后台手工补否词——那是 Agent 误判;用上面命令即可。
|
|
302
|
+
|
|
289
303
|
---
|
|
290
304
|
|
|
291
305
|
## ad batch — 异步批量创建记录
|
|
@@ -403,6 +417,8 @@ BatchJob 中 `ExtensionsForBatchJob`(尤其 **Sitelink**)失败率较高,
|
|
|
403
417
|
|
|
404
418
|
**关键词匹配:** 写在 `KeywordsForBatchJob` 块内;同一块同匹配类型,`MatchTypeV2` 与 `KeywordText` 词面格式必须对齐(`PHRASE` 用 `"词"`、`EXACT` 用 `[词]`、`BROAD` 直写)。
|
|
405
419
|
|
|
420
|
+
**多词短语:** `KeywordText` / 否词 `NegativeKeywordsForBatchJob[].KeywordText` 是 **JSON 字符串数组**,**空格多词合法**(如 `"how to make"`)。与 `keyword-negative-create` 不同,这里**没有**逗号 CLI 解析问题——**禁止**因「多词」省略否词或让用户去后台手补。
|
|
421
|
+
|
|
406
422
|
广告组/关键词/创意的分步创建仍用 `adgroup-create`、`keyword-create`、`ad-create`。
|
|
407
423
|
|
|
408
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 |
|
|
@@ -26,11 +26,15 @@
|
|
|
26
26
|
|
|
27
27
|
| 检查意图 | 可关注的 JSON 字段(键名以实际 stdout 为准) |
|
|
28
28
|
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
29
|
+
| **区间合计费用** | 系列/组/创意列表中的 **`spend`** = **`--start`~`--end` 整段合计**(**不是**「一天的消耗」;窗口 7 天则约为 7 日之和) |
|
|
30
|
+
| **日预算** | **`ad campaigns --json-out ./snap`** 的 **`budget`**(CLI 已为元)= **每日预算上限**,与 `spend` **量纲不同** |
|
|
31
|
+
| 日预算与「当日」费用比 | **仅当** `--start` 与 `--end` 为**同一统计日**时,才可用 `spend` 对比 `budget`;**禁止**用多日 `spend` 去除以或对比日预算(会把周/月合计误当成「一天花了这么多」) |
|
|
32
|
+
| 转化次数 | **`conversions`**(窗口内合计) |
|
|
33
|
+
| 实际 CPA | 宿主侧 **`spend / conversions`**(同窗口合计;`conversions` 为 0 时不做 CPA 判断) |
|
|
33
34
|
| 目标 CPA(系列 / 组) | 系列列表 JSON 中的 **`targetCpa_BidingAmount`** 等;组列表 **`ad groups --json-out ./snap`** 中的 **`targetCpaAmount`**(写入口径见 `references/google-ads/google-ads.md`「广告组编辑」) |
|
|
35
|
+
| 日均消耗(续航用) | 用 `balance-scan` 的 **`dailySpend`**(近 7 日合计/7),或自算 `spend / 活跃天数`;**禁止**把多日 `spend` 直接当「日消耗」 |
|
|
36
|
+
|
|
37
|
+
> **硬约束(自动化 / 预警必读)**:凡带 `--start` / `--end` 的读命令,返回的 `spend` / `impressions` / `clicks` / `conversions` 均为**该闭区间合计**。要「一天的消耗」必须 `start=end=该日`,或显式 `合计 ÷ 天数`;要「日均」用 `balance-scan.dailySpend` / `overview.averageDailyCost`。
|
|
34
38
|
|
|
35
39
|
若某条检查所需字段在**当前**落盘 JSON 中不存在,**禁止猜测**:先换用 `references/analytics/account-analytics.md` 中其它子命令拉数,或与维护方确认是否需扩展 `siluzan-tso`;宿主侧不得编造字段。
|
|
36
40
|
|
|
@@ -38,21 +42,22 @@
|
|
|
38
42
|
|
|
39
43
|
## 场景 1:单日预算熔断(Campaign)
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
**业务目标**:当**当日**系列花费相对**日预算**达到设定比例(如 **≥ 110%**,建议 **110%–120%** 缓冲 API 延迟)时,**将系列状态置为 Paused**;恢复由人工加预算后在网页或 CLI 再启用。
|
|
42
46
|
|
|
43
47
|
### 检查项(宿主每轮执行)
|
|
44
48
|
|
|
45
49
|
1. 解析 `mediaCustomerId`(见上文前置)。
|
|
46
|
-
2.
|
|
50
|
+
2. 拉系列列表(**必须** `--start` = `--end` = **当日**统计日;**禁止**用默认近 7 天窗口做本场景):
|
|
47
51
|
|
|
48
52
|
```bash
|
|
49
|
-
siluzan-tso ad campaigns -a <mediaCustomerId> --start
|
|
53
|
+
siluzan-tso ad campaigns -a <mediaCustomerId> --start <当日YYYY-MM-DD> --end <当日YYYY-MM-DD> --json-out ./snap
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
3. 对 `items[]` 中每条系列(`id` 为系列 ID):
|
|
53
|
-
- 读取 **`spend
|
|
54
|
-
- 计算阈值:`threshold =
|
|
57
|
+
- 读取 **`spend`**(= **仅当日**合计费用)与 **`budget`**(**日预算**,元)。
|
|
58
|
+
- 计算阈值:`threshold = budget * (coefficient / 100)`,系数 `coefficient` 建议 **110–120**(配置项)。
|
|
55
59
|
- **IF** `spend >= threshold` **且**宿主策略允许对该系列熔断(如排除白名单),则进入写操作。
|
|
60
|
+
- **禁止**:`--start`≠`--end` 时仍用 `spend` 对比 `budget`(会把多日合计误判成单日超预算)。
|
|
56
61
|
|
|
57
62
|
### 最终操作
|
|
58
63
|
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
**总索引**:[`references/operations/hosted-automation-scenarios.md`](references/operations/hosted-automation-scenarios.md) · **自控全文**:[`references/operations/hosted-automation-self-control.md`](references/operations/hosted-automation-self-control.md) · **金额硬规范**(展示用 `*Display` 等):[`SKILL.md`](../SKILL.md)「金额与货币单位硬约束」。
|
|
16
16
|
|
|
17
|
+
> **消耗 ≠ 日消耗**:`ad campaigns` / `stats` / `accounts-digest` 的 **`spend` 是 `--start`~`--end` 合计**。单日熔断必须 `start=end=当日`;续航日均用 **`balance-scan.dailySpend`**,禁止把默认近 7 天 `spend` 当成「一天的花费」去比日预算。
|
|
18
|
+
|
|
17
19
|
## 异常监控与报警系统
|
|
18
20
|
|
|
19
21
|
编排(定时、HTTP 探活、通知)在宿主;**读哪些 JSON 字段、用什么命令** 以 **[`references/operations/hosted-automation-monitoring-json.md`](references/operations/hosted-automation-monitoring-json.md)** 为准(键名以当次 `--json-out` 落盘 JSON 为准,勿套用 Google Ads API 官方字段名解析本 CLI 输出)。
|
|
@@ -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"
|