addashboard-cli 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +3 -0
  2. package/dist/commands/ads.js +4 -7
  3. package/dist/commands/ads.js.map +1 -1
  4. package/dist/commands/authDevice.js +10 -2
  5. package/dist/commands/authDevice.js.map +1 -1
  6. package/dist/commands/authGoogle.js +39 -42
  7. package/dist/commands/authGoogle.js.map +1 -1
  8. package/dist/commands/freee.js +2 -18
  9. package/dist/commands/freee.js.map +1 -1
  10. package/dist/commands/pl.js +4 -8
  11. package/dist/commands/pl.js.map +1 -1
  12. package/dist/commands/review.js +4 -8
  13. package/dist/commands/review.js.map +1 -1
  14. package/dist/commands/status.js +188 -0
  15. package/dist/commands/status.js.map +1 -0
  16. package/dist/commands/submission.js +236 -21
  17. package/dist/commands/submission.js.map +1 -1
  18. package/dist/index.js +86 -31
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/apiError.js +63 -0
  21. package/dist/lib/apiError.js.map +1 -0
  22. package/dist/lib/authFlow.js +82 -0
  23. package/dist/lib/authFlow.js.map +1 -0
  24. package/dist/lib/googleImageMetadata.js +145 -0
  25. package/dist/lib/googleImageMetadata.js.map +1 -0
  26. package/dist/lib/openBrowser.js +63 -0
  27. package/dist/lib/openBrowser.js.map +1 -0
  28. package/dist/schemas/submit-plan.js +27 -1
  29. package/dist/schemas/submit-plan.js.map +1 -1
  30. package/package.json +4 -3
  31. package/skills/ad-ops/SKILL.md +108 -10
  32. package/skills/ad-ops/ad-planner.html +257 -20
  33. package/skills/ad-ops/scripts/adops.sh +39 -9
  34. package/skills/ad-ops/scripts/plan_lint.py +262 -28
  35. package/skills/ad-ops/scripts/refs_import.py +251 -20
  36. package/skills/ad-ops/scripts/test_plan_lint.sh +126 -0
  37. package/skills/ad-ops//343/202/242/343/202/253/343/202/246/343/203/263/343/203/210/google/sample_account.json +4 -0
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.STATUS_MAX_IDS = exports.STATUS_LEVELS = exports.STATUS_PLATFORMS = void 0;
7
+ exports.parseIds = parseIds;
8
+ exports.formatStatusSummary = formatStatusSummary;
9
+ exports.runUpdateStatus = runUpdateStatus;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const apiClient_1 = require("../lib/apiClient");
12
+ const apiError_1 = require("../lib/apiError");
13
+ /** ステータス更新に対応している媒体。現状 YDA のみ (Meta 以降は未実装)。 */
14
+ exports.STATUS_PLATFORMS = ['yda'];
15
+ exports.STATUS_LEVELS = ['campaign', 'adGroup', 'ad'];
16
+ /** backend が ids に載せる上限。ここでも先に弾いて無駄な往復を避ける。 */
17
+ exports.STATUS_MAX_IDS = 200;
18
+ /**
19
+ * ID 列をパースする。
20
+ *
21
+ * 貼り付け元が管理画面のコピペ・CSV・改行区切りテキストのいずれでも通るように、
22
+ * カンマ / 空白 / 改行 / タブ をすべて区切りとして扱う。
23
+ * 重複は畳む (同じ ID を2回送っても set を2回叩く意味がなく、件数上限の判定も狂う)。
24
+ */
25
+ function parseIds(raw) {
26
+ return Array.from(new Set(raw
27
+ .split(/[\s,]+/)
28
+ .map((s) => s.trim())
29
+ .filter(Boolean)));
30
+ }
31
+ const LEVEL_LABEL = {
32
+ campaign: 'キャンペーン',
33
+ adGroup: '広告グループ',
34
+ ad: '広告',
35
+ };
36
+ /** 人が目で確認するためのサマリを組み立てる。 */
37
+ function formatStatusSummary(level, data) {
38
+ const lines = [];
39
+ const label = LEVEL_LABEL[level];
40
+ for (const t of data.targets ?? []) {
41
+ const parent = level === 'ad' ? ` (CP:${t.campaignId} AG:${t.adGroupId})`
42
+ : level === 'adGroup' ? ` (CP:${t.campaignId})`
43
+ : '';
44
+ const arrow = t.skipped
45
+ ? `${t.currentStatus} (変更なし)`
46
+ : `${t.currentStatus} → ${t.newStatus}`;
47
+ lines.push(` ${t.id} ${arrow} ${t.name ?? ''}${parent}`);
48
+ }
49
+ for (const id of data.notFound ?? []) {
50
+ lines.push(` ${id} ✗ 見つかりません`);
51
+ }
52
+ for (const id of data.unusable ?? []) {
53
+ lines.push(` ${id} ✗ 親IDが解決できません`);
54
+ }
55
+ lines.push(`${label}: 指定 ${data.requested ?? 0} 件 / 解決 ${data.resolved ?? 0} 件 / ` +
56
+ `変更 ${data.willChange ?? 0} 件 / 変更不要 ${data.unchanged ?? 0} 件 / ` +
57
+ `未解決 ${(data.notFound?.length ?? 0) + (data.unusable?.length ?? 0)} 件`);
58
+ if (data.results) {
59
+ const failed = data.results.filter((r) => !r.ok);
60
+ lines.push(`結果: 成功 ${data.successCount ?? 0} 件 / 失敗 ${data.failureCount ?? 0} 件`);
61
+ for (const f of failed)
62
+ lines.push(` ✗ ${f.id}: ${f.error ?? '不明なエラー'}`);
63
+ }
64
+ return lines.join('\n');
65
+ }
66
+ /**
67
+ * webgram update-status の本体。
68
+ *
69
+ * 安全弁 (submit / submit-plan と同思想):
70
+ * 1. dryRun=true (default) → ?dry_run=1 で POST。実 API を叩かず「現status → 新status」を返すだけ
71
+ * 2. dryRun=false AND yes=false → exit 2
72
+ * 3. status=ACTIVE の本実行は confirmActivate も必須 (配信開始・課金が発生する操作のため)
73
+ * 4. ids 上限 STATUS_MAX_IDS
74
+ */
75
+ async function runUpdateStatus(input) {
76
+ const log = input.logger ?? ((line) => console.error(line));
77
+ if (!exports.STATUS_PLATFORMS.includes(input.platform)) {
78
+ return {
79
+ ok: false,
80
+ exitCode: 1,
81
+ error: `webgram update-status は ${input.platform} に未対応です (対応: ${exports.STATUS_PLATFORMS.join(', ')})`,
82
+ };
83
+ }
84
+ if (!exports.STATUS_LEVELS.includes(input.level)) {
85
+ return { ok: false, exitCode: 1, error: `unsupported level: ${input.level}` };
86
+ }
87
+ if (input.status !== 'ACTIVE' && input.status !== 'PAUSED') {
88
+ return { ok: false, exitCode: 1, error: `--status は ACTIVE か PAUSED です (指定: ${input.status})` };
89
+ }
90
+ if (!input.accountId) {
91
+ return { ok: false, exitCode: 1, error: '--account-id は必須です' };
92
+ }
93
+ // ID の収集 (--ids と --ids-file はマージする)
94
+ const rawParts = [];
95
+ if (input.ids)
96
+ rawParts.push(input.ids);
97
+ if (input.idsFile) {
98
+ try {
99
+ rawParts.push(fs_1.default.readFileSync(input.idsFile, 'utf8'));
100
+ }
101
+ catch (e) {
102
+ return { ok: false, exitCode: 1, error: `ids_file_read_error: ${e.message}` };
103
+ }
104
+ }
105
+ if (rawParts.length === 0) {
106
+ return { ok: false, exitCode: 1, error: '--ids か --ids-file のどちらかが必要です' };
107
+ }
108
+ const ids = parseIds(rawParts.join(','));
109
+ if (ids.length === 0) {
110
+ return { ok: false, exitCode: 1, error: '有効な ID がありません' };
111
+ }
112
+ if (ids.length > exports.STATUS_MAX_IDS) {
113
+ return {
114
+ ok: false,
115
+ exitCode: 1,
116
+ error: `ids は最大 ${exports.STATUS_MAX_IDS} 件までです (指定: ${ids.length} 件)`,
117
+ };
118
+ }
119
+ // 安全弁: 本実行には --yes、ACTIVE 化にはさらに --confirm-activate。
120
+ // dry-run を挟まずいきなり ACTIVE にできてしまうと、ON 漏れの逆 (誤 ON による
121
+ // 意図しない配信・課金) が起きる。
122
+ if (!input.dryRun && !input.yes) {
123
+ return {
124
+ ok: false,
125
+ exitCode: 2,
126
+ error: '本実行するには --yes を明示してください (dry-run なしで実 API を叩く操作です)',
127
+ };
128
+ }
129
+ if (!input.dryRun && input.status === 'ACTIVE' && !input.confirmActivate) {
130
+ return {
131
+ ok: false,
132
+ exitCode: 2,
133
+ error: 'ACTIVE 化 (配信開始) の本実行には --confirm-activate も必要です ' +
134
+ '(先に dry-run で対象を確認してください)',
135
+ };
136
+ }
137
+ let client;
138
+ try {
139
+ client = (input.clientFactory ?? apiClient_1.buildAuthedApiClient)(input.store);
140
+ }
141
+ catch (e) {
142
+ return { ok: false, exitCode: 1, error: e.message };
143
+ }
144
+ log(`[cli/update-status] platform=${input.platform} level=${input.level} ` +
145
+ `account=${input.accountId} status=${input.status} ids=${ids.length} dry_run=${input.dryRun}`);
146
+ try {
147
+ const params = {};
148
+ if (input.dryRun)
149
+ params.dry_run = '1';
150
+ const res = await client.post(`/api/submission/${input.platform}/status`, {
151
+ accountId: input.accountId,
152
+ level: input.level,
153
+ ids,
154
+ status: input.status,
155
+ ...(input.status === 'ACTIVE' && !input.dryRun ? { confirmActivate: true } : {}),
156
+ }, {
157
+ params,
158
+ // 200 件を数チャンクに割って媒体 API を順に叩くため、既定 15 秒では足りない
159
+ ...(input.dryRun ? {} : { timeout: apiClient_1.SUBMIT_TIMEOUT_MS }),
160
+ });
161
+ const data = res.data;
162
+ log(formatStatusSummary(input.level, data));
163
+ // 部分失敗は成功扱いにしない。exit 0 だと CI / スクリプトが握り潰す。
164
+ if (!input.dryRun && (data.failureCount ?? 0) > 0) {
165
+ return {
166
+ ok: false,
167
+ exitCode: 3,
168
+ data,
169
+ error: `${data.failureCount} 件の更新に失敗しました`,
170
+ };
171
+ }
172
+ return { ok: true, exitCode: 0, data };
173
+ }
174
+ catch (err) {
175
+ const status = err?.response?.status;
176
+ const data = err
177
+ ?.response?.data;
178
+ if (status === 401)
179
+ return { ok: false, exitCode: 1, error: 'unauthorized' };
180
+ // 未解決IDでの中断は「何が解決できなかったか」を出さないと直しようがない
181
+ if (data?.error === 'unresolved_ids') {
182
+ log(formatStatusSummary(input.level, data));
183
+ }
184
+ const msg = data?.message ?? (0, apiError_1.describeApiError)(err);
185
+ return { ok: false, exitCode: 1, error: msg };
186
+ }
187
+ }
188
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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"}
@@ -13,7 +13,9 @@ const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const apiClient_1 = require("../lib/apiClient");
15
15
  const localPath_1 = require("../lib/localPath");
