siluzan-tso-cli 1.1.22-beta.12 → 1.1.22-beta.14
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 +444 -150
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/assets/pmax-create-template.md +6 -0
- package/dist/skill/references/google-ads/google-ads.md +2 -0
- package/dist/skill/references/google-ads/pmax-api.md +6 -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.22-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.22-beta.14),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -2262,13 +2262,13 @@ function redactSensitive(input) {
|
|
|
2262
2262
|
}
|
|
2263
2263
|
async function apiFetch(url, config, options = {}, verbose = false) {
|
|
2264
2264
|
const method = options.method ?? "GET";
|
|
2265
|
-
const
|
|
2265
|
+
const authHeaders2 = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
|
|
2266
2266
|
const reqHeaders = {
|
|
2267
2267
|
"Content-Type": "application/json",
|
|
2268
2268
|
"Accept-Language": "zh-CN",
|
|
2269
2269
|
// 声明支持 gzip/deflate/br;服务端不支持则按 identity 返回,rawRequest 会原样收取
|
|
2270
2270
|
"Accept-Encoding": "gzip, deflate, br",
|
|
2271
|
-
...
|
|
2271
|
+
...authHeaders2,
|
|
2272
2272
|
// dataPermission 仅 TSO 使用;CSO 未设置时为空字符串,服务端忽略该头
|
|
2273
2273
|
Datapermission: config.dataPermission ?? "",
|
|
2274
2274
|
...options.headers ?? {}
|
|
@@ -2296,12 +2296,12 @@ async function apiFetch(url, config, options = {}, verbose = false) {
|
|
|
2296
2296
|
}
|
|
2297
2297
|
async function apiFetchWithHeaders(url, config, options = {}, verbose = false) {
|
|
2298
2298
|
const method = options.method ?? "GET";
|
|
2299
|
-
const
|
|
2299
|
+
const authHeaders2 = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
|
|
2300
2300
|
const reqHeaders = {
|
|
2301
2301
|
"Content-Type": "application/json",
|
|
2302
2302
|
"Accept-Language": "zh-CN",
|
|
2303
2303
|
"Accept-Encoding": "gzip, deflate, br",
|
|
2304
|
-
...
|
|
2304
|
+
...authHeaders2,
|
|
2305
2305
|
Datapermission: config.dataPermission ?? "",
|
|
2306
2306
|
...options.headers ?? {}
|
|
2307
2307
|
};
|
|
@@ -2616,14 +2616,14 @@ function parseMeResponse(text) {
|
|
|
2616
2616
|
async function fetchSiluzanCurrentUser(apiBase, config) {
|
|
2617
2617
|
const mainOrigin = deriveMainApiOrigin(apiBase);
|
|
2618
2618
|
const meUrl = `${mainOrigin.replace(/\/$/, "")}/query/account/me`;
|
|
2619
|
-
const
|
|
2619
|
+
const authHeaders2 = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
|
|
2620
2620
|
try {
|
|
2621
2621
|
const res = await rawRequest(meUrl, {
|
|
2622
2622
|
method: "GET",
|
|
2623
2623
|
headers: {
|
|
2624
2624
|
"Content-Type": "application/json",
|
|
2625
2625
|
"Accept-Language": "zh-CN",
|
|
2626
|
-
...
|
|
2626
|
+
...authHeaders2
|
|
2627
2627
|
}
|
|
2628
2628
|
});
|
|
2629
2629
|
if (res.status < 200 || res.status >= 300) return null;
|
|
@@ -100904,15 +100904,15 @@ function computeBackoffMs(attempt, policy) {
|
|
|
100904
100904
|
const jitter = Math.floor(Math.random() * policy.baseMs);
|
|
100905
100905
|
return capped + jitter;
|
|
100906
100906
|
}
|
|
100907
|
-
function
|
|
100908
|
-
return new Promise((
|
|
100907
|
+
function sleep3(ms, signal) {
|
|
100908
|
+
return new Promise((resolve13, reject) => {
|
|
100909
100909
|
if (signal?.aborted) {
|
|
100910
100910
|
reject(new Error("retry sleep aborted"));
|
|
100911
100911
|
return;
|
|
100912
100912
|
}
|
|
100913
100913
|
const timer = setTimeout(() => {
|
|
100914
100914
|
signal?.removeEventListener("abort", onAbort);
|
|
100915
|
-
|
|
100915
|
+
resolve13();
|
|
100916
100916
|
}, ms);
|
|
100917
100917
|
const onAbort = () => {
|
|
100918
100918
|
clearTimeout(timer);
|
|
@@ -100973,7 +100973,7 @@ async function fetchWithRetry(factory, options) {
|
|
|
100973
100973
|
const backoffMs = computeBackoffMs(attempt, policy);
|
|
100974
100974
|
options.onRetry?.({ attempt, backoffMs, status: cls.status, error: err });
|
|
100975
100975
|
try {
|
|
100976
|
-
await
|
|
100976
|
+
await sleep3(backoffMs, options.signal);
|
|
100977
100977
|
} catch {
|
|
100978
100978
|
throw new AbortRunError({
|
|
100979
100979
|
message: `\u91CD\u8BD5\u7B49\u5F85\u88AB\u4E2D\u6B62\uFF1A${options.label}`,
|
|
@@ -113852,6 +113852,10 @@ function runPmaxCreateValidation(cfg) {
|
|
|
113852
113852
|
}
|
|
113853
113853
|
}
|
|
113854
113854
|
}
|
|
113855
|
+
const youtube = cfg.youtubeUrlOrId?.trim();
|
|
113856
|
+
if (youtube && youtube.length < 6) {
|
|
113857
|
+
pushErr3(errors, "youtubeUrlOrId \u8FC7\u77ED\uFF0C\u8BF7\u586B\u5199\u5B8C\u6574 YouTube URL \u6216 11 \u4F4D\u89C6\u9891 ID");
|
|
113858
|
+
}
|
|
113855
113859
|
validateTextLengths(
|
|
113856
113860
|
errors,
|
|
113857
113861
|
lengthViolations,
|
|
@@ -114319,8 +114323,129 @@ function assertPmaxImageSlotsResolved(slots) {
|
|
|
114319
114323
|
}
|
|
114320
114324
|
}
|
|
114321
114325
|
|
|
114322
|
-
// src/commands/ad/pmax-
|
|
114326
|
+
// src/commands/ad/pmax-shared.ts
|
|
114327
|
+
init_auth();
|
|
114328
|
+
init_cli_json_snapshot();
|
|
114323
114329
|
import { readFileSync as readFileSync6 } from "fs";
|
|
114330
|
+
import { dirname as dirname9, isAbsolute as isAbsolute4, resolve as resolve8 } from "path";
|
|
114331
|
+
var PMAX_MONEY_KEYS = /* @__PURE__ */ new Set(["budget", "targetCpa_BidingAmount"]);
|
|
114332
|
+
function loadPmaxJsonFile(configFile) {
|
|
114333
|
+
let raw;
|
|
114334
|
+
try {
|
|
114335
|
+
raw = JSON.parse(readFileSync6(configFile, "utf8"));
|
|
114336
|
+
} catch (e) {
|
|
114337
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
114338
|
+
console.error(`
|
|
114339
|
+
\u274C \u8BFB\u53D6 JSON \u5931\u8D25\uFF08${configFile}\uFF09\uFF1A${msg}
|
|
114340
|
+
`);
|
|
114341
|
+
process.exit(1);
|
|
114342
|
+
}
|
|
114343
|
+
return stripMetaKeys(raw);
|
|
114344
|
+
}
|
|
114345
|
+
function resolvePmaxImagePath2(configFile, relOrAbs) {
|
|
114346
|
+
const trimmed = relOrAbs.trim();
|
|
114347
|
+
if (isAbsolute4(trimmed)) return trimmed;
|
|
114348
|
+
return resolve8(dirname9(configFile), trimmed);
|
|
114349
|
+
}
|
|
114350
|
+
function convertPmaxMoneyInObject(body) {
|
|
114351
|
+
const out = { ...body };
|
|
114352
|
+
for (const key of PMAX_MONEY_KEYS) {
|
|
114353
|
+
const val = out[key];
|
|
114354
|
+
if (typeof val === "number" && Number.isFinite(val)) {
|
|
114355
|
+
out[key] = toCentAmount(val);
|
|
114356
|
+
}
|
|
114357
|
+
}
|
|
114358
|
+
return out;
|
|
114359
|
+
}
|
|
114360
|
+
function buildPmaxAssetGroupApiBody(cfg, imageSlots) {
|
|
114361
|
+
assertPmaxImageSlotsResolved(imageSlots);
|
|
114362
|
+
return {
|
|
114363
|
+
name: cfg.name.trim(),
|
|
114364
|
+
finalUrls: cfg.finalUrls.map((u) => u.trim()).filter(Boolean),
|
|
114365
|
+
headlines: cfg.headlines.map((h) => h.trim()).filter(Boolean),
|
|
114366
|
+
longHeadlines: cfg.longHeadlines.map((h) => h.trim()).filter(Boolean),
|
|
114367
|
+
descriptions: cfg.descriptions.map((d) => d.trim()).filter(Boolean),
|
|
114368
|
+
businessName: cfg.businessName.trim(),
|
|
114369
|
+
...imageSlots
|
|
114370
|
+
};
|
|
114371
|
+
}
|
|
114372
|
+
async function processAssetsUpdateBodyWithUpload(configFile, body, accountId, config, googleApiUrl, verbose) {
|
|
114373
|
+
const out = { ...body };
|
|
114374
|
+
const links = out["assetsToLink"];
|
|
114375
|
+
if (!Array.isArray(links)) return out;
|
|
114376
|
+
out["assetsToLink"] = await Promise.all(
|
|
114377
|
+
links.map(async (item) => {
|
|
114378
|
+
if (!item || typeof item !== "object") return item;
|
|
114379
|
+
const row = { ...item };
|
|
114380
|
+
const imagePath = row["imagePath"];
|
|
114381
|
+
if (typeof imagePath === "string" && imagePath.trim()) {
|
|
114382
|
+
const abs = resolvePmaxImagePath2(configFile, imagePath);
|
|
114383
|
+
row["assetId"] = await uploadPmaxImageFile(
|
|
114384
|
+
config,
|
|
114385
|
+
googleApiUrl,
|
|
114386
|
+
accountId,
|
|
114387
|
+
abs,
|
|
114388
|
+
void 0,
|
|
114389
|
+
verbose
|
|
114390
|
+
);
|
|
114391
|
+
delete row["imagePath"];
|
|
114392
|
+
delete row["imageBase64"];
|
|
114393
|
+
}
|
|
114394
|
+
return row;
|
|
114395
|
+
})
|
|
114396
|
+
);
|
|
114397
|
+
return out;
|
|
114398
|
+
}
|
|
114399
|
+
async function withGoogleApi(opts, fn) {
|
|
114400
|
+
const config = await ensureDataPermission(loadConfig(opts.token));
|
|
114401
|
+
const googleApiUrl = requireGoogleApi(config);
|
|
114402
|
+
return fn(config, googleApiUrl);
|
|
114403
|
+
}
|
|
114404
|
+
async function pmaxApiFetch(url, config, init, verbose) {
|
|
114405
|
+
return apiFetch2(url, config, init ?? {}, verbose);
|
|
114406
|
+
}
|
|
114407
|
+
async function postPmaxYoutubeLink(opts) {
|
|
114408
|
+
const body = {
|
|
114409
|
+
youtubeUrlOrId: opts.youtubeUrlOrId.trim()
|
|
114410
|
+
};
|
|
114411
|
+
if (opts.campaignId?.trim()) body["campaignId"] = opts.campaignId.trim();
|
|
114412
|
+
if (opts.assetName?.trim()) body["assetName"] = opts.assetName.trim();
|
|
114413
|
+
const url = pmaxAssetGroupUrl(
|
|
114414
|
+
opts.googleApiUrl,
|
|
114415
|
+
opts.accountId,
|
|
114416
|
+
opts.assetGroupId,
|
|
114417
|
+
"youtube"
|
|
114418
|
+
);
|
|
114419
|
+
const raw = await pmaxApiFetch(
|
|
114420
|
+
url,
|
|
114421
|
+
opts.config,
|
|
114422
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
114423
|
+
opts.verbose
|
|
114424
|
+
);
|
|
114425
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : { data: raw };
|
|
114426
|
+
}
|
|
114427
|
+
async function emitPmaxResult(opts, section, commandLabel, payload, idSuffix) {
|
|
114428
|
+
return emitCliJsonOrSnapshot(opts, {
|
|
114429
|
+
section,
|
|
114430
|
+
commandLabel,
|
|
114431
|
+
commandHint: idSuffix ?? opts.account ?? "",
|
|
114432
|
+
payload,
|
|
114433
|
+
idSuffix: idSuffix ?? opts.account
|
|
114434
|
+
});
|
|
114435
|
+
}
|
|
114436
|
+
function requireAccountId(account, label = "-a, --account") {
|
|
114437
|
+
const id = account?.toString().trim();
|
|
114438
|
+
if (!id) {
|
|
114439
|
+
console.error(`
|
|
114440
|
+
\u274C \u8BF7\u6307\u5B9A\u5A92\u4F53\u8D26\u6237 ID\uFF08${label}\uFF09
|
|
114441
|
+
`);
|
|
114442
|
+
process.exit(1);
|
|
114443
|
+
}
|
|
114444
|
+
return id;
|
|
114445
|
+
}
|
|
114446
|
+
|
|
114447
|
+
// src/commands/ad/pmax-load.ts
|
|
114448
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
114324
114449
|
function loadPmaxCreateConfig(configFile) {
|
|
114325
114450
|
const cfg = tryLoadPmaxCreateConfig(configFile);
|
|
114326
114451
|
if (!cfg) {
|
|
@@ -114331,15 +114456,36 @@ function loadPmaxCreateConfig(configFile) {
|
|
|
114331
114456
|
}
|
|
114332
114457
|
return cfg;
|
|
114333
114458
|
}
|
|
114459
|
+
var DEPRECATED_PMAX_VIDEO_KEYS = ["videoPaths", "localVideo", "videoFile", "video"];
|
|
114460
|
+
function detectDeprecatedPmaxVideoKeys(raw) {
|
|
114461
|
+
const msgs = [];
|
|
114462
|
+
for (const key of DEPRECATED_PMAX_VIDEO_KEYS) {
|
|
114463
|
+
const v = raw[key];
|
|
114464
|
+
if (v == null) continue;
|
|
114465
|
+
if (typeof v === "string" && !v.trim()) continue;
|
|
114466
|
+
if (typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0) {
|
|
114467
|
+
continue;
|
|
114468
|
+
}
|
|
114469
|
+
msgs.push(`\u914D\u7F6E\u542B\u5DF2\u5E9F\u5F03\u5B57\u6BB5\u300C${key}\u300D\uFF0C\u8BF7\u6539\u7528 videoPath\uFF08\u672C\u5730\u6587\u4EF6\uFF09\u6216 youtubeUrlOrId`);
|
|
114470
|
+
}
|
|
114471
|
+
return msgs;
|
|
114472
|
+
}
|
|
114334
114473
|
function tryLoadPmaxCreateConfig(configFile) {
|
|
114335
114474
|
let raw;
|
|
114336
114475
|
try {
|
|
114337
|
-
raw = JSON.parse(
|
|
114476
|
+
raw = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
114338
114477
|
} catch {
|
|
114339
114478
|
return null;
|
|
114340
114479
|
}
|
|
114341
114480
|
return stripMetaKeys(raw);
|
|
114342
114481
|
}
|
|
114482
|
+
function loadPmaxCreateConfigRaw(configFile) {
|
|
114483
|
+
try {
|
|
114484
|
+
return stripMetaKeys(JSON.parse(readFileSync7(configFile, "utf8")));
|
|
114485
|
+
} catch {
|
|
114486
|
+
return null;
|
|
114487
|
+
}
|
|
114488
|
+
}
|
|
114343
114489
|
function parseLocationLanguageIds(items) {
|
|
114344
114490
|
if (!items?.length) return void 0;
|
|
114345
114491
|
return items.map((item) => ({
|
|
@@ -114381,9 +114527,182 @@ function buildPmaxCreateUrl(googleApiUrl, accountId) {
|
|
|
114381
114527
|
return `${base}/accounts/${accountId}/campaign/pmax`;
|
|
114382
114528
|
}
|
|
114383
114529
|
|
|
114530
|
+
// src/commands/ad/pmax-video-upload.ts
|
|
114531
|
+
import { existsSync as existsSync3, readFileSync as readFileSync8 } from "fs";
|
|
114532
|
+
import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
|
|
114533
|
+
var VIDEO_UPLOAD_SUFFIX = /\.(mp4|mov|webm|avi|mpeg|mpg)$/i;
|
|
114534
|
+
var TERMINAL_VIDEO_STATES = /* @__PURE__ */ new Set(["PROCESSED", "FAILED", "REJECTED", "UNAVAILABLE", "NOT_FOUND"]);
|
|
114535
|
+
var FAILED_VIDEO_STATES = /* @__PURE__ */ new Set(["FAILED", "REJECTED", "UNAVAILABLE", "NOT_FOUND"]);
|
|
114536
|
+
var PMAX_VIDEO_POLL_INTERVAL_MS = 1e4;
|
|
114537
|
+
var PMAX_VIDEO_POLL_MAX_ATTEMPTS = 120;
|
|
114538
|
+
function pyapiVideoUploadUrl(googleApiUrl) {
|
|
114539
|
+
const base = googleApiUrl.replace(/\/$/, "");
|
|
114540
|
+
return `${base}/pyapi/video/upload`;
|
|
114541
|
+
}
|
|
114542
|
+
function pyapiVideoStatusUrl(googleApiUrl, mediaAccountId, resourceName) {
|
|
114543
|
+
const base = googleApiUrl.replace(/\/$/, "");
|
|
114544
|
+
const params = new URLSearchParams({
|
|
114545
|
+
media_account_id: mediaAccountId.replace(/-/g, ""),
|
|
114546
|
+
resource_name: resourceName
|
|
114547
|
+
});
|
|
114548
|
+
return `${base}/pyapi/video/upload/status?${params.toString()}`;
|
|
114549
|
+
}
|
|
114550
|
+
function resolveVideoPath(configFile, relOrAbs) {
|
|
114551
|
+
const trimmed = relOrAbs.trim();
|
|
114552
|
+
if (isAbsolute5(trimmed)) return trimmed;
|
|
114553
|
+
return resolve9(dirname10(configFile), trimmed);
|
|
114554
|
+
}
|
|
114555
|
+
function authHeaders(config) {
|
|
114556
|
+
const headers = {
|
|
114557
|
+
"Accept-Language": "zh-CN",
|
|
114558
|
+
Datapermission: config.dataPermission ?? ""
|
|
114559
|
+
};
|
|
114560
|
+
if (config.apiKey) headers["x-api-key"] = config.apiKey;
|
|
114561
|
+
else headers.Authorization = `Bearer ${config.authToken}`;
|
|
114562
|
+
return headers;
|
|
114563
|
+
}
|
|
114564
|
+
function guessVideoMimeType(fileName) {
|
|
114565
|
+
const lower = fileName.toLowerCase();
|
|
114566
|
+
if (lower.endsWith(".mp4")) return "video/mp4";
|
|
114567
|
+
if (lower.endsWith(".mov")) return "video/quicktime";
|
|
114568
|
+
if (lower.endsWith(".webm")) return "video/webm";
|
|
114569
|
+
if (lower.endsWith(".avi")) return "video/x-msvideo";
|
|
114570
|
+
if (lower.endsWith(".mpeg") || lower.endsWith(".mpg")) return "video/mpeg";
|
|
114571
|
+
return "application/octet-stream";
|
|
114572
|
+
}
|
|
114573
|
+
function runPmaxVideoValidation(configFile, cfg) {
|
|
114574
|
+
const errors = [];
|
|
114575
|
+
const warnings = [];
|
|
114576
|
+
const videoPath = cfg.videoPath?.trim();
|
|
114577
|
+
const youtube = cfg.youtubeUrlOrId?.trim();
|
|
114578
|
+
if (videoPath && youtube) {
|
|
114579
|
+
errors.push("videoPath \u4E0E youtubeUrlOrId \u53EA\u80FD\u4E8C\u9009\u4E00\uFF08\u672C\u5730\u6587\u4EF6\u8D70 PyAPI \u4E0A\u4F20\uFF0C\u540E\u8005\u4E3A\u5DF2\u6709 YouTube\uFF09");
|
|
114580
|
+
return { errors, warnings };
|
|
114581
|
+
}
|
|
114582
|
+
if (!videoPath) return { errors, warnings };
|
|
114583
|
+
if (!VIDEO_UPLOAD_SUFFIX.test(videoPath)) {
|
|
114584
|
+
errors.push(
|
|
114585
|
+
`videoPath \u6269\u5C55\u540D\u4E0D\u53D7\u652F\u6301\uFF08${videoPath}\uFF09\uFF0C\u8BF7\u4F7F\u7528 .mp4 / .mov / .webm / .avi / .mpeg / .mpg`
|
|
114586
|
+
);
|
|
114587
|
+
}
|
|
114588
|
+
const abs = resolveVideoPath(configFile, videoPath);
|
|
114589
|
+
if (!existsSync3(abs)) {
|
|
114590
|
+
errors.push(`videoPath \u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${abs}`);
|
|
114591
|
+
return { errors, warnings };
|
|
114592
|
+
}
|
|
114593
|
+
try {
|
|
114594
|
+
const buf = readFileSync8(abs);
|
|
114595
|
+
if (buf.length === 0) errors.push(`videoPath \u4E3A\u7A7A\u6587\u4EF6\uFF1A${abs}`);
|
|
114596
|
+
} catch (e) {
|
|
114597
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
114598
|
+
errors.push(`\u65E0\u6CD5\u8BFB\u53D6 videoPath\uFF08${abs}\uFF09\uFF1A${msg}`);
|
|
114599
|
+
}
|
|
114600
|
+
if (!cfg.videoTitle?.trim()) {
|
|
114601
|
+
warnings.push("\u672A\u8BBE\u7F6E videoTitle\uFF0C\u5C06\u4F7F\u7528\u6587\u4EF6\u540D\u4F5C\u4E3A YouTube \u4E0A\u4F20\u6807\u9898");
|
|
114602
|
+
}
|
|
114603
|
+
return { errors, warnings };
|
|
114604
|
+
}
|
|
114605
|
+
async function fetchVideoUploadStatus(config, googleApiUrl, mediaAccountId, resourceName, verbose) {
|
|
114606
|
+
const url = pyapiVideoStatusUrl(googleApiUrl, mediaAccountId, resourceName);
|
|
114607
|
+
const res = await fetch(url, { method: "GET", headers: authHeaders(config) });
|
|
114608
|
+
const text = await res.text();
|
|
114609
|
+
if (!res.ok) {
|
|
114610
|
+
const detail = verbose ? `\uFF1A${text.slice(0, 300)}` : "";
|
|
114611
|
+
throw new Error(`\u67E5\u8BE2\u89C6\u9891\u5904\u7406\u72B6\u6001 HTTP ${res.status}${detail}`);
|
|
114612
|
+
}
|
|
114613
|
+
try {
|
|
114614
|
+
return JSON.parse(text);
|
|
114615
|
+
} catch {
|
|
114616
|
+
throw new Error("\u67E5\u8BE2\u89C6\u9891\u5904\u7406\u72B6\u6001\u54CD\u5E94\u975E JSON");
|
|
114617
|
+
}
|
|
114618
|
+
}
|
|
114619
|
+
function sleep2(ms) {
|
|
114620
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
114621
|
+
}
|
|
114622
|
+
async function pollVideoUntilProcessed(config, googleApiUrl, mediaAccountId, resourceName, verbose, onProgress) {
|
|
114623
|
+
for (let attempt = 1; attempt <= PMAX_VIDEO_POLL_MAX_ATTEMPTS; attempt++) {
|
|
114624
|
+
const status = await fetchVideoUploadStatus(
|
|
114625
|
+
config,
|
|
114626
|
+
googleApiUrl,
|
|
114627
|
+
mediaAccountId,
|
|
114628
|
+
resourceName,
|
|
114629
|
+
verbose
|
|
114630
|
+
);
|
|
114631
|
+
const state = (status.state ?? "UNKNOWN").toUpperCase();
|
|
114632
|
+
onProgress?.(state, attempt);
|
|
114633
|
+
if (status.video_id?.trim()) {
|
|
114634
|
+
return status.video_id.trim();
|
|
114635
|
+
}
|
|
114636
|
+
if (FAILED_VIDEO_STATES.has(state)) {
|
|
114637
|
+
throw new Error(`\u89C6\u9891\u5904\u7406\u5931\u8D25\uFF08state=${state}\uFF0Cresource=${resourceName}\uFF09`);
|
|
114638
|
+
}
|
|
114639
|
+
if (TERMINAL_VIDEO_STATES.has(state) && state === "PROCESSED" && !status.video_id) {
|
|
114640
|
+
throw new Error(`\u89C6\u9891\u5DF2 PROCESSED \u4F46\u672A\u8FD4\u56DE video_id\uFF08resource=${resourceName}\uFF09`);
|
|
114641
|
+
}
|
|
114642
|
+
if (TERMINAL_VIDEO_STATES.has(state) && state !== "PROCESSED") {
|
|
114643
|
+
throw new Error(`\u89C6\u9891\u5904\u7406\u7ED3\u675F\u4F46\u65E0 video_id\uFF08state=${state}\uFF09`);
|
|
114644
|
+
}
|
|
114645
|
+
if (attempt < PMAX_VIDEO_POLL_MAX_ATTEMPTS) {
|
|
114646
|
+
await sleep2(PMAX_VIDEO_POLL_INTERVAL_MS);
|
|
114647
|
+
}
|
|
114648
|
+
}
|
|
114649
|
+
throw new Error(
|
|
114650
|
+
`\u7B49\u5F85\u89C6\u9891\u5904\u7406\u8D85\u65F6\uFF08\u5DF2\u8F6E\u8BE2 ${PMAX_VIDEO_POLL_MAX_ATTEMPTS} \u6B21\uFF0C\u7EA6 ${PMAX_VIDEO_POLL_MAX_ATTEMPTS * PMAX_VIDEO_POLL_INTERVAL_MS / 6e4} \u5206\u949F\uFF09`
|
|
114651
|
+
);
|
|
114652
|
+
}
|
|
114653
|
+
async function uploadPmaxLocalVideo(opts) {
|
|
114654
|
+
const fileName = basename5(opts.absVideoPath);
|
|
114655
|
+
const title = (opts.title ?? fileName).trim() || fileName;
|
|
114656
|
+
const description = (opts.description ?? "Uploaded via siluzan-tso PMax video upload").trim() || "Uploaded via siluzan-tso";
|
|
114657
|
+
const fileBuffer = readFileSync8(opts.absVideoPath);
|
|
114658
|
+
const form = new FormData();
|
|
114659
|
+
form.append("media_account_id", opts.accountId.replace(/-/g, ""));
|
|
114660
|
+
form.append("title", title);
|
|
114661
|
+
form.append("description", description);
|
|
114662
|
+
form.append("wait_for_processed", "false");
|
|
114663
|
+
form.append(
|
|
114664
|
+
"file",
|
|
114665
|
+
new Blob([new Uint8Array(fileBuffer)], { type: guessVideoMimeType(fileName) }),
|
|
114666
|
+
fileName
|
|
114667
|
+
);
|
|
114668
|
+
const url = pyapiVideoUploadUrl(opts.googleApiUrl);
|
|
114669
|
+
const res = await fetch(url, { method: "POST", headers: authHeaders(opts.config), body: form });
|
|
114670
|
+
const text = await res.text();
|
|
114671
|
+
if (!res.ok) {
|
|
114672
|
+
const detail = opts.verbose ? `\uFF1A${text.slice(0, 400)}` : "";
|
|
114673
|
+
throw new Error(`\u4E0A\u4F20\u89C6\u9891 HTTP ${res.status}${detail}\uFF08${opts.absVideoPath}\uFF09`);
|
|
114674
|
+
}
|
|
114675
|
+
let data;
|
|
114676
|
+
try {
|
|
114677
|
+
data = JSON.parse(text);
|
|
114678
|
+
} catch {
|
|
114679
|
+
throw new Error(`\u4E0A\u4F20\u89C6\u9891\u54CD\u5E94\u975E JSON\uFF08${opts.absVideoPath}\uFF09`);
|
|
114680
|
+
}
|
|
114681
|
+
if (data.video_id?.trim()) {
|
|
114682
|
+
return data.video_id.trim();
|
|
114683
|
+
}
|
|
114684
|
+
const resourceName = data.resource_name?.trim();
|
|
114685
|
+
if (!resourceName) {
|
|
114686
|
+
throw new Error(`\u4E0A\u4F20\u89C6\u9891\u672A\u8FD4\u56DE resource_name \u6216 video_id\uFF08${opts.absVideoPath}\uFF09`);
|
|
114687
|
+
}
|
|
114688
|
+
opts.onProgress?.(`\u5DF2\u4E0A\u4F20\uFF0C\u7B49\u5F85 Google \u5904\u7406\uFF08state=${data.state ?? "UPLOADED"}\uFF09\u2026`);
|
|
114689
|
+
return pollVideoUntilProcessed(
|
|
114690
|
+
opts.config,
|
|
114691
|
+
opts.googleApiUrl,
|
|
114692
|
+
opts.accountId,
|
|
114693
|
+
resourceName,
|
|
114694
|
+
opts.verbose,
|
|
114695
|
+
(state, attempt) => {
|
|
114696
|
+
opts.onProgress?.(`\u5904\u7406\u4E2D state=${state}\uFF08\u7B2C ${attempt}/${PMAX_VIDEO_POLL_MAX_ATTEMPTS} \u6B21\u67E5\u8BE2\uFF09`);
|
|
114697
|
+
}
|
|
114698
|
+
);
|
|
114699
|
+
}
|
|
114700
|
+
|
|
114384
114701
|
// src/commands/ad/pmax-create.ts
|
|
114385
114702
|
async function runAdPmaxCreate(opts) {
|
|
114386
114703
|
const cfg = loadPmaxCreateConfig(opts.configFile);
|
|
114704
|
+
const rawCfg = loadPmaxCreateConfigRaw(opts.configFile) ?? {};
|
|
114705
|
+
const deprecatedVideoKeys = detectDeprecatedPmaxVideoKeys(rawCfg);
|
|
114387
114706
|
const {
|
|
114388
114707
|
errors: cfgErrors,
|
|
114389
114708
|
warnings: cfgWarnings,
|
|
@@ -114393,8 +114712,12 @@ async function runAdPmaxCreate(opts) {
|
|
|
114393
114712
|
opts.configFile,
|
|
114394
114713
|
cfg
|
|
114395
114714
|
);
|
|
114396
|
-
const errors =
|
|
114397
|
-
|
|
114715
|
+
const { errors: videoErrors, warnings: videoWarnings } = runPmaxVideoValidation(
|
|
114716
|
+
opts.configFile,
|
|
114717
|
+
cfg
|
|
114718
|
+
);
|
|
114719
|
+
const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors];
|
|
114720
|
+
const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings];
|
|
114398
114721
|
if (warnings.length > 0) {
|
|
114399
114722
|
console.warn("\n\u26A0\uFE0F PMax \u914D\u7F6E\u8B66\u544A\uFF08\u4E0D\u963B\u65AD\u63D0\u4EA4\uFF09\uFF1A");
|
|
114400
114723
|
for (const w of warnings) console.warn(` \u2022 ${w}`);
|
|
@@ -114464,6 +114787,65 @@ async function runAdPmaxCreate(opts) {
|
|
|
114464
114787
|
const campaignId = data["campaignId"] ?? data["campaign_id"];
|
|
114465
114788
|
const assetGroupId = data["assetGroupId"] ?? data["asset_group_id"];
|
|
114466
114789
|
const budgetId = data["budgetId"] ?? data["budget_id"];
|
|
114790
|
+
let youtubeTarget = cfg.youtubeUrlOrId?.trim();
|
|
114791
|
+
const videoPath = cfg.videoPath?.trim();
|
|
114792
|
+
if (videoPath && assetGroupId != null) {
|
|
114793
|
+
const absVideo = resolvePmaxImagePath2(opts.configFile, videoPath);
|
|
114794
|
+
if (!opts.jsonOut) console.log(`
|
|
114795
|
+
\u{1F4E4} \u4E0A\u4F20\u672C\u5730\u89C6\u9891\uFF08PyAPI\uFF09\u2026`);
|
|
114796
|
+
try {
|
|
114797
|
+
youtubeTarget = await uploadPmaxLocalVideo({
|
|
114798
|
+
config,
|
|
114799
|
+
googleApiUrl,
|
|
114800
|
+
accountId,
|
|
114801
|
+
absVideoPath: absVideo,
|
|
114802
|
+
title: cfg.videoTitle,
|
|
114803
|
+
description: cfg.videoDescription,
|
|
114804
|
+
verbose: opts.verbose,
|
|
114805
|
+
onProgress: opts.jsonOut ? void 0 : (msg) => console.log(` ${msg}`)
|
|
114806
|
+
});
|
|
114807
|
+
if (!opts.jsonOut) console.log(` video_id\uFF1A${youtubeTarget}`);
|
|
114808
|
+
} catch (err) {
|
|
114809
|
+
console.error(
|
|
114810
|
+
`
|
|
114811
|
+
\u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u672C\u5730\u89C6\u9891\u4E0A\u4F20\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
|
|
114812
|
+
);
|
|
114813
|
+
console.error(
|
|
114814
|
+
` \u53EF\u624B\u52A8\u4E0A\u4F20\u540E\u94FE\u63A5\uFF1Asiluzan-tso ad pmax-youtube-link -a ${accountId} --asset-group-id ${assetGroupId}` + (campaignId != null ? ` --campaign-id ${campaignId}` : "") + ` --youtube "<video_id>"
|
|
114815
|
+
`
|
|
114816
|
+
);
|
|
114817
|
+
youtubeTarget = void 0;
|
|
114818
|
+
}
|
|
114819
|
+
}
|
|
114820
|
+
if (youtubeTarget && assetGroupId != null) {
|
|
114821
|
+
try {
|
|
114822
|
+
const yt = await postPmaxYoutubeLink({
|
|
114823
|
+
config,
|
|
114824
|
+
googleApiUrl,
|
|
114825
|
+
accountId,
|
|
114826
|
+
assetGroupId: String(assetGroupId),
|
|
114827
|
+
youtubeUrlOrId: youtubeTarget,
|
|
114828
|
+
campaignId: campaignId != null ? String(campaignId) : void 0,
|
|
114829
|
+
assetName: cfg.youtubeAssetName,
|
|
114830
|
+
verbose: opts.verbose
|
|
114831
|
+
});
|
|
114832
|
+
if (!opts.jsonOut) {
|
|
114833
|
+
console.log(`
|
|
114834
|
+
\u2705 \u5DF2\u94FE\u63A5 YouTube\uFF1A${youtubeTarget}`);
|
|
114835
|
+
const arn = yt["assetResourceName"];
|
|
114836
|
+
if (arn != null) console.log(` assetResourceName\uFF1A${arn}`);
|
|
114837
|
+
}
|
|
114838
|
+
} catch (err) {
|
|
114839
|
+
console.error(
|
|
114840
|
+
`
|
|
114841
|
+
\u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46 YouTube \u94FE\u63A5\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
|
|
114842
|
+
);
|
|
114843
|
+
console.error(
|
|
114844
|
+
` \u8BF7\u624B\u52A8\u6267\u884C\uFF1Asiluzan-tso ad pmax-youtube-link -a ${accountId} --asset-group-id ${assetGroupId}` + (campaignId != null ? ` --campaign-id ${campaignId}` : "") + ` --youtube "${youtubeTarget}"
|
|
114845
|
+
`
|
|
114846
|
+
);
|
|
114847
|
+
}
|
|
114848
|
+
}
|
|
114467
114849
|
console.log("\n\u2705 PMax \u5E7F\u544A\u7CFB\u5217\u5DF2\u521B\u5EFA\uFF08\u540C\u6B65\uFF09");
|
|
114468
114850
|
console.log(` \u6D3B\u52A8\u540D\u79F0\uFF1A${cfg.name}`);
|
|
114469
114851
|
if (campaignId != null) console.log(` campaignId\uFF1A${campaignId}`);
|
|
@@ -114473,6 +114855,11 @@ async function runAdPmaxCreate(opts) {
|
|
|
114473
114855
|
`
|
|
114474
114856
|
\u590D\u6838\uFF1Asiluzan-tso ad campaigns -a ${accountId} --json-out # channelTypeV2 \u5E94\u4E3A PERFORMANCE_MAX`
|
|
114475
114857
|
);
|
|
114858
|
+
if (!youtubeTarget && !videoPath) {
|
|
114859
|
+
console.log(
|
|
114860
|
+
" \u89C6\u9891\uFF1A\u914D\u7F6E videoPath\uFF08\u672C\u5730\uFF09\u6216 youtubeUrlOrId \u53EF\u5728\u521B\u5EFA\u65F6\u81EA\u52A8\u94FE\u63A5\uFF1B\u6216\u521B\u5EFA\u540E ad pmax-youtube-link"
|
|
114861
|
+
);
|
|
114862
|
+
}
|
|
114476
114863
|
console.log();
|
|
114477
114864
|
}
|
|
114478
114865
|
|
|
@@ -114481,6 +114868,8 @@ import { writeFileSync as writeFileSync3 } from "fs";
|
|
|
114481
114868
|
init_cli_json_snapshot();
|
|
114482
114869
|
async function runAdPmaxValidate(opts) {
|
|
114483
114870
|
const cfg = loadPmaxCreateConfig(opts.configFile);
|
|
114871
|
+
const rawCfg = loadPmaxCreateConfigRaw(opts.configFile) ?? {};
|
|
114872
|
+
const deprecatedVideoKeys = detectDeprecatedPmaxVideoKeys(rawCfg);
|
|
114484
114873
|
const {
|
|
114485
114874
|
errors: cfgErrors,
|
|
114486
114875
|
warnings: cfgWarnings,
|
|
@@ -114490,8 +114879,12 @@ async function runAdPmaxValidate(opts) {
|
|
|
114490
114879
|
opts.configFile,
|
|
114491
114880
|
cfg
|
|
114492
114881
|
);
|
|
114493
|
-
const errors =
|
|
114494
|
-
|
|
114882
|
+
const { errors: videoErrors, warnings: videoWarnings } = runPmaxVideoValidation(
|
|
114883
|
+
opts.configFile,
|
|
114884
|
+
cfg
|
|
114885
|
+
);
|
|
114886
|
+
const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors];
|
|
114887
|
+
const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings];
|
|
114495
114888
|
const lengthViolations = cfgLengthViolations;
|
|
114496
114889
|
if (opts.writeNormalized) {
|
|
114497
114890
|
const toWrite = stripMetaKeysForExport(cfg);
|
|
@@ -114540,110 +114933,7 @@ async function runAdPmaxValidate(opts) {
|
|
|
114540
114933
|
}
|
|
114541
114934
|
|
|
114542
114935
|
// src/commands/ad/pmax-mgmt.ts
|
|
114543
|
-
import { basename as
|
|
114544
|
-
|
|
114545
|
-
// src/commands/ad/pmax-shared.ts
|
|
114546
|
-
init_auth();
|
|
114547
|
-
init_cli_json_snapshot();
|
|
114548
|
-
import { readFileSync as readFileSync7 } from "fs";
|
|
114549
|
-
import { dirname as dirname9, isAbsolute as isAbsolute4, resolve as resolve8 } from "path";
|
|
114550
|
-
var PMAX_MONEY_KEYS = /* @__PURE__ */ new Set(["budget", "targetCpa_BidingAmount"]);
|
|
114551
|
-
function loadPmaxJsonFile(configFile) {
|
|
114552
|
-
let raw;
|
|
114553
|
-
try {
|
|
114554
|
-
raw = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
114555
|
-
} catch (e) {
|
|
114556
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
114557
|
-
console.error(`
|
|
114558
|
-
\u274C \u8BFB\u53D6 JSON \u5931\u8D25\uFF08${configFile}\uFF09\uFF1A${msg}
|
|
114559
|
-
`);
|
|
114560
|
-
process.exit(1);
|
|
114561
|
-
}
|
|
114562
|
-
return stripMetaKeys(raw);
|
|
114563
|
-
}
|
|
114564
|
-
function resolvePmaxImagePath2(configFile, relOrAbs) {
|
|
114565
|
-
const trimmed = relOrAbs.trim();
|
|
114566
|
-
if (isAbsolute4(trimmed)) return trimmed;
|
|
114567
|
-
return resolve8(dirname9(configFile), trimmed);
|
|
114568
|
-
}
|
|
114569
|
-
function convertPmaxMoneyInObject(body) {
|
|
114570
|
-
const out = { ...body };
|
|
114571
|
-
for (const key of PMAX_MONEY_KEYS) {
|
|
114572
|
-
const val = out[key];
|
|
114573
|
-
if (typeof val === "number" && Number.isFinite(val)) {
|
|
114574
|
-
out[key] = toCentAmount(val);
|
|
114575
|
-
}
|
|
114576
|
-
}
|
|
114577
|
-
return out;
|
|
114578
|
-
}
|
|
114579
|
-
function buildPmaxAssetGroupApiBody(cfg, imageSlots) {
|
|
114580
|
-
assertPmaxImageSlotsResolved(imageSlots);
|
|
114581
|
-
return {
|
|
114582
|
-
name: cfg.name.trim(),
|
|
114583
|
-
finalUrls: cfg.finalUrls.map((u) => u.trim()).filter(Boolean),
|
|
114584
|
-
headlines: cfg.headlines.map((h) => h.trim()).filter(Boolean),
|
|
114585
|
-
longHeadlines: cfg.longHeadlines.map((h) => h.trim()).filter(Boolean),
|
|
114586
|
-
descriptions: cfg.descriptions.map((d) => d.trim()).filter(Boolean),
|
|
114587
|
-
businessName: cfg.businessName.trim(),
|
|
114588
|
-
...imageSlots
|
|
114589
|
-
};
|
|
114590
|
-
}
|
|
114591
|
-
async function processAssetsUpdateBodyWithUpload(configFile, body, accountId, config, googleApiUrl, verbose) {
|
|
114592
|
-
const out = { ...body };
|
|
114593
|
-
const links = out["assetsToLink"];
|
|
114594
|
-
if (!Array.isArray(links)) return out;
|
|
114595
|
-
out["assetsToLink"] = await Promise.all(
|
|
114596
|
-
links.map(async (item) => {
|
|
114597
|
-
if (!item || typeof item !== "object") return item;
|
|
114598
|
-
const row = { ...item };
|
|
114599
|
-
const imagePath = row["imagePath"];
|
|
114600
|
-
if (typeof imagePath === "string" && imagePath.trim()) {
|
|
114601
|
-
const abs = resolvePmaxImagePath2(configFile, imagePath);
|
|
114602
|
-
row["assetId"] = await uploadPmaxImageFile(
|
|
114603
|
-
config,
|
|
114604
|
-
googleApiUrl,
|
|
114605
|
-
accountId,
|
|
114606
|
-
abs,
|
|
114607
|
-
void 0,
|
|
114608
|
-
verbose
|
|
114609
|
-
);
|
|
114610
|
-
delete row["imagePath"];
|
|
114611
|
-
delete row["imageBase64"];
|
|
114612
|
-
}
|
|
114613
|
-
return row;
|
|
114614
|
-
})
|
|
114615
|
-
);
|
|
114616
|
-
return out;
|
|
114617
|
-
}
|
|
114618
|
-
async function withGoogleApi(opts, fn) {
|
|
114619
|
-
const config = await ensureDataPermission(loadConfig(opts.token));
|
|
114620
|
-
const googleApiUrl = requireGoogleApi(config);
|
|
114621
|
-
return fn(config, googleApiUrl);
|
|
114622
|
-
}
|
|
114623
|
-
async function pmaxApiFetch(url, config, init, verbose) {
|
|
114624
|
-
return apiFetch2(url, config, init ?? {}, verbose);
|
|
114625
|
-
}
|
|
114626
|
-
async function emitPmaxResult(opts, section, commandLabel, payload, idSuffix) {
|
|
114627
|
-
return emitCliJsonOrSnapshot(opts, {
|
|
114628
|
-
section,
|
|
114629
|
-
commandLabel,
|
|
114630
|
-
commandHint: idSuffix ?? opts.account ?? "",
|
|
114631
|
-
payload,
|
|
114632
|
-
idSuffix: idSuffix ?? opts.account
|
|
114633
|
-
});
|
|
114634
|
-
}
|
|
114635
|
-
function requireAccountId(account, label = "-a, --account") {
|
|
114636
|
-
const id = account?.toString().trim();
|
|
114637
|
-
if (!id) {
|
|
114638
|
-
console.error(`
|
|
114639
|
-
\u274C \u8BF7\u6307\u5B9A\u5A92\u4F53\u8D26\u6237 ID\uFF08${label}\uFF09
|
|
114640
|
-
`);
|
|
114641
|
-
process.exit(1);
|
|
114642
|
-
}
|
|
114643
|
-
return id;
|
|
114644
|
-
}
|
|
114645
|
-
|
|
114646
|
-
// src/commands/ad/pmax-mgmt.ts
|
|
114936
|
+
import { basename as basename6, isAbsolute as isAbsolute6 } from "path";
|
|
114647
114937
|
function buildReportQuery(opts) {
|
|
114648
114938
|
const params = new URLSearchParams();
|
|
114649
114939
|
params.set("startDate", toGoogleDate(opts.startDate, -30).replace(/\//g, "-"));
|
|
@@ -114991,27 +115281,31 @@ async function runAdPmaxAssetsUpdate(opts) {
|
|
|
114991
115281
|
async function runAdPmaxYoutubeLink(opts) {
|
|
114992
115282
|
const accountId = requireAccountId(opts.account);
|
|
114993
115283
|
const assetGroupId = opts.assetGroupId.trim();
|
|
114994
|
-
let body;
|
|
114995
|
-
if (opts.bodyFile) {
|
|
114996
|
-
body = loadPmaxJsonFile(opts.bodyFile);
|
|
114997
|
-
} else {
|
|
114998
|
-
body = {
|
|
114999
|
-
youtubeUrlOrId: opts.youtubeUrlOrId.trim()
|
|
115000
|
-
};
|
|
115001
|
-
if (opts.campaignId?.trim()) body["campaignId"] = opts.campaignId.trim();
|
|
115002
|
-
if (opts.assetName?.trim()) body["assetName"] = opts.assetName.trim();
|
|
115003
|
-
}
|
|
115004
115284
|
await withGoogleApi(opts, async (config, googleApiUrl) => {
|
|
115005
|
-
const url = pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, "youtube");
|
|
115006
115285
|
let data;
|
|
115007
115286
|
try {
|
|
115008
|
-
|
|
115009
|
-
url,
|
|
115010
|
-
|
|
115011
|
-
|
|
115012
|
-
|
|
115013
|
-
|
|
115014
|
-
|
|
115287
|
+
if (opts.bodyFile) {
|
|
115288
|
+
const url = pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, "youtube");
|
|
115289
|
+
const body = loadPmaxJsonFile(opts.bodyFile);
|
|
115290
|
+
const raw = await pmaxApiFetch(
|
|
115291
|
+
url,
|
|
115292
|
+
config,
|
|
115293
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
115294
|
+
opts.verbose
|
|
115295
|
+
);
|
|
115296
|
+
data = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : { data: raw };
|
|
115297
|
+
} else {
|
|
115298
|
+
data = await postPmaxYoutubeLink({
|
|
115299
|
+
config,
|
|
115300
|
+
googleApiUrl,
|
|
115301
|
+
accountId,
|
|
115302
|
+
assetGroupId,
|
|
115303
|
+
youtubeUrlOrId: opts.youtubeUrlOrId,
|
|
115304
|
+
campaignId: opts.campaignId,
|
|
115305
|
+
assetName: opts.assetName,
|
|
115306
|
+
verbose: opts.verbose
|
|
115307
|
+
});
|
|
115308
|
+
}
|
|
115015
115309
|
} catch (err) {
|
|
115016
115310
|
console.error(
|
|
115017
115311
|
`
|
|
@@ -115202,8 +115496,8 @@ async function runAdPmaxImageUpload(opts) {
|
|
|
115202
115496
|
let data;
|
|
115203
115497
|
try {
|
|
115204
115498
|
if (opts.imagePath?.trim()) {
|
|
115205
|
-
const abs =
|
|
115206
|
-
const name2 = (opts.name ??
|
|
115499
|
+
const abs = isAbsolute6(opts.imagePath) ? opts.imagePath.trim() : resolvePmaxImagePath2(process.cwd(), opts.imagePath);
|
|
115500
|
+
const name2 = (opts.name ?? basename6(abs)).trim();
|
|
115207
115501
|
const id = await uploadPmaxImageFile(
|
|
115208
115502
|
config,
|
|
115209
115503
|
googleApiUrl,
|
|
@@ -115391,8 +115685,8 @@ async function runAdCampaignValidate(opts) {
|
|
|
115391
115685
|
}
|
|
115392
115686
|
|
|
115393
115687
|
// src/commands/ad/pmax-image-convert.ts
|
|
115394
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as
|
|
115395
|
-
import { resolve as
|
|
115688
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as readFileSync9, existsSync as existsSync4 } from "fs";
|
|
115689
|
+
import { resolve as resolve10, dirname as dirname11, basename as basename7, extname } from "path";
|
|
115396
115690
|
var SPECS = [
|
|
115397
115691
|
{ kind: "marketing", width: 1200, height: 628, fit: "cover", suffix: "_marketing" },
|
|
115398
115692
|
{ kind: "square", width: 1200, height: 1200, fit: "cover", suffix: "_square" },
|
|
@@ -115439,9 +115733,9 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
115439
115733
|
(s) => s.kind === "logo" && opts.logoUseCover ? { ...s, fit: "cover" } : s
|
|
115440
115734
|
);
|
|
115441
115735
|
const firstInput = input ?? inputMarketing ?? inputSquare ?? inputLogo;
|
|
115442
|
-
const outputDir = opts.outputDir ?
|
|
115736
|
+
const outputDir = opts.outputDir ? resolve10(opts.outputDir) : dirname11(resolve10(firstInput));
|
|
115443
115737
|
mkdirSync3(outputDir, { recursive: true });
|
|
115444
|
-
const prefix = opts.prefix ??
|
|
115738
|
+
const prefix = opts.prefix ?? basename7(firstInput, extname(firstInput));
|
|
115445
115739
|
const quality = opts.quality ?? 85;
|
|
115446
115740
|
const sharpMod = await loadSharp();
|
|
115447
115741
|
const sharpFn = (p) => sharpMod(p);
|
|
@@ -115453,15 +115747,15 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
115453
115747
|
console.warn(`\u26A0\uFE0F ${spec.kind}\uFF1A\u672A\u6307\u5B9A\u8F93\u5165\u56FE\uFF0C\u8DF3\u8FC7`);
|
|
115454
115748
|
continue;
|
|
115455
115749
|
}
|
|
115456
|
-
const absInput =
|
|
115457
|
-
if (!
|
|
115750
|
+
const absInput = resolve10(src);
|
|
115751
|
+
if (!existsSync4(absInput)) {
|
|
115458
115752
|
console.error(`\u274C \u8F93\u5165\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${absInput}`);
|
|
115459
115753
|
hasError = true;
|
|
115460
115754
|
continue;
|
|
115461
115755
|
}
|
|
115462
115756
|
const outExt = extname(absInput).toLowerCase() === ".png" ? ".png" : ".jpg";
|
|
115463
115757
|
const outName = `${prefix}${spec.suffix}${outExt}`;
|
|
115464
|
-
const absOutput =
|
|
115758
|
+
const absOutput = resolve10(outputDir, outName);
|
|
115465
115759
|
try {
|
|
115466
115760
|
await convertOne(sharpFn, absInput, absOutput, spec, quality);
|
|
115467
115761
|
const sizeKB = Math.ceil(
|
|
@@ -115480,9 +115774,9 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
115480
115774
|
process.exit(1);
|
|
115481
115775
|
}
|
|
115482
115776
|
if (opts.updateConfig) {
|
|
115483
|
-
const configPath =
|
|
115777
|
+
const configPath = resolve10(opts.updateConfig);
|
|
115484
115778
|
try {
|
|
115485
|
-
const raw = JSON.parse(
|
|
115779
|
+
const raw = JSON.parse(readFileSync9(configPath, "utf8"));
|
|
115486
115780
|
const imgPaths = raw["imagePaths"] ?? {};
|
|
115487
115781
|
if (outputPaths.marketing) imgPaths["marketing"] = outputPaths.marketing;
|
|
115488
115782
|
if (outputPaths.square) imgPaths["square"] = outputPaths.square;
|
|
@@ -115891,7 +116185,7 @@ function register20(program2) {
|
|
|
115891
116185
|
}
|
|
115892
116186
|
);
|
|
115893
116187
|
adCmd.command("pmax-create").description(
|
|
115894
|
-
'\u65B0\u5EFA Performance Max \u5E7F\u544A\u7CFB\u5217\uFF08\u540C\u6B65 API\uFF1B\u4EC5\u652F\u6301 JSON \u914D\u7F6E\u6587\u4EF6\uFF09\n\n \u7528\u6CD5\uFF1A\n 1. \u590D\u5236 pmax-create-template.json\uFF0C\u5B57\u6BB5\u8BF4\u660E\u89C1 pmax-create-template.md\n 2. siluzan-tso ad geo search -a <accountId> -q "United States"\n 3. siluzan-tso ad pmax-validate --config-file ./pmax.json\n 4. siluzan-tso ad pmax-create --config-file ./pmax.json # imagePaths \u81EA\u52A8\u4E0A\u4F20\n 5. siluzan-tso ad campaigns -a <accountId> --json-out # \u786E\u8BA4 PERFORMANCE_MAX'
|
|
116188
|
+
'\u65B0\u5EFA Performance Max \u5E7F\u544A\u7CFB\u5217\uFF08\u540C\u6B65 API\uFF1B\u4EC5\u652F\u6301 JSON \u914D\u7F6E\u6587\u4EF6\uFF09\n\n \u7528\u6CD5\uFF1A\n 1. \u590D\u5236 pmax-create-template.json\uFF0C\u5B57\u6BB5\u8BF4\u660E\u89C1 pmax-create-template.md\n 2. siluzan-tso ad geo search -a <accountId> -q "United States"\n 3. siluzan-tso ad pmax-validate --config-file ./pmax.json\n 4. siluzan-tso ad pmax-create --config-file ./pmax.json # imagePaths \u81EA\u52A8\u4E0A\u4F20\uFF1BvideoPath \u6216 youtubeUrlOrId \u94FE\u63A5\u89C6\u9891\n 5. siluzan-tso ad campaigns -a <accountId> --json-out # \u786E\u8BA4 PERFORMANCE_MAX\n \u89C6\u9891\uFF1AvideoPath\uFF08PyAPI \u4E0A\u4F20\uFF09\u6216 youtubeUrlOrId\uFF1B\u6216\u521B\u5EFA\u540E pmax-youtube-link'
|
|
115895
116189
|
).requiredOption(
|
|
115896
116190
|
"--config-file <path>",
|
|
115897
116191
|
"JSON \u914D\u7F6E\u6587\u4EF6\uFF08\u6A21\u677F\u89C1 assets/siluzan-ads/assets/pmax-create-*.json|md\uFF09"
|
package/dist/skill/_meta.json
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
| 文案超长 | `pmax-validate --json-out` 读 `lengthViolations`(含完整 `text`);**勿自动截断**,列改写方案给用户确认后再改 JSON 并重跑 validate(同 Search `campaign-validate`) |
|
|
17
17
|
| 金额 | JSON 填**主币种「元」**;CLI 提交前 `budget`、`targetCpa_BidingAmount` ×100 |
|
|
18
18
|
| 图片 | **只填 `imagePaths`** 指向本地 PNG/JPEG;`pmax-create` 会自动 multipart 上传并用 assetId 创建(勿把 Base64 提交进 Git) |
|
|
19
|
+
| 视频 | **`videoPath`**(本地,PyAPI 上传)或 **`youtubeUrlOrId`**(已有 YouTube)二选一;`pmax-create` 成功后自动链接;或 `ad pmax-youtube-link` |
|
|
19
20
|
| 改已上线 PMax | `ad pmax-get` / `pmax-edit` / `pmax-assets-update` 等(见 `references/google-ads/pmax-api.md`) |
|
|
20
21
|
| 列表复核 | `ad campaigns -a <id> --json-out ./snap`,`channelTypeV2` 应为 `PERFORMANCE_MAX` |
|
|
21
22
|
|
|
@@ -61,6 +62,11 @@ siluzan-tso ad campaigns -a <accountId> --json-out ./snap
|
|
|
61
62
|
| `biddingStrategyTypeV2` | string | | 默认 `MAXIMIZE_CONVERSIONS` |
|
|
62
63
|
| `targetCpa_BidingAmount` | number | | `TARGET_CPA` 或带目标 CPA 的 `MAXIMIZE_CONVERSIONS` 时必填(**元**) |
|
|
63
64
|
| `targetRoas` | number | | `TARGET_ROAS` 时必填(如 `2.5` = 250%) |
|
|
65
|
+
| `videoPath` | string | | 本地视频路径(`.mp4` / `.mov` 等);经 PyAPI 上传后自动链接(与 `youtubeUrlOrId` 二选一) |
|
|
66
|
+
| `videoTitle` | string | | PyAPI 上传标题(默认文件名) |
|
|
67
|
+
| `videoDescription` | string | | PyAPI 上传描述(可选) |
|
|
68
|
+
| `youtubeUrlOrId` | string | | 已有 YouTube URL 或 11 位 ID;`pmax-create` 后自动链接 |
|
|
69
|
+
| `youtubeAssetName` | string | | 链接 YouTube 时的资产显示名 |
|
|
64
70
|
|
|
65
71
|
\* 三张图各须有一种来源(路径或 Base64)。
|
|
66
72
|
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
|
|
49
49
|
**图片**:配置 `imagePaths`(相对 JSON 文件目录)或 `marketingImageBase64` / `squareMarketingImageBase64` / `logoImageBase64`。`pmax-validate` 会自动校验图片尺寸(最小值 / 推荐值 / 宽高比 ±2% / 文件大小 ≤5120 KB)。如需将任意图片转为合规素材,用 `ad pmax-image-convert`(`marketing` / `square` / `logo` 三种格式,sharp 处理,居中裁切)。
|
|
50
50
|
|
|
51
|
+
**视频**:`videoPath`(本地文件,经 `{googleApiUrl}/pyapi` 上传并轮询 `video_id` 后自动链接)与 `youtubeUrlOrId` **二选一**;或创建后 `ad pmax-youtube-link`。
|
|
52
|
+
|
|
51
53
|
**禁止**:对已创建的 PMax 系列使用 `ad campaign-edit`(旧 PUT 会 **400**)。
|
|
52
54
|
|
|
53
55
|
### 已上线 PMax 管理(CLI)
|
|
@@ -38,6 +38,12 @@
|
|
|
38
38
|
2. `ad pmax-validate` → 用户确认 → `ad pmax-create`:CLI **自动上传**三张图并写入 `*ImageAssetId`(Body 不含 Base64)。
|
|
39
39
|
3. 可选预上传:`ad pmax-image-upload -a <id> --image-path ./x.png`,将返回 `id` 写入 JSON。
|
|
40
40
|
|
|
41
|
+
### 视频
|
|
42
|
+
|
|
43
|
+
- **`videoPath`**:本地 `.mp4` 等;`pmax-create` 经 **GoogleAdsPyAPI** `POST {googleApiUrl}/pyapi/video/upload` 上传,轮询 `GET .../upload/status` 得 `video_id`,再 `POST .../asset-group/{id}/youtube` 链接(与 `youtubeUrlOrId` 二选一)。
|
|
44
|
+
- **`youtubeUrlOrId`**:已有 YouTube URL 或 11 位 ID;创建成功后自动链接。
|
|
45
|
+
- 或创建后:`ad pmax-youtube-link -a <id> --asset-group-id <agId> --campaign-id <cid> --youtube "<url或ID>"`。
|
|
46
|
+
|
|
41
47
|
---
|
|
42
48
|
|
|
43
49
|
## 编辑流程
|
|
@@ -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.22-beta.
|
|
12
|
+
$PKG_VERSION = '1.1.22-beta.14'
|
|
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.22-beta.
|
|
12
|
+
readonly PKG_VERSION="1.1.22-beta.14"
|
|
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"
|