addashboard-cli 0.8.1 → 0.9.1

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.
@@ -12,9 +12,17 @@
12
12
  set -uo pipefail
13
13
 
14
14
  SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
15
- PORT=8799 # cli/playwright.config.ts の webServer と同じポートを共用する
15
+ # 配信ポート。既定 8799 は cli/playwright.config.ts の webServer と共用する。
16
+ # **worktree を分けて並行作業していると別 worktree のサーバを再利用してしまう**
17
+ # (curl が応答すれば「起動済み」と判断するので、別ブランチのプランナーが開く)。
18
+ # 実測では「追加した媒体がプルダウンに出ない」と誤診した。
19
+ # ADOPS_PORT=8801 adops.sh planner ... で自分のディレクトリを配信する専用サーバを立てる。
20
+ PORT="${ADOPS_PORT:-8799}"
16
21
  URL="http://127.0.0.1:${PORT}/ad-planner.html"
17
- CLI="npx -y addashboard-cli@latest"
22
+ # 既定は npm 公開版。開発中の未publish な機能を Skill フローで通したいときは
23
+ # WEBGRAM_CLI で手元のビルドに差し替える:
24
+ # WEBGRAM_CLI="node $PWD/cli/dist/index.js" adops.sh preflight google 1234567890
25
+ CLI="${WEBGRAM_CLI:-npx -y addashboard-cli@latest}"
18
26
 
19
27
  die() { echo "✗ $*" >&2; exit 1; }
20
28
 
@@ -69,9 +77,11 @@ PY
69
77
  write_active_plan() {
70
78
  local platform="$1" account="$2" sample
71
79
  case "$platform" in
72
- yda) sample='サンプルプラン.json' ;;
73
- meta) sample='サンプルプラン_meta.json' ;;
74
- *) die "未知の媒体: ${platform}(yda / meta)" ;;
80
+ yda) sample='サンプルプラン.json' ;;
81
+ meta) sample='サンプルプラン_meta.json' ;;
82
+ google) sample='サンプルプラン_google.json' ;;
83
+ bytedance) sample='サンプルプラン_bytedance.json' ;;
84
+ *) die "未知の媒体: ${platform}(yda / meta / google / bytedance)" ;;
75
85
  esac
76
86
 
77
87
  # 参照データが無いまま指定するとプランナーが同梱デフォルトへ黙って落ちるので、先に止める
@@ -118,6 +128,8 @@ cmd_planner() {
118
128
 
119
129
  if curl -sf -o /dev/null "$URL"; then
120
130
  echo "✓ 配信は起動済み(再利用)"
131
+ echo " ⚠ 別の作業ディレクトリ (worktree) が起動したサーバの可能性があります。" >&2
132
+ echo " プランナーの内容が想定と違うときは ADOPS_PORT=8801 adops.sh planner ... で専用ポートに切り替えてください。" >&2
121
133
  else
122
134
  echo "▸ http.server を起動中 (port ${PORT})..."
123
135
  # nohup でデタッチしないと、呼び出し元シェルの終了に巻き込まれて死ぬ
@@ -204,7 +216,44 @@ PY
204
216
  local mres="$key"; [ "$key" = "adgroups" ] && mres="adsets"
205
217
  url="https://adsmanager.facebook.com/adsmanager/manage/${mres}?act=${account#act_}"
206
218
  ;;
207
- *) die "open が未対応の媒体です: ${platform}(yda / meta)" ;;
219
+ google)
220
+ # Google は MCC 配下のアカウントを __c(manager) / __u(customer) で指定する。
221
+ # mccId 相当はアカウントJSONの consoleMcc(プランナーの consoleUrl と同じ供給元)。
222
+ mcc="$(ACCOUNT_ID="$account" SKILL_DIR="$SKILL_DIR" python3 - <<'PY'
223
+ import glob, json, os
224
+ want, skill = os.environ["ACCOUNT_ID"], os.environ["SKILL_DIR"]
225
+ paths = [p for p in glob.glob(os.path.join(skill, "アカウント", "google", "*.json"))
226
+ if os.path.basename(p) != "index.json"]
227
+ paths.sort(key=lambda p: os.path.basename(p) == "sample_account.json")
228
+ for p in paths:
229
+ try:
230
+ d = json.load(open(p, encoding="utf-8"))
231
+ except Exception:
232
+ continue
233
+ if str(d.get("accountId", "")) == want:
234
+ print(str(d.get("consoleMcc") or ""))
235
+ break
236
+ PY
237
+ )"
238
+ if [ -z "$mcc" ]; then
239
+ echo "⚠ アカウントJSONに consoleMcc がありません。MCC 指定なしで開きます。" >&2
240
+ echo " アカウントが切り替わらない場合は、管理画面URLの '__c=' の値を控えて" >&2
241
+ echo " アカウント/google/<名前>.json に \"consoleMcc\": \"...\" を追記してください。" >&2
242
+ url="https://ads.google.com/aw/${key}?__u=${account}"
243
+ else
244
+ url="https://ads.google.com/aw/${key}?ocid=&__c=${mcc}&__u=${account}"
245
+ fi
246
+ ;;
247
+ bytedance)
248
+ # TikTok Ads Manager は キャンペーン / 広告グループ / 広告 を URL で指定できず、
249
+ # 広告管理画面のタブで切り替える。resource は受け取るが URL には反映しない
250
+ # (実 API 検証で URL 構造が確定したら分岐を足す)。
251
+ url="https://ads.tiktok.com/i18n/perf/creative?aadvid=${account}"
252
+ if [ "$key" != "campaigns" ]; then
253
+ echo "ℹ TikTok は ${key} 単位のURLが無いため広告管理画面を開きます(画面上のタブで切り替えてください)。" >&2
254
+ fi
255
+ ;;
256
+ *) die "open が未対応の媒体です: ${platform}(yda / meta / google / bytedance)" ;;
208
257
  esac
