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.
Files changed (34) hide show
  1. package/README.md +1 -2
  2. package/dist/index.js +651 -179
  3. package/dist/skill/_meta.json +2 -2
  4. package/dist/skill/assets/campaign-create-template.md +12 -12
  5. package/dist/skill/assets/pmax-create-template.md +8 -2
  6. package/dist/skill/references/README.md +1 -1
  7. package/dist/skill/references/accounts/accounts.md +14 -26
  8. package/dist/skill/references/accounts/finance.md +9 -11
  9. package/dist/skill/references/accounts/open-account-by-media.md +2 -3
  10. package/dist/skill/references/accounts/open-account-google-ui.md +43 -74
  11. package/dist/skill/references/analytics/account-analytics.md +27 -2
  12. package/dist/skill/references/analytics/keyword-planner-workflows.md +5 -5
  13. package/dist/skill/references/analytics/rag.md +2 -2
  14. package/dist/skill/references/analytics/reporting.md +10 -17
  15. package/dist/skill/references/core/setup.md +6 -6
  16. package/dist/skill/references/core/skill-authoring.md +1 -1
  17. package/dist/skill/references/core/workflows.md +19 -41
  18. package/dist/skill/references/google-ads/google-ads.md +9 -13
  19. package/dist/skill/references/google-ads/pmax-api.md +43 -41
  20. package/dist/skill/references/misc/tso-home.md +20 -84
  21. package/dist/skill/references/operations/clue.md +1 -1
  22. package/dist/skill/references/operations/forewarning.md +0 -2
  23. package/dist/skill/references/operations/optimize.md +2 -4
  24. package/dist/skill/report-templates/README.md +1 -1
  25. package/dist/skill/report-templates/bing-period-report.md +2 -3
  26. package/dist/skill/report-templates/google-ads-diagnosis.md +2 -2
  27. package/dist/skill/report-templates/google-inquiry-analysis.md +4 -4
  28. package/dist/skill/report-templates/google-period-report.md +1 -1
  29. package/dist/skill/report-templates/meta-period-report.md +2 -3
  30. package/dist/skill/report-templates/okki-weekly-google-client.md +1 -1
  31. package/dist/skill/report-templates/tiktok-period-report.md +4 -7
  32. package/dist/skill/scripts/install.ps1 +3 -3
  33. package/dist/skill/scripts/install.sh +3 -3
  34. 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 authHeaders = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
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
- ...authHeaders,
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 authHeaders = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
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
- ...authHeaders,
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 authHeaders = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
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
- ...authHeaders
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-ci.siluzan.com";
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
- const accounts = r.accounts ?? {};
4149
- const rows = [];
4150
- for (const [id, item] of Object.entries(
4151
- accounts
4152
- )) {
4153
- const data = item?.data;
4154
- if (!data) continue;
4155
- rows.push({
4156
- mode: "googleCombined",
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
- return rows;
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(...GOOGLE_ANALYSIS_CAMPAIGNS_OUTLINE_BUDGET_HINTS);
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 sleep2(ms, signal) {
100872
- return new Promise((resolve12, reject) => {
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
- resolve12();
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 sleep2(backoffMs, options.signal);
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-load.ts
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(readFileSync6(configFile, "utf8"));
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 = [...cfgErrors, ...imgErrors];
114361
- const warnings = [...cfgWarnings, ...imgWarnings];
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 = [...cfgErrors, ...imgErrors];
114458
- const warnings = [...cfgWarnings, ...imgWarnings];
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 basename5, isAbsolute as isAbsolute5 } from "path";
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
- let body;
114959
- if (opts.bodyFile) {
114960
- body = loadPmaxJsonFile(opts.bodyFile);
114961
- } else {
114962
- body = {
114963
- youtubeUrlOrId: opts.youtubeUrlOrId.trim()
114964
- };
114965
- if (opts.campaignId?.trim()) body["campaignId"] = opts.campaignId.trim();
114966
- if (opts.assetName?.trim()) body["assetName"] = opts.assetName.trim();
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
- const raw = await pmaxApiFetch(
114973
- url,
114974
- config,
114975
- { method: "POST", body: JSON.stringify(body) },
114976
- opts.verbose
114977
- );
114978
- data = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : { data: raw };
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 = isAbsolute5(opts.imagePath) ? opts.imagePath.trim() : resolvePmaxImagePath2(process.cwd(), opts.imagePath);
115170
- const name2 = (opts.name ?? basename5(abs)).trim();
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 readFileSync8, existsSync as existsSync3 } from "fs";
115359
- import { resolve as resolve9, dirname as dirname10, basename as basename6, extname } from "path";
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 ? resolve9(opts.outputDir) : dirname10(resolve9(firstInput));
115796
+ const outputDir = opts.outputDir ? resolve10(opts.outputDir) : dirname11(resolve10(firstInput));
115407
115797
  mkdirSync3(outputDir, { recursive: true });
115408
- const prefix = opts.prefix ?? basename6(firstInput, extname(firstInput));
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 = resolve9(src);
115421
- if (!existsSync3(absInput)) {
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 = resolve9(outputDir, outName);
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 = resolve9(opts.updateConfig);
115837
+ const configPath = resolve10(opts.updateConfig);
115448
115838
  try {
115449
- const raw = JSON.parse(readFileSync8(configPath, "utf8"));
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("\u4E3A\u8D44\u4EA7\u7EC4\u94FE\u63A5 YouTube \u89C6\u9891\uFF08POST .../youtube\uFF09").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").option("--asset-name <name>", "\u8D44\u4EA7\u663E\u793A\u540D").option("--body-file <path>", "\u5B8C\u6574 JSON body\uFF08\u8986\u76D6 --youtube\uFF09").option("-t, --token <token>", "Auth Token")
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,