16
+ const googleImageMetadata_1 = require("../lib/googleImageMetadata");
16
17
  const submit_plan_1 = require("../schemas/submit-plan");
18
+ const apiError_1 = require("../lib/apiError");
17
19
  exports.SUBMIT_PLATFORMS = ['meta', 'yda', 'google', 'smartnews', 'bytedance'];
18
20
  /** v5 §5.3: platform → submit エンドポイント (T-02 yda, T-03 google, T-04 smartnews, T-05 bytedance) */
19
21
  function resolveSubmitPath(platform) {
@@ -283,18 +285,28 @@ async function resolveYdaImages(client, body, jsonFile, log) {
283
285
  async function resolveGoogleImages(client, body, jsonFile, log) {
284
286
  const baseDir = path_1.default.dirname(path_1.default.resolve(jsonFile));
285
287
  const customerId = String(body?.customerId ?? '');
286
- const uploadOne = async (relPath) => {
288
+ const uploadOne = async (relPath, purpose) => {
287
289
  const abs = (0, localPath_1.resolveLocalMediaPath)(relPath, baseDir, 'image');
288
290
  const buf = fs_1.default.readFileSync(abs);
289
291
  const fileName = path_1.default.basename(abs);
290
292
  const form = new FormData();
291
293
  form.append('customerId', customerId);
292
294
  form.append('image', new Blob([buf]), fileName);
295
+ if (purpose)
296
+ form.append('purpose', purpose);
293
297
  const res = await client.post('/api/submission/google/upload-image', form, { timeout: 120000 });
294
298
  const rn = res.data?.asset_resource_name;
295
299
  if (!rn)
296
300
  throw new Error(`upload-image が asset_resource_name を返しませんでした (${fileName})`);
297
301
  log(`[cli/submit] uploaded image ${fileName} → ${rn}`);
302
+ // 入稿できる範囲の指摘 (推奨サイズ超過など)。握り潰すと運用者が気づけない
303
+ for (const w of res.data?.warnings ?? [])
304
+ log(`[cli/submit] ⚠ ${w}`);
305
+ /* 作った asset は削除できない。どのファイルから何を作ったかを計画ファイルに
306
+ 残しておかないと、アカウントJSONへの書き戻し (refs-import --from-plan) で
307
+ 追えなくなる。 */
308
+ if (purpose === 'demand_gen_logo')
309
+ recordUploadedLogo(body, rn, relPath, fileName);
298
310
  return rn;
299
311
  };
300
312
  const fields = [
@@ -302,25 +314,69 @@ async function resolveGoogleImages(client, body, jsonFile, log) {
302
314
  ['squareMarketingImagePaths', 'squareMarketingImages'],
303
315
  ['logoImagePaths', 'logoImages'],
304
316
  ];
317
+ const resolveField = async (ad, pathField, assetField, purpose) => {
318
+ const paths = Array.isArray(ad?.[pathField]) ? ad[pathField] : [];
319
+ if (paths.length === 0)
320
+ return;
321
+ const uploaded = [];
322
+ for (const p of paths)
323
+ uploaded.push(await uploadOne(p, purpose));
324
+ ad[assetField] = [...(Array.isArray(ad[assetField]) ? ad[assetField] : []), ...uploaded];
325
+ // 消さないと計画ファイルの再実行で同じ画像をもう一度アップロードしてしまう
326
+ delete ad[pathField];
327
+ };
305
328
  for (const cp of body?.campaigns ?? []) {
306
329
  for (const ag of cp?.adGroups ?? []) {
307
330
  for (const ad of ag?.ads ?? []) {
308
- if (ad?.type !== 'RESPONSIVE_DISPLAY_AD')
331
+ if (ad?.type === 'RESPONSIVE_DISPLAY_AD') {
332
+ for (const [pathField, assetField] of fields) {
333
+ await resolveField(ad, pathField, assetField);
334
+ }
309
335
  continue;
310
- for (const [pathField, assetField] of fields) {
311
- const paths = Array.isArray(ad?.[pathField]) ? ad[pathField] : [];
312
- if (paths.length === 0)
313
- continue;
314
- const uploaded = [];
315
- for (const p of paths)
316
- uploaded.push(await uploadOne(p));
317
- ad[assetField] = [...(Array.isArray(ad[assetField]) ? ad[assetField] : []), ...uploaded];
318
- delete ad[pathField];
336
+ }
337
+ /* Demand Gen は**ロゴだけ**。マーケティング画像のローカルパス対応は
338
+ 別スコープで、ここで一緒に上げると意図しないアセットが増える。 */
339
+ if (ad?.type === 'DEMAND_GEN_MULTI_ASSET_AD') {
340
+ await resolveField(ad, 'logoImagePaths', 'logoImages', 'demand_gen_logo');
319
341
  }
320
342
  }
321
343
  }
322
344
  }
323
345
  }
346
+ /**
347
+ * アップロードしたロゴを計画ファイルへ記録する。
348
+ *
349
+ * `refs-import --from-plan` がこれを読んでアカウントJSONの `refs.logos` へ入れる。
350
+ * CLI が入稿の途中でアカウントJSONを直接書かないのは、実クライアントの参照データを
351
+ * 誤って壊す経路を増やさないため(過去に上書きで飛ばした事例がある)。
352
+ */
353
+ /**
354
+ * 1 キャンペーン分の記録を計画ファイル側へ集める。
355
+ *
356
+ * **Google のロゴ専用。** 重複判定を `resourceName` でしているため、他媒体
357
+ * (Meta の imageHash / YDA の mediaId / TikTok の videoId) をそのまま流すと
358
+ * 全件 undefined 同士になって 2 件目以降が落ちる。媒体を増やすときは
359
+ * キーの決め方から作り直す。
360
+ */
361
+ function mergeUploadedGoogleLogos(plan, uploaded) {
362
+ const logos = uploaded?.google?.logos;
363
+ if (!Array.isArray(logos) || logos.length === 0)
364
+ return;
365
+ const dest = ((plan.uploadedAssets ??= {}).google ??= {}).logos ??= [];
366
+ for (const item of logos) {
367
+ if (dest.some((d) => d?.resourceName === item?.resourceName))
368
+ continue;
369
+ dest.push(item);
370
+ }
371
+ }
372
+ function recordUploadedLogo(body, resourceName, originalPath, fileName) {
373
+ const bucket = (body.uploadedAssets ??= {});
374
+ const google = (bucket.google ??= {});
375
+ const logos = (google.logos ??= []);
376
+ if (logos.some((l) => l?.resourceName === resourceName))
377
+ return;
378
+ logos.push({ resourceName, originalPath, fileName });
379
+ }
324
380
  /**
325
381
  * TikTok のアップロード対象を 1 箇所で決める。
326
382
  *
@@ -478,16 +534,24 @@ function collectMediaRefs(platform, body) {
478
534
  return refs;
479
535
  }
480
536
  if (platform === 'google') {
481
- const pathFields = ['marketingImagePaths', 'squareMarketingImagePaths', 'logoImagePaths'];
537
+ const displayPathFields = ['marketingImagePaths', 'squareMarketingImagePaths', 'logoImagePaths'];
482
538
  for (const cp of body?.campaigns ?? []) {
483
539
  for (const ag of cp?.adGroups ?? []) {
484
540
  for (const ad of ag?.ads ?? []) {
485
- if (ad?.type !== 'RESPONSIVE_DISPLAY_AD')
541
+ if (ad?.type === 'RESPONSIVE_DISPLAY_AD') {
542
+ for (const f of displayPathFields) {
543
+ const paths = Array.isArray(ad?.[f]) ? ad[f] : [];
544
+ for (const p of paths)
545
+ refs.push({ value: p, kind: 'image' });
546
+ }
486
547
  continue;
487
- for (const f of pathFields) {
488
- const paths = Array.isArray(ad?.[f]) ? ad[f] : [];
548
+ }
549
+ /* Demand Gen は**ロゴだけ**アップロードの対象にする。マーケティング画像の
550
+ ローカルパス対応は別スコープで、ここに混ぜると意図せず上がってしまう。 */
551
+ if (ad?.type === 'DEMAND_GEN_MULTI_ASSET_AD') {
552
+ const paths = Array.isArray(ad?.logoImagePaths) ? ad.logoImagePaths : [];
489
553
  for (const p of paths)
490
- refs.push({ value: p, kind: 'image' });
554
+ refs.push({ value: p, kind: 'image', purpose: 'demand_gen_logo' });
491
555
  }
492
556
  }
493
557
  }
@@ -495,6 +559,39 @@ function collectMediaRefs(platform, body) {
495
559
  }
496
560
  return refs;
497
561
  }
562
+ /**
563
+ * Demand Gen のロゴ指定が backend の受け入れ条件を満たすかを、**アップロードの前に**見る。
564
+ *
565
+ * backend の superRefine (either 必須 / 同時指定不可 / 合計5件) は入稿 POST の時点で効くが、
566
+ * CLI は**その前にアップロードを済ませてしまう**。同時指定や6件を CLI が素通りさせると、
567
+ * backend が 400 を返す頃には削除できない asset が作られている。同じ条件をここでも見る。
568
+ */
569
+ function validateGoogleLogoSources(platform, body) {
570
+ if (platform !== 'google')
571
+ return [];
572
+ const errors = [];
573
+ for (const cp of body?.campaigns ?? []) {
574
+ for (const ag of cp?.adGroups ?? []) {
575
+ for (const ad of ag?.ads ?? []) {
576
+ if (ad?.type !== 'DEMAND_GEN_MULTI_ASSET_AD')
577
+ continue;
578
+ const assets = Array.isArray(ad?.logoImages) ? ad.logoImages.length : 0;
579
+ const paths = Array.isArray(ad?.logoImagePaths) ? ad.logoImagePaths.length : 0;
580
+ const where = `広告「${ad?.adName ?? '(名前なし)'}」`;
581
+ if (assets === 0 && paths === 0) {
582
+ errors.push(`${where}: ロゴが指定されていません (logoImages、または未登録なら logoImagePaths)`);
583
+ }
584
+ if (assets > 0 && paths > 0) {
585
+ errors.push(`${where}: logoImages と logoImagePaths は同時に指定できません(二重にアセットを作ってしまいます)`);
586
+ }
587
+ if (assets + paths > 5) {
588
+ errors.push(`${where}: ロゴは合計 5 件までです (received ${assets + paths})`);
589
+ }
590
+ }
591
+ }
592
+ }
593
+ return errors;
594
+ }
498
595
  /**
499
596
  * ローカルメディアパスを **アップロード前に全件** 解決・検証する。
500
597
  *
@@ -508,12 +605,23 @@ function collectMediaRefs(platform, body) {
508
605
  function validateLocalMediaPaths(platform, body, baseFile) {
509
606
  const baseDir = path_1.default.dirname(path_1.default.resolve(baseFile));
510
607
  const errors = [];
608
+ errors.push(...validateGoogleLogoSources(platform, body));
511
609
  for (const ref of collectMediaRefs(platform, body)) {
610
+ let abs;
512
611
  try {
513
- (0, localPath_1.resolveLocalMediaPath)(ref.value, baseDir, ref.kind);
612
+ abs = (0, localPath_1.resolveLocalMediaPath)(ref.value, baseDir, ref.kind);
514
613
  }
515
614
  catch (e) {
516
615
  errors.push(e.message);
616
+ continue;
617
+ }
618
+ /* Google Ads のアセットは削除できないので、要件違反はアップロードを1件も
619
+ 始める前に落とす。存在確認と同じループで見るのは、5枚中3枚目で気づいて
620
+ 2枚が本番に残る事態を避けるため。 */
621
+ if (ref.purpose === 'demand_gen_logo') {
622
+ const check = (0, googleImageMetadata_1.validateDemandGenLogoImage)(fs_1.default.readFileSync(abs), path_1.default.basename(abs));
623
+ if (!check.ok)
624
+ errors.push(...check.errors);
517
625
  }
518
626
  }
519
627
  return errors;
@@ -610,6 +718,14 @@ async function runSubmit(input) {
610
718
  return { ok: false, exitCode: 1, error: `image_upload_error: ${e.message}` };
611
719
  }
612
720
  }
