自動化の失敗には二種類あります。

ひとつは、何も起きないまま成功したことになっている失敗。これは前に書きました。

もうひとつが、今回です。エラーは正しく出ている。ログにも残っている。ただしそのエラーメッセージが指している原因が、間違っている。

こちらのほうが厄介でした。読んだ側が「通信の問題か、じゃあそのうち直るな」と判断して、そこで調査が止まるからです。

何が起きていたか

Instagram(@kai_nisa_guide)に「一覧型リール」を1日数本投稿する仕組みを動かしています。題材を決める → 構成をChatGPTに作らせる → 画像を描画 → ナレーション音声を作る → 動画に組む → 投稿する、という流れです。

ログを見返して気づきました。

最後に投稿できた日時: 2026/9/29 22:15
それ以降の実行: すべて失敗

28日間、1本も出ていませんでした。

失敗の内訳はこうです。

99回  ChatGPTの入力欄が表示されませんでした(通信不良かUI変更の可能性)
36回  net::ERR_INTERNET_DISCONNECTED
10回  net::ERR_NAME_NOT_RESOLVED
 2回  locator.click: Timeout
 その他 3回

150回落ちていました。そして、タスクスケジューラ上はずっと緑でした。スクリプト自体は「失敗しました」と正しく終了コードを返して終わるので、スケジューラから見れば予定どおり起動して予定どおり終わっています。異常として浮かび上がる場所がどこにもありませんでした。

最初の読み違い

目を引くのは下の46回、通信系のエラーのほうです。ERR_INTERNET_DISCONNECTED と ERR_NAME_NOT_RESOLVED が合計46回。実際、この環境では以前から回線の瞬断が起きていて、遷移にリトライを入れる対処を一度しています。

なので最初は「また回線か」と読みました。

これが間違いでした。通信エラーは結果の46回であって、本丸は上の99回のほうです。そして99回のメッセージには、こう書いてあります。

通信不良かUI変更の可能性

このメッセージを書いたのは自分です。入力欄が見つからなかったときに考えられる原因を、当時思いつく範囲で並べただけでした。そこに正解が入っていなかったので、以後99回、自分で自分に嘘をつき続けることになりました。

本当の原因

スクリプトは失敗時に必ずスクリーンショットを残すようにしてあります。それを開きました。

Cloudflareの「あなたが人間であることを確認します」のチェックボックスが出ていました。

ChatGPTが自動操作を検知して、ボットチェックを挟むようになっていたのです。入力欄は「表示されていない」のではなく、まだそのページに到達していませんでした。

ではなぜ、スクリプトはそれに気づけなかったのか。

ログアウトやエラー画面の検知は、こう書いていました。

const body = await page.evaluate(`document.body.innerText`);
if (/ログイン|Log in|Sign in/.test(body)) { ... }

ページ本文の文字列を見て判断しています。素直な実装です。

Cloudflareのチェックボックスは、iframeの中にあります。

document.body.innerText は親ページのテキストしか拾いません。iframeの中身は別ドキュメントなので、ここには1文字も現れません。スクリプトから見た画面は「なぜか空っぽのページ」でした。だから「通信不良かUI変更」という、自分が用意した外れの選択肢に流れ込みました。

直した内容

iframeの存在そのものを見るようにしました。中身は読めなくても、そこにCloudflareのiframeが刺さっている事実は親ページから見えます。

const challengeFrame = await page
  .locator('iframe[src*="challenges.cloudflare.com"], iframe[title*="Cloudflare"], iframe[title*="セキュリティ"]')
  .count()
  .catch(() => 0);

const looksChallenge =
  challengeFrame > 0 ||
  /確認しています|ロボットでは|Verify you are human|Just a moment|Checking your browser/i.test(body);

if (looksChallenge || looksLoggedOut) {
  reelsLog(`🔴【人間の操作が必要】ChatGPTが${what}。`);
  throw new Error(`【人間の操作が必要】ChatGPT: ${what}`);
}

ポイントは下の2行です。ボットチェックもログアウトも、人間がブラウザを開いて1回クリックするまで、何度リトライしても絶対に直りません。リトライで直る失敗と、人間を呼ばないと直らない失敗は、ログ上で区別がつく必要があります。だから目印を付けました。

通信エラーなら明日直るかもしれない。ボットチェックは明日も明後日も直らない。この2つが同じ見た目でログに並んでいたことが、28日の正体です。

学んだこと

1. エラーメッセージに書く「考えられる原因」は、嘘になりうる

「通信不良かUI変更の可能性」は、親切のつもりで書いた一文でした。結果として、読む人(自分)の思考をそこで止める装置になりました。原因が特定できていないなら、推測を書かずに観測した事実だけを書くほうが安全です。「入力欄が見つかりません(画面の先頭200文字: …)」のほうが、よほど役に立ちました。

2. 画面の判定を innerText だけでやると、iframeを丸ごと見落とす

ボットチェック、決済フォーム、埋め込みログイン。自動操作で詰まりやすい場所ほどiframeの中にあります。本文テキストで判定しているすべての箇所が、同じ穴を持っています。

3. 「失敗が続いていること」自体を誰も見ていなかった

150回落ちても、誰も気づきませんでした。1回ごとの失敗は正しく記録されていたのに、何日連続で失敗しているかを見る場所がなかったからです。1回の失敗は異常ではありません。28日連続は異常です。その差を機械が言える形にしていませんでした。

いま入れているのは、前回成功からの経過を実行のたびにログへ出すこと。これだけでも「久しぶりに見たら数字が二桁になっている」で気づけます。

おわりに

自動化を組むとき、つい「落ちたら気づけるようにエラーを出しておこう」で安心してしまいます。

今回わかったのは、エラーは出ていたということです。99回も出ていました。

足りなかったのは、そのエラーが本当のことを言っているかどうかと、それが何日続いているかでした。