209
258
 
210
259
  echo "$url"
@@ -110,6 +110,56 @@ def pick_plan(argv: list[str]) -> str:
110
110
 
111
111
 
112
112
  # ------------------------------------------------------- 共通: 値の取り出し
113
+ def final_url_of(ad: dict) -> str:
114
+ """最終リンク先URL。キーの形が媒体で違う。
115
+
116
+ yda / meta = finalUrl / google = finalUrls[] / bytedance = landingPageUrl
117
+ """
118
+ if ad.get("finalUrl"):
119
+ return str(ad["finalUrl"])
120
+ if ad.get("landingPageUrl"):
121
+ return str(ad["landingPageUrl"])
122
+ urls = ad.get("finalUrls") or []
123
+ return str(urls[0]) if urls else ""
124
+
125
+
126
+ def status_of(node: dict) -> str:
127
+ """配信状態。**キー名も値も媒体で違う**。
128
+
129
+ キー: yda / meta / google = status / bytedance = operationStatus
130
+ 配信オンの値: yda / meta = ACTIVE / google = ENABLED / bytedance = ENABLE
131
+ """
132
+ return str(node.get("status") or node.get("operationStatus") or "")
133
+
134
+
135
+ # 配信オンを表す値の全集合。1 つでも取りこぼすと「配信オンの計画」が lint を素通りする
136
+ ON_STATUSES = ("ACTIVE", "ENABLED", "ENABLE")
137
+ # 実 API での入稿検証が未了の媒体。配信オンは warn ではなく err で止める
138
+ UNVERIFIED_PLATFORMS = ("google", "bytedance")
139
+
140
+
141
+ def budget_of(campaign: dict) -> int:
142
+ """日予算。キー名がファイル種別で違い、**予算を持つ階層も媒体で違う**。
143
+
144
+ キャンペーン階層: yda = budget / google = budgetAmountYen / サンプル = dailyBudget
145
+ 広告グループ階層: meta / bytedance (キャンペーンに予算を持たない)
146
+
147
+ 階層差を吸収しないと、予算が広告グループにある媒体では常に 0 と読めてしまい
148
+ 「日予算 0 が想定レンジ外」の警告が毎回出る (= 本物の桁ミスが埋もれる)。
149
+ """
150
+ direct = (
151
+ campaign.get("budget")
152
+ or campaign.get("budgetAmountYen")
153
+ or campaign.get("dailyBudget")
154
+ )
155
+ if direct:
156
+ return int(direct)
157
+ return sum(
158
+ int(g.get("budget") or g.get("budgetAmountYen") or g.get("dailyBudget") or 0)
159
+ for g in ad_groups(campaign)
160
+ )
161
+
162
+
113
163
  def param_code_of(ad: dict, group: dict) -> str:
114
164
  """広告グループの paramCode。
115
165
 
@@ -118,7 +168,7 @@ def param_code_of(ad: dict, group: dict) -> str:
118
168
  """
119
169
  if group.get("paramCode"):
120
170
  return str(group["paramCode"])
121
- url = ad.get("finalUrl") or ""
171
+ url = final_url_of(ad)
122
172
  q = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
123
173
  code = (q.get("ad") or [""])[0]
124
174
  if code:
@@ -131,12 +181,13 @@ def image_no_of(ad: dict) -> str:
131
181
  """画像番号。ad.imageNo → creative パラメータ → 広告名 '1047c-...' の順で復元する。"""
132
182
  if ad.get("imageNo"):
133
183
  return str(ad["imageNo"])
134
- url = ad.get("finalUrl") or ""
184
+ url = final_url_of(ad)
135
185
  q = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
136
186
  m = re.search(r"-(\d+)[a-z]-", (q.get("creative") or [""])[0])
137
187
  if m:
138
188
  return m.group(1)
139
- m = re.match(r"^(\d+)[cs]-", str(ad.get("adName") or ""))
189
+ # 形式記号は媒体で違う (yda: c/s / meta・google: i/v)。1文字の英小文字として扱う
190
+ m = re.match(r"^(\d+)[a-z]-", str(ad.get("adName") or ""))
140
191
  return m.group(1) if m else ""
141
192
 
142
193
 
@@ -158,11 +209,14 @@ def summarize(plan: dict) -> None:
158
209
  print(f"\n── 入稿計画 ── platform={plan.get('platform', 'yda')} "
159
210
  f"accountId={plan.get('accountId')} planVersion={plan.get('planVersion')}")
160
211
  for ci, c in enumerate(plan.get("campaigns") or []):
161
- bid = c.get("biddingStrategy", "")
162
- if c.get("targetCpa"):
163
- bid += f" (tCPA {c['targetCpa']:,})"
212
+ bid = c.get("biddingStrategy") or c.get("advertisingChannelType") or ""
213
+ # 目標CPA のキー名も媒体で違う (yda: targetCpa / google: targetCpaYen)
214
+ tcpa = c.get("targetCpa") or c.get("targetCpaYen")
215
+ if tcpa:
216
+ bid += f" (tCPA {tcpa:,})"
217
+ budget = budget_of(c)
164
218
  print(f"\n CP[{ci}] {c.get('name')}")
165
- print(f" 予算 {c.get('budget', 0):,}/日 | {bid} | status {c.get('status')} "
219
+ print(f" 予算 {budget:,}/日 | {bid} | status {status_of(c) or '-'} "
166
220
  f"| 開始 {c.get('startDate', '-')}")
167
221
  for g in ad_groups(c):
168
222
  tg = "/".join(filter(None, [
@@ -170,12 +224,20 @@ def summarize(plan: dict) -> None:
170
224
  ",".join(g.get("ages") or []) or "全年齢",
171
225
  ",".join(g.get("genders") or []) or "全性別",
172
226
  ]))
173
- print(f" AG {g.get('name')} | {tg} | status {g.get('status')}")
227
+ print(f" AG {g.get('name')} | {tg} | status {status_of(g) or '-'}")
174
228
  for a in g.get("ads") or []:
175
- media = ",".join(a.get("mediaIds") or []) or a.get("videoId") or a.get("imageHash") or "-"
229
+ # クリエイティブの持ち方は媒体で違う(google は YouTube動画ID / 画像アセット)
230
+ media = (
231
+ ",".join(a.get("mediaIds") or [])
232
+ or a.get("videoId")
233
+ or a.get("imageHash")
234
+ or a.get("youtubeVideoId")
235
+ or ",".join((a.get("marketingImages") or []) + (a.get("squareMarketingImages") or []))
236
+ or "-"
237
+ )
176
238
  head = (a.get("headlines") or ["(空)"])[0]
177
239
  print(f" AD media {media} | 画像No {image_no_of(a) or '-'} | {head}")
178
- print(f" {a.get('finalUrl')}")
240
+ print(f" {final_url_of(a)}")
179
241
 
180
242
 
181
243
  # --------------------------------------------------------------- 3. lint
@@ -192,6 +254,13 @@ def lint(plan: dict, plan_path: str = "") -> None:
192
254
  for v in (account.get("refs", {}).get("adGroups") or {}).values()
193
255
  if v.get("paramCode")
194
256
  }