721
+ /* Google は同名キャンペーンを拒否する。**アップロードより前に**止めないと、
722
+ CP を作れないのに削除できないアセットだけが残る(submit-plan 側と同じ理由)。 */
723
+ if (input.platform === 'google' && !input.dryRun) {
724
+ const b = body;
725
+ const dup = await preflightGoogleCampaignNames(client, String(b?.customerId ?? b?.accountId ?? ''), Array.isArray(b?.campaigns) ? b.campaigns : [], log);
726
+ if (dup)
727
+ return dup;
728
+ }
613
729
  // 本実行時のみ: Google ディスプレイ広告のローカル画像(*ImagePaths)を先に
614
730
  // アップロードして asset resource_name へ解決。dry-run では実 API を叩かない
615
731
  if (input.platform === 'google' && !input.dryRun) {
@@ -678,8 +794,7 @@ async function runSubmit(input) {
678
794
  }
679
795
  if (status === 401)
680
796
  return { ok: false, exitCode: 1, error: 'unauthorized' };
681
- const msg = data?.error ?? err.message ?? 'network_error';
682
- return { ok: false, exitCode: 1, error: msg };
797
+ return { ok: false, exitCode: 1, error: (0, apiError_1.describeApiError)(err) };
683
798
  }
684
799
  }
