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.
- package/README.md +3 -0
- package/dist/commands/ads.js +4 -7
- package/dist/commands/ads.js.map +1 -1
- package/dist/commands/authDevice.js +10 -2
- package/dist/commands/authDevice.js.map +1 -1
- package/dist/commands/authGoogle.js +39 -42
- package/dist/commands/authGoogle.js.map +1 -1
- package/dist/commands/freee.js +2 -18
- package/dist/commands/freee.js.map +1 -1
- package/dist/commands/pl.js +4 -8
- package/dist/commands/pl.js.map +1 -1
- package/dist/commands/review.js +4 -8
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/status.js +188 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/submission.js +236 -21
- package/dist/commands/submission.js.map +1 -1
- package/dist/index.js +86 -31
- package/dist/index.js.map +1 -1
- package/dist/lib/apiError.js +63 -0
- package/dist/lib/apiError.js.map +1 -0
- package/dist/lib/authFlow.js +82 -0
- package/dist/lib/authFlow.js.map +1 -0
- package/dist/lib/googleImageMetadata.js +145 -0
- package/dist/lib/googleImageMetadata.js.map +1 -0
- package/dist/lib/openBrowser.js +63 -0
- package/dist/lib/openBrowser.js.map +1 -0
- package/dist/schemas/submit-plan.js +27 -1
- package/dist/schemas/submit-plan.js.map +1 -1
- package/package.json +4 -3
- package/skills/ad-ops/SKILL.md +108 -10
- package/skills/ad-ops/ad-planner.html +257 -20
- package/skills/ad-ops/scripts/adops.sh +39 -9
- package/skills/ad-ops/scripts/plan_lint.py +262 -28
- package/skills/ad-ops/scripts/refs_import.py +251 -20
- package/skills/ad-ops/scripts/test_plan_lint.sh +126 -0
- 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
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
1167
|
+
const msg = (0, apiError_1.describeApiError)(err);
|
|
953
1168
|
// タイムアウト / 接続断は「失敗」ではなく「成否不明」。backend は入稿を続けており、
|
|
954
1169
|
// 実際にキャンペーンと広告が作成済みのことがある(実測あり)。failed にすると
|
|
955
1170
|
// 再実行で同じキャンペーンをもう一度作ってしまうので、unknown で止める。
|