siluzan-tso-cli 1.1.22-beta.7 → 1.1.22
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 -2
- package/dist/index.js +651 -179
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/assets/campaign-create-template.md +12 -12
- package/dist/skill/assets/pmax-create-template.md +8 -2
- package/dist/skill/references/README.md +1 -1
- package/dist/skill/references/accounts/accounts.md +14 -26
- package/dist/skill/references/accounts/finance.md +9 -11
- package/dist/skill/references/accounts/open-account-by-media.md +2 -3
- package/dist/skill/references/accounts/open-account-google-ui.md +43 -74
- package/dist/skill/references/analytics/account-analytics.md +27 -2
- package/dist/skill/references/analytics/keyword-planner-workflows.md +5 -5
- package/dist/skill/references/analytics/rag.md +2 -2
- package/dist/skill/references/analytics/reporting.md +10 -17
- package/dist/skill/references/core/setup.md +6 -6
- package/dist/skill/references/core/skill-authoring.md +1 -1
- package/dist/skill/references/core/workflows.md +19 -41
- package/dist/skill/references/google-ads/google-ads.md +9 -13
- package/dist/skill/references/google-ads/pmax-api.md +43 -41
- package/dist/skill/references/misc/tso-home.md +20 -84
- package/dist/skill/references/operations/clue.md +1 -1
- package/dist/skill/references/operations/forewarning.md +0 -2
- package/dist/skill/references/operations/optimize.md +2 -4
- package/dist/skill/report-templates/README.md +1 -1
- package/dist/skill/report-templates/bing-period-report.md +2 -3
- package/dist/skill/report-templates/google-ads-diagnosis.md +2 -2
- package/dist/skill/report-templates/google-inquiry-analysis.md +4 -4
- package/dist/skill/report-templates/google-period-report.md +1 -1
- package/dist/skill/report-templates/meta-period-report.md +2 -3
- package/dist/skill/report-templates/okki-weekly-google-client.md +1 -1
- package/dist/skill/report-templates/tiktok-period-report.md +4 -7
- package/dist/skill/scripts/install.ps1 +3 -3
- package/dist/skill/scripts/install.sh +3 -3
- package/package.json +1 -1
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;
|
|
@@ -3328,7 +3328,7 @@ var DEFAULT_API_BASE;
|
|
|
3328
3328
|
var init_defaults = __esm({
|
|
3329
3329
|
"src/config/defaults.ts"() {
|
|
3330
3330
|
"use strict";
|
|
3331
|
-
DEFAULT_API_BASE = "https://tso-api
|
|
3331
|
+
DEFAULT_API_BASE = "https://tso-api.siluzan.com";
|
|
3332
3332
|
}
|
|
3333
3333
|
});
|
|
3334
3334
|
|
|
@@ -4121,6 +4121,23 @@ function parseGoogleAccountSpendOverviewRows(raw) {
|
|
|
4121
4121
|
if (!Number.isFinite(n)) return null;
|
|
4122
4122
|
return Math.round(n / 1e6 * 100) / 100;
|
|
4123
4123
|
};
|
|
4124
|
+
const rowsFromAccountsDict = (accounts2, mode) => {
|
|
4125
|
+
const rows = [];
|
|
4126
|
+
for (const [id, item] of Object.entries(accounts2)) {
|
|
4127
|
+
const data = item?.data;
|
|
4128
|
+
if (!data) continue;
|
|
4129
|
+
rows.push({
|
|
4130
|
+
mode,
|
|
4131
|
+
mediaAccountId: String(id),
|
|
4132
|
+
spend: typeof data.spend === "number" ? data.spend : void 0,
|
|
4133
|
+
impressions: typeof data.impressions === "number" ? data.impressions : void 0,
|
|
4134
|
+
clicks: typeof data.clicks === "number" ? data.clicks : void 0,
|
|
4135
|
+
conversions: typeof data.conversions === "number" ? data.conversions : void 0,
|
|
4136
|
+
costPerClick: typeof data.averageCpc === "number" ? data.averageCpc : void 0
|
|
4137
|
+
});
|
|
4138
|
+
}
|
|
4139
|
+
return rows;
|
|
4140
|
+
};
|
|
4124
4141
|
const fromDbItem = (it) => {
|
|
4125
4142
|
if (it == null || it.mediaAccountId == null) return null;
|
|
4126
4143
|
return {
|
|
@@ -4145,25 +4162,23 @@ function parseGoogleAccountSpendOverviewRows(raw) {
|
|
|
4145
4162
|
if (!raw || typeof raw !== "object") return [];
|
|
4146
4163
|
const r = raw;
|
|
4147
4164
|
if (r.mode === "googleCombined") {
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
mediaAccountId: String(id),
|
|
4158
|
-
spend: typeof data.spend === "number" ? data.spend : void 0,
|
|
4159
|
-
impressions: typeof data.impressions === "number" ? data.impressions : void 0,
|
|
4160
|
-
clicks: typeof data.clicks === "number" ? data.clicks : void 0,
|
|
4161
|
-
conversions: typeof data.conversions === "number" ? data.conversions : void 0,
|
|
4162
|
-
// 实时模式下 averageCpc 即点击均价(CPC)
|
|
4163
|
-
costPerClick: typeof data.averageCpc === "number" ? data.averageCpc : void 0
|
|
4164
|
-
});
|
|
4165
|
+
return rowsFromAccountsDict(
|
|
4166
|
+
r.accounts ?? {},
|
|
4167
|
+
"googleCombined"
|
|
4168
|
+
);
|
|
4169
|
+
}
|
|
4170
|
+
if (r.mode === "database") {
|
|
4171
|
+
const items2 = Array.isArray(r.items) ? r.items : [];
|
|
4172
|
+
if (items2.length > 0) {
|
|
4173
|
+
return items2.map(fromDbItem).filter((row) => row !== null);
|
|
4165
4174
|
}
|
|
4166
|
-
|
|
4175
|
+
}
|
|
4176
|
+
const accounts = r.accounts;
|
|
4177
|
+
if (accounts && Object.keys(accounts).length > 0) {
|
|
4178
|
+
return rowsFromAccountsDict(
|
|
4179
|
+
accounts,
|
|
4180
|
+
"database"
|
|
4181
|
+
);
|
|
4167
4182
|
}
|
|
4168
4183
|
const items = Array.isArray(r.items) ? r.items : [];
|
|
4169
4184
|
return items.map(fromDbItem).filter((row) => row !== null);
|
|
@@ -4608,7 +4623,10 @@ function buildOutlineHints(section) {
|
|
|
4608
4623
|
hints.push(...GOOGLE_ANALYSIS_RATE_NORMALIZED_OUTLINE_HINTS);
|
|
4609
4624
|
}
|
|
4610
4625
|
if (section === "campaigns") {
|
|
4611
|
-
hints.push(
|
|
4626
|
+
hints.push(
|
|
4627
|
+
...GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS,
|
|
4628
|
+
...GOOGLE_ANALYSIS_CAMPAIGNS_COMPETITIVE_METRICS_HINTS
|
|
4629
|
+
);
|
|
4612
4630
|
}
|
|
4613
4631
|
const zhHints = GOOGLE_ANALYSIS_ZH_FIELD_HINTS_BY_SECTION[section];
|
|
4614
4632
|
if (zhHints) hints.push(...zhHints);
|
|
@@ -4717,7 +4735,7 @@ async function writeGoogleAnalysisSnapshot(params) {
|
|
|
4717
4735
|
agentHint: OUTLINE_AGENT_HINT
|
|
4718
4736
|
};
|
|
4719
4737
|
}
|
|
4720
|
-
var LEGACY_MANIFEST_FILE, CLI_PACKAGE2, SCHEMA_VERSION2, DEFAULT_FIELD_GUIDE2, GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS, RATE_BEARING_SECTIONS, GOOGLE_ANALYSIS_RATE_NORMALIZED_OUTLINE_HINTS, GOOGLE_ANALYSIS_ZH_FIELD_HINTS_BY_SECTION;
|
|
4738
|
+
var LEGACY_MANIFEST_FILE, CLI_PACKAGE2, SCHEMA_VERSION2, DEFAULT_FIELD_GUIDE2, GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS, GOOGLE_ANALYSIS_CAMPAIGNS_COMPETITIVE_METRICS_HINTS, RATE_BEARING_SECTIONS, GOOGLE_ANALYSIS_RATE_NORMALIZED_OUTLINE_HINTS, GOOGLE_ANALYSIS_ZH_FIELD_HINTS_BY_SECTION;
|
|
4721
4739
|
var init_google_analysis = __esm({
|
|
4722
4740
|
"src/utils/snapshot/google-analysis.ts"() {
|
|
4723
4741
|
"use strict";
|
|
@@ -4734,6 +4752,10 @@ var init_google_analysis = __esm({
|
|
|
4734
4752
|
"// \u91D1\u989D\uFF1A`campaigns[].budgetAmountYuan` / `campaignTargetCpaYuan` / `maximizeConversionsTargetCpaYuan` \u5747\u4E3A**\u5143**\uFF08CLI \u51FA\u53E3\u7EDF\u4E00\uFF09\uFF1B\u540C\u884C `spend` / `averageCpc` / `costPerConversion` \u4E5F\u662F\u5143\u3002",
|
|
4735
4753
|
"// \u65E7\u5B57\u6BB5 `budgetAmount`\uFF08\u5206\uFF09/ `*Micros`\uFF08\u5FAE\u5143\uFF09\u5DF2\u4E0D\u518D\u843D\u76D8\uFF0C**\u7981\u6B62**\u518D\u505A \xF7100 / \xF71_000_000 \u6362\u7B97\u3002"
|
|
4736
4754
|
];
|
|
4755
|
+
GOOGLE_ANALYSIS_CAMPAIGNS_COMPETITIVE_METRICS_HINTS = [
|
|
4756
|
+
"// \u7ADE\u4E89\u6307\u6807\uFF1A`campaigns[].competitiveMetrics` \u4E3A Google \u539F\u751F **0~1 \u5C0F\u6570**\uFF08\u5982 `0.0999` = 9.99%\uFF09\uFF0C\u542B Top/AbsoluteTop/Content/ClickShare \u7B49 15 \u9879\uFF1B\u65E0 GAQL \u7ADE\u4E89\u6570\u636E\u65F6\u4E3A `null`\u3002",
|
|
4757
|
+
"// \u884C\u9876 `searchImpressionShare` / `searchBudgetLostImpressionShare` / `searchRankLostImpressionShare` \u4ECD\u4E3A **0~100 \u767E\u5206\u6570**\uFF08\u4EC5 3 \u9879\uFF09\uFF1B**\u7981\u6B62**\u4E0E `competitiveMetrics.*` \u76F8\u52A0\u6216\u6DF7\u7B97\u3002"
|
|
4758
|
+
];
|
|
4737
4759
|
RATE_BEARING_SECTIONS = /* @__PURE__ */ new Set([
|
|
4738
4760
|
"overview",
|
|
4739
4761
|
"keywords",
|
|
@@ -100525,6 +100547,12 @@ function annotateZhFields(payload, section) {
|
|
|
100525
100547
|
case "search-terms": {
|
|
100526
100548
|
const rows = payload.data;
|
|
100527
100549
|
annotateArrayRows(rows, "matchType", "matchTypeZh", MATCH_TYPE_DICT);
|
|
100550
|
+
annotateArrayRows(
|
|
100551
|
+
rows,
|
|
100552
|
+
"queryTargetingStatus",
|
|
100553
|
+
"queryTargetingStatusZh",
|
|
100554
|
+
QUERY_TARGETING_STATUS_ZH
|
|
100555
|
+
);
|
|
100528
100556
|
break;
|
|
100529
100557
|
}
|
|
100530
100558
|
case "campaign-geo":
|
|
@@ -100541,7 +100569,7 @@ function annotateZhFields(payload, section) {
|
|
|
100541
100569
|
}
|
|
100542
100570
|
return payload;
|
|
100543
100571
|
}
|
|
100544
|
-
var MATCH_TYPE_DICT, GEO_DICT, CITY_DICT, ISO_ZH_DICT;
|
|
100572
|
+
var MATCH_TYPE_DICT, GEO_DICT, CITY_DICT, ISO_ZH_DICT, QUERY_TARGETING_STATUS_ZH;
|
|
100545
100573
|
var init_translate_fields = __esm({
|
|
100546
100574
|
"src/commands/google-analysis/translate-fields.ts"() {
|
|
100547
100575
|
"use strict";
|
|
@@ -100552,6 +100580,14 @@ var init_translate_fields = __esm({
|
|
|
100552
100580
|
GEO_DICT = geo_en2zh_default;
|
|
100553
100581
|
CITY_DICT = city_to_country_default.byName ?? {};
|
|
100554
100582
|
ISO_ZH_DICT = city_to_country_default.isoToZh ?? {};
|
|
100583
|
+
QUERY_TARGETING_STATUS_ZH = {
|
|
100584
|
+
Added: "\u5DF2\u6DFB\u52A0",
|
|
100585
|
+
Excluded: "\u5DF2\u6392\u9664",
|
|
100586
|
+
None: "\u90FD\u6CA1\u6709",
|
|
100587
|
+
AddedExcluded: "\u5DF2\u6DFB\u52A0/\u5DF2\u6392\u9664",
|
|
100588
|
+
UNSPECIFIED: "\u90FD\u6CA1\u6709",
|
|
100589
|
+
UNKNOWN: "\u90FD\u6CA1\u6709"
|
|
100590
|
+
};
|
|
100555
100591
|
}
|
|
100556
100592
|
});
|
|
100557
100593
|
|
|
@@ -100868,15 +100904,15 @@ function computeBackoffMs(attempt, policy) {
|
|
|
100868
100904
|
const jitter = Math.floor(Math.random() * policy.baseMs);
|
|
100869
100905
|
return capped + jitter;
|
|
100870
100906
|
}
|
|
100871
|
-
function
|
|
100872
|
-
return new Promise((
|
|
100907
|
+
function sleep3(ms, signal) {
|
|
100908
|
+
return new Promise((resolve13, reject) => {
|
|
100873
100909
|
if (signal?.aborted) {
|
|
100874
100910
|
reject(new Error("retry sleep aborted"));
|
|
100875
100911
|
return;
|
|
100876
100912
|
}
|
|
100877
100913
|
const timer = setTimeout(() => {
|
|
100878
100914
|
signal?.removeEventListener("abort", onAbort);
|
|
100879
|
-
|
|
100915
|
+
resolve13();
|
|
100880
100916
|
}, ms);
|
|
100881
100917
|
const onAbort = () => {
|
|
100882
100918
|
clearTimeout(timer);
|
|
@@ -100937,7 +100973,7 @@ async function fetchWithRetry(factory, options) {
|
|
|
100937
100973
|
const backoffMs = computeBackoffMs(attempt, policy);
|
|
100938
100974
|
options.onRetry?.({ attempt, backoffMs, status: cls.status, error: err });
|
|
100939
100975
|
try {
|
|
100940
|
-
await
|
|
100976
|
+
await sleep3(backoffMs, options.signal);
|
|
100941
100977
|
} catch {
|
|
100942
100978
|
throw new AbortRunError({
|
|
100943
100979
|
message: `\u91CD\u8BD5\u7B49\u5F85\u88AB\u4E2D\u6B62\uFF1A${options.label}`,
|
|
@@ -113816,6 +113852,10 @@ function runPmaxCreateValidation(cfg) {
|
|
|
113816
113852
|
}
|
|
113817
113853
|
}
|
|
113818
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
|
+
}
|
|
113819
113859
|
validateTextLengths(
|
|
113820
113860
|
errors,
|
|
113821
113861
|
lengthViolations,
|
|
@@ -114283,8 +114323,129 @@ function assertPmaxImageSlotsResolved(slots) {
|
|
|
114283
114323
|
}
|
|
114284
114324
|
}
|
|
114285
114325
|
|
|
114286
|
-
// src/commands/ad/pmax-
|
|
114326
|
+
// src/commands/ad/pmax-shared.ts
|
|
114327
|
+
init_auth();
|
|
114328
|
+
init_cli_json_snapshot();
|
|
114287
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";
|
|
114288
114449
|
function loadPmaxCreateConfig(configFile) {
|
|
114289
114450
|
const cfg = tryLoadPmaxCreateConfig(configFile);
|
|
114290
114451
|
if (!cfg) {
|
|
@@ -114295,15 +114456,36 @@ function loadPmaxCreateConfig(configFile) {
|
|
|
114295
114456
|
}
|
|
114296
114457
|
return cfg;
|
|
114297
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
|
+
}
|
|
114298
114473
|
function tryLoadPmaxCreateConfig(configFile) {
|
|
114299
114474
|
let raw;
|
|
114300
114475
|
try {
|
|
114301
|
-
raw = JSON.parse(
|
|
114476
|
+
raw = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
114302
114477
|
} catch {
|
|
114303
114478
|
return null;
|
|
114304
114479
|
}
|
|
114305
114480
|
return stripMetaKeys(raw);
|
|
114306
114481
|
}
|
|
114482
|
+
function loadPmaxCreateConfigRaw(configFile) {
|
|
114483
|
+
try {
|
|
114484
|
+
return stripMetaKeys(JSON.parse(readFileSync7(configFile, "utf8")));
|
|
114485
|
+
} catch {
|
|
114486
|
+
return null;
|
|
114487
|
+
}
|
|
114488
|
+
}
|
|
114307
114489
|
function parseLocationLanguageIds(items) {
|
|
114308
114490
|
if (!items?.length) return void 0;
|
|
114309
114491
|
return items.map((item) => ({
|
|
@@ -114345,9 +114527,202 @@ function buildPmaxCreateUrl(googleApiUrl, accountId) {
|
|
|
114345
114527
|
return `${base}/accounts/${accountId}/campaign/pmax`;
|
|
114346
114528
|
}
|
|
114347
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
|
+
|
|
114348
114721
|
// src/commands/ad/pmax-create.ts
|
|
114349
114722
|
async function runAdPmaxCreate(opts) {
|
|
114350
114723
|
const cfg = loadPmaxCreateConfig(opts.configFile);
|
|
114724
|
+
const rawCfg = loadPmaxCreateConfigRaw(opts.configFile) ?? {};
|
|
114725
|
+
const deprecatedVideoKeys = detectDeprecatedPmaxVideoKeys(rawCfg);
|
|
114351
114726
|
const {
|
|
114352
114727
|
errors: cfgErrors,
|
|
114353
114728
|
warnings: cfgWarnings,
|
|
@@ -114357,8 +114732,12 @@ async function runAdPmaxCreate(opts) {
|
|
|
114357
114732
|
opts.configFile,
|
|
114358
114733
|
cfg
|
|
114359
114734
|
);
|
|
114360
|
-
const errors =
|
|
114361
|
-
|
|
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];
|
|
114362
114741
|
if (warnings.length > 0) {
|
|
114363
114742
|
console.warn("\n\u26A0\uFE0F PMax \u914D\u7F6E\u8B66\u544A\uFF08\u4E0D\u963B\u65AD\u63D0\u4EA4\uFF09\uFF1A");
|
|
114364
114743
|
for (const w of warnings) console.warn(` \u2022 ${w}`);
|
|
@@ -114428,6 +114807,65 @@ async function runAdPmaxCreate(opts) {
|
|
|
114428
114807
|
const campaignId = data["campaignId"] ?? data["campaign_id"];
|
|
114429
114808
|
const assetGroupId = data["assetGroupId"] ?? data["asset_group_id"];
|
|
114430
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
|
+
}
|
|
114431
114869
|
console.log("\n\u2705 PMax \u5E7F\u544A\u7CFB\u5217\u5DF2\u521B\u5EFA\uFF08\u540C\u6B65\uFF09");
|
|
114432
114870
|
console.log(` \u6D3B\u52A8\u540D\u79F0\uFF1A${cfg.name}`);
|
|
114433
114871
|
if (campaignId != null) console.log(` campaignId\uFF1A${campaignId}`);
|
|
@@ -114437,6 +114875,11 @@ async function runAdPmaxCreate(opts) {
|
|
|
114437
114875
|
`
|
|
114438
114876
|
\u590D\u6838\uFF1Asiluzan-tso ad campaigns -a ${accountId} --json-out # channelTypeV2 \u5E94\u4E3A PERFORMANCE_MAX`
|
|
114439
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
|
+
}
|
|
114440
114883
|
console.log();
|
|
114441
114884
|
}
|
|
114442
114885
|
|
|
@@ -114445,6 +114888,8 @@ import { writeFileSync as writeFileSync3 } from "fs";
|
|
|
114445
114888
|
init_cli_json_snapshot();
|
|
114446
114889
|
async function runAdPmaxValidate(opts) {
|
|
114447
114890
|
const cfg = loadPmaxCreateConfig(opts.configFile);
|
|
114891
|
+
const rawCfg = loadPmaxCreateConfigRaw(opts.configFile) ?? {};
|
|
114892
|
+
const deprecatedVideoKeys = detectDeprecatedPmaxVideoKeys(rawCfg);
|
|
114448
114893
|
const {
|
|
114449
114894
|
errors: cfgErrors,
|
|
114450
114895
|
warnings: cfgWarnings,
|
|
@@ -114454,8 +114899,12 @@ async function runAdPmaxValidate(opts) {
|
|
|
114454
114899
|
opts.configFile,
|
|
114455
114900
|
cfg
|
|
114456
114901
|
);
|
|
114457
|
-
const errors =
|
|
114458
|
-
|
|
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];
|
|
114459
114908
|
const lengthViolations = cfgLengthViolations;
|
|
114460
114909
|
if (opts.writeNormalized) {
|
|
114461
114910
|
const toWrite = stripMetaKeysForExport(cfg);
|
|
@@ -114504,110 +114953,7 @@ async function runAdPmaxValidate(opts) {
|
|
|
114504
114953
|
}
|
|
114505
114954
|
|
|
114506
114955
|
// src/commands/ad/pmax-mgmt.ts
|
|
114507
|
-
import { basename as
|
|
114508
|
-
|
|
114509
|
-
// src/commands/ad/pmax-shared.ts
|
|
114510
|
-
init_auth();
|
|
114511
|
-
init_cli_json_snapshot();
|
|
114512
|
-
import { readFileSync as readFileSync7 } from "fs";
|
|
114513
|
-
import { dirname as dirname9, isAbsolute as isAbsolute4, resolve as resolve8 } from "path";
|
|
114514
|
-
var PMAX_MONEY_KEYS = /* @__PURE__ */ new Set(["budget", "targetCpa_BidingAmount"]);
|
|
114515
|
-
function loadPmaxJsonFile(configFile) {
|
|
114516
|
-
let raw;
|
|
114517
|
-
try {
|
|
114518
|
-
raw = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
114519
|
-
} catch (e) {
|
|
114520
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
114521
|
-
console.error(`
|
|
114522
|
-
\u274C \u8BFB\u53D6 JSON \u5931\u8D25\uFF08${configFile}\uFF09\uFF1A${msg}
|
|
114523
|
-
`);
|
|
114524
|
-
process.exit(1);
|
|
114525
|
-
}
|
|
114526
|
-
return stripMetaKeys(raw);
|
|
114527
|
-
}
|
|
114528
|
-
function resolvePmaxImagePath2(configFile, relOrAbs) {
|
|
114529
|
-
const trimmed = relOrAbs.trim();
|
|
114530
|
-
if (isAbsolute4(trimmed)) return trimmed;
|
|
114531
|
-
return resolve8(dirname9(configFile), trimmed);
|
|
114532
|
-
}
|
|
114533
|
-
function convertPmaxMoneyInObject(body) {
|
|
114534
|
-
const out = { ...body };
|
|
114535
|
-
for (const key of PMAX_MONEY_KEYS) {
|
|
114536
|
-
const val = out[key];
|
|
114537
|
-
if (typeof val === "number" && Number.isFinite(val)) {
|
|
114538
|
-
out[key] = toCentAmount(val);
|
|
114539
|
-
}
|
|
114540
|
-
}
|
|
114541
|
-
return out;
|
|
114542
|
-
}
|
|
114543
|
-
function buildPmaxAssetGroupApiBody(cfg, imageSlots) {
|
|
114544
|
-
assertPmaxImageSlotsResolved(imageSlots);
|
|
114545
|
-
return {
|
|
114546
|
-
name: cfg.name.trim(),
|
|
114547
|
-
finalUrls: cfg.finalUrls.map((u) => u.trim()).filter(Boolean),
|
|
114548
|
-
headlines: cfg.headlines.map((h) => h.trim()).filter(Boolean),
|
|
114549
|
-
longHeadlines: cfg.longHeadlines.map((h) => h.trim()).filter(Boolean),
|
|
114550
|
-
descriptions: cfg.descriptions.map((d) => d.trim()).filter(Boolean),
|
|
114551
|
-
businessName: cfg.businessName.trim(),
|
|
114552
|
-
...imageSlots
|
|
114553
|
-
};
|
|
114554
|
-
}
|
|
114555
|
-
async function processAssetsUpdateBodyWithUpload(configFile, body, accountId, config, googleApiUrl, verbose) {
|
|
114556
|
-
const out = { ...body };
|
|
114557
|
-
const links = out["assetsToLink"];
|
|
114558
|
-
if (!Array.isArray(links)) return out;
|
|
114559
|
-
out["assetsToLink"] = await Promise.all(
|
|
114560
|
-
links.map(async (item) => {
|
|
114561
|
-
if (!item || typeof item !== "object") return item;
|
|
114562
|
-
const row = { ...item };
|
|
114563
|
-
const imagePath = row["imagePath"];
|
|
114564
|
-
if (typeof imagePath === "string" && imagePath.trim()) {
|
|
114565
|
-
const abs = resolvePmaxImagePath2(configFile, imagePath);
|
|
114566
|
-
row["assetId"] = await uploadPmaxImageFile(
|
|
114567
|
-
config,
|
|
114568
|
-
googleApiUrl,
|
|
114569
|
-
accountId,
|
|
114570
|
-
abs,
|
|
114571
|
-
void 0,
|
|
114572
|
-
verbose
|
|
114573
|
-
);
|
|
114574
|
-
delete row["imagePath"];
|
|
114575
|
-
delete row["imageBase64"];
|
|
114576
|
-
}
|
|
114577
|
-
return row;
|
|
114578
|
-
})
|
|
114579
|
-
);
|
|
114580
|
-
return out;
|
|
114581
|
-
}
|
|
114582
|
-
async function withGoogleApi(opts, fn) {
|
|
114583
|
-
const config = await ensureDataPermission(loadConfig(opts.token));
|
|
114584
|
-
const googleApiUrl = requireGoogleApi(config);
|
|
114585
|
-
return fn(config, googleApiUrl);
|
|
114586
|
-
}
|
|
114587
|
-
async function pmaxApiFetch(url, config, init, verbose) {
|
|
114588
|
-
return apiFetch2(url, config, init ?? {}, verbose);
|
|
114589
|
-
}
|
|
114590
|
-
async function emitPmaxResult(opts, section, commandLabel, payload, idSuffix) {
|
|
114591
|
-
return emitCliJsonOrSnapshot(opts, {
|
|
114592
|
-
section,
|
|
114593
|
-
commandLabel,
|
|
114594
|
-
commandHint: idSuffix ?? opts.account ?? "",
|
|
114595
|
-
payload,
|
|
114596
|
-
idSuffix: idSuffix ?? opts.account
|
|
114597
|
-
});
|
|
114598
|
-
}
|
|
114599
|
-
function requireAccountId(account, label = "-a, --account") {
|
|
114600
|
-
const id = account?.toString().trim();
|
|
114601
|
-
if (!id) {
|
|
114602
|
-
console.error(`
|
|
114603
|
-
\u274C \u8BF7\u6307\u5B9A\u5A92\u4F53\u8D26\u6237 ID\uFF08${label}\uFF09
|
|
114604
|
-
`);
|
|
114605
|
-
process.exit(1);
|
|
114606
|
-
}
|
|
114607
|
-
return id;
|
|
114608
|
-
}
|
|
114609
|
-
|
|
114610
|
-
// src/commands/ad/pmax-mgmt.ts
|
|
114956
|
+
import { basename as basename6, isAbsolute as isAbsolute6 } from "path";
|
|
114611
114957
|
function buildReportQuery(opts) {
|
|
114612
114958
|
const params = new URLSearchParams();
|
|
114613
114959
|
params.set("startDate", toGoogleDate(opts.startDate, -30).replace(/\//g, "-"));
|
|
@@ -114955,31 +115301,74 @@ async function runAdPmaxAssetsUpdate(opts) {
|
|
|
114955
115301
|
async function runAdPmaxYoutubeLink(opts) {
|
|
114956
115302
|
const accountId = requireAccountId(opts.account);
|
|
114957
115303
|
const assetGroupId = opts.assetGroupId.trim();
|
|
114958
|
-
|
|
114959
|
-
|
|
114960
|
-
|
|
114961
|
-
|
|
114962
|
-
|
|
114963
|
-
|
|
114964
|
-
}
|
|
114965
|
-
if (
|
|
114966
|
-
|
|
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
|
+
}
|
|
114967
115324
|
}
|
|
114968
115325
|
await withGoogleApi(opts, async (config, googleApiUrl) => {
|
|
114969
|
-
const url = pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, "youtube");
|
|
114970
115326
|
let data;
|
|
115327
|
+
let linkedTarget = youtubeInput;
|
|
114971
115328
|
try {
|
|
114972
|
-
|
|
114973
|
-
url,
|
|
114974
|
-
|
|
114975
|
-
|
|
114976
|
-
|
|
114977
|
-
|
|
114978
|
-
|
|
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
|
+
}
|
|
114979
115368
|
} catch (err) {
|
|
114980
115369
|
console.error(
|
|
114981
115370
|
`
|
|
114982
|
-
\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)}
|
|
114983
115372
|
`
|
|
114984
115373
|
);
|
|
114985
115374
|
process.exit(1);
|
|
@@ -114994,7 +115383,8 @@ async function runAdPmaxYoutubeLink(opts) {
|
|
|
114994
115383
|
return;
|
|
114995
115384
|
}
|
|
114996
115385
|
console.log(`
|
|
114997
|
-
\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}`);
|
|
114998
115388
|
console.log(` assetResourceName\uFF1A${data["assetResourceName"] ?? "\u2014"}
|
|
114999
115389
|
`);
|
|
115000
115390
|
});
|
|
@@ -115166,8 +115556,8 @@ async function runAdPmaxImageUpload(opts) {
|
|
|
115166
115556
|
let data;
|
|
115167
115557
|
try {
|
|
115168
115558
|
if (opts.imagePath?.trim()) {
|
|
115169
|
-
const abs =
|
|
115170
|
-
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();
|
|
115171
115561
|
const id = await uploadPmaxImageFile(
|
|
115172
115562
|
config,
|
|
115173
115563
|
googleApiUrl,
|
|
@@ -115355,8 +115745,8 @@ async function runAdCampaignValidate(opts) {
|
|
|
115355
115745
|
}
|
|
115356
115746
|
|
|
115357
115747
|
// src/commands/ad/pmax-image-convert.ts
|
|
115358
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as
|
|
115359
|
-
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";
|
|
115360
115750
|
var SPECS = [
|
|
115361
115751
|
{ kind: "marketing", width: 1200, height: 628, fit: "cover", suffix: "_marketing" },
|
|
115362
115752
|
{ kind: "square", width: 1200, height: 1200, fit: "cover", suffix: "_square" },
|
|
@@ -115403,9 +115793,9 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
115403
115793
|
(s) => s.kind === "logo" && opts.logoUseCover ? { ...s, fit: "cover" } : s
|
|
115404
115794
|
);
|
|
115405
115795
|
const firstInput = input ?? inputMarketing ?? inputSquare ?? inputLogo;
|
|
115406
|
-
const outputDir = opts.outputDir ?
|
|
115796
|
+
const outputDir = opts.outputDir ? resolve10(opts.outputDir) : dirname11(resolve10(firstInput));
|
|
115407
115797
|
mkdirSync3(outputDir, { recursive: true });
|
|
115408
|
-
const prefix = opts.prefix ??
|
|
115798
|
+
const prefix = opts.prefix ?? basename7(firstInput, extname(firstInput));
|
|
115409
115799
|
const quality = opts.quality ?? 85;
|
|
115410
115800
|
const sharpMod = await loadSharp();
|
|
115411
115801
|
const sharpFn = (p) => sharpMod(p);
|
|
@@ -115417,15 +115807,15 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
115417
115807
|
console.warn(`\u26A0\uFE0F ${spec.kind}\uFF1A\u672A\u6307\u5B9A\u8F93\u5165\u56FE\uFF0C\u8DF3\u8FC7`);
|
|
115418
115808
|
continue;
|
|
115419
115809
|
}
|
|
115420
|
-
const absInput =
|
|
115421
|
-
if (!
|
|
115810
|
+
const absInput = resolve10(src);
|
|
115811
|
+
if (!existsSync4(absInput)) {
|
|
115422
115812
|
console.error(`\u274C \u8F93\u5165\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${absInput}`);
|
|
115423
115813
|
hasError = true;
|
|
115424
115814
|
continue;
|
|
115425
115815
|
}
|
|
115426
115816
|
const outExt = extname(absInput).toLowerCase() === ".png" ? ".png" : ".jpg";
|
|
115427
115817
|
const outName = `${prefix}${spec.suffix}${outExt}`;
|
|
115428
|
-
const absOutput =
|
|
115818
|
+
const absOutput = resolve10(outputDir, outName);
|
|
115429
115819
|
try {
|
|
115430
115820
|
await convertOne(sharpFn, absInput, absOutput, spec, quality);
|
|
115431
115821
|
const sizeKB = Math.ceil(
|
|
@@ -115444,9 +115834,9 @@ async function runAdPmaxImageConvert(opts) {
|
|
|
115444
115834
|
process.exit(1);
|
|
115445
115835
|
}
|
|
115446
115836
|
if (opts.updateConfig) {
|
|
115447
|
-
const configPath =
|
|
115837
|
+
const configPath = resolve10(opts.updateConfig);
|
|
115448
115838
|
try {
|
|
115449
|
-
const raw = JSON.parse(
|
|
115839
|
+
const raw = JSON.parse(readFileSync9(configPath, "utf8"));
|
|
115450
115840
|
const imgPaths = raw["imagePaths"] ?? {};
|
|
115451
115841
|
if (outputPaths.marketing) imgPaths["marketing"] = outputPaths.marketing;
|
|
115452
115842
|
if (outputPaths.square) imgPaths["square"] = outputPaths.square;
|
|
@@ -115855,7 +116245,7 @@ function register20(program2) {
|
|
|
115855
116245
|
}
|
|
115856
116246
|
);
|
|
115857
116247
|
adCmd.command("pmax-create").description(
|
|
115858
|
-
'\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'
|
|
115859
116249
|
).requiredOption(
|
|
115860
116250
|
"--config-file <path>",
|
|
115861
116251
|
"JSON \u914D\u7F6E\u6587\u4EF6\uFF08\u6A21\u677F\u89C1 assets/siluzan-ads/assets/pmax-create-*.json|md\uFF09"
|
|
@@ -115939,18 +116329,19 @@ function register20(program2) {
|
|
|
115939
116329
|
}
|
|
115940
116330
|
);
|
|
115941
116331
|
addPmaxJsonOptions(
|
|
115942
|
-
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")
|
|
115943
116335
|
).action(
|
|
115944
116336
|
async (opts) => {
|
|
115945
|
-
if (!opts.bodyFile && !opts.youtube?.trim()) {
|
|
115946
|
-
console.error("\n\u274C \u8BF7\u6307\u5B9A --youtube \u6216 --body-file\n");
|
|
115947
|
-
process.exit(1);
|
|
115948
|
-
}
|
|
115949
116337
|
await runAdPmaxYoutubeLink({
|
|
115950
116338
|
account: opts.account,
|
|
115951
116339
|
assetGroupId: opts.assetGroupId,
|
|
115952
116340
|
campaignId: opts.campaignId,
|
|
115953
|
-
youtubeUrlOrId: opts.youtube
|
|
116341
|
+
youtubeUrlOrId: opts.youtube,
|
|
116342
|
+
videoPath: opts.videoPath,
|
|
116343
|
+
videoTitle: opts.videoTitle,
|
|
116344
|
+
videoDescription: opts.videoDescription,
|
|
115954
116345
|
assetName: opts.assetName,
|
|
115955
116346
|
bodyFile: opts.bodyFile,
|
|
115956
116347
|
token: opts.token,
|
|
@@ -116535,6 +116926,62 @@ init_auth();
|
|
|
116535
116926
|
init_cli_json_snapshot();
|
|
116536
116927
|
init_cli_table();
|
|
116537
116928
|
|
|
116929
|
+
// src/commands/keyword-account-match.ts
|
|
116930
|
+
init_auth();
|
|
116931
|
+
function normalizeKeywordForAccountMatch(raw) {
|
|
116932
|
+
return unwrapKeywordDisplayTextForEdit(raw.trim()).toLowerCase();
|
|
116933
|
+
}
|
|
116934
|
+
function extractKeywordTextFromManagementRow(row) {
|
|
116935
|
+
const kt = row["keywordText"];
|
|
116936
|
+
if (Array.isArray(kt) && kt.length > 0 && typeof kt[0] === "string") {
|
|
116937
|
+
return kt[0];
|
|
116938
|
+
}
|
|
116939
|
+
const t = row["text"];
|
|
116940
|
+
if (typeof t === "string") return t;
|
|
116941
|
+
const kw = row["keyword"];
|
|
116942
|
+
if (typeof kw === "string") return kw;
|
|
116943
|
+
return "";
|
|
116944
|
+
}
|
|
116945
|
+
async function fetchAccountPositiveKeywordNormSet(config, googleApiUrl, accountId, opts) {
|
|
116946
|
+
const lookback = opts?.lookbackDays ?? 3650;
|
|
116947
|
+
const params = new URLSearchParams();
|
|
116948
|
+
params.set("startDate", toGoogleDate(void 0, -lookback));
|
|
116949
|
+
params.set("endDate", toGoogleDate(void 0, 0));
|
|
116950
|
+
const url = `${googleApiUrl}/keywordmanagement/v2/list/${accountId}?${params}`;
|
|
116951
|
+
const data = await apiFetch2(
|
|
116952
|
+
url,
|
|
116953
|
+
config,
|
|
116954
|
+
{},
|
|
116955
|
+
opts?.verbose
|
|
116956
|
+
);
|
|
116957
|
+
const rawItems = data.data ?? [];
|
|
116958
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
116959
|
+
const normSet = /* @__PURE__ */ new Set();
|
|
116960
|
+
for (const row of rawItems) {
|
|
116961
|
+
const id = row["id"];
|
|
116962
|
+
if (id !== void 0) {
|
|
116963
|
+
if (seenIds.has(id)) continue;
|
|
116964
|
+
seenIds.add(id);
|
|
116965
|
+
}
|
|
116966
|
+
const text = extractKeywordTextFromManagementRow(row);
|
|
116967
|
+
if (!text) continue;
|
|
116968
|
+
const norm = normalizeKeywordForAccountMatch(text);
|
|
116969
|
+
if (norm) normSet.add(norm);
|
|
116970
|
+
}
|
|
116971
|
+
return normSet;
|
|
116972
|
+
}
|
|
116973
|
+
function keywordAlreadyInAccountFlag(recommendKeyword, accountNormSet) {
|
|
116974
|
+
const norm = normalizeKeywordForAccountMatch(recommendKeyword ?? "");
|
|
116975
|
+
if (!norm) return 0;
|
|
116976
|
+
return accountNormSet.has(norm) ? 1 : 0;
|
|
116977
|
+
}
|
|
116978
|
+
function annotateKeywordSuggestWithAccountPresence(items, accountNormSet) {
|
|
116979
|
+
return items.map((item) => {
|
|
116980
|
+
const flag = keywordAlreadyInAccountFlag(item.keyword, accountNormSet);
|
|
116981
|
+
return { ...item, alreadyInAccount: flag };
|
|
116982
|
+
});
|
|
116983
|
+
}
|
|
116984
|
+
|
|
116538
116985
|
// src/commands/keyword-geo.ts
|
|
116539
116986
|
function parseGeoTargetConstantIds(raw) {
|
|
116540
116987
|
if (!raw?.trim()) return [];
|
|
@@ -116799,6 +117246,27 @@ async function runKeywordSuggest(opts) {
|
|
|
116799
117246
|
return !excludes.some((exc) => kw.includes(exc));
|
|
116800
117247
|
});
|
|
116801
117248
|
}
|
|
117249
|
+
if (accountId) {
|
|
117250
|
+
try {
|
|
117251
|
+
const accountNormSet = await fetchAccountPositiveKeywordNormSet(
|
|
117252
|
+
config,
|
|
117253
|
+
googleApiUrl,
|
|
117254
|
+
accountId,
|
|
117255
|
+
{ verbose: opts.verbose }
|
|
117256
|
+
);
|
|
117257
|
+
items = annotateKeywordSuggestWithAccountPresence(items, accountNormSet);
|
|
117258
|
+
if (opts.verbose) {
|
|
117259
|
+
const matched = items.filter((r) => r.alreadyInAccount === 1).length;
|
|
117260
|
+
console.error(
|
|
117261
|
+
` [keyword] \u8D26\u6237\u8BCD\u5E93 ${accountNormSet.size} \u4E2A\u8BCD\u5E72\uFF1B\u63A8\u8350 ${items.length} \u6761\uFF0C\u5176\u4E2D\u5DF2\u5728\u8D26\u6237 ${matched} \u6761`
|
|
117262
|
+
);
|
|
117263
|
+
}
|
|
117264
|
+
} catch (err) {
|
|
117265
|
+
console.error(
|
|
117266
|
+
` [keyword] \u8B66\u544A\uFF1A\u62C9\u53D6\u8D26\u6237\u5173\u952E\u8BCD\u5931\u8D25\uFF0C\u8DF3\u8FC7 alreadyInAccount \u6807\u6CE8\uFF1A${err instanceof Error ? err.message : String(err)}`
|
|
117267
|
+
);
|
|
117268
|
+
}
|
|
117269
|
+
}
|
|
116802
117270
|
const n = items.length;
|
|
116803
117271
|
const kwPayload = buildKeywordSuggestJsonPayload(items, { bidAmountCurrency });
|
|
116804
117272
|
const displayItems = kwPayload.items;
|
|
@@ -116820,6 +117288,7 @@ async function runKeywordSuggest(opts) {
|
|
|
116820
117288
|
const cur = bidAmountCurrency;
|
|
116821
117289
|
const cols = [
|
|
116822
117290
|
{ key: "keyword", header: "\u5173\u952E\u8BCD" },
|
|
117291
|
+
...accountId ? [{ key: "alreadyInAccount", header: "\u5DF2\u5728\u8D26\u6237\u4E2D" }] : [],
|
|
116823
117292
|
{ key: "montlySearch", header: "\u6708\u5747\u641C\u7D22" },
|
|
116824
117293
|
{ key: "cpcMain", header: `\u5E73\u5747CPC(${cur})` },
|
|
116825
117294
|
{ key: "bidRangeMain", header: `\u9875\u9996\u51FA\u4EF7(${cur})` },
|
|
@@ -116833,6 +117302,9 @@ async function runKeywordSuggest(opts) {
|
|
|
116833
117302
|
const competitionDisplay = item.competitionV2 ?? (item.competition != null ? item.competition.toFixed(2) : "\u2014");
|
|
116834
117303
|
return {
|
|
116835
117304
|
keyword: item.keyword ?? "",
|
|
117305
|
+
...accountId ? {
|
|
117306
|
+
alreadyInAccount: item.alreadyInAccount === 1 ? "1" : item.alreadyInAccount === 0 ? "0" : "\u2014"
|
|
117307
|
+
} : {},
|
|
116836
117308
|
montlySearch: String(item.montlySearch ?? "\u2014"),
|
|
116837
117309
|
cpcMain,
|
|
116838
117310
|
bidRangeMain,
|