685
800
  /** submit-plan が対応する媒体 (プランナーがスキーマ駆動で書き出せる媒体)。 */
@@ -734,6 +849,78 @@ function collectTargetErrors(data) {
734
849
  }
735
850
  return out;
736
851
  }
852
+ /**
853
+ * これから送る Google キャンペーン名が、アカウントの既存 CP と衝突しないかを確かめる。
854
+ *
855
+ * **本実行の最初に、素材のアップロードより前に行う。** Google は同名 CP を拒否するので、
856
+ * 後で気づくと「削除できないアセットだけ作られて CP は作れない」状態になる。
857
+ *
858
+ * 一覧が取得上限で切れていたら (`truncated`)、見えている範囲に無くても
859
+ * 「重複なし」とは断定できない。**それでも止めない** — 大きなアカウントでは常に
860
+ * 切れてしまい、機能が使えなくなる。警告を出して進め、隠れた重複は Google 側の
861
+ * エラー (CP 本体を作る前に落ちる) に任せる。
862
+ */
863
+ async function preflightGoogleCampaignNames(client, accountId, pending, log) {
864
+ if (pending.length === 0)
865
+ return null;
866
+ const conflicts = [];
867
+ /* 計画の中での重複は API を必要としない。一覧の取得可否と切り離して先に見る
868
+ (取得に失敗したときに、ローカルで分かる不整合まで見逃してしまう)。 */
869
+ const seen = new Set();
870
+ for (const cp of pending) {
871
+ const name = String(cp?.name ?? '').trim();
872
+ if (!name)
873
+ continue;
874
+ const key = name.toLowerCase();
875
+ if (seen.has(key))
876
+ conflicts.push(`「${name}」がこの計画の中で重複しています`);
877
+ seen.add(key);
878
+ }
879
+ let existing = [];
880
+ let truncated = false;
881
+ let listed = true;
882
+ try {
883
+ const res = await client.get('/api/submission/google/campaigns', {
884
+ params: { accountId },
885
+ });
886
+ existing = res.data?.campaigns ?? [];
887
+ truncated = !!res.data?.truncated;
888
+ }
889
+ catch (e) {
890
+ /* 一覧を引けないことを入稿の中止理由にはしない。プランナーと backend にも
891
+ 同じ判定があり、ここは「早く気づける」ための層なので落とさない。 */
892
+ log(`[cli/submit] ⚠ 既存キャンペーン名を確認できませんでした: ${e.message}`);
893
+ listed = false;
894
+ }
895
+ if (listed) {
896
+ const known = new Set();
897
+ for (const c of existing) {
898
+ const n = String(c?.name ?? '').trim();
899
+ if (n)
900
+ known.add(n.toLowerCase());
901
+ }
902
+ for (const cp of pending) {
903
+ const name = String(cp?.name ?? '').trim();
904
+ if (name && known.has(name.toLowerCase())) {
905
+ conflicts.push(`「${name}」はアカウントの既存キャンペーンと同名です`);
906
+ }
907
+ }
908
+ }
909
+ if (conflicts.length > 0) {
910
+ return {
911
+ ok: false,
912
+ exitCode: 1,
913
+ error: 'campaign_name_duplicate: Google は同名キャンペーンを拒否します\n - '
914
+ + conflicts.join('\n - ')
915
+ + '\n 計画ファイルのキャンペーン名を変えてから再実行してください',
916
+ };
917
+ }
918
+ if (truncated) {
919
+ log('[cli/submit] ⚠ 既存キャンペーンが取得上限を超えています。'
920
+ + '重複が無いことを確認しきれていません(一覧に無くても既存の可能性があります)');
921
+ }
922
+ return null;
923
+ }
737
924
  /** 計画ファイルを現在の campaigns/progress で上書き保存する (再開の要)。 */
