addashboard-cli 0.12.0 → 0.14.0
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/dist/commands/submission.js +95 -60
- package/dist/commands/submission.js.map +1 -1
- package/dist/schemas/submit-plan.js +11 -0
- package/dist/schemas/submit-plan.js.map +1 -1
- package/package.json +3 -2
- package/skills/ad-ops/SKILL.md +8 -1
- package/skills/ad-ops/ad-planner.html +769 -62
- package/skills/ad-ops/scripts/adops.sh +69 -7
- package/skills/ad-ops/scripts/plan_lint.py +34 -9
- package/skills/ad-ops//345/205/245/347/250/277/350/250/210/347/224/273/.ad-planner-plans +1 -0
|
@@ -13,8 +13,16 @@
|
|
|
13
13
|
#
|
|
14
14
|
# 覚えるコマンドを1本に畳むためのラッパー。個別の python3 -c / curl / ls -t を書かない。
|
|
15
15
|
set -uo pipefail
|
|
16
|
+
# Windows の既定コンソール (cp932) だと Python の ✓ や日本語の出力で UnicodeEncodeError になる
|
|
17
|
+
export PYTHONUTF8=1 PYTHONIOENCODING=utf-8
|
|
18
|
+
# Windows の Python (subprocess) は npx を見つけられない (実体が npx.cmd で WinError 2 になる)。
|
|
19
|
+
# refs_import.py / thumbs_download.py は ADOPS_CLI で CLI を呼ぶので、既定をここで合わせる
|
|
20
|
+
case "${OSTYPE:-}" in
|
|
21
|
+
msys*|cygwin*) export ADOPS_CLI="${ADOPS_CLI:-npx.cmd -y addashboard-cli@latest}" ;;
|
|
22
|
+
esac
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
# Windows ではネイティブ形式のパスで持つ (Python に渡すため)。pwd -W が無い Mac は pwd
|
|
25
|
+
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && { pwd -W 2>/dev/null || pwd; })"
|
|
18
26
|
# 配信ポート。既定 8799 は cli/playwright.config.ts の webServer と共用する。
|
|
19
27
|
# **worktree を分けて並行作業していると別 worktree のサーバを再利用してしまう**
|
|
20
28
|
# (curl が応答すれば「起動済み」と判断するので、別ブランチのプランナーが開く)。
|
|
@@ -29,6 +37,20 @@ CLI="${WEBGRAM_CLI:-npx -y addashboard-cli@latest}"
|
|
|
29
37
|
|
|
30
38
|
die() { echo "✗ $*" >&2; exit 1; }
|
|
31
39
|
|
|
40
|
+
# URL をブラウザで開く。Mac は従来どおり open に渡す (`open -a ...` の引数もそのまま通す)。
|
|
41
|
+
# Git Bash (Windows) には open が無いので Chrome を直接起動し、無ければ既定ブラウザで開く。
|
|
42
|
+
# rundll32 は shell を経由しないので、& を含む URL (Google 管理画面など) でも欠けない。
|
|
43
|
+
open_url() {
|
|
44
|
+
if command -v open >/dev/null 2>&1; then open "$@"; return; fi
|
|
45
|
+
local url="${!#}" c
|
|
46
|
+
for c in "/c/Program Files/Google/Chrome/Application/chrome.exe" \
|
|
47
|
+
"/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" \
|
|
48
|
+
"${LOCALAPPDATA:-}/Google/Chrome/Application/chrome.exe"; do
|
|
49
|
+
[ -x "$c" ] && { "$c" "$url" >/dev/null 2>&1 & return; }
|
|
50
|
+
done
|
|
51
|
+
rundll32 url.dll,FileProtocolHandler "$url"
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
# ------------------------------------------------------------- ensure_login
|
|
33
55
|
# 未ログインなら auth login を起動して完了まで待ち、そのまま処理を続ける。
|
|
34
56
|
# 以前は「ターミナルで auth login を実行してください」と案内して止めていたため、
|
|
@@ -132,6 +154,35 @@ print(f"✓ 作業中プランを設定: {os.environ['ACC']} / {os.environ['PF']
|
|
|
132
154
|
PY
|
|
133
155
|
}
|
|
134
156
|
|
|
157
|
+
# ------------------------------------------------------------ planner env
|
|
158
|
+
# ブラウザはフォルダの絶対パスを知る手段が無いので、プランナーが「保存フォルダ(入稿計画/)」の
|
|
159
|
+
# 場所を案内できるように、配信元のスキルから絶対パスを渡す (.planner-env.json)。
|
|
160
|
+
# あわせて 入稿計画/ と目印ファイルを用意する (プランナーは目印の有無で入稿計画フォルダかを判定する)。
|
|
161
|
+
# 各自の環境のパスなので git では追跡せず、npm にも載せない。
|
|
162
|
+
write_planner_env() {
|
|
163
|
+
local plans="${SKILL_DIR}/入稿計画"
|
|
164
|
+
mkdir -p "$plans"
|
|
165
|
+
[ -f "${plans}/.ad-planner-plans" ] \
|
|
166
|
+
|| printf 'ad-planner の入稿計画フォルダの目印です。消さないでください。\n' > "${plans}/.ad-planner-plans"
|
|
167
|
+
PLANS_DIR="$plans" OUT="${SKILL_DIR}/.planner-env.json" python3 - <<'PY'
|
|
168
|
+
import json, os
|
|
169
|
+
with open(os.environ["OUT"], "w", encoding="utf-8") as f:
|
|
170
|
+
json.dump({"plansDir": os.environ["PLANS_DIR"]}, f, ensure_ascii=False)
|
|
171
|
+
f.write("\n")
|
|
172
|
+
PY
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# 配信サーバーを止める。Mac は従来どおり pkill。
|
|
176
|
+
# Git Bash (Windows) には pkill が無いので、ポートを LISTEN している python だけを taskkill で止める
|
|
177
|
+
# (/PID 形式は Git Bash がパスに変換してしまうので - 形式で渡す)。
|
|
178
|
+
stop_server() {
|
|
179
|
+
if command -v pkill >/dev/null 2>&1; then pkill -f "http.server ${PORT}"; return; fi
|
|
180
|
+
local pid
|
|
181
|
+
pid="$(netstat -ano | awk -v p=":${PORT}" '$1 == "TCP" && $2 ~ p"$" && $4 == "LISTENING" { print $5; exit }')"
|
|
182
|
+
[ -n "$pid" ] && tasklist -FI "PID eq ${pid}" -NH | grep -qi python \
|
|
183
|
+
&& taskkill -PID "$pid" -F >/dev/null 2>&1
|
|
184
|
+
}
|
|
185
|
+
|
|
135
186
|
# file:// で開くと fetch('サンプルプラン.json') がブロックされ、
|
|
136
187
|
# エラーも出ないまま空のプランナーが表示される。必ずHTTPで開く。
|
|
137
188
|
cmd_planner() {
|
|
@@ -139,7 +190,7 @@ cmd_planner() {
|
|
|
139
190
|
while [ $# -gt 0 ]; do
|
|
140
191
|
case "$1" in
|
|
141
192
|
--stop)
|
|
142
|
-
|
|
193
|
+
stop_server && echo "✓ プランナーの配信を停止しました" \
|
|
143
194
|
|| echo "(起動中のサーバーはありませんでした)"
|
|
144
195
|
return 0 ;;
|
|
145
196
|
--platform) platform="${2:-}"; shift 2 ;;
|
|
@@ -153,8 +204,9 @@ cmd_planner() {
|
|
|
153
204
|
|| die "--platform と --account は両方指定してください"
|
|
154
205
|
write_active_plan "$platform" "$account"
|
|
155
206
|
fi
|
|
207
|
+
write_planner_env
|
|
156
208
|
|
|
157
|
-
if curl -sf
|
|
209
|
+
if curl -sf "$URL" >/dev/null; then
|
|
158
210
|
echo "✓ 配信は起動済み(再利用)"
|
|
159
211
|
echo " ⚠ 別の作業ディレクトリ (worktree) が起動したサーバの可能性があります。" >&2
|
|
160
212
|
echo " プランナーの内容が想定と違うときは ADOPS_PORT=8801 adops.sh planner ... で専用ポートに切り替えてください。" >&2
|
|
@@ -164,14 +216,24 @@ cmd_planner() {
|
|
|
164
216
|
nohup python3 -m http.server "$PORT" --directory "$SKILL_DIR" \
|
|
165
217
|
>/tmp/adops-planner.log 2>&1 &
|
|
166
218
|
for _ in $(seq 1 20); do
|
|
167
|
-
curl -sf
|
|
219
|
+
curl -sf "$URL" >/dev/null && break
|
|
168
220
|
sleep 0.3
|
|
169
221
|
done
|
|
170
|
-
curl -sf
|
|
222
|
+
curl -sf "$URL" >/dev/null || die "配信を起動できませんでした(/tmp/adops-planner.log を確認)"
|
|
171
223
|
echo "✓ 起動しました"
|
|
172
224
|
fi
|
|
173
225
|
|
|
174
|
-
|
|
226
|
+
# 素材の新規登録・入稿計画の保存は File System Access API (フォルダの読み書き) を使うので Chrome で開く。
|
|
227
|
+
# 素の `open` は BizAgent / VS Code のターミナルだと内蔵ブラウザに渡され、
|
|
228
|
+
# 内蔵ブラウザはフォルダの読み書きを許可しない (許可の確認も出ずに黙って拒否される)。
|
|
229
|
+
if [ -d "/Applications/Google Chrome.app" ] || [ -d "$HOME/Applications/Google Chrome.app" ]; then
|
|
230
|
+
open -a "Google Chrome" "$URL"
|
|
231
|
+
else
|
|
232
|
+
open_url "$URL"
|
|
233
|
+
# Windows は open_url が Chrome を直接探して起動するので、Mac (open がある) のときだけ警告する
|
|
234
|
+
command -v open >/dev/null 2>&1 \
|
|
235
|
+
&& echo " ⚠ Google Chrome が見つかりません。素材の登録と入稿計画の保存は Chrome 系ブラウザでしか動きません" >&2
|
|
236
|
+
fi
|
|
175
237
|
echo " $URL"
|
|
176
238
|
echo " 停止: adops.sh planner --stop"
|
|
177
239
|
}
|
|
@@ -285,7 +347,7 @@ PY
|
|
|
285
347
|
esac
|
|
286
348
|
|
|
287
349
|
echo "$url"
|
|
288
|
-
[ -n "$dry" ] ||
|
|
350
|
+
[ -n "$dry" ] || open_url "$url"
|
|
289
351
|
}
|
|
290
352
|
|
|
291
353
|
# --------------------------------------------------------------- refs-import
|
|
@@ -432,6 +432,7 @@ def summarize(plan: dict) -> None:
|
|
|
432
432
|
or a.get("videoId")
|
|
433
433
|
or a.get("imageHash")
|
|
434
434
|
or a.get("youtubeVideoId")
|
|
435
|
+
or ",".join(v.get("youtubeVideoId") or v.get("videoPath") or "?" for v in a.get("videos") or [])
|
|
435
436
|
or ",".join((a.get("marketingImages") or []) + (a.get("squareMarketingImages") or []))
|
|
436
437
|
or "-"
|
|
437
438
|
)
|
|
@@ -530,7 +531,7 @@ def lint(plan: dict, plan_path: str = "") -> None:
|
|
|
530
531
|
# クリエイティブの持ち方は媒体で違う。
|
|
531
532
|
# yda : mediaIds / videoMediaId
|
|
532
533
|
# meta : imageHash / videoId
|
|
533
|
-
# google : youtubeVideoId / marketingImages / squareMarketingImages
|
|
534
|
+
# google : youtubeVideoId / videos[] / marketingImages / squareMarketingImages
|
|
534
535
|
# bytedance : videoId + imageIds (カバー画像) / アップロード前は videoPath
|
|
535
536
|
has_creative = bool(
|
|
536
537
|
media
|
|
@@ -539,6 +540,7 @@ def lint(plan: dict, plan_path: str = "") -> None:
|
|
|
539
540
|
or a.get("imageHash")
|
|
540
541
|
or a.get("imageIds")
|
|
541
542
|
or a.get("youtubeVideoId")
|
|
543
|
+
or a.get("videos")
|
|
542
544
|
or a.get("marketingImages")
|
|
543
545
|
or a.get("squareMarketingImages")
|
|
544
546
|
)
|
|
@@ -569,6 +571,7 @@ def lint(plan: dict, plan_path: str = "") -> None:
|
|
|
569
571
|
if a.get("coverImagePath"):
|
|
570
572
|
local_paths.append(a["coverImagePath"])
|
|
571
573
|
local_paths.extend(a.get("coverImagePaths") or [])
|
|
574
|
+
local_paths.extend(v["videoPath"] for v in a.get("videos") or [] if v.get("videoPath"))
|
|
572
575
|
for raw in local_paths:
|
|
573
576
|
resolved = resolve_media_path(raw, base_dir)
|
|
574
577
|
if not os.path.isfile(resolved):
|
|
@@ -580,15 +583,37 @@ def lint(plan: dict, plan_path: str = "") -> None:
|
|
|
580
583
|
# 両方あると CLI が余計にアップロードし、使われない動画が YouTube に残る。
|
|
581
584
|
# backend も superRefine で 400 にするが、CLI はその手前でアップロードを
|
|
582
585
|
# 済ませてしまうので lint でも止める(Google の安全弁は二重が原則)。
|
|
586
|
+
# 複数本(縦・横など・#347)は videos[]。1 本だけなら旧形式でもよい。
|
|
583
587
|
if platform == "google" and a.get("type") == "DEMAND_GEN_VIDEO_RESPONSIVE_AD":
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
588
|
+
label = a.get("adName") or ""
|
|
589
|
+
legacy = a.get("youtubeVideoId") or a.get("videoPath")
|
|
590
|
+
videos = a.get("videos")
|
|
591
|
+
if videos is not None and legacy:
|
|
592
|
+
err(f"CP「{name}」の動画広告に videos と youtubeVideoId / videoPath が両方あります"
|
|
593
|
+
f"({label})\n → どちらか一方の形式で書いてください")
|
|
594
|
+
videos = []
|
|
595
|
+
else:
|
|
596
|
+
if videos is None:
|
|
597
|
+
videos = [a] if legacy else []
|
|
598
|
+
if not videos:
|
|
599
|
+
err(f"CP「{name}」の動画広告に動画がありません({label})\n"
|
|
600
|
+
f" → videos、または youtubeVideoId / アップロード前なら videoPath が必要")
|
|
601
|
+
if len(videos) > 5:
|
|
602
|
+
err(f"CP「{name}」の動画広告の動画が {len(videos)} 本あります({label})"
|
|
603
|
+
f"\n → 1 広告 5 本までです")
|
|
604
|
+
seen_v = set()
|
|
605
|
+
for k, v in enumerate(videos, 1):
|
|
606
|
+
if v.get("youtubeVideoId") and v.get("videoPath"):
|
|
607
|
+
err(f"CP「{name}」の動画{k}本目に youtubeVideoId と videoPath が両方あります"
|
|
608
|
+
f"({label})\n"
|
|
609
|
+
f" → アップロードした動画は使われません。差し替えたいなら "
|
|
610
|
+
f"youtubeVideoId を、既存動画のままでよいなら videoPath を消してください")
|
|
611
|
+
key = v.get("youtubeVideoId") or v.get("videoPath")
|
|
612
|
+
if not key:
|
|
613
|
+
err(f"CP「{name}」の動画{k}本目が空です({label})")
|
|
614
|
+
elif key in seen_v:
|
|
615
|
+
err(f"CP「{name}」の動画{k}本目が他の動画と重複しています: {key}({label})")
|
|
616
|
+
seen_v.add(key)
|
|
592
617
|
|
|
593
618
|
# TikTok の SINGLE_VIDEO は **動画とカバー画像の両方**が必要。
|
|
594
619
|
# 片方だけだと lint を通り、live で動画だけアップロードされた後に
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ad-planner の入稿計画フォルダの目印です。消さないでください。
|