siluzan-tso-cli 1.1.29-beta.1 → 1.1.29-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +598 -21
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/assets/pmax-create-template.json +26 -2
- package/dist/skill/assets/pmax-create-template.md +16 -0
- package/dist/skill/assets/pmax-lead-form-template.json +35 -0
- package/dist/skill/assets/pmax-lead-form-template.md +63 -0
- package/dist/skill/references/core/agent-conventions.md +11 -19
- package/dist/skill/references/core/cli-enums.md +2 -0
- package/dist/skill/references/google-ads/google-ads.md +22 -3
- package/dist/skill/references/google-ads/pmax-api.md +55 -1
- package/dist/skill/scripts/install.ps1 +1 -1
- package/dist/skill/scripts/install.sh +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
|
|
|
51
51
|
siluzan-tso init --force # 强制覆盖已存在文件
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
> **注意**:当前为测试版(1.1.29-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.29-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -112493,6 +112493,120 @@ async function runAdEdit(opts) {
|
|
|
112493
112493
|
init_auth();
|
|
112494
112494
|
init_cli_json_snapshot();
|
|
112495
112495
|
init_strip_legacy_google_fields();
|
|
112496
|
+
|
|
112497
|
+
// src/commands/ad/extension-lead-form.ts
|
|
112498
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
112499
|
+
var VALID_LEAD_FORM_INPUT_TYPES = [
|
|
112500
|
+
"FULL_NAME",
|
|
112501
|
+
"FIRST_NAME",
|
|
112502
|
+
"LAST_NAME",
|
|
112503
|
+
"EMAIL",
|
|
112504
|
+
"PHONE_NUMBER",
|
|
112505
|
+
"COMPANY_NAME",
|
|
112506
|
+
"WORK_EMAIL",
|
|
112507
|
+
"WORK_PHONE",
|
|
112508
|
+
"JOB_TITLE",
|
|
112509
|
+
"CITY",
|
|
112510
|
+
"REGION",
|
|
112511
|
+
"COUNTRY",
|
|
112512
|
+
"POSTAL_CODE",
|
|
112513
|
+
"STREET_ADDRESS"
|
|
112514
|
+
];
|
|
112515
|
+
function loadLeadFormExtensionConfig(configFile) {
|
|
112516
|
+
let raw;
|
|
112517
|
+
try {
|
|
112518
|
+
raw = JSON.parse(readFileSync5(configFile, "utf8"));
|
|
112519
|
+
} catch {
|
|
112520
|
+
console.error(`
|
|
112521
|
+
\u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${configFile}\uFF09
|
|
112522
|
+
`);
|
|
112523
|
+
process.exit(1);
|
|
112524
|
+
}
|
|
112525
|
+
const cfg = stripMetaKeys(raw);
|
|
112526
|
+
const result = validateLeadFormExtensionConfig(cfg);
|
|
112527
|
+
if (!result.ok) {
|
|
112528
|
+
console.error("\n\u274C Lead Form \u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A\n");
|
|
112529
|
+
for (const e of result.errors) console.error(` - ${e}`);
|
|
112530
|
+
console.error();
|
|
112531
|
+
process.exit(1);
|
|
112532
|
+
}
|
|
112533
|
+
return cfg;
|
|
112534
|
+
}
|
|
112535
|
+
function validateLeadFormPayload(lf, prefix = "leadForm") {
|
|
112536
|
+
const errors = [];
|
|
112537
|
+
if (!lf || typeof lf !== "object") {
|
|
112538
|
+
errors.push(`${prefix} \u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A`);
|
|
112539
|
+
return errors;
|
|
112540
|
+
}
|
|
112541
|
+
if (!lf.businessName?.trim()) errors.push(`${prefix}.businessName \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
112542
|
+
if (!lf.headline?.trim()) errors.push(`${prefix}.headline \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
112543
|
+
if (!lf.description?.trim()) errors.push(`${prefix}.description \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
112544
|
+
if (!lf.privacyPolicyUrl?.trim()) errors.push(`${prefix}.privacyPolicyUrl \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
112545
|
+
if (!lf.finalUrl?.trim()) errors.push(`${prefix}.finalUrl \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
112546
|
+
if (!Array.isArray(lf.fields) || lf.fields.length === 0) {
|
|
112547
|
+
errors.push(`${prefix}.fields \u81F3\u5C11 1 \u9879`);
|
|
112548
|
+
} else {
|
|
112549
|
+
for (let i = 0; i < lf.fields.length; i++) {
|
|
112550
|
+
const f = lf.fields[i];
|
|
112551
|
+
const t = f?.inputType?.trim();
|
|
112552
|
+
if (!t) {
|
|
112553
|
+
errors.push(`${prefix}.fields[${i}].inputType \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
112554
|
+
continue;
|
|
112555
|
+
}
|
|
112556
|
+
if (!VALID_LEAD_FORM_INPUT_TYPES.includes(t)) {
|
|
112557
|
+
errors.push(
|
|
112558
|
+
`${prefix}.fields[${i}].inputType \u65E0\u6548\uFF1A${t}\uFF1B\u652F\u6301 ${VALID_LEAD_FORM_INPUT_TYPES.join(", ")}`
|
|
112559
|
+
);
|
|
112560
|
+
}
|
|
112561
|
+
}
|
|
112562
|
+
}
|
|
112563
|
+
return errors;
|
|
112564
|
+
}
|
|
112565
|
+
function validateLeadFormExtensionConfig(cfg) {
|
|
112566
|
+
const errors = [];
|
|
112567
|
+
if (!cfg.account?.trim()) errors.push("account\uFF08\u5A92\u4F53\u5BA2\u6237 ID\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
|
|
112568
|
+
if (!cfg.campaignId?.trim()) errors.push("campaignId\uFF08PMax \u6D3B\u52A8 ID\uFF09\u4E0D\u80FD\u4E3A\u7A7A");
|
|
112569
|
+
errors.push(...validateLeadFormPayload(cfg.leadForm, "leadForm"));
|
|
112570
|
+
return { ok: errors.length === 0, errors };
|
|
112571
|
+
}
|
|
112572
|
+
function buildLeadFormExtensionBody(cfg, extensionId) {
|
|
112573
|
+
const lf = cfg.leadForm;
|
|
112574
|
+
const body = {
|
|
112575
|
+
activeuseridg: cfg.account,
|
|
112576
|
+
campaignId: cfg.campaignId,
|
|
112577
|
+
level: "Campaign",
|
|
112578
|
+
typeV2: "LEAD_FORM",
|
|
112579
|
+
assetFieldType: "LEAD_FORM",
|
|
112580
|
+
leadForm: {
|
|
112581
|
+
businessName: lf.businessName.trim(),
|
|
112582
|
+
headline: lf.headline.trim(),
|
|
112583
|
+
description: lf.description.trim(),
|
|
112584
|
+
privacyPolicyUrl: lf.privacyPolicyUrl.trim(),
|
|
112585
|
+
finalUrl: lf.finalUrl.trim(),
|
|
112586
|
+
callToActionType: lf.callToActionType?.trim() || "LEARN_MORE",
|
|
112587
|
+
callToActionDescription: lf.callToActionDescription?.trim() || "Contact us",
|
|
112588
|
+
fields: lf.fields.map((f) => ({
|
|
112589
|
+
inputType: f.inputType.trim(),
|
|
112590
|
+
...f.singleChoiceAnswers?.length ? { singleChoiceAnswers: f.singleChoiceAnswers } : {}
|
|
112591
|
+
})),
|
|
112592
|
+
...lf.postSubmitHeadline?.trim() ? { postSubmitHeadline: lf.postSubmitHeadline.trim() } : {},
|
|
112593
|
+
...lf.postSubmitDescription?.trim() ? { postSubmitDescription: lf.postSubmitDescription.trim() } : {},
|
|
112594
|
+
...lf.postSubmitCallToActionType?.trim() ? { postSubmitCallToActionType: lf.postSubmitCallToActionType.trim() } : {},
|
|
112595
|
+
...lf.customDisclosure?.trim() ? { customDisclosure: lf.customDisclosure.trim() } : {},
|
|
112596
|
+
...lf.webhook ? {
|
|
112597
|
+
webhook: {
|
|
112598
|
+
...lf.webhook.advertiserWebhookUrl?.trim() ? { advertiserWebhookUrl: lf.webhook.advertiserWebhookUrl.trim() } : {},
|
|
112599
|
+
...lf.webhook.googleSecret?.trim() ? { googleSecret: lf.webhook.googleSecret.trim() } : {},
|
|
112600
|
+
payloadSchemaVersion: lf.webhook.payloadSchemaVersion ?? 3
|
|
112601
|
+
}
|
|
112602
|
+
} : {}
|
|
112603
|
+
}
|
|
112604
|
+
};
|
|
112605
|
+
if (extensionId) body["id"] = extensionId;
|
|
112606
|
+
return body;
|
|
112607
|
+
}
|
|
112608
|
+
|
|
112609
|
+
// src/commands/ad/extension.ts
|
|
112496
112610
|
async function runAdExtensionList(opts) {
|
|
112497
112611
|
const config = loadConfig(opts.token);
|
|
112498
112612
|
const googleApiUrl = requireGoogleApi(config);
|
|
@@ -112513,6 +112627,10 @@ async function runAdExtensionList(opts) {
|
|
|
112513
112627
|
(i) => String(i["typeV2"] ?? i["assetFieldType"] ?? "").toUpperCase() === filterType
|
|
112514
112628
|
);
|
|
112515
112629
|
}
|
|
112630
|
+
if (opts.campaignId) {
|
|
112631
|
+
const cid = opts.campaignId.trim();
|
|
112632
|
+
items = items.filter((i) => String(i["campaignId"] ?? "") === cid);
|
|
112633
|
+
}
|
|
112516
112634
|
const n = items.length;
|
|
112517
112635
|
const extPayload = stripLegacyGoogleFieldsIfV2Present(
|
|
112518
112636
|
wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items })
|
|
@@ -112550,8 +112668,12 @@ async function runAdExtensionList(opts) {
|
|
|
112550
112668
|
} else if (type === "STRUCTURED_SNIPPET") {
|
|
112551
112669
|
const ssn = item["structuredSnippetHeaderValue"];
|
|
112552
112670
|
if (ssn) detail = `${ssn["key"]}: ${ssn["value"]?.join(", ")}`;
|
|
112671
|
+
} else if (type === "LEAD_FORM") {
|
|
112672
|
+
const lf = item["leadForm"];
|
|
112673
|
+
if (lf) detail = `\u300C${lf["headline"] ?? ""}\u300D business=${lf["businessName"] ?? ""}`;
|
|
112553
112674
|
}
|
|
112554
|
-
|
|
112675
|
+
const camp = item["campaignId"] ? ` campaign:${item["campaignId"]}` : "";
|
|
112676
|
+
console.log(` [${type}] id:${id} level:${level}${camp} ${detail}`);
|
|
112555
112677
|
}
|
|
112556
112678
|
console.log();
|
|
112557
112679
|
}
|
|
@@ -112658,6 +112780,168 @@ async function runAdExtensionDelete(opts) {
|
|
|
112658
112780
|
\u2705 \u9644\u52A0\u4FE1\u606F ${opts.id} \u5DF2\u5220\u9664
|
|
112659
112781
|
`);
|
|
112660
112782
|
}
|
|
112783
|
+
async function runAdExtensionPmaxTypes(opts) {
|
|
112784
|
+
const config = loadConfig(opts.token);
|
|
112785
|
+
const googleApiUrl = requireGoogleApi(config);
|
|
112786
|
+
const url = `${googleApiUrl}/extensionmanagement/pmaxSupportedTypeList`;
|
|
112787
|
+
let data;
|
|
112788
|
+
try {
|
|
112789
|
+
data = await apiFetch2(url, config, {}, opts.verbose);
|
|
112790
|
+
} catch (err) {
|
|
112791
|
+
console.error(`
|
|
112792
|
+
\u274C \u67E5\u8BE2 PMax \u9644\u52A0\u8D44\u4EA7\u7C7B\u578B\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
112793
|
+
`);
|
|
112794
|
+
process.exit(1);
|
|
112795
|
+
}
|
|
112796
|
+
if (await emitCliJsonOrSnapshot(opts, {
|
|
112797
|
+
section: "ad-extension-pmax-types",
|
|
112798
|
+
commandLabel: "ad extension pmax-types",
|
|
112799
|
+
payload: data
|
|
112800
|
+
})) {
|
|
112801
|
+
return;
|
|
112802
|
+
}
|
|
112803
|
+
const obj = data ?? {};
|
|
112804
|
+
const types = obj["supportedTypes"] ?? [];
|
|
112805
|
+
console.log("\nPMax \u652F\u6301\u7684\u9644\u52A0\u8D44\u4EA7\u7C7B\u578B\uFF1A\n");
|
|
112806
|
+
for (const t of types) {
|
|
112807
|
+
console.log(` - ${t["name"] ?? t["typeV2"]} (id=${t["id"]})`);
|
|
112808
|
+
}
|
|
112809
|
+
const levels = obj["supportedLevels"]?.join(", ") ?? "";
|
|
112810
|
+
const lfLevels = obj["leadFormSupportedLevels"]?.join(", ") ?? "";
|
|
112811
|
+
console.log(`
|
|
112812
|
+
\u652F\u6301\u5C42\u7EA7\uFF1A${levels}`);
|
|
112813
|
+
console.log(` Lead Form \u5C42\u7EA7\uFF1A${lfLevels}`);
|
|
112814
|
+
console.log(" \u4E0D\u652F\u6301\uFF1AAd Group\uFF08PMax \u4F1A 400\uFF09");
|
|
112815
|
+
console.log(" WhatsApp\uFF1A\u540E\u7AEF\u5C1A\u672A\u5F00\u653E\uFF08\u540E\u7EED\u9636\u6BB5\uFF09\n");
|
|
112816
|
+
}
|
|
112817
|
+
async function runAdExtensionSnippetHeaders(opts) {
|
|
112818
|
+
const config = loadConfig(opts.token);
|
|
112819
|
+
const googleApiUrl = requireGoogleApi(config);
|
|
112820
|
+
const url = `${googleApiUrl}/extensionmanagement/structuredSnippetHeaders`;
|
|
112821
|
+
let data;
|
|
112822
|
+
try {
|
|
112823
|
+
data = await apiFetch2(url, config, {}, opts.verbose);
|
|
112824
|
+
} catch (err) {
|
|
112825
|
+
console.error(
|
|
112826
|
+
`
|
|
112827
|
+
\u274C \u67E5\u8BE2\u7ED3\u6784\u5316\u6458\u8981\u6807\u5934\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
112828
|
+
`
|
|
112829
|
+
);
|
|
112830
|
+
process.exit(1);
|
|
112831
|
+
}
|
|
112832
|
+
if (await emitCliJsonOrSnapshot(opts, {
|
|
112833
|
+
section: "ad-extension-snippet-headers",
|
|
112834
|
+
commandLabel: "ad extension snippet-headers",
|
|
112835
|
+
payload: data
|
|
112836
|
+
})) {
|
|
112837
|
+
return;
|
|
112838
|
+
}
|
|
112839
|
+
const rows = data ?? [];
|
|
112840
|
+
console.log("\n\u7ED3\u6784\u5316\u6458\u8981\u6807\u5934\uFF08\u6309\u8BED\u8A00\uFF09\uFF1A\n");
|
|
112841
|
+
for (const row of rows) {
|
|
112842
|
+
if (Array.isArray(row)) {
|
|
112843
|
+
console.log(` ${row[0]}: ${row[1].join(", ")}`);
|
|
112844
|
+
} else if (row && typeof row === "object") {
|
|
112845
|
+
const lang = row.Key ?? "Unknown";
|
|
112846
|
+
const vals = row.Value ?? [];
|
|
112847
|
+
console.log(` ${lang}: ${vals.join(", ")}`);
|
|
112848
|
+
}
|
|
112849
|
+
}
|
|
112850
|
+
console.log();
|
|
112851
|
+
}
|
|
112852
|
+
async function runAdExtensionLeadFormCreate(opts) {
|
|
112853
|
+
const config = loadConfig(opts.token);
|
|
112854
|
+
const googleApiUrl = requireGoogleApi(config);
|
|
112855
|
+
let cfg;
|
|
112856
|
+
if (opts.configFile) {
|
|
112857
|
+
cfg = loadLeadFormExtensionConfig(opts.configFile);
|
|
112858
|
+
if (opts.account && opts.account !== cfg.account) {
|
|
112859
|
+
console.error("\n\u274C --account \u4E0E\u914D\u7F6E\u6587\u4EF6 account \u4E0D\u4E00\u81F4\n");
|
|
112860
|
+
process.exit(1);
|
|
112861
|
+
}
|
|
112862
|
+
if (opts.campaignId && opts.campaignId !== cfg.campaignId) {
|
|
112863
|
+
console.error("\n\u274C --campaign-id \u4E0E\u914D\u7F6E\u6587\u4EF6 campaignId \u4E0D\u4E00\u81F4\n");
|
|
112864
|
+
process.exit(1);
|
|
112865
|
+
}
|
|
112866
|
+
} else {
|
|
112867
|
+
console.error(
|
|
112868
|
+
"\n\u274C Lead Form \u987B\u4F7F\u7528 --config-file\uFF08\u6A21\u677F\u89C1 assets/pmax-lead-form-template.json\uFF09\n"
|
|
112869
|
+
);
|
|
112870
|
+
process.exit(1);
|
|
112871
|
+
}
|
|
112872
|
+
const body = buildLeadFormExtensionBody(cfg);
|
|
112873
|
+
const url = `${googleApiUrl}/extensionmanagement/extension/${cfg.account}`;
|
|
112874
|
+
let response;
|
|
112875
|
+
try {
|
|
112876
|
+
response = await apiFetch2(
|
|
112877
|
+
url,
|
|
112878
|
+
config,
|
|
112879
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
112880
|
+
opts.verbose
|
|
112881
|
+
);
|
|
112882
|
+
} catch (err) {
|
|
112883
|
+
console.error(`
|
|
112884
|
+
\u274C \u521B\u5EFA Lead Form \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
112885
|
+
`);
|
|
112886
|
+
process.exit(1);
|
|
112887
|
+
}
|
|
112888
|
+
if (await emitCliJsonOrSnapshot(opts, {
|
|
112889
|
+
section: `ad-extension-lead-form-${cfg.account}`,
|
|
112890
|
+
commandLabel: "ad extension lead-form",
|
|
112891
|
+
commandHint: `${cfg.account}:${cfg.campaignId}`,
|
|
112892
|
+
payload: response,
|
|
112893
|
+
idSuffix: cfg.campaignId
|
|
112894
|
+
})) {
|
|
112895
|
+
return;
|
|
112896
|
+
}
|
|
112897
|
+
const resObj = response ?? {};
|
|
112898
|
+
const extId = resObj["id"];
|
|
112899
|
+
console.log(
|
|
112900
|
+
`
|
|
112901
|
+
\u2705 PMax Lead Form \u5DF2\u521B\u5EFA\uFF08campaignId: ${cfg.campaignId}${extId ? `\uFF0Cid: ${extId}` : ""}\uFF09
|
|
112902
|
+
`
|
|
112903
|
+
);
|
|
112904
|
+
console.log(
|
|
112905
|
+
"\u63D0\u793A\uFF1A\u521B\u5EFA\u6210\u529F\u540E\u540E\u7AEF\u4F1A\u81EA\u52A8\u5C1D\u8BD5\u5F00\u542F SUBMIT_LEAD_FORM + GOOGLE_HOSTED \u8F6C\u5316\u76EE\u6807\uFF08biddable=true\uFF09\u3002\n"
|
|
112906
|
+
);
|
|
112907
|
+
}
|
|
112908
|
+
async function runAdExtensionUpdate(opts) {
|
|
112909
|
+
const config = loadConfig(opts.token);
|
|
112910
|
+
const googleApiUrl = requireGoogleApi(config);
|
|
112911
|
+
const cfg = loadLeadFormExtensionConfig(opts.configFile);
|
|
112912
|
+
if (opts.account !== cfg.account) {
|
|
112913
|
+
console.error("\n\u274C --account \u4E0E\u914D\u7F6E\u6587\u4EF6 account \u4E0D\u4E00\u81F4\n");
|
|
112914
|
+
process.exit(1);
|
|
112915
|
+
}
|
|
112916
|
+
const body = buildLeadFormExtensionBody(cfg, opts.id);
|
|
112917
|
+
const url = `${googleApiUrl}/extensionmanagement/extension/${opts.account}/${opts.id}`;
|
|
112918
|
+
let response;
|
|
112919
|
+
try {
|
|
112920
|
+
response = await apiFetch2(
|
|
112921
|
+
url,
|
|
112922
|
+
config,
|
|
112923
|
+
{ method: "PUT", body: JSON.stringify(body) },
|
|
112924
|
+
opts.verbose
|
|
112925
|
+
);
|
|
112926
|
+
} catch (err) {
|
|
112927
|
+
console.error(`
|
|
112928
|
+
\u274C \u66F4\u65B0\u9644\u52A0\u4FE1\u606F\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
112929
|
+
`);
|
|
112930
|
+
process.exit(1);
|
|
112931
|
+
}
|
|
112932
|
+
if (await emitCliJsonOrSnapshot(opts, {
|
|
112933
|
+
section: `ad-extension-update-${opts.account}`,
|
|
112934
|
+
commandLabel: "ad extension update",
|
|
112935
|
+
commandHint: `${opts.account}:${opts.id}`,
|
|
112936
|
+
payload: response,
|
|
112937
|
+
idSuffix: opts.id
|
|
112938
|
+
})) {
|
|
112939
|
+
return;
|
|
112940
|
+
}
|
|
112941
|
+
console.log(`
|
|
112942
|
+
\u2705 \u9644\u52A0\u4FE1\u606F ${opts.id} \u5DF2\u66F4\u65B0
|
|
112943
|
+
`);
|
|
112944
|
+
}
|
|
112661
112945
|
|
|
112662
112946
|
// src/commands/ad/search-terms.ts
|
|
112663
112947
|
init_auth();
|
|
@@ -114606,7 +114890,7 @@ async function runPmaxImageValidation(configFile, cfg) {
|
|
|
114606
114890
|
// src/commands/ad/pmax-image-resolve.ts
|
|
114607
114891
|
init_auth();
|
|
114608
114892
|
import { basename as basename4, dirname as dirname8, isAbsolute as isAbsolute3, resolve as resolve7 } from "path";
|
|
114609
|
-
import { readFileSync as
|
|
114893
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
114610
114894
|
|
|
114611
114895
|
// src/commands/ad/pmax-urls.ts
|
|
114612
114896
|
function pmaxChannelTypesUrl(googleApiUrl) {
|
|
@@ -114763,7 +115047,7 @@ async function uploadPmaxImageFileBase64(config, googleApiUrl, accountId, absIma
|
|
|
114763
115047
|
async function uploadPmaxImageFile(config, googleApiUrl, accountId, absImagePath, name2, verbose) {
|
|
114764
115048
|
const fileName = basename4(absImagePath);
|
|
114765
115049
|
const assetName = (name2 ?? fileName).trim() || fileName;
|
|
114766
|
-
const fileBuffer =
|
|
115050
|
+
const fileBuffer = readFileSync6(absImagePath);
|
|
114767
115051
|
if (fileBuffer.length <= MAX_BASE64_UPLOAD_BYTES) {
|
|
114768
115052
|
return uploadPmaxImageFileBase64(
|
|
114769
115053
|
config,
|
|
@@ -114849,7 +115133,7 @@ function assertPmaxImageSlotsResolved(slots) {
|
|
|
114849
115133
|
}
|
|
114850
115134
|
|
|
114851
115135
|
// src/commands/ad/pmax-load.ts
|
|
114852
|
-
import { readFileSync as
|
|
115136
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
114853
115137
|
function loadPmaxCreateConfig(configFile) {
|
|
114854
115138
|
const cfg = tryLoadPmaxCreateConfig(configFile);
|
|
114855
115139
|
if (!cfg) {
|
|
@@ -114920,7 +115204,7 @@ function detectDeprecatedPmaxVideoKeys(raw) {
|
|
|
114920
115204
|
function tryLoadPmaxCreateConfig(configFile) {
|
|
114921
115205
|
let raw;
|
|
114922
115206
|
try {
|
|
114923
|
-
raw = JSON.parse(
|
|
115207
|
+
raw = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
114924
115208
|
} catch {
|
|
114925
115209
|
return null;
|
|
114926
115210
|
}
|
|
@@ -114928,7 +115212,7 @@ function tryLoadPmaxCreateConfig(configFile) {
|
|
|
114928
115212
|
}
|
|
114929
115213
|
function loadPmaxCreateConfigRaw(configFile) {
|
|
114930
115214
|
try {
|
|
114931
|
-
const raw = JSON.parse(
|
|
115215
|
+
const raw = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
114932
115216
|
return stripMetaKeys(raw);
|
|
114933
115217
|
} catch {
|
|
114934
115218
|
return null;
|
|
@@ -114976,19 +115260,19 @@ function buildPmaxCreateUrl(googleApiUrl, accountId) {
|
|
|
114976
115260
|
}
|
|
114977
115261
|
|
|
114978
115262
|
// src/commands/ad/pmax-video-upload.ts
|
|
114979
|
-
import { existsSync as existsSync3, readFileSync as
|
|
115263
|
+
import { existsSync as existsSync3, readFileSync as readFileSync9 } from "fs";
|
|
114980
115264
|
import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
|
|
114981
115265
|
|
|
114982
115266
|
// src/commands/ad/pmax-shared.ts
|
|
114983
115267
|
init_auth();
|
|
114984
115268
|
init_cli_json_snapshot();
|
|
114985
|
-
import { readFileSync as
|
|
115269
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
114986
115270
|
import { dirname as dirname9, isAbsolute as isAbsolute4, resolve as resolve8 } from "path";
|
|
114987
115271
|
var PMAX_MONEY_KEYS = /* @__PURE__ */ new Set(["budget", "targetCpa_BidingAmount"]);
|
|
114988
115272
|
function loadPmaxJsonFile(configFile) {
|
|
114989
115273
|
let raw;
|
|
114990
115274
|
try {
|
|
114991
|
-
raw = JSON.parse(
|
|
115275
|
+
raw = JSON.parse(readFileSync8(configFile, "utf8"));
|
|
114992
115276
|
} catch (e) {
|
|
114993
115277
|
const msg = e instanceof Error ? e.message : String(e);
|
|
114994
115278
|
console.error(`
|
|
@@ -115175,7 +115459,7 @@ function runPmaxVideoValidation(configFile, cfg) {
|
|
|
115175
115459
|
return { errors, warnings };
|
|
115176
115460
|
}
|
|
115177
115461
|
try {
|
|
115178
|
-
const buf =
|
|
115462
|
+
const buf = readFileSync9(abs);
|
|
115179
115463
|
if (buf.length === 0) errors.push(`videoPath \u4E3A\u7A7A\u6587\u4EF6\uFF1A${abs}`);
|
|
115180
115464
|
} catch (e) {
|
|
115181
115465
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -115238,7 +115522,7 @@ async function uploadPmaxLocalVideo(opts) {
|
|
|
115238
115522
|
const fileName = basename5(opts.absVideoPath);
|
|
115239
115523
|
const title = (opts.title ?? fileName).trim() || fileName;
|
|
115240
115524
|
const description = (opts.description ?? "Uploaded via siluzan-tso PMax video upload").trim() || "Uploaded via siluzan-tso";
|
|
115241
|
-
const fileBuffer =
|
|
115525
|
+
const fileBuffer = readFileSync9(opts.absVideoPath);
|
|
115242
115526
|
const form = new FormData();
|
|
115243
115527
|
form.append("media_account_id", opts.accountId.replace(/-/g, ""));
|
|
115244
115528
|
form.append("title", title);
|
|
@@ -115294,7 +115578,7 @@ function validatePmaxVideoPathQuick(absPath) {
|
|
|
115294
115578
|
return `\u89C6\u9891\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${absPath}`;
|
|
115295
115579
|
}
|
|
115296
115580
|
try {
|
|
115297
|
-
if (
|
|
115581
|
+
if (readFileSync9(absPath).length === 0) return `\u89C6\u9891\u6587\u4EF6\u4E3A\u7A7A\uFF1A${absPath}`;
|
|
115298
115582
|
} catch (e) {
|
|
115299
115583
|
const msg = e instanceof Error ? e.message : String(e);
|
|
115300
115584
|
return `\u65E0\u6CD5\u8BFB\u53D6\u89C6\u9891\u6587\u4EF6\uFF08${absPath}\uFF09\uFF1A${msg}`;
|
|
@@ -115358,6 +115642,197 @@ async function linkPmaxVideoAfterCreate(opts) {
|
|
|
115358
115642
|
}
|
|
115359
115643
|
}
|
|
115360
115644
|
|
|
115645
|
+
// src/commands/ad/pmax-campaign-extensions.ts
|
|
115646
|
+
init_auth();
|
|
115647
|
+
function validatePmaxCampaignExtensions(ext) {
|
|
115648
|
+
const errors = [];
|
|
115649
|
+
const warnings = [];
|
|
115650
|
+
if (!ext) return { errors, warnings };
|
|
115651
|
+
const callouts = ext.callouts ?? [];
|
|
115652
|
+
for (let i = 0; i < callouts.length; i++) {
|
|
115653
|
+
const text = callouts[i]?.trim();
|
|
115654
|
+
if (!text) {
|
|
115655
|
+
errors.push(`campaignExtensions.callouts[${i}] \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
115656
|
+
continue;
|
|
115657
|
+
}
|
|
115658
|
+
if (text.length > CALLOUT_TEXT_MAX_LEN) {
|
|
115659
|
+
errors.push(
|
|
115660
|
+
`campaignExtensions.callouts[${i}] \u8D85\u8FC7 ${CALLOUT_TEXT_MAX_LEN} \u5B57\u7B26\uFF08\u5F53\u524D ${text.length}\uFF09\uFF1A"${text}"`
|
|
115661
|
+
);
|
|
115662
|
+
}
|
|
115663
|
+
}
|
|
115664
|
+
const snippets = ext.structuredSnippets ?? [];
|
|
115665
|
+
for (let i = 0; i < snippets.length; i++) {
|
|
115666
|
+
const s = snippets[i];
|
|
115667
|
+
const prefix = `campaignExtensions.structuredSnippets[${i}]`;
|
|
115668
|
+
if (!s?.header?.trim()) {
|
|
115669
|
+
errors.push(`${prefix}.header \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
115670
|
+
}
|
|
115671
|
+
const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
|
|
115672
|
+
if (values.length < 3) {
|
|
115673
|
+
errors.push(`${prefix}.values \u81F3\u5C11 3 \u4E2A\u975E\u7A7A\u503C\uFF08\u5F53\u524D ${values.length}\uFF09`);
|
|
115674
|
+
}
|
|
115675
|
+
}
|
|
115676
|
+
if (ext.leadForm) {
|
|
115677
|
+
const lfErrors = validateLeadFormPayload(ext.leadForm, "campaignExtensions.leadForm");
|
|
115678
|
+
errors.push(...lfErrors);
|
|
115679
|
+
}
|
|
115680
|
+
if (callouts.length === 0 && snippets.length === 0 && !ext.leadForm) {
|
|
115681
|
+
warnings.push(
|
|
115682
|
+
"campaignExtensions \u5DF2\u58F0\u660E\u4F46 callouts / structuredSnippets / leadForm \u5747\u4E3A\u7A7A\uFF0C\u521B\u5EFA\u65F6\u5C06\u8DF3\u8FC7\u9644\u52A0\u8D44\u4EA7"
|
|
115683
|
+
);
|
|
115684
|
+
}
|
|
115685
|
+
return { errors, warnings };
|
|
115686
|
+
}
|
|
115687
|
+
function hasExtensionsToAttach(ext) {
|
|
115688
|
+
if (!ext) return false;
|
|
115689
|
+
const hasCallouts = (ext.callouts ?? []).some((t) => t?.trim());
|
|
115690
|
+
const hasSnippets = (ext.structuredSnippets ?? []).some(
|
|
115691
|
+
(s) => s?.header?.trim() && (s.values ?? []).some((v) => v?.trim())
|
|
115692
|
+
);
|
|
115693
|
+
return hasCallouts || hasSnippets || Boolean(ext.leadForm);
|
|
115694
|
+
}
|
|
115695
|
+
async function postExtension(config, googleApiUrl, accountId, body, verbose) {
|
|
115696
|
+
const url = `${googleApiUrl}/extensionmanagement/extension/${accountId}`;
|
|
115697
|
+
try {
|
|
115698
|
+
const data = await apiFetch2(
|
|
115699
|
+
url,
|
|
115700
|
+
config,
|
|
115701
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
115702
|
+
verbose
|
|
115703
|
+
);
|
|
115704
|
+
return { ok: true, data };
|
|
115705
|
+
} catch (err) {
|
|
115706
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
115707
|
+
}
|
|
115708
|
+
}
|
|
115709
|
+
function buildCalloutBody(accountId, campaignId, text) {
|
|
115710
|
+
return {
|
|
115711
|
+
activeuseridg: accountId,
|
|
115712
|
+
campaignId,
|
|
115713
|
+
level: "Campaign",
|
|
115714
|
+
typeV2: "CALLOUT",
|
|
115715
|
+
assetFieldType: "CALLOUT",
|
|
115716
|
+
properties: { Text: text.trim() }
|
|
115717
|
+
};
|
|
115718
|
+
}
|
|
115719
|
+
function buildSnippetBody(accountId, campaignId, header, values) {
|
|
115720
|
+
return {
|
|
115721
|
+
activeuseridg: accountId,
|
|
115722
|
+
campaignId,
|
|
115723
|
+
level: "Campaign",
|
|
115724
|
+
typeV2: "STRUCTURED_SNIPPET",
|
|
115725
|
+
assetFieldType: "STRUCTURED_SNIPPET",
|
|
115726
|
+
structuredSnippetHeaderValue: {
|
|
115727
|
+
key: header.trim(),
|
|
115728
|
+
value: values.map((v) => v.trim()).filter(Boolean)
|
|
115729
|
+
}
|
|
115730
|
+
};
|
|
115731
|
+
}
|
|
115732
|
+
function buildLeadFormBodyForCampaign(accountId, campaignId, leadForm) {
|
|
115733
|
+
return buildLeadFormExtensionBody({ account: accountId, campaignId, leadForm });
|
|
115734
|
+
}
|
|
115735
|
+
async function attachPmaxCampaignExtensions(opts) {
|
|
115736
|
+
const ext = opts.extensions;
|
|
115737
|
+
if (!hasExtensionsToAttach(ext)) {
|
|
115738
|
+
return {
|
|
115739
|
+
callouts: [],
|
|
115740
|
+
structuredSnippets: [],
|
|
115741
|
+
allOk: true,
|
|
115742
|
+
skippedReason: "\u672A\u914D\u7F6E campaignExtensions \u6216\u5185\u5BB9\u4E3A\u7A7A"
|
|
115743
|
+
};
|
|
115744
|
+
}
|
|
115745
|
+
const result = {
|
|
115746
|
+
callouts: [],
|
|
115747
|
+
structuredSnippets: [],
|
|
115748
|
+
allOk: true
|
|
115749
|
+
};
|
|
115750
|
+
for (const raw of ext.callouts ?? []) {
|
|
115751
|
+
const text = raw?.trim();
|
|
115752
|
+
if (!text) continue;
|
|
115753
|
+
if (opts.logProgress) console.log(`
|
|
115754
|
+
\u6302\u8F7D\u5BA3\u4F20\u4FE1\u606F\uFF1A${text}`);
|
|
115755
|
+
const posted = await postExtension(
|
|
115756
|
+
opts.config,
|
|
115757
|
+
opts.googleApiUrl,
|
|
115758
|
+
opts.accountId,
|
|
115759
|
+
buildCalloutBody(opts.accountId, opts.campaignId, text),
|
|
115760
|
+
opts.verbose
|
|
115761
|
+
);
|
|
115762
|
+
if (posted.ok) {
|
|
115763
|
+
result.callouts.push({
|
|
115764
|
+
ok: true,
|
|
115765
|
+
id: String(posted.data["id"] ?? ""),
|
|
115766
|
+
label: text
|
|
115767
|
+
});
|
|
115768
|
+
} else {
|
|
115769
|
+
result.allOk = false;
|
|
115770
|
+
result.callouts.push({ ok: false, error: posted.error, label: text });
|
|
115771
|
+
}
|
|
115772
|
+
}
|
|
115773
|
+
for (const s of ext.structuredSnippets ?? []) {
|
|
115774
|
+
const header = s?.header?.trim();
|
|
115775
|
+
const values = (s?.values ?? []).map((v) => v?.trim()).filter(Boolean);
|
|
115776
|
+
if (!header || values.length < 3) continue;
|
|
115777
|
+
const label = `${header}: ${values.join(", ")}`;
|
|
115778
|
+
if (opts.logProgress) console.log(`
|
|
115779
|
+
\u6302\u8F7D\u7ED3\u6784\u5316\u6458\u8981\uFF1A${label}`);
|
|
115780
|
+
const posted = await postExtension(
|
|
115781
|
+
opts.config,
|
|
115782
|
+
opts.googleApiUrl,
|
|
115783
|
+
opts.accountId,
|
|
115784
|
+
buildSnippetBody(opts.accountId, opts.campaignId, header, values),
|
|
115785
|
+
opts.verbose
|
|
115786
|
+
);
|
|
115787
|
+
if (posted.ok) {
|
|
115788
|
+
result.structuredSnippets.push({
|
|
115789
|
+
ok: true,
|
|
115790
|
+
id: String(posted.data["id"] ?? ""),
|
|
115791
|
+
label
|
|
115792
|
+
});
|
|
115793
|
+
} else {
|
|
115794
|
+
result.allOk = false;
|
|
115795
|
+
result.structuredSnippets.push({ ok: false, error: posted.error, label });
|
|
115796
|
+
}
|
|
115797
|
+
}
|
|
115798
|
+
if (ext.leadForm) {
|
|
115799
|
+
const headline = ext.leadForm.headline?.trim() || "Lead Form";
|
|
115800
|
+
if (opts.logProgress) console.log(`
|
|
115801
|
+
\u6302\u8F7D\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF1A${headline}`);
|
|
115802
|
+
const posted = await postExtension(
|
|
115803
|
+
opts.config,
|
|
115804
|
+
opts.googleApiUrl,
|
|
115805
|
+
opts.accountId,
|
|
115806
|
+
buildLeadFormBodyForCampaign(opts.accountId, opts.campaignId, ext.leadForm),
|
|
115807
|
+
opts.verbose
|
|
115808
|
+
);
|
|
115809
|
+
if (posted.ok) {
|
|
115810
|
+
result.leadForm = {
|
|
115811
|
+
ok: true,
|
|
115812
|
+
id: String(posted.data["id"] ?? ""),
|
|
115813
|
+
label: headline
|
|
115814
|
+
};
|
|
115815
|
+
} else {
|
|
115816
|
+
result.allOk = false;
|
|
115817
|
+
result.leadForm = { ok: false, error: posted.error, label: headline };
|
|
115818
|
+
}
|
|
115819
|
+
}
|
|
115820
|
+
return result;
|
|
115821
|
+
}
|
|
115822
|
+
function formatPmaxExtensionsAttachErrors(result) {
|
|
115823
|
+
const msgs = [];
|
|
115824
|
+
for (const c of result.callouts) {
|
|
115825
|
+
if (!c.ok) msgs.push(`\u5BA3\u4F20\u4FE1\u606F\u300C${c.label}\u300D\u5931\u8D25\uFF1A${c.error}`);
|
|
115826
|
+
}
|
|
115827
|
+
for (const s of result.structuredSnippets) {
|
|
115828
|
+
if (!s.ok) msgs.push(`\u7ED3\u6784\u5316\u6458\u8981\u300C${s.label}\u300D\u5931\u8D25\uFF1A${s.error}`);
|
|
115829
|
+
}
|
|
115830
|
+
if (result.leadForm && !result.leadForm.ok) {
|
|
115831
|
+
msgs.push(`\u6F5C\u5728\u5BA2\u6237\u8868\u5355\u300C${result.leadForm.label}\u300D\u5931\u8D25\uFF1A${result.leadForm.error}`);
|
|
115832
|
+
}
|
|
115833
|
+
return msgs;
|
|
115834
|
+
}
|
|
115835
|
+
|
|
115361
115836
|
// src/commands/ad/pmax-create.ts
|
|
115362
115837
|
function printVideoLinkRecoveryHint(accountId, assetGroupId, campaignId, youtubeTarget) {
|
|
115363
115838
|
const cid = campaignId != null ? ` --campaign-id ${campaignId}` : "";
|
|
@@ -115384,8 +115859,11 @@ async function runAdPmaxCreate(opts) {
|
|
|
115384
115859
|
opts.configFile,
|
|
115385
115860
|
cfg
|
|
115386
115861
|
);
|
|
115387
|
-
const errors
|
|
115388
|
-
|
|
115862
|
+
const { errors: extErrors, warnings: extWarnings } = validatePmaxCampaignExtensions(
|
|
115863
|
+
cfg.campaignExtensions
|
|
115864
|
+
);
|
|
115865
|
+
const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors, ...extErrors];
|
|
115866
|
+
const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings, ...extWarnings];
|
|
115389
115867
|
if (warnings.length > 0) {
|
|
115390
115868
|
console.warn("\n\u26A0\uFE0F PMax \u914D\u7F6E\u8B66\u544A\uFF08\u4E0D\u963B\u65AD\u63D0\u4EA4\uFF09\uFF1A");
|
|
115391
115869
|
for (const w of warnings) console.warn(` \u2022 ${w}`);
|
|
@@ -115457,6 +115935,21 @@ async function runAdPmaxCreate(opts) {
|
|
|
115457
115935
|
verbose: opts.verbose,
|
|
115458
115936
|
logProgress: !opts.jsonOut
|
|
115459
115937
|
});
|
|
115938
|
+
let extensionsResult;
|
|
115939
|
+
if (campaignId != null && cfg.campaignExtensions) {
|
|
115940
|
+
if (!opts.jsonOut) {
|
|
115941
|
+
console.log("\n \u6302\u8F7D Campaign \u9644\u52A0\u8D44\u4EA7\u2026");
|
|
115942
|
+
}
|
|
115943
|
+
extensionsResult = await attachPmaxCampaignExtensions({
|
|
115944
|
+
config,
|
|
115945
|
+
googleApiUrl,
|
|
115946
|
+
accountId,
|
|
115947
|
+
campaignId: String(campaignId),
|
|
115948
|
+
extensions: cfg.campaignExtensions,
|
|
115949
|
+
verbose: opts.verbose,
|
|
115950
|
+
logProgress: !opts.jsonOut
|
|
115951
|
+
});
|
|
115952
|
+
}
|
|
115460
115953
|
const payload = {
|
|
115461
115954
|
...data,
|
|
115462
115955
|
videoLink: {
|
|
@@ -115466,7 +115959,8 @@ async function runAdPmaxCreate(opts) {
|
|
|
115466
115959
|
uploadError: videoResult.uploadError,
|
|
115467
115960
|
linkError: videoResult.linkError,
|
|
115468
115961
|
skippedReason: videoResult.skippedReason
|
|
115469
|
-
}
|
|
115962
|
+
},
|
|
115963
|
+
campaignExtensions: extensionsResult ?? { allOk: true, skippedReason: "\u672A\u914D\u7F6E campaignExtensions" }
|
|
115470
115964
|
};
|
|
115471
115965
|
if (videoResult.uploadError) {
|
|
115472
115966
|
console.error(`
|
|
@@ -115490,6 +115984,20 @@ async function runAdPmaxCreate(opts) {
|
|
|
115490
115984
|
\u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u89C6\u9891\u672A\u94FE\u63A5\uFF1A${videoResult.skippedReason}
|
|
115491
115985
|
`);
|
|
115492
115986
|
}
|
|
115987
|
+
if (extensionsResult && !extensionsResult.allOk) {
|
|
115988
|
+
console.error("\n\u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u90E8\u5206\u9644\u52A0\u8D44\u4EA7\u6302\u8F7D\u5931\u8D25\uFF1A");
|
|
115989
|
+
for (const msg of formatPmaxExtensionsAttachErrors(extensionsResult)) {
|
|
115990
|
+
console.error(` \u2022 ${msg}`);
|
|
115991
|
+
}
|
|
115992
|
+
console.error(
|
|
115993
|
+
` \u53EF\u624B\u52A8\u8865\u6302\uFF1Asiluzan-tso ad extension callout|snippet|lead-form -a ${accountId} --campaign-id ${campaignId} \u2026
|
|
115994
|
+
`
|
|
115995
|
+
);
|
|
115996
|
+
} else if (extensionsResult?.allOk && extensionsResult.callouts.length + extensionsResult.structuredSnippets.length + (extensionsResult.leadForm ? 1 : 0) > 0) {
|
|
115997
|
+
if (!opts.jsonOut) {
|
|
115998
|
+
console.log("\n\u2705 Campaign \u9644\u52A0\u8D44\u4EA7\u5DF2\u6302\u8F7D");
|
|
115999
|
+
}
|
|
116000
|
+
}
|
|
115493
116001
|
if (await emitCliJsonOrSnapshot(opts, {
|
|
115494
116002
|
section: `ad-pmax-create-${accountId}`,
|
|
115495
116003
|
commandLabel: "ad pmax-create",
|
|
@@ -115542,8 +116050,11 @@ async function runAdPmaxValidate(opts) {
|
|
|
115542
116050
|
opts.configFile,
|
|
115543
116051
|
cfg
|
|
115544
116052
|
);
|
|
115545
|
-
const errors
|
|
115546
|
-
|
|
116053
|
+
const { errors: extErrors, warnings: extWarnings } = validatePmaxCampaignExtensions(
|
|
116054
|
+
cfg.campaignExtensions
|
|
116055
|
+
);
|
|
116056
|
+
const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors, ...extErrors];
|
|
116057
|
+
const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings, ...extWarnings];
|
|
115547
116058
|
const lengthViolations = cfgLengthViolations;
|
|
115548
116059
|
if (opts.writeNormalized) {
|
|
115549
116060
|
const toWrite = stripMetaKeysForExport(cfg);
|
|
@@ -116384,7 +116895,7 @@ async function runAdCampaignValidate(opts) {
|
|
|
116384
116895
|
}
|
|
116385
116896
|
|
|
116386
116897
|
// src/commands/ad/pmax-image-convert.ts
|
|
116387
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as
|
|
116898
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as readFileSync10, existsSync as existsSync4 } from "fs";
|
|
116388
116899
|
import { resolve as resolve10, dirname as dirname11, basename as basename7, extname } from "path";
|
|
116389
116900
|
var SPECS = [
|
|
116390
116901
|
{ kind: "marketing", width: 1200, height: 628, fit: "cover", suffix: "_marketing" },
|
|
@@ -116475,7 +116986,7 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
116475
116986
|
if (opts.updateConfig) {
|
|
116476
116987
|
const configPath = resolve10(opts.updateConfig);
|
|
116477
116988
|
try {
|
|
116478
|
-
const raw = JSON.parse(
|
|
116989
|
+
const raw = JSON.parse(readFileSync10(configPath, "utf8"));
|
|
116479
116990
|
const imgPaths = raw["imagePaths"] ?? {};
|
|
116480
116991
|
if (outputPaths.marketing) imgPaths["marketing"] = outputPaths.marketing;
|
|
116481
116992
|
if (outputPaths.square) imgPaths["square"] = outputPaths.square;
|
|
@@ -117292,8 +117803,39 @@ function register20(program2) {
|
|
|
117292
117803
|
});
|
|
117293
117804
|
}
|
|
117294
117805
|
);
|
|
117295
|
-
const extensionCmd = adCmd.command("extension").description(
|
|
117296
|
-
|
|
117806
|
+
const extensionCmd = adCmd.command("extension").description(
|
|
117807
|
+
"\u9644\u52A0\u4FE1\u606F\u7BA1\u7406\uFF08\u9644\u52A0\u94FE\u63A5/\u7535\u8BDD/\u5BA3\u4F20\u4FE1\u606F/\u7ED3\u6784\u5316\u6458\u8981/\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF1BPMax \u89C1 pmax-types\uFF09"
|
|
117808
|
+
);
|
|
117809
|
+
extensionCmd.command("pmax-types").description("\u67E5\u8BE2 PMax \u652F\u6301\u7684\u9644\u52A0\u8D44\u4EA7\u7C7B\u578B\u4E0E\u5C42\u7EA7\uFF08\u542B LEAD_FORM\uFF09").option("-t, --token <token>", "Auth Token").option(
|
|
117810
|
+
"--json-out <path>",
|
|
117811
|
+
"\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
|
|
117812
|
+
void 0
|
|
117813
|
+
).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
|
|
117814
|
+
async (opts) => {
|
|
117815
|
+
await runAdExtensionPmaxTypes({
|
|
117816
|
+
token: opts.token,
|
|
117817
|
+
jsonOut: opts.jsonOut,
|
|
117818
|
+
verbose: opts.verbose
|
|
117819
|
+
});
|
|
117820
|
+
}
|
|
117821
|
+
);
|
|
117822
|
+
extensionCmd.command("snippet-headers").description("\u67E5\u8BE2\u7ED3\u6784\u5316\u6458\u8981\u53EF\u9009\u6807\u5934\uFF08\u6309\u8BED\u8A00\u5206\u7EC4\uFF09").option("-t, --token <token>", "Auth Token").option(
|
|
117823
|
+
"--json-out <path>",
|
|
117824
|
+
"\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
|
|
117825
|
+
void 0
|
|
117826
|
+
).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
|
|
117827
|
+
async (opts) => {
|
|
117828
|
+
await runAdExtensionSnippetHeaders({
|
|
117829
|
+
token: opts.token,
|
|
117830
|
+
jsonOut: opts.jsonOut,
|
|
117831
|
+
verbose: opts.verbose
|
|
117832
|
+
});
|
|
117833
|
+
}
|
|
117834
|
+
);
|
|
117835
|
+
extensionCmd.command("list").description("\u67E5\u8BE2\u9644\u52A0\u4FE1\u606F\u5217\u8868").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").option(
|
|
117836
|
+
"--type <type>",
|
|
117837
|
+
"\u6309\u7C7B\u578B\u7B5B\u9009\uFF1ASITELINK | CALL | CALLOUT | STRUCTURED_SNIPPET | LEAD_FORM"
|
|
117838
|
+
).option("--campaign-id <id>", "\u6309\u5E7F\u544A\u7CFB\u5217 ID \u7B5B\u9009\uFF08PMax Campaign \u7EA7\u6269\u5C55\uFF09").option("-t, --token <token>", "Auth Token").option(
|
|
117297
117839
|
"--json-out <path>",
|
|
117298
117840
|
"\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json\uFF1B\u76EE\u5F55\u6A21\u5F0F\u6587\u4EF6\u540D\u4E3A `<section>[-<\u67E5\u8BE2id>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09",
|
|
117299
117841
|
void 0
|
|
@@ -117303,6 +117845,41 @@ function register20(program2) {
|
|
|
117303
117845
|
token: opts.token,
|
|
117304
117846
|
account: opts.account,
|
|
117305
117847
|
type: opts.type,
|
|
117848
|
+
campaignId: opts.campaignId,
|
|
117849
|
+
jsonOut: opts.jsonOut,
|
|
117850
|
+
verbose: opts.verbose
|
|
117851
|
+
});
|
|
117852
|
+
}
|
|
117853
|
+
);
|
|
117854
|
+
extensionCmd.command("lead-form").description(
|
|
117855
|
+
"\u4E3A PMax \u6D3B\u52A8\u6DFB\u52A0\u6F5C\u5728\u5BA2\u6237\u8868\u5355\uFF08LEAD_FORM\uFF1B\u4EC5 Campaign \u7EA7\uFF1B\u987B --config-file\uFF09\n \u6A21\u677F\uFF1Aassets/pmax-lead-form-template.json"
|
|
117856
|
+
).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--config-file <path>", "Lead Form JSON \u914D\u7F6E\uFF08\u89C1 pmax-lead-form-template.json\uFF09").option("--campaign-id <id>", "\u8986\u76D6\u914D\u7F6E\u6587\u4EF6\u4E2D\u7684 campaignId").option("-t, --token <token>", "Auth Token").option(
|
|
117857
|
+
"--json-out <path>",
|
|
117858
|
+
"\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
|
|
117859
|
+
void 0
|
|
117860
|
+
).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
|
|
117861
|
+
async (opts) => {
|
|
117862
|
+
await runAdExtensionLeadFormCreate({
|
|
117863
|
+
token: opts.token,
|
|
117864
|
+
account: opts.account,
|
|
117865
|
+
campaignId: opts.campaignId,
|
|
117866
|
+
configFile: opts.configFile,
|
|
117867
|
+
jsonOut: opts.jsonOut,
|
|
117868
|
+
verbose: opts.verbose
|
|
117869
|
+
});
|
|
117870
|
+
}
|
|
117871
|
+
);
|
|
117872
|
+
extensionCmd.command("update").description("\u66F4\u65B0\u9644\u52A0\u4FE1\u606F\uFF08\u5F53\u524D\u652F\u6301 LEAD_FORM\uFF1BPUT \u5168\u91CF\u66FF\u6362 leadForm \u5BF9\u8C61\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <extensionId>", "\u9644\u52A0\u4FE1\u606F ID\uFF08\u5148 ad extension list \u67E5\u8BE2\uFF09").requiredOption("--config-file <path>", "Lead Form JSON \u914D\u7F6E").option("-t, --token <token>", "Auth Token").option(
|
|
117873
|
+
"--json-out <path>",
|
|
117874
|
+
"\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
|
|
117875
|
+
void 0
|
|
117876
|
+
).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
|
|
117877
|
+
async (opts) => {
|
|
117878
|
+
await runAdExtensionUpdate({
|
|
117879
|
+
token: opts.token,
|
|
117880
|
+
account: opts.account,
|
|
117881
|
+
id: opts.id,
|
|
117882
|
+
configFile: opts.configFile,
|
|
117306
117883
|
jsonOut: opts.jsonOut,
|
|
117307
117884
|
verbose: opts.verbose
|
|
117308
117885
|
});
|
package/dist/skill/_meta.json
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"budget / targetCpa_BidingAmount 填主币种「元」,CLI 提交前 ×100 转 API 整型",
|
|
9
9
|
"三张图:只填 imagePaths(pmax-create 会自动上传为 assetId);勿把 Base64 写入 JSON",
|
|
10
10
|
"本地视频:填 videoPath(或别名 video);创建后 CLI 自动 PyAPI 上传并链接,与 --json-out 无关",
|
|
11
|
-
"提交前:ad pmax-validate → 用户确认 → ad pmax-create"
|
|
11
|
+
"提交前:ad pmax-validate → 用户确认 → ad pmax-create",
|
|
12
|
+
"campaignExtensions 可选:创建成功后 CLI 自动挂 callout/snippet/leadForm(非 pmax POST body)",
|
|
13
|
+
"不需要附加资产时删除整个 campaignExtensions 块"
|
|
12
14
|
]
|
|
13
15
|
},
|
|
14
16
|
|
|
@@ -38,5 +40,27 @@
|
|
|
38
40
|
"targetedLanguages": [{ "id": 1000 }],
|
|
39
41
|
"biddingStrategyTypeV2": "MAXIMIZE_CONVERSIONS",
|
|
40
42
|
"targetCpa_BidingAmount": 0,
|
|
41
|
-
"targetRoas": 0
|
|
43
|
+
"targetRoas": 0,
|
|
44
|
+
|
|
45
|
+
"campaignExtensions": {
|
|
46
|
+
"callouts": ["Free shipping", "24/7 support"],
|
|
47
|
+
"structuredSnippets": [
|
|
48
|
+
{
|
|
49
|
+
"header": "Services",
|
|
50
|
+
"values": ["Design", "Install", "Support"]
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"leadForm": {
|
|
54
|
+
"businessName": "Example Brand Co",
|
|
55
|
+
"headline": "Get a free quote",
|
|
56
|
+
"description": "Tell us about your project.",
|
|
57
|
+
"privacyPolicyUrl": "https://www.example.com/privacy",
|
|
58
|
+
"finalUrl": "https://www.example.com",
|
|
59
|
+
"fields": [
|
|
60
|
+
{ "inputType": "FULL_NAME" },
|
|
61
|
+
{ "inputType": "EMAIL" },
|
|
62
|
+
{ "inputType": "PHONE_NUMBER" }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
42
66
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
| 金额 | JSON 填**主币种「元」**;CLI 提交前 `budget`、`targetCpa_BidingAmount` ×100 |
|
|
18
18
|
| 图片 | **只填 `imagePaths`** 指向本地 PNG/JPEG;`pmax-create` 会自动 multipart 上传并用 assetId 创建(勿把 Base64 提交进 Git) |
|
|
19
19
|
| 视频 | JSON 填 **`videoPath`**(别名 `video` 亦可);`pmax-create` 成功后 **必定**经 PyAPI 上传并链接(含 `--json-out`)。已有 YouTube 用 `youtubeUrlOrId` |
|
|
20
|
+
| 附加资产 | 可选填 **`campaignExtensions`** |
|
|
20
21
|
| 改已上线 PMax | `ad pmax-get` / `pmax-edit` / `pmax-assets-update` 等(见 `references/google-ads/pmax-api.md`) |
|
|
21
22
|
| 列表复核 | `ad campaigns -a <id> --json-out ./snap`,`channelTypeV2` 应为 `PERFORMANCE_MAX` |
|
|
22
23
|
|
|
@@ -67,9 +68,24 @@ siluzan-tso ad campaigns -a <accountId> --json-out ./snap
|
|
|
67
68
|
| `videoDescription` | string | | PyAPI 上传描述(可选) |
|
|
68
69
|
| `youtubeUrlOrId` | string | | 已有 YouTube URL 或 11 位 ID;`pmax-create` 后自动链接 |
|
|
69
70
|
| `youtubeAssetName` | string | | 链接 YouTube 时的资产显示名 |
|
|
71
|
+
| `campaignExtensions` | object | | 创建成功后自动挂载的 Campaign 级扩展(见下表) |
|
|
70
72
|
|
|
71
73
|
\* 三张图各须有一种来源(路径或 Base64)。
|
|
72
74
|
|
|
75
|
+
### `campaignExtensions`(可选,CLI 编排)
|
|
76
|
+
|
|
77
|
+
**不**随 `POST .../campaign/pmax` 提交;活动创建成功后 CLI 自动调用 `extensionmanagement`。
|
|
78
|
+
|
|
79
|
+
| 子字段 | 类型 | 说明 |
|
|
80
|
+
|--------|------|------|
|
|
81
|
+
| `callouts` | string[] | 宣传信息(CALLOUT),每条 ≤25 字符,各创建 1 个扩展 |
|
|
82
|
+
| `structuredSnippets` | object[] | `{ header, values }`;`values` 至少 3 项 |
|
|
83
|
+
| `leadForm` | object | 潜在客户表单;字段同 `pmax-lead-form-template.json` 的 `leadForm` |
|
|
84
|
+
|
|
85
|
+
标头可选值:`ad extension snippet-headers`。WhatsApp 尚未支持。
|
|
86
|
+
|
|
87
|
+
若挂载失败,活动仍已创建;`--json-out` 的 `campaignExtensions` 段含各条 `ok` / `error`,可手动 `ad extension *` 补挂。
|
|
88
|
+
|
|
73
89
|
### 出价策略(PMax 支持子集)
|
|
74
90
|
|
|
75
91
|
| 值 | 说明 |
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"schema": "pmax-lead-form/v1",
|
|
4
|
+
"doc": "pmax-lead-form-template.md",
|
|
5
|
+
"note": "以 _ 开头的键仅作说明,CLI 提交前会剥离",
|
|
6
|
+
"agentPitfalls": [
|
|
7
|
+
"LEAD_FORM 仅支持 Campaign 级,须挂到已存在的 PMax 活动 campaignId",
|
|
8
|
+
"勿在 pmax-create 流程内创建;先 pmax-create 再 ad extension lead-form",
|
|
9
|
+
"每个 PMax 活动通常仅允许 1 个 Lead Form;创建前用 ad extension list --type LEAD_FORM --campaign-id 检查",
|
|
10
|
+
"账户须在 Google Ads UI 接受 Lead Form ToS",
|
|
11
|
+
"WhatsApp 私信扩展后端尚未开放"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"account": "REPLACE_mediaCustomerId",
|
|
15
|
+
"campaignId": "REPLACE_pmaxCampaignId",
|
|
16
|
+
"leadForm": {
|
|
17
|
+
"businessName": "Example Brand Co",
|
|
18
|
+
"headline": "Get a free quote",
|
|
19
|
+
"description": "Tell us about your project and we will contact you.",
|
|
20
|
+
"privacyPolicyUrl": "https://www.example.com/privacy",
|
|
21
|
+
"finalUrl": "https://www.example.com",
|
|
22
|
+
"callToActionType": "LEARN_MORE",
|
|
23
|
+
"callToActionDescription": "Contact us",
|
|
24
|
+
"fields": [
|
|
25
|
+
{ "inputType": "FULL_NAME" },
|
|
26
|
+
{ "inputType": "EMAIL" },
|
|
27
|
+
{ "inputType": "PHONE_NUMBER" }
|
|
28
|
+
],
|
|
29
|
+
"webhook": {
|
|
30
|
+
"advertiserWebhookUrl": "https://your-crm.example.com/webhook",
|
|
31
|
+
"googleSecret": "your-google-secret",
|
|
32
|
+
"payloadSchemaVersion": 3
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# `ad extension lead-form` JSON 配置说明
|
|
2
|
+
|
|
3
|
+
为**已创建的 PMax 活动**挂载潜在客户表单(`LEAD_FORM`)。不走 `pmax-create` 主流程。
|
|
4
|
+
|
|
5
|
+
模板 JSON:同目录 [`pmax-lead-form-template.json`](pmax-lead-form-template.json)。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 推荐命令顺序
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 1. 确认 PMax 活动 ID
|
|
13
|
+
siluzan-tso ad campaigns -a <accountId> --json-out ./snap
|
|
14
|
+
|
|
15
|
+
# 2. 查看 PMax 支持的扩展类型
|
|
16
|
+
siluzan-tso ad extension pmax-types --json-out ./snap
|
|
17
|
+
|
|
18
|
+
# 3. 检查活动上是否已有 Lead Form
|
|
19
|
+
siluzan-tso ad extension list -a <accountId> --type LEAD_FORM --campaign-id <campaignId> --json-out ./snap
|
|
20
|
+
|
|
21
|
+
# 4. 创建
|
|
22
|
+
siluzan-tso ad extension lead-form -a <accountId> --config-file ./lead-form.json --json-out ./snap
|
|
23
|
+
|
|
24
|
+
# 5. 更新(须带完整 leadForm 对象)
|
|
25
|
+
siluzan-tso ad extension update -a <accountId> --id <extensionId> --config-file ./lead-form.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 字段说明
|
|
31
|
+
|
|
32
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
33
|
+
|------|------|:----:|------|
|
|
34
|
+
| `account` | string | ✅ | Google 媒体客户 ID |
|
|
35
|
+
| `campaignId` | string | ✅ | PMax 活动 ID(`channelTypeV2=PERFORMANCE_MAX`) |
|
|
36
|
+
| `leadForm.businessName` | string | ✅ | 商家名称 |
|
|
37
|
+
| `leadForm.headline` | string | ✅ | 表单标题 |
|
|
38
|
+
| `leadForm.description` | string | ✅ | 表单描述 |
|
|
39
|
+
| `leadForm.privacyPolicyUrl` | string | ✅ | 隐私政策 URL |
|
|
40
|
+
| `leadForm.finalUrl` | string | ✅ | 落地页 URL |
|
|
41
|
+
| `leadForm.callToActionType` | string | | 默认 `LEARN_MORE` |
|
|
42
|
+
| `leadForm.callToActionDescription` | string | | 默认 `Contact us` |
|
|
43
|
+
| `leadForm.fields` | object[] | ✅ | 至少 1 项;`inputType` 如 `FULL_NAME`、`EMAIL`、`PHONE_NUMBER` |
|
|
44
|
+
| `leadForm.webhook` | object | | 写入 Google 的 Webhook(非平台接收端) |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## PMax 附加资产对照
|
|
49
|
+
|
|
50
|
+
| 中文 | 类型 | CLI 命令 |
|
|
51
|
+
|------|------|----------|
|
|
52
|
+
| 宣传信息 | `CALLOUT` | `ad extension callout` |
|
|
53
|
+
| 结构化摘要 | `STRUCTURED_SNIPPET` | `ad extension snippet` |
|
|
54
|
+
| 潜在客户表单 | `LEAD_FORM` | `ad extension lead-form` |
|
|
55
|
+
| 附加链接 | `SITELINK` | `ad extension sitelink` |
|
|
56
|
+
| 附加电话 | `CALL` | `ad extension call` |
|
|
57
|
+
| 私信 WhatsApp | — | **后端尚未开放** |
|
|
58
|
+
|
|
59
|
+
**层级**:PMax 仅 `Account` / `Campaign`;`Ad Group` 会 **400**。
|
|
60
|
+
|
|
61
|
+
**副作用**:创建 Lead Form 后,后端会自动尝试将 `SUBMIT_LEAD_FORM` + `GOOGLE_HOSTED` 转化目标设为 `biddable=true`。
|
|
62
|
+
|
|
63
|
+
详见 `references/google-ads/pmax-api.md` § 附加资产。
|
|
@@ -20,26 +20,18 @@
|
|
|
20
20
|
| **上下文被压缩 / 记不清字段或命令** | 重读 `SKILL.md` 路由表 + 当次任务 references |
|
|
21
21
|
| **CLI 返回 400 / 字段对不上** | 回到对应 reference 核对参数名与口径,勿猜 |
|
|
22
22
|
|
|
23
|
-
###
|
|
23
|
+
### 应当遵循(及原因)
|
|
24
|
+
|
|
25
|
+
| 应当怎么做 | 原因 |
|
|
26
|
+
| -------- | ---- |
|
|
27
|
+
| **每个新任务先 Read 当次必读 references,再执行 CLI** — 即使用户刚查过余额、对话里刚读过 `accounts/accounts.md` 或 `google-ads/google-ads.md`,换话题(建系列、预警、报告等)仍按 `SKILL.md` 路由表 **重新 Read** 对应文档后再 `siluzan-tso …` | 不同任务的命令、参数口径、Playbook 编号各不相同;对话会被压缩,「读过」≠ 当前上下文仍含正确字段名与 flags |
|
|
28
|
+
| **所有 ID、金额、命令 flags 以当次 Read 文档 + 当次 CLI 输出为准** — 计划与执行前对照 reference 确认 `-a`/`-m`/日期等参数;数值只来自本次 stdout 或脚本读盘结果 | Skill 文档与 CLI 会迭代,对话记忆里的示例 ID、占位金额易串户、串币种或触发 400 |
|
|
29
|
+
| **数据转换严格按 outline → 脚本读 JSON 顺序** — Read `references/core/tips.md` § 处理顺序;解析 stdout 得 `outlineFile` → Read outline → 编写并执行脚本读 `writtenFiles` | outline 是字段类型的唯一轻量索引;跳步会导致列名猜错(如 `keywordText` vs `keyword`)或把 MB 级 JSON 误读进对话 |
|
|
30
|
+
| **大 JSON 只经脚本读盘,对话里只 Read 小文件** — Read 仅 `references/`、`*.outline.txt`、stdout 一行摘要;业务 `*.json`(`campaigns-*.json`、`list-accounts-*.json` 等)由 Node/Python `readFileSync` / `JSON.parse` 处理 | `--json-out` 落盘常为 MB 级,Read 整文件会撑爆上下文且无法可靠汇总;脚本可精确取所需字段并与落盘 JSON 复核 |
|
|
31
|
+
| **报告 / Excel 走本 Skill 拉数 + outline + 脚本全流程** — 按 Playbook 与 `report-templates/*.md` 拉数、落盘、脚本转换后交付;不另加载宿主 xlsx / Excel 第三方 Skill 代劳 | 第三方 Skill 不知 TSO 字段口径、账户核验与 `deliverable-preflight`;易跳过 `-k` 核验或编造未落盘数据 |
|
|
32
|
+
| **用户给出的 `mediaCustomerId` 必须 `-k` 核验,全链路同一 ID** — `list-accounts -m <媒体> -k <用户ID>`;无结果则告知用户并停止;拉数、脚本、报告文件名均用该 ID(以 stdout `accountId` 为准) | 列表第一页可能不含目标户;自行换 ID 会导致报告错户,属严重交付事故 |
|
|
33
|
+
| 交付前自检产物,是否有为空的部分,是否有数据明显异常的部分,是否有字段使用错误的部分 | 如果不自检的话,出来的产物很可能无法使用 |
|
|
24
34
|
|
|
25
|
-
- **禁止**「上次已经读过 `accounts/accounts.md` / `google-ads/google-ads.md`,本任务直接 `siluzan-tso …`」
|
|
26
|
-
- **禁止**用对话记忆中的示例 ID、金额、命令 flags 代替当次 Read 文档
|
|
27
|
-
- **禁止**跳过 `references/core/tips.md` 里的 outline→脚本读 JSON 顺序
|
|
28
|
-
- **禁止**用 Read / `cat` / `type` / `head` 打开 `--json-out` 落盘的**业务数据** `*.json`(`campaigns-*.json`、`list-accounts-*.json` 等);只允许 Read 体积小的 `*.outline.txt` 与 `references/` 文档
|
|
29
|
-
- **禁止**在 siluzan-tso 报告任务中加载宿主「xlsx / Excel」第三方 Skill 代替本 Skill 的拉数 + outline + 脚本流程
|
|
30
|
-
- **禁止**用户给出 `mediaCustomerId` 后不用 `-k` 核验、翻页 `grep` 列表并**换成另一个账户 ID** 继续拉数/写报告
|
|
31
|
-
|
|
32
|
-
### 报告类常见违规(对照修复)
|
|
33
|
-
|
|
34
|
-
| 违规现象 | 正确做法 |
|
|
35
|
-
| -------- | -------- |
|
|
36
|
-
| 未读 `*.outline.txt` 就写 Excel,列名用 `keywordText` / `query` / `geoName` | 先 Read 各 section 的 outline;字段以最后一行 TS 类型为准(如 `keyword`、`searchTermText`、`countryOrRegion`) |
|
|
37
|
-
| `cat keywords-*.json \| python -m json.tool \| head` 探结构 | 只 Read `keywords-*.outline.txt`;业务 JSON 仅在脚本内读取 |
|
|
38
|
-
| `list-accounts` 第一页未见 ID,改用列表里「另一个」账户 | `list-accounts -m Google -k <用户ID>`;无结果则告知用户,停止 |
|
|
39
|
-
| 用户要 `2702960720`,拉数/脚本却用 `9322098303` | 全流程同一 `mediaCustomerId`;文件名以 stdout `accountId` 为准 |
|
|
40
|
-
| 交付 xlsx 前未对照 `deliverable-preflight.md` | 交付前自检表 A/B/C;xlsx 附脚本 stdout 行数/汇总摘要 |
|
|
41
|
-
|
|
42
|
-
---
|
|
43
35
|
|
|
44
36
|
## 执行流程
|
|
45
37
|
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
| 命令 | 选项 | 合法值 |
|
|
69
69
|
| ---- | ---- | ------ |
|
|
70
70
|
| ad extension sitelink, callout, snippet, … | --level | Account \| Campaign \| AdGroup |
|
|
71
|
+
| ad extension list | --type | SITELINK \| CALL \| CALLOUT \| STRUCTURED_SNIPPET \| LEAD_FORM |
|
|
72
|
+
| ad extension lead-form / update | --config-file | JSON(见 `assets/pmax-lead-form-template.json`) |
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
### 搜索系列 `BiddingStrategyTypeV2`
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
| 改资产 | `ad pmax-assets-update --config-file …` |
|
|
65
65
|
| YouTube 追加 | `ad pmax-youtube-link`(单条 `--youtube` / `--video-path`);批量见 `ad pmax-assets-update` |
|
|
66
66
|
| 信号 | `ad pmax-signals-get` / `ad pmax-signals-set`;受众下拉 `ad pmax-audiences` |
|
|
67
|
+
| 附加资产 | `ad extension pmax-types`;`callout` / `snippet` / `lead-form`(见 `pmax-api.md` § 附加资产) |
|
|
67
68
|
| 图片库 | `ad pmax-image-upload` |
|
|
68
69
|
| 报表 | `ad pmax-report-asset-groups` / `ad pmax-report-geo` |
|
|
69
70
|
| 删活动 | `ad campaign-delete -a <id> --id <campaignId>`(与 Search 共用;勿用 `ad campaign-edit`) |
|
|
@@ -512,11 +513,19 @@ siluzan-tso ad keyword-negative-edit \
|
|
|
512
513
|
|
|
513
514
|
## ad extension — 附加信息管理
|
|
514
515
|
|
|
515
|
-
|
|
516
|
+
Callout / Snippet / Sitelink / Call 等类型修改可**先删后建**;**Lead Form** 支持 `ad extension update`(PUT 全量替换 `leadForm`)。所有 `extension <type>` 子命令均支持 `--json-out`,输出网关返回的扩展对象(含 `id`),批量脚本可据此回填。
|
|
516
517
|
|
|
517
518
|
```bash
|
|
519
|
+
# PMax 支持的类型与层级(含 LEAD_FORM)
|
|
520
|
+
siluzan-tso ad extension pmax-types [--json-out ./snap]
|
|
521
|
+
|
|
522
|
+
# 结构化摘要标头(按语言)
|
|
523
|
+
siluzan-tso ad extension snippet-headers [--json-out ./snap]
|
|
524
|
+
|
|
518
525
|
# 查询
|
|
519
|
-
siluzan-tso ad extension list -a <accountId>
|
|
526
|
+
siluzan-tso ad extension list -a <accountId> \
|
|
527
|
+
[--type SITELINK|CALL|CALLOUT|STRUCTURED_SNIPPET|LEAD_FORM] \
|
|
528
|
+
[--campaign-id <campaignId>] [--json-out ./snap]
|
|
520
529
|
|
|
521
530
|
# 附加链接
|
|
522
531
|
siluzan-tso ad extension sitelink -a <accountId> --text "文字" --url "https://..." \
|
|
@@ -532,11 +541,21 @@ siluzan-tso ad extension callout -a <accountId> --text "免费送货上门" [--l
|
|
|
532
541
|
# 附加结构化摘要
|
|
533
542
|
siluzan-tso ad extension snippet -a <accountId> --header "Brands" --values "A,B,C" [--level Account] [--json-out ./snap]
|
|
534
543
|
|
|
544
|
+
# PMax 潜在客户表单(仅 Campaign 级;模板 assets/pmax-lead-form-template.json)
|
|
545
|
+
siluzan-tso ad extension lead-form -a <accountId> --config-file ./lead-form.json [--json-out ./snap]
|
|
546
|
+
|
|
547
|
+
# 更新 Lead Form
|
|
548
|
+
siluzan-tso ad extension update -a <accountId> --id <extensionId> --config-file ./lead-form.json
|
|
549
|
+
|
|
535
550
|
# 删除
|
|
536
551
|
siluzan-tso ad extension delete -a <accountId> --id <extensionId>
|
|
537
552
|
```
|
|
538
553
|
|
|
539
|
-
|
|
554
|
+
**PMax 约束**:仅 `Account` / `Campaign` 层级;`Ad Group` 会 400。`LEAD_FORM` 仅挂 Campaign。WhatsApp 私信扩展后端尚未开放。
|
|
555
|
+
|
|
556
|
+
`--header` 常用值:`Brands`/`Services`/`Amenities`/`Types`/`Styles`/`Courses`/`Models` 等(完整列表:`ad extension snippet-headers`)。
|
|
557
|
+
|
|
558
|
+
**网关**:`ExtensionManagementController.cs` — `pmaxSupportedTypeList`、`structuredSnippetHeaders`、`extension/{accountId}` POST/PUT/DELETE。
|
|
540
559
|
|
|
541
560
|
---
|
|
542
561
|
|
|
@@ -29,8 +29,62 @@
|
|
|
29
29
|
| `ad pmax-image-upload` | 单张图片上传 |
|
|
30
30
|
| `ad pmax-report-asset-groups` / `ad pmax-report-geo` | 报表 |
|
|
31
31
|
| `ad campaigns` | 列表(筛 `PERFORMANCE_MAX`) |
|
|
32
|
+
| `ad extension pmax-types` | PMax 支持的附加资产类型与层级 |
|
|
33
|
+
| `ad extension callout` / `snippet` / `lead-form` | 宣传信息 / 结构化摘要 / 潜在客户表单 |
|
|
34
|
+
| `ad extension list` | 查询已挂载扩展(`--type` / `--campaign-id`) |
|
|
32
35
|
|
|
33
|
-
模板:`assets/pmax-create-template.json`、`pmax-asset-group-template.json`、`pmax-assets-update-template.json`、`pmax-signals-template.json`、`pmax-patch-campaign-template.json`。
|
|
36
|
+
模板:`assets/pmax-create-template.json`、`pmax-asset-group-template.json`、`pmax-assets-update-template.json`、`pmax-signals-template.json`、`pmax-patch-campaign-template.json`、`pmax-lead-form-template.json`。
|
|
37
|
+
|
|
38
|
+
### 附加资产(Callout / Snippet / Lead Form)
|
|
39
|
+
|
|
40
|
+
网关 `POST .../campaign/pmax` **不**接受扩展字段;CLI 通过 **`campaignExtensions`** 在 `pmax-create` 成功后**自动编排** `extensionmanagement`(与手动 `ad extension *` 等价)。
|
|
41
|
+
|
|
42
|
+
**方式 A(推荐,一条命令)**:在 `pmax-create` JSON 中填 `campaignExtensions`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"campaignExtensions": {
|
|
47
|
+
"callouts": ["Free shipping"],
|
|
48
|
+
"structuredSnippets": [{ "header": "Services", "values": ["A", "B", "C"] }],
|
|
49
|
+
"leadForm": { "businessName": "...", "headline": "...", "description": "...", "privacyPolicyUrl": "...", "finalUrl": "...", "fields": [{ "inputType": "EMAIL" }] }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`pmax-validate` 会一并校验;`pmax-create` 成功后在同一次调用内挂载(`--json-out` 含 `campaignExtensions` 结果)。
|
|
55
|
+
|
|
56
|
+
**方式 B(分步)**:活动创建后手动走 `extensionmanagement`(PMax 限制 Account/Campaign 层级)。
|
|
57
|
+
|
|
58
|
+
| 中文 | 类型 | 命令 |
|
|
59
|
+
|------|------|------|
|
|
60
|
+
| 宣传信息 | `CALLOUT` | `ad extension callout -a <id> --text "..." --level Campaign --campaign-id <cid>` |
|
|
61
|
+
| 结构化摘要 | `STRUCTURED_SNIPPET` | `ad extension snippet -a <id> --header Brands --values "A,B,C" --level Campaign --campaign-id <cid>` |
|
|
62
|
+
| 潜在客户表单 | `LEAD_FORM` | `ad extension lead-form -a <id> --config-file ./lead-form.json` |
|
|
63
|
+
| 附加链接 / 电话 | `SITELINK` / `CALL` | `ad extension sitelink` / `call`(同上 `--level Campaign`) |
|
|
64
|
+
| 私信 WhatsApp | — | **后端尚未开放**(后续阶段) |
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 查 PMax 支持类型
|
|
68
|
+
siluzan-tso ad extension pmax-types --json-out ./snap
|
|
69
|
+
|
|
70
|
+
# 结构化摘要标头下拉
|
|
71
|
+
siluzan-tso ad extension snippet-headers --json-out ./snap
|
|
72
|
+
|
|
73
|
+
# Lead Form(模板见 assets/pmax-lead-form-template.md)
|
|
74
|
+
siluzan-tso ad extension lead-form -a <accountId> --config-file ./lead-form.json --json-out ./snap
|
|
75
|
+
|
|
76
|
+
# 复核 / 更新 / 删除
|
|
77
|
+
siluzan-tso ad extension list -a <accountId> --type LEAD_FORM --campaign-id <cid> --json-out ./snap
|
|
78
|
+
siluzan-tso ad extension update -a <accountId> --id <extId> --config-file ./lead-form.json
|
|
79
|
+
siluzan-tso ad extension delete -a <accountId> --id <extId>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**约束**:
|
|
83
|
+
- PMax **不支持** `level: Ad Group`(会 400)
|
|
84
|
+
- `LEAD_FORM` **仅** `Campaign` 级;每活动通常 1 个
|
|
85
|
+
- 创建 Lead Form 后后端自动尝试开启 `SUBMIT_LEAD_FORM` + `GOOGLE_HOSTED` 转化目标
|
|
86
|
+
|
|
87
|
+
**网关**:`GET /extensionmanagement/pmaxSupportedTypeList`;`POST|PUT|DELETE /extensionmanagement/extension/{accountId}[/{id}]`(Sammamish `ExtensionManagementController.cs`)。
|
|
34
88
|
|
|
35
89
|
### 图片(推荐流程)
|
|
36
90
|
|
|
@@ -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.29-beta.
|
|
12
|
+
$PKG_VERSION = '1.1.29-beta.3'
|
|
13
13
|
$CLI_BIN = 'siluzan-tso'
|
|
14
14
|
$SKILL_LABEL = 'Siluzan TSO'
|
|
15
15
|
$INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
|
|
@@ -9,7 +9,7 @@ set -euo pipefail
|
|
|
9
9
|
# -- Package info (injected at build time) ------------------------------------
|
|
10
10
|
readonly PKG_NAME="siluzan-tso-cli"
|
|
11
11
|
# PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
|
|
12
|
-
readonly PKG_VERSION="1.1.29-beta.
|
|
12
|
+
readonly PKG_VERSION="1.1.29-beta.3"
|
|
13
13
|
readonly CLI_BIN="siluzan-tso"
|
|
14
14
|
readonly SKILL_LABEL="Siluzan TSO"
|
|
15
15
|
readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
|