738
925
  function persistPlan(planFile, plan) {
739
926
  fs_1.default.writeFileSync(planFile, JSON.stringify(plan, null, 2), 'utf8');
@@ -822,6 +1009,20 @@ async function runSubmitPlan(input) {
822
1009
  * すれば再開時に再アップロードしない。
823
1010
  */
824
1011
  const resolveMedia = async (singleBody) => {
1012
+ try {
1013
+ await resolveMediaInner(singleBody);
1014
+ }
1015
+ finally {
1016
+ /* cp は plan.campaigns[i] と同一参照なので広告階層の解決結果は plan に載るが、
1017
+ top-level の uploadedAssets は singleBody 側にしか付かない。
1018
+
1019
+ **finally なのは、途中で失敗しても既に作った asset を記録するため。**
1020
+ 3枚中2枚目で落ちたとき、1枚目は Google に残る (asset は削除できない)。
1021
+ 記録を捨てると、その asset を追跡する手段が無くなる。 */
1022
+ mergeUploadedGoogleLogos(plan, singleBody.uploadedAssets);
1023
+ }
1024
+ };
1025
+ const resolveMediaInner = async (singleBody) => {
825
1026
  if (platform === 'meta') {
826
1027
  await resolveMetaVideos(client, singleBody, input.planFile, log);
827
1028
  await resolveMetaImages(client, singleBody, input.planFile, log);
@@ -855,7 +1056,7 @@ async function runSubmitPlan(input) {
855
1056
  checks.push({ name: String(cp?.name ?? ''), ok: true });
856
1057
  }
857
1058
  catch (err) {
858
- const msg = err?.response?.data?.error ?? err?.message ?? 'network_error';
1059
+ const msg = (0, apiError_1.describeApiError)(err);
859
1060
  checks.push({ name: String(cp?.name ?? ''), ok: false, error: msg });
860
1061
  }
861
1062
  }
@@ -863,6 +1064,20 @@ async function runSubmitPlan(input) {
863
1064
  log(`[cli/submit-plan] dry-run: ${checks.filter(c => c.ok).length}/${checks.length} OK`);
864
1065
  return { ok: allOk, exitCode: allOk ? 0 : 1, data: { dry_run: true, checks } };
865
1066
  }
1067
+ /* Google は同名キャンペーンを拒否する。**ロゴのアップロードより前に**止めないと、
1068
+ CP 作成で落ちる前に削除できないアセットを作ってしまう。
1069
+ プランナーは refs のスナップショットしか見ないので、ここで実データと突き合わせる。 */
1070
+ if (platform === 'google') {
1071
+ /* 送らない CP は対象外。再開時に、既に入稿済みの CP 名で自分自身と
1072
+ 衝突していると誤検知してしまう。 */
1073
+ const pending = plan.campaigns.filter((_, i) => {
1074
+ const st = plan.progress?.[i]?.status;
1075
+ return st !== 'submitted' && st !== 'unknown';
1076
+ });
1077
+ const dup = await preflightGoogleCampaignNames(client, plan.accountId, pending, log);
1078
+ if (dup)
1079
+ return dup;
1080
+ }
866
1081
  // ---- 本実行: 1 キャンペーンずつ入稿し、進捗を計画ファイルへ書き戻す ----
867
1082
  const nowIso = input.nowIso ?? new Date().toISOString();
868
1083
  for (let i = 0; i < plan.campaigns.length; i++) {
@@ -949,7 +1164,7 @@ async function runSubmitPlan(input) {
949
1164
  }
950
1165
  }
951
1166
  catch (err) {
952
- const msg = err?.response?.data?.error ?? err?.message ?? 'network_error';
1167
+ const msg = (0, apiError_1.describeApiError)(err);
953
1168
  // タイムアウト / 接続断は「失敗」ではなく「成否不明」。backend は入稿を続けており、
954
1169
  // 実際にキャンペーンと広告が作成済みのことがある(実測あり)。failed にすると
955
1170
  // 再実行で同じキャンペーンをもう一度作ってしまうので、unknown で止める。