257
+ # Google は同名キャンペーンを拒否する(2026-08-23 実測。他媒体は未確認なので google 限定で見る)。
258
+ # dry-run は実 API を叩かないので通ってしまい、本実行の campaign 作成で初めて落ちる。
259
+ known_cp_names = {
260
+ str(v.get("name"))
261
+ for v in (account.get("refs", {}).get("campaigns") or {}).values()
262
+ if v.get("name")
263
+ }
195
264
 
196
265
  if not base:
197
266
  warn(f"アカウントJSONに baseArticleUrl がありません({platform}/{plan.get('accountId')})。"
@@ -200,13 +269,28 @@ def lint(plan: dict, plan_path: str = "") -> None:
200
269
  seen_media: dict[str, str] = {}
201
270
  for c in plan.get("campaigns") or []:
202
271
  name = c.get("name", "")
203
- budget = c.get("budget") or 0
272
+ budget = budget_of(c)
204
273
  if not (BUDGET_MIN <= budget <= BUDGET_MAX):
205
274
  warn(f"CP「{name}」の日予算 {budget:,} が想定レンジ外({BUDGET_MIN:,}〜{BUDGET_MAX:,})。桁を確認してください")
206
- if c.get("status") == "ACTIVE":
207
- warn(f"CP「{name}」が ACTIVE です。初回・PoC は PAUSED(配信オフ)を推奨します")
275
+ # 配信オンを表す値は媒体で違う: yda / meta は ACTIVE、**google は ENABLED**。
276
+ # ACTIVE だけ見ていると Google の配信オンが素通りする。
277
+ cp_status = status_of(c)
278
+ if cp_status in ON_STATUSES:
279
+ if platform in UNVERIFIED_PLATFORMS:
280
+ # 実 API での入稿検証が未了の媒体は配信オンでの入稿を止める
281
+ off = "DISABLE" if platform == "bytedance" else "PAUSED"
282
+ err(f"CP「{name}」が {cp_status}(配信オン)です。"
283
+ f" {platform} は実 API での入稿検証が未了なので、{off}(配信オフ)で入稿してください")
284
+ else:
285
+ warn(f"CP「{name}」が {cp_status} です。初回・PoC は PAUSED(配信オフ)を推奨します")
208
286
  if SUSPICIOUS_NAME.search(name):
209
287
  warn(f"CP名「{name}」に検証用らしき語が残っています。本番入稿の命名か確認してください")
288
+ # 同名 CP を拒否するのは **google だけ**(2026-08-23 実測。他媒体は未確認)。
289
+ # YDA は `_2` サフィックスで自動リトライする先例があるので、全媒体で止めると
290
+ # これまで通っていた計画をブロックしてしまう。
291
+ if platform == "google" and name in known_cp_names:
292
+ err(f"CP名「{name}」はアカウントの既存キャンペーンと同名です。"
293
+ " Google は同名キャンペーンを拒否するため、本実行の作成で落ちます")
210
294
 
211
295
  for g in ad_groups(c):
212
296
  gname = g.get("name", "")
@@ -217,7 +301,7 @@ def lint(plan: dict, plan_path: str = "") -> None:
217
301
  warn(f"広告グループ「{gname}」の smartTargeting が false です"
218
302
  f"(方針は常時ON)。意図的でなければ外してください")
219
303
  for a in g.get("ads") or []:
220
- url = a.get("finalUrl") or ""
304
+ url = final_url_of(a)
221
305
  if not re.match(r"https?://", url):
222
306
  err(f"CP「{name}」の finalUrl が相対パスです: {url}\n"
223
307
  f" → アカウントJSONの baseArticleUrl 未設定。"
@@ -227,8 +311,23 @@ def lint(plan: dict, plan_path: str = "") -> None:
227
311
  f"({base_host}) と違います: {urllib.parse.urlparse(url).netloc}")
228
312
 
229
313
  media = a.get("mediaIds") or []
230
- if not media and not a.get("videoId") and not a.get("imageHash"):
231
- err(f"CP「{name}」の広告にクリエイティブがありません(mediaIds が空)")
314
+ # クリエイティブの持ち方は媒体で違う。
315
+ # yda : mediaIds / videoMediaId
316
+ # meta : imageHash / videoId
317
+ # google : youtubeVideoId / marketingImages / squareMarketingImages
318
+ # bytedance : videoId + imageIds (カバー画像) / アップロード前は videoPath
319
+ has_creative = bool(
320
+ media
321
+ or a.get("videoId")
322
+ or a.get("videoPath")
323
+ or a.get("imageHash")
324
+ or a.get("imageIds")
325
+ or a.get("youtubeVideoId")
326
+ or a.get("marketingImages")
327
+ or a.get("squareMarketingImages")
328
+ )
329
+ if not has_creative:
330
+ err(f"CP「{name}」の広告にクリエイティブがありません")
232
331
  for m in media:
233
332
  if m in seen_media:
234
333
  warn(f"mediaId {m} が複数の広告で使われています"
@@ -246,14 +345,43 @@ def lint(plan: dict, plan_path: str = "") -> None:
246
345
  f"(backend は mediaIds[0] のみ使用)。差し替えたいなら mediaIds を空にし、"
247
346
  f"既存画像のままでよいなら imagePaths を消してください")
248
347
 
249
- # ローカル画像パスの存在チェック(dry-run は検出しない)
250
- for raw in a.get("imagePaths") or []:
348
+ # ローカル素材パスの存在チェック(dry-run は検出しない)
349
+ # bytedance は videoPath / coverImagePath(s) も CLI がアップロードする
350
+ local_paths = list(a.get("imagePaths") or [])
351
+ if a.get("videoPath"):
352
+ local_paths.append(a["videoPath"])
353
+ if a.get("coverImagePath"):
354
+ local_paths.append(a["coverImagePath"])
355
+ local_paths.extend(a.get("coverImagePaths") or [])
356
+ for raw in local_paths:
251
357
  resolved = resolve_media_path(raw, base_dir)
252
358
  if not os.path.isfile(resolved):
253
- err(f"CP「{name}」の imagePaths のファイルが見つかりません: {raw}\n"
359
+ err(f"CP「{name}」のローカル素材が見つかりません: {raw}\n"
254
360
  f" → 解決先 {resolved}(相対パスの基準は計画JSONのディレクトリ "
255
361
  f"{base_dir})")
256
362
 
363
+ # TikTok の SINGLE_VIDEO は **動画とカバー画像の両方**が必要。
364
+ # 片方だけだと lint を通り、live で動画だけアップロードされた後に
365
+ # backend の「カバー画像が指定されていません」で落ちる。
366
+ if platform == "bytedance":
367
+ has_video = bool(a.get("videoId") or a.get("videoPath"))
368
+ has_cover = bool(
369
+ a.get("imageIds") or a.get("coverImagePath") or a.get("coverImagePaths")
370
+ )
371
+ if not has_video:
372
+ err(f"CP「{name}」の広告に動画がありません({a.get('adName') or ''})\n"
373
+ f" → videoId か、アップロード前なら videoPath が必要")
374
+ if not has_cover:
375
+ err(f"CP「{name}」の広告にカバー画像がありません({a.get('adName') or ''})\n"
376
+ f" → SINGLE_VIDEO では必須。imageIds か、アップロード前なら "
377
+ f"coverImagePath が必要")
378
+ # 解決済みなのにパスも入っていると CLI が余計にアップロードする
379
+ if a.get("videoId") and a.get("videoPath"):
380
+ err(f"CP「{name}」の広告に videoId と videoPath が両方あります"
381
+ f"({a.get('adName') or ''})\n"
382
+ f" → アップロードした動画は使われません。差し替えたいなら videoId を、"
383
+ f"既存動画のままでよいなら videoPath を消してください")
384
+
257
385
  query = urllib.parse.urlparse(url).query
258
386
  if not url_params and query:
259
387
  warn(f"CP「{name}」は urlParamsEnabled: false ですが finalUrl に"
@@ -284,8 +412,16 @@ def flatten(plan: dict, is_sample: bool) -> dict:
284
412
  for ci, c in enumerate(plan.get("campaigns") or []):
285
413
  p = f"CP[{ci}]"
286
414
  out[f"{p}.name"] = c.get("name")
287
- out[f"{p}.budget"] = c.get("budget")
288
- out[f"{p}.tCPA"] = c.get("tcpa") if is_sample else c.get("targetCpa")
415
+ # 予算・目標CPA のキー名は媒体で違う。google は budgetAmountYen / targetCpaYen。
416
+ # budget / targetCpa だけを見ると Google の差分が常に None 同士になり検出できない。
417
+ out[f"{p}.budget"] = budget_of(c) or None
418
+ # tCPA のキー: サンプルは tcpa (yda/meta) か targetCpa (google)、入稿計画は
419
+ # targetCpa (yda/meta) か targetCpaYen (google)
420
+ out[f"{p}.tCPA"] = (
421
+ (c.get("tcpa") or c.get("targetCpa"))
422
+ if is_sample
423
+ else (c.get("targetCpa") or c.get("targetCpaYen"))
424
+ )
289
425
  groups = ad_groups(c)
290
426
  out[f"{p}.広告グループ数"] = len(groups)
291
427
  for gi, g in enumerate(groups):
@@ -301,9 +437,13 @@ def flatten(plan: dict, is_sample: bool) -> dict:
301
437
 
302
438
  def diff_against_sample(plan: dict) -> None:
303
439
  platform = plan.get("platform", "yda")
304
- sample_path = os.path.join(
305
- SKILL_DIR, "サンプルプラン_meta.json" if platform == "meta" else "サンプルプラン.json"
306
- )
440
+ # 媒体を足したらここにも足す (合意内容の正となるサンプルプランは媒体別)
441
+ SAMPLE_BY_PF = {
442
+ "meta": "サンプルプラン_meta.json",
443
+ "google": "サンプルプラン_google.json",
444
+ "bytedance": "サンプルプラン_bytedance.json",
445
+ }
446
+ sample_path = os.path.join(SKILL_DIR, SAMPLE_BY_PF.get(platform, "サンプルプラン.json"))
307
447
  if not os.path.exists(sample_path):
308
448
  print("\n(サンプルプランが無いため差分比較をスキップしました)")
309
449
  return
@@ -0,0 +1,55 @@
1
+ {
2
+ "accountJsonVersion": 1,
3
+ "platform": "bytedance",
4
+ "accountId": "7000000000000000001",
5
+ "name": "sample_account",
6
+ "projectName": "TikTokサンプル案件",
7
+ "baseArticleUrl": "https://example.com/tiktok-lp/",
8
+ "identityId": "7100000000000000001",
9
+ "identityType": "BC_AUTH_TT",
10
+ "refs": {
11
+ "titles": {
12
+ "1": "サンプル広告文1。商材の特徴とオファーを簡潔に伝えます。",
13
+ "2": "サンプル広告文2。ターゲットの悩みに寄せた訴求を入れます。",
14
+ "3": "サンプル広告文3。行動を促すメッセージを入れます。"
15
+ },
16
+ "media": {
17
+ "1": {
18
+ "video": "v0200000000000000001",
19
+ "cover": "ad-site-i18n-sg/000000000000000001"
20
+ },
21
+ "2": {
22
+ "video": "v0200000000000000002",
23
+ "cover": "ad-site-i18n-sg/000000000000000002"
24
+ },
25
+ "3": {
26
+ "video": "v0200000000000000003",
27
+ "cover": ""
28
+ }
29
+ },
30
+ "regions": {
31
+ "日本": "1200000000"
32
+ },
33
+ "campaigns": {
34
+ "1780000000000000001": {
35
+ "name": "TikTokサンプルキャンペーンA"
36
+ },
37
+ "1780000000000000002": {
38
+ "name": "TikTokサンプルキャンペーンB"
39
+ }
40
+ },
41
+ "adGroups": {
42
+ "1790000000000000001": {
43
+ "name": "サンプル広告グループA",
44
+ "campaignId": "1780000000000000001",
45
+ "paramCode": "b_sample_01"
46
+ },
47
+ "1790000000000000002": {
48
+ "name": "サンプル広告グループB",
49
+ "campaignId": "1780000000000000002",
50
+ "paramCode": "b_sample_02"
51
+ }
52
+ }
53
+ },
54
+ "identityAuthorizedBcId": "7100000000000000009"
55
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "accountJsonVersion": 1,
3
+ "platform": "google",
4
+ "accountId": "1000000001",
5
+ "name": "sample_account",
6
+ "projectName": "Googleサンプル案件",
7
+ "baseArticleUrl": "https://example.com/google-lp/",
8
+ "consoleMcc": "9000000001",
9
+ "businessName": "サンプルブランド",
10
+ "logoAsset": "customers/1000000001/assets/900000001",
11
+ "refs": {
12
+ "titles": {
13
+ "1": "サンプル見出し1(訴求A)",
14
+ "2": "サンプル見出し2(訴求B)",
15
+ "3": "サンプル見出し3(訴求C)"
16
+ },
17
+ "longHeadlines": {
18
+ "1": "サンプル長見出し1。動画広告で使う長めの見出しです。",
19
+ "2": "サンプル長見出し2。訴求のポイントを補足する長見出しです。"
20
+ },
21
+ "descriptions": {
22
+ "1": "サンプル説明文1。商材の特徴とオファーを簡潔に伝える説明文です。",
23
+ "2": "サンプル説明文2。ターゲットの悩みに寄せた訴求を入れる説明文です。",
24
+ "3": "サンプル説明文3。行動を促すメッセージを入れる説明文です。"
25
+ },
26
+ "media": {
27
+ "1": { "image": "customers/1000000001/assets/800000001", "square": "customers/1000000001/assets/800000002", "video": "" },
28
+ "2": { "image": "", "square": "", "video": "dQw4w9WgXcQ" },
29
+ "3": { "image": "customers/1000000001/assets/800000003", "square": "", "video": "M7lc1UVf-VE" }
30
+ },
31
+ "campaigns": {
32
+ "20000000001": { "name": "GoogleサンプルキャンペーンA" },
33
+ "20000000002": { "name": "GoogleサンプルキャンペーンB" }
34
+ },
35
+ "adGroups": {
36
+ "30000000001": { "name": "サンプル広告グループA", "campaignId": "20000000001", "paramCode": "g_sample_01" },
37
+ "30000000002": { "name": "サンプル広告グループB", "campaignId": "20000000002", "paramCode": "g_sample_02" }
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "schemaVersion": 3,
3
+ "platform": "bytedance",
4
+ "accountId": "7000000000000000001",
5
+ "campaigns": [
6
+ {
7
+ "uid": "c1",
8
+ "name": "TikTokサンプル検証_0823",
9
+ "campaignId": "",
10
+ "isExisting": false,
11
+ "urlNumber": "01",
12
+ "adGroups": [
13
+ {
14
+ "uid": "g1",
15
+ "name": "AG1",
16
+ "paramCode": "sample_b1",
17
+ "isExisting": false,
18
+ "dailyBudget": 5000,
19
+ "tg_location": "日本",
20
+ "tg_placement": "自動配置",
21
+ "deliveryStatus": "",
22
+ "ads": [
23
+ { "uid": "a1", "imageNo": "1", "titleNo": 1 },
24
+ { "uid": "a2", "imageNo": "2", "titleNo": 2 }
25
+ ]
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "schemaVersion": 3,
3
+ "platform": "google",
4
+ "accountId": "1000000001",
5
+ "campaigns": [
6
+ {
7
+ "uid": "c1",
8
+ "name": "GoogleサンプルDemandGen検証_0818",
9
+ "campaignId": "",
10
+ "isExisting": false,
11
+ "dailyBudget": 12000,
12
+ "targetCpa": 8500,
13
+ "urlNumber": "01",
14
+ "adGroups": [
15
+ {
16
+ "uid": "g1",
17
+ "name": "AG1",
18
+ "paramCode": "sample_g1",
19
+ "isExisting": false,
20
+ "tg_location": "日本",
21
+ "tg_language": "日本語",
22
+ "tg_channel": "YouTube",
23
+ "deliveryStatus": "",
24
+ "ads": [
25
+ { "uid": "a1", "format": "VIDEO", "imageNo": "2", "titleNo": 1, "longNo": 1, "descNo": 1 },
26
+ { "uid": "a2", "format": "VIDEO", "imageNo": "3", "titleNo": 2, "longNo": 2, "descNo": 2 },
27
+ { "uid": "a3", "format": "IMAGE", "imageNo": "1", "titleNo": 1, "longNo": "", "descNo": 1 }
28
+ ]
29
+ }
30
+ ]
31
+ }
32
+ ]
33
+ }