siluzan-tso-cli 1.1.22-beta.12 → 1.1.22-beta.15
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 +512 -157
- 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 +3 -1
- package/dist/skill/references/google-ads/pmax-api.md +9 -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.22-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.22-beta.15),供内部测试使用。正式发布后安装命令将改为 `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,202 @@ 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
|
+
function resolvePmaxVideoAbsPath(relOrAbs, baseDir = process.cwd()) {
|
|
114701
|
+
const trimmed = relOrAbs.trim();
|
|
114702
|
+
if (isAbsolute5(trimmed)) return trimmed;
|
|
114703
|
+
return resolve9(baseDir, trimmed);
|
|
114704
|
+
}
|
|
114705
|
+
function validatePmaxVideoPathQuick(absPath) {
|
|
114706
|
+
if (!VIDEO_UPLOAD_SUFFIX.test(absPath)) {
|
|
114707
|
+
return `videoPath \u6269\u5C55\u540D\u4E0D\u53D7\u652F\u6301\uFF0C\u8BF7\u4F7F\u7528 .mp4 / .mov / .webm / .avi / .mpeg / .mpg`;
|
|
114708
|
+
}
|
|
114709
|
+
if (!existsSync3(absPath)) {
|
|
114710
|
+
return `\u89C6\u9891\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${absPath}`;
|
|
114711
|
+
}
|
|
114712
|
+
try {
|
|
114713
|
+
if (readFileSync8(absPath).length === 0) return `\u89C6\u9891\u6587\u4EF6\u4E3A\u7A7A\uFF1A${absPath}`;
|
|
114714
|
+
} catch (e) {
|
|
114715
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
114716
|
+
return `\u65E0\u6CD5\u8BFB\u53D6\u89C6\u9891\u6587\u4EF6\uFF08${absPath}\uFF09\uFF1A${msg}`;
|
|
114717
|
+
}
|
|
114718
|
+
return null;
|
|
114719
|
+
}
|
|
114720
|
+
|
|
114384
114721
|
// src/commands/ad/pmax-create.ts
|
|
114385
114722
|
async function runAdPmaxCreate(opts) {
|
|
114386
114723
|
const cfg = loadPmaxCreateConfig(opts.configFile);
|
|
114724
|
+
const rawCfg = loadPmaxCreateConfigRaw(opts.configFile) ?? {};
|
|
114725
|
+
const deprecatedVideoKeys = detectDeprecatedPmaxVideoKeys(rawCfg);
|
|
114387
114726
|
const {
|
|
114388
114727
|
errors: cfgErrors,
|
|
114389
114728
|
warnings: cfgWarnings,
|
|
@@ -114393,8 +114732,12 @@ async function runAdPmaxCreate(opts) {
|
|
|
114393
114732
|
opts.configFile,
|
|
114394
114733
|
cfg
|
|
114395
114734
|
);
|
|
114396
|
-
const errors =
|
|
114397
|
-
|
|
114735
|
+
const { errors: videoErrors, warnings: videoWarnings } = runPmaxVideoValidation(
|
|
114736
|
+
opts.configFile,
|
|
114737
|
+
cfg
|
|
114738
|
+
);
|
|
114739
|
+
const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors];
|
|
114740
|
+
const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings];
|
|
114398
114741
|
if (warnings.length > 0) {
|
|
114399
114742
|
console.warn("\n\u26A0\uFE0F PMax \u914D\u7F6E\u8B66\u544A\uFF08\u4E0D\u963B\u65AD\u63D0\u4EA4\uFF09\uFF1A");
|
|
114400
114743
|
for (const w of warnings) console.warn(` \u2022 ${w}`);
|
|
@@ -114464,6 +114807,65 @@ async function runAdPmaxCreate(opts) {
|
|
|
114464
114807
|
const campaignId = data["campaignId"] ?? data["campaign_id"];
|
|
114465
114808
|
const assetGroupId = data["assetGroupId"] ?? data["asset_group_id"];
|
|
114466
114809
|
const budgetId = data["budgetId"] ?? data["budget_id"];
|
|
114810
|
+
let youtubeTarget = cfg.youtubeUrlOrId?.trim();
|
|
114811
|
+
const videoPath = cfg.videoPath?.trim();
|
|
114812
|
+
if (videoPath && assetGroupId != null) {
|
|
114813
|
+
const absVideo = resolvePmaxImagePath2(opts.configFile, videoPath);
|
|
114814
|
+
if (!opts.jsonOut) console.log(`
|
|
114815
|
+
\u{1F4E4} \u4E0A\u4F20\u672C\u5730\u89C6\u9891\uFF08PyAPI\uFF09\u2026`);
|
|
114816
|
+
try {
|
|
114817
|
+
youtubeTarget = await uploadPmaxLocalVideo({
|
|
114818
|
+
config,
|
|
114819
|
+
googleApiUrl,
|
|
114820
|
+
accountId,
|
|
114821
|
+
absVideoPath: absVideo,
|
|
114822
|
+
title: cfg.videoTitle,
|
|
114823
|
+
description: cfg.videoDescription,
|
|
114824
|
+
verbose: opts.verbose,
|
|
114825
|
+
onProgress: opts.jsonOut ? void 0 : (msg) => console.log(` ${msg}`)
|
|
114826
|
+
});
|
|
114827
|
+
if (!opts.jsonOut) console.log(` video_id\uFF1A${youtubeTarget}`);
|
|
114828
|
+
} catch (err) {
|
|
114829
|
+
console.error(
|
|
114830
|
+
`
|
|
114831
|
+
\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)}`
|
|
114832
|
+
);
|
|
114833
|
+
console.error(
|
|
114834
|
+
` \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>"
|
|
114835
|
+
`
|
|
114836
|
+
);
|
|
114837
|
+
youtubeTarget = void 0;
|
|
114838
|
+
}
|
|
114839
|
+
}
|
|
114840
|
+
if (youtubeTarget && assetGroupId != null) {
|
|
114841
|
+
try {
|
|
114842
|
+
const yt = await postPmaxYoutubeLink({
|
|
114843
|
+
config,
|
|
114844
|
+
googleApiUrl,
|
|
114845
|
+
accountId,
|
|
114846
|
+
assetGroupId: String(assetGroupId),
|
|
114847
|
+
youtubeUrlOrId: youtubeTarget,
|
|
114848
|
+
campaignId: campaignId != null ? String(campaignId) : void 0,
|
|
114849
|
+
assetName: cfg.youtubeAssetName,
|
|
114850
|
+
verbose: opts.verbose
|
|
114851
|
+
});
|
|
114852
|
+
if (!opts.jsonOut) {
|
|
114853
|
+
console.log(`
|
|
114854
|
+
\u2705 \u5DF2\u94FE\u63A5 YouTube\uFF1A${youtubeTarget}`);
|
|
114855
|
+
const arn = yt["assetResourceName"];
|
|
114856
|
+
if (arn != null) console.log(` assetResourceName\uFF1A${arn}`);
|
|
114857
|
+
}
|
|
114858
|
+
} catch (err) {
|
|
114859
|
+
console.error(
|
|
114860
|
+
`
|
|
114861
|
+
\u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46 YouTube \u94FE\u63A5\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
|
|
114862
|
+
);
|
|
114863
|
+
console.error(
|
|
114864
|
+
` \u8BF7\u624B\u52A8\u6267\u884C\uFF1Asiluzan-tso ad pmax-youtube-link -a ${accountId} --asset-group-id ${assetGroupId}` + (campaignId != null ? ` --campaign-id ${campaignId}` : "") + ` --youtube "${youtubeTarget}"
|
|
114865
|
+
`
|
|
114866
|
+
);
|
|
114867
|
+
}
|
|
114868
|
+
}
|
|
114467
114869
|
console.log("\n\u2705 PMax \u5E7F\u544A\u7CFB\u5217\u5DF2\u521B\u5EFA\uFF08\u540C\u6B65\uFF09");
|
|
114468
114870
|
console.log(` \u6D3B\u52A8\u540D\u79F0\uFF1A${cfg.name}`);
|
|
114469
114871
|
if (campaignId != null) console.log(` campaignId\uFF1A${campaignId}`);
|
|
@@ -114473,6 +114875,11 @@ async function runAdPmaxCreate(opts) {
|
|
|
114473
114875
|
`
|
|
114474
114876
|
\u590D\u6838\uFF1Asiluzan-tso ad campaigns -a ${accountId} --json-out # channelTypeV2 \u5E94\u4E3A PERFORMANCE_MAX`
|
|
114475
114877
|
);
|
|
114878
|
+
if (!youtubeTarget && !videoPath) {
|
|
114879
|
+
console.log(
|
|
114880
|
+
" \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"
|
|
114881
|
+
);
|
|
114882
|
+
}
|
|
114476
114883
|
console.log();
|
|
114477
114884
|
}
|
|
114478
114885
|
|
|
@@ -114481,6 +114888,8 @@ import { writeFileSync as writeFileSync3 } from "fs";
|
|
|
114481
114888
|
init_cli_json_snapshot();
|
|
114482
114889
|
async function runAdPmaxValidate(opts) {
|
|
114483
114890
|
const cfg = loadPmaxCreateConfig(opts.configFile);
|
|
114891
|
+
const rawCfg = loadPmaxCreateConfigRaw(opts.configFile) ?? {};
|
|
114892
|
+
const deprecatedVideoKeys = detectDeprecatedPmaxVideoKeys(rawCfg);
|
|
114484
114893
|
const {
|
|
114485
114894
|
errors: cfgErrors,
|
|
114486
114895
|
warnings: cfgWarnings,
|
|
@@ -114490,8 +114899,12 @@ async function runAdPmaxValidate(opts) {
|
|
|
114490
114899
|
opts.configFile,
|
|
114491
114900
|
cfg
|
|
114492
114901
|
);
|
|
114493
|
-
const errors =
|
|
114494
|
-
|
|
114902
|
+
const { errors: videoErrors, warnings: videoWarnings } = runPmaxVideoValidation(
|
|
114903
|
+
opts.configFile,
|
|
114904
|
+
cfg
|
|
114905
|
+
);
|
|
114906
|
+
const errors = [...deprecatedVideoKeys, ...cfgErrors, ...imgErrors, ...videoErrors];
|
|
114907
|
+
const warnings = [...cfgWarnings, ...imgWarnings, ...videoWarnings];
|
|
114495
114908
|
const lengthViolations = cfgLengthViolations;
|
|
114496
114909
|
if (opts.writeNormalized) {
|
|
114497
114910
|
const toWrite = stripMetaKeysForExport(cfg);
|
|
@@ -114540,110 +114953,7 @@ async function runAdPmaxValidate(opts) {
|
|
|
114540
114953
|
}
|
|
114541
114954
|
|
|
114542
114955
|
// 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
|
|
114956
|
+
import { basename as basename6, isAbsolute as isAbsolute6 } from "path";
|
|
114647
114957
|
function buildReportQuery(opts) {
|
|
114648
114958
|
const params = new URLSearchParams();
|
|
114649
114959
|
params.set("startDate", toGoogleDate(opts.startDate, -30).replace(/\//g, "-"));
|
|
@@ -114991,31 +115301,74 @@ async function runAdPmaxAssetsUpdate(opts) {
|
|
|
114991
115301
|
async function runAdPmaxYoutubeLink(opts) {
|
|
114992
115302
|
const accountId = requireAccountId(opts.account);
|
|
114993
115303
|
const assetGroupId = opts.assetGroupId.trim();
|
|
114994
|
-
|
|
114995
|
-
|
|
114996
|
-
|
|
114997
|
-
|
|
114998
|
-
|
|
114999
|
-
|
|
115000
|
-
}
|
|
115001
|
-
if (
|
|
115002
|
-
|
|
115304
|
+
const youtubeInput = opts.youtubeUrlOrId?.trim();
|
|
115305
|
+
const videoPathInput = opts.videoPath?.trim();
|
|
115306
|
+
if (!opts.bodyFile) {
|
|
115307
|
+
if (youtubeInput && videoPathInput) {
|
|
115308
|
+
console.error("\n\u274C --youtube \u4E0E --video-path \u53EA\u80FD\u4E8C\u9009\u4E00\n");
|
|
115309
|
+
process.exit(1);
|
|
115310
|
+
}
|
|
115311
|
+
if (!youtubeInput && !videoPathInput) {
|
|
115312
|
+
console.error("\n\u274C \u8BF7\u6307\u5B9A --youtube\u3001--video-path \u6216 --body-file\n");
|
|
115313
|
+
process.exit(1);
|
|
115314
|
+
}
|
|
115315
|
+
if (videoPathInput) {
|
|
115316
|
+
const pathErr = validatePmaxVideoPathQuick(resolvePmaxVideoAbsPath(videoPathInput));
|
|
115317
|
+
if (pathErr) {
|
|
115318
|
+
console.error(`
|
|
115319
|
+
\u274C ${pathErr}
|
|
115320
|
+
`);
|
|
115321
|
+
process.exit(1);
|
|
115322
|
+
}
|
|
115323
|
+
}
|
|
115003
115324
|
}
|
|
115004
115325
|
await withGoogleApi(opts, async (config, googleApiUrl) => {
|
|
115005
|
-
const url = pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, "youtube");
|
|
115006
115326
|
let data;
|
|
115327
|
+
let linkedTarget = youtubeInput;
|
|
115007
115328
|
try {
|
|
115008
|
-
|
|
115009
|
-
url,
|
|
115010
|
-
|
|
115011
|
-
|
|
115012
|
-
|
|
115013
|
-
|
|
115014
|
-
|
|
115329
|
+
if (opts.bodyFile) {
|
|
115330
|
+
const url = pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, "youtube");
|
|
115331
|
+
const body = loadPmaxJsonFile(opts.bodyFile);
|
|
115332
|
+
linkedTarget = typeof body["youtubeUrlOrId"] === "string" ? body["youtubeUrlOrId"].trim() : linkedTarget;
|
|
115333
|
+
const raw = await pmaxApiFetch(
|
|
115334
|
+
url,
|
|
115335
|
+
config,
|
|
115336
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
115337
|
+
opts.verbose
|
|
115338
|
+
);
|
|
115339
|
+
data = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : { data: raw };
|
|
115340
|
+
} else {
|
|
115341
|
+
if (videoPathInput) {
|
|
115342
|
+
const absVideo = resolvePmaxVideoAbsPath(videoPathInput);
|
|
115343
|
+
if (!opts.jsonOut) console.log(`
|
|
115344
|
+
\u{1F4E4} \u4E0A\u4F20\u672C\u5730\u89C6\u9891\uFF08PyAPI\uFF09\u2026`);
|
|
115345
|
+
linkedTarget = await uploadPmaxLocalVideo({
|
|
115346
|
+
config,
|
|
115347
|
+
googleApiUrl,
|
|
115348
|
+
accountId,
|
|
115349
|
+
absVideoPath: absVideo,
|
|
115350
|
+
title: opts.videoTitle,
|
|
115351
|
+
description: opts.videoDescription,
|
|
115352
|
+
verbose: opts.verbose,
|
|
115353
|
+
onProgress: opts.jsonOut ? void 0 : (msg) => console.log(` ${msg}`)
|
|
115354
|
+
});
|
|
115355
|
+
if (!opts.jsonOut) console.log(` video_id\uFF1A${linkedTarget}`);
|
|
115356
|
+
}
|
|
115357
|
+
data = await postPmaxYoutubeLink({
|
|
115358
|
+
config,
|
|
115359
|
+
googleApiUrl,
|
|
115360
|
+
accountId,
|
|
115361
|
+
assetGroupId,
|
|
115362
|
+
youtubeUrlOrId: linkedTarget,
|
|
115363
|
+
campaignId: opts.campaignId,
|
|
115364
|
+
assetName: opts.assetName,
|
|
115365
|
+
verbose: opts.verbose
|
|
115366
|
+
});
|
|
115367
|
+
}
|
|
115015
115368
|
} catch (err) {
|
|
115016
115369
|
console.error(
|
|
115017
115370
|
`
|
|
115018
|
-
\u274C \u94FE\u63A5 YouTube \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
115371
|
+
\u274C \u94FE\u63A5/\u66FF\u6362 YouTube \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
115019
115372
|
`
|
|
115020
115373
|
);
|
|
115021
115374
|
process.exit(1);
|
|
@@ -115030,7 +115383,8 @@ async function runAdPmaxYoutubeLink(opts) {
|
|
|
115030
115383
|
return;
|
|
115031
115384
|
}
|
|
115032
115385
|
console.log(`
|
|
115033
|
-
\u2705 \u5DF2\u94FE\u63A5 YouTube \u5230\u8D44\u4EA7\u7EC4 ${assetGroupId}`);
|
|
115386
|
+
\u2705 \u5DF2\u94FE\u63A5/\u66FF\u6362 YouTube \u5230\u8D44\u4EA7\u7EC4 ${assetGroupId}`);
|
|
115387
|
+
if (linkedTarget) console.log(` youtube\uFF1A${linkedTarget}`);
|
|
115034
115388
|
console.log(` assetResourceName\uFF1A${data["assetResourceName"] ?? "\u2014"}
|
|
115035
115389
|
`);
|
|
115036
115390
|
});
|
|
@@ -115202,8 +115556,8 @@ async function runAdPmaxImageUpload(opts) {
|
|
|
115202
115556
|
let data;
|
|
115203
115557
|
try {
|
|
115204
115558
|
if (opts.imagePath?.trim()) {
|
|
115205
|
-
const abs =
|
|
115206
|
-
const name2 = (opts.name ??
|
|
115559
|
+
const abs = isAbsolute6(opts.imagePath) ? opts.imagePath.trim() : resolvePmaxImagePath2(process.cwd(), opts.imagePath);
|
|
115560
|
+
const name2 = (opts.name ?? basename6(abs)).trim();
|
|
115207
115561
|
const id = await uploadPmaxImageFile(
|
|
115208
115562
|
config,
|
|
115209
115563
|
googleApiUrl,
|
|
@@ -115391,8 +115745,8 @@ async function runAdCampaignValidate(opts) {
|
|
|
115391
115745
|
}
|
|
115392
115746
|
|
|
115393
115747
|
// src/commands/ad/pmax-image-convert.ts
|
|
115394
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as
|
|
115395
|
-
import { resolve as
|
|
115748
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as readFileSync9, existsSync as existsSync4 } from "fs";
|
|
115749
|
+
import { resolve as resolve10, dirname as dirname11, basename as basename7, extname } from "path";
|
|
115396
115750
|
var SPECS = [
|
|
115397
115751
|
{ kind: "marketing", width: 1200, height: 628, fit: "cover", suffix: "_marketing" },
|
|
115398
115752
|
{ kind: "square", width: 1200, height: 1200, fit: "cover", suffix: "_square" },
|
|
@@ -115439,9 +115793,9 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
115439
115793
|
(s) => s.kind === "logo" && opts.logoUseCover ? { ...s, fit: "cover" } : s
|
|
115440
115794
|
);
|
|
115441
115795
|
const firstInput = input ?? inputMarketing ?? inputSquare ?? inputLogo;
|
|
115442
|
-
const outputDir = opts.outputDir ?
|
|
115796
|
+
const outputDir = opts.outputDir ? resolve10(opts.outputDir) : dirname11(resolve10(firstInput));
|
|
115443
115797
|
mkdirSync3(outputDir, { recursive: true });
|
|
115444
|
-
const prefix = opts.prefix ??
|
|
115798
|
+
const prefix = opts.prefix ?? basename7(firstInput, extname(firstInput));
|
|
115445
115799
|
const quality = opts.quality ?? 85;
|
|
115446
115800
|
const sharpMod = await loadSharp();
|
|
115447
115801
|
const sharpFn = (p) => sharpMod(p);
|
|
@@ -115453,15 +115807,15 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
115453
115807
|
console.warn(`\u26A0\uFE0F ${spec.kind}\uFF1A\u672A\u6307\u5B9A\u8F93\u5165\u56FE\uFF0C\u8DF3\u8FC7`);
|
|
115454
115808
|
continue;
|
|
115455
115809
|
}
|
|
115456
|
-
const absInput =
|
|
115457
|
-
if (!
|
|
115810
|
+
const absInput = resolve10(src);
|
|
115811
|
+
if (!existsSync4(absInput)) {
|
|
115458
115812
|
console.error(`\u274C \u8F93\u5165\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${absInput}`);
|
|
115459
115813
|
hasError = true;
|
|
115460
115814
|
continue;
|
|
115461
115815
|
}
|
|
115462
115816
|
const outExt = extname(absInput).toLowerCase() === ".png" ? ".png" : ".jpg";
|
|
115463
115817
|
const outName = `${prefix}${spec.suffix}${outExt}`;
|
|
115464
|
-
const absOutput =
|
|
115818
|
+
const absOutput = resolve10(outputDir, outName);
|
|
115465
115819
|
try {
|
|
115466
115820
|
await convertOne(sharpFn, absInput, absOutput, spec, quality);
|
|
115467
115821
|
const sizeKB = Math.ceil(
|
|
@@ -115480,9 +115834,9 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
115480
115834
|
process.exit(1);
|
|
115481
115835
|
}
|
|
115482
115836
|
if (opts.updateConfig) {
|
|
115483
|
-
const configPath =
|
|
115837
|
+
const configPath = resolve10(opts.updateConfig);
|
|
115484
115838
|
try {
|
|
115485
|
-
const raw = JSON.parse(
|
|
115839
|
+
const raw = JSON.parse(readFileSync9(configPath, "utf8"));
|
|
115486
115840
|
const imgPaths = raw["imagePaths"] ?? {};
|
|
115487
115841
|
if (outputPaths.marketing) imgPaths["marketing"] = outputPaths.marketing;
|
|
115488
115842
|
if (outputPaths.square) imgPaths["square"] = outputPaths.square;
|
|
@@ -115891,7 +116245,7 @@ function register20(program2) {
|
|
|
115891
116245
|
}
|
|
115892
116246
|
);
|
|
115893
116247
|
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'
|
|
116248
|
+
'\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
116249
|
).requiredOption(
|
|
115896
116250
|
"--config-file <path>",
|
|
115897
116251
|
"JSON \u914D\u7F6E\u6587\u4EF6\uFF08\u6A21\u677F\u89C1 assets/siluzan-ads/assets/pmax-create-*.json|md\uFF09"
|
|
@@ -115975,18 +116329,19 @@ function register20(program2) {
|
|
|
115975
116329
|
}
|
|
115976
116330
|
);
|
|
115977
116331
|
addPmaxJsonOptions(
|
|
115978
|
-
adCmd.command("pmax-youtube-link").description(
|
|
116332
|
+
adCmd.command("pmax-youtube-link").description(
|
|
116333
|
+
"\u4E3A\u8D44\u4EA7\u7EC4\u94FE\u63A5\u6216\u66FF\u6362 YouTube \u89C6\u9891\uFF08POST .../youtube\uFF1B\u4F1A\u5148\u79FB\u9664\u5DF2\u6709 YOUTUBE_VIDEO \u518D\u6302\u65B0\u7247\uFF09\n --youtube\uFF1A\u5DF2\u6709 URL/ID\uFF1B--video-path\uFF1A\u672C\u5730\u6587\u4EF6\u7ECF PyAPI \u4E0A\u4F20\u540E\u94FE\u63A5"
|
|
116334
|
+
).requiredOption("-a, --account <id>", "Google \u5A92\u4F53\u5BA2\u6237 ID").requiredOption("--asset-group-id <id>", "\u8D44\u4EA7\u7EC4 ID").option("--campaign-id <id>", "\u6240\u5C5E\u6D3B\u52A8 ID\uFF08\u5EFA\u8BAE\u4F20\uFF09").option("--youtube <urlOrId>", "YouTube URL \u6216 11 \u4F4D\u89C6\u9891 ID\uFF08\u4E0E --video-path \u4E8C\u9009\u4E00\uFF09").option("--video-path <path>", "\u672C\u5730\u89C6\u9891\u8DEF\u5F84\uFF08PyAPI \u4E0A\u4F20\uFF1B\u4E0E --youtube \u4E8C\u9009\u4E00\uFF09").option("--video-title <title>", "PyAPI \u4E0A\u4F20\u6807\u9898\uFF08\u9ED8\u8BA4\u6587\u4EF6\u540D\uFF09").option("--video-description <text>", "PyAPI \u4E0A\u4F20\u63CF\u8FF0").option("--asset-name <name>", "\u8D44\u4EA7\u663E\u793A\u540D").option("--body-file <path>", "\u5B8C\u6574 JSON body\uFF08\u542B youtubeUrlOrId\uFF1B\u8986\u76D6 --youtube/--video-path\uFF09").option("-t, --token <token>", "Auth Token")
|
|
115979
116335
|
).action(
|
|
115980
116336
|
async (opts) => {
|
|
115981
|
-
if (!opts.bodyFile && !opts.youtube?.trim()) {
|
|
115982
|
-
console.error("\n\u274C \u8BF7\u6307\u5B9A --youtube \u6216 --body-file\n");
|
|
115983
|
-
process.exit(1);
|
|
115984
|
-
}
|
|
115985
116337
|
await runAdPmaxYoutubeLink({
|
|
115986
116338
|
account: opts.account,
|
|
115987
116339
|
assetGroupId: opts.assetGroupId,
|
|
115988
116340
|
campaignId: opts.campaignId,
|
|
115989
|
-
youtubeUrlOrId: opts.youtube
|
|
116341
|
+
youtubeUrlOrId: opts.youtube,
|
|
116342
|
+
videoPath: opts.videoPath,
|
|
116343
|
+
videoTitle: opts.videoTitle,
|
|
116344
|
+
videoDescription: opts.videoDescription,
|
|
115990
116345
|
assetName: opts.assetName,
|
|
115991
116346
|
bodyFile: opts.bodyFile,
|
|
115992
116347
|
token: opts.token,
|
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` / `youtubeUrlOrId`(二选一,`pmax-create` 后自动链接)。**换片**:`ad pmax-youtube-link --youtube …` 或 `--video-path …`(会替换资产组内已有 YouTube 视频) |
|
|
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)
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
| 改资产组 | `ad pmax-asset-group-edit` |
|
|
61
63
|
| 删资产组 | `ad pmax-asset-group-edit --status REMOVED`(软删;网关无 DELETE 端点) |
|
|
62
64
|
| 改资产 | `ad pmax-assets-update --config-file …` |
|
|
63
|
-
| YouTube | `ad pmax-youtube-link` |
|
|
65
|
+
| YouTube 链接/替换 | `ad pmax-youtube-link`(`--youtube` 或 `--video-path`) |
|
|
64
66
|
| 信号 | `ad pmax-signals-get` / `ad pmax-signals-set`;受众下拉 `ad pmax-audiences` |
|
|
65
67
|
| 图片库 | `ad pmax-image-upload` |
|
|
66
68
|
| 报表 | `ad pmax-report-asset-groups` / `ad pmax-report-geo` |
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
| `ad pmax-asset-group-create` | 新建资产组 |
|
|
24
24
|
| `ad pmax-asset-group-edit` | 编辑资产组 |
|
|
25
25
|
| `ad pmax-assets-update` | 更新资产 |
|
|
26
|
-
| `ad pmax-youtube-link` |
|
|
26
|
+
| `ad pmax-youtube-link` | 关联/替换 YouTube(会移除资产组内已有视频再挂新片) |
|
|
27
27
|
| `ad pmax-signals-get` / `ad pmax-signals-set` | 读/写信号(**全量** PUT) |
|
|
28
28
|
| `ad pmax-audiences` | 受众列表 |
|
|
29
29
|
| `ad pmax-image-upload` | 单张图片上传 |
|
|
@@ -38,6 +38,14 @@
|
|
|
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`(同一接口;后端先删旧 `YOUTUBE_VIDEO` 再链接新片):
|
|
46
|
+
- `--youtube "<url或ID>"` 或 `--video-path ./new.mp4`(可选 `--video-title`)
|
|
47
|
+
- 示例:`siluzan-tso ad pmax-youtube-link -a <id> --asset-group-id <agId> --campaign-id <cid> --video-path ./promo-v2.mp4`
|
|
48
|
+
|
|
41
49
|
---
|
|
42
50
|
|
|
43
51
|
## 编辑流程
|
|
@@ -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.15'
|
|
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.15"
|
|
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"
|