こんな人に役立ちます
AIと作った自動化ツールが動かなくなり、どこから調べればよいか分からない人
この記事を読むと、コードだけでなく、設定・データ・認証・運用を順番に切り分ける考え方が分かります。
はじめに
前回の実験では、Google Apps Script(GAS)を使って、YouTubeの新着動画を毎朝収集し、画像付きのWebページとして表示するダイジェストツールを作った。
今回の実験は、その続きである。
ただし今回は、きれいに機能追加が進んだ記録ではない。
むしろ、AIエージェントに修正を任せた結果、既存コードが壊れ、画面が白紙になり、さらに「直ったはず」と実機動作が食い違う状態を何度も経験した。
その意味では、今回は「AIエージェントでGASアプリを作れるか」ではなく、
壊れたGASアプリを、AIエージェントと一緒に原因切り分けしながら修理できるか
を検証した実験だった。
今回作っていたもの
対象は、GASで構築したYouTube新着ダイジェストツール。
主な仕様は次の通り。
- 複数ユーザーが、それぞれ関心のあるYouTubeチャンネルやキーワードを登録する
- 毎朝の時間主導トリガーで新着動画を収集する
- 収集した動画をスプレッドシートに蓄積する
- GAS Web Appの
doGet()で、画像付きカードUIとして表示する - タブで「いつものチャンネル」と「注目動画」を切り替える
- 既読にした動画はグレーアウトする
構成ファイルは以下のように分けていた。
コード.gs:収集処理のメインSheets.gs:スプレッドシート操作YouTube.gs:YouTube API / RSS連携WebApp.gs:Web App表示ロジックConfig.gs:定数管理Utils.gs:共通処理index.html:フロントエンド
スプレッドシート側には、以下のシートを用意していた。
Config_UsersConfig_SourcesData_ItemsLog_Runs
特に Data_Items は、動画ごとの情報を保存する中心テーブルで、以下の17列を持つ。
item_id
user_id
source_id
platform
fetched_date
title
url
thumbnail_url
author
published_at
view_count
summary
score
dup_group_id
is_primary
read_flag
created_at
実装は、コーディング特化AIエージェントにほぼ一任した。
依頼者側は、要件の提示、レビュー、実機での動作確認に徹する形で進めた。
なぜこのテーマにしたのか
前回の実験で、GAS製のYouTubeダイジェストツールは一応動くところまで到達していた。
ただ、まだ完成版ではなかった。
特にUI面では、タブ切り替え、注目動画の見せ方、既読管理、日付移動など、実際に毎日使うためにはもう少し整える必要があった。
そこで今回は、前回作ったMVPを少しずつ実用寄りに近づけることを目指した。
しかし、実際には機能追加よりも先に、修理と原因切り分けが中心になった。
AIエージェントはコードを書くのは速い。
一方で、GAS Web App特有の実行環境、特にGoogleのサンドボックス化されたiframe内で動くフロントエンドの検証には弱さがあった。
今回は、その弱さがかなりはっきり見えた。
今回の設計
このツールでは、収集処理と表示処理を分けている。
収集側では、YouTubeのRSSフィードを使って、登録済みチャンネルの新着動画を取得する。
RSSフィードを使うため、YouTube Data APIのquotaを消費しない。
表示側では、WebApp.gs がスプレッドシートから対象日のデータを読み出し、index.html に渡す。
フロントエンドでは、カード形式で動画を並べ、タブや既読状態をUIに反映する。
今回の修理完了時点で実装されていた主な機能は以下。
- チャンネル登録型の新着収集
- YouTube RSSフィードによるquota消費ゼロの取得
- カード型UI
- 「いつものチャンネル」「注目動画」のタブ切り替え
- タブごとの件数表示
summarizeItem_()による要約文の整形markAsRead()による既読管理- 日付移動を
google.script.run方式に変更
ただし、まだ未実装の部分もある。
現時点の「注目動画」タブは、登録済みチャンネル内の再ランキングに近い。
未登録チャンネルや海外動画を自動発見する仕組みは、次フェーズの予定である。
また、重要度スコアも全件 50 の固定値になっている。
Claude APIなどを使った実スコアリングや要約生成は、まだ入っていない。
AIエージェントに任せたこと
今回も、コードの修正作業はAIエージェントに任せた。
具体的には、以下の作業を依頼した。
- タブUIの改善
- カード表示の復旧
- 要約文の整形
- 既読管理の実装
- 日付移動UIの修正
- 本番デプロイ状態の確認
- 不要なデプロイの整理
- GASエディタ上でのコード修正
特徴的だったのは、AIエージェントがブラウザ操作型だったことだ。
つまり、ローカルファイルを編集するのではなく、GASエディタをブラウザで直接操作しながら修正する方式だった。
この方式には利点もある。
実際のGASプロジェクトに直接反映できるため、ローカルとGAS側の同期ずれが起きにくい。
一方で、GASエディタやWeb AppのUI構造に強く依存するため、画面操作の失敗や、実機検証の限界も起きやすい。
実際にできたこと
最終的に、壊れていた表示は復旧した。
具体的には、以下の状態まで戻すことができた。
/devURLでカード一覧が表示される- タブ切り替えが動作する
- 日付移動で白紙ページにならない
- 既読ボタンで
read_flagが更新される - 既読済みカードがUI上でグレーアウトする
- 要約文がある程度読みやすい形に整形される
- 本番デプロイの重複状態を整理できた
ただし、ここに至るまでには複数の問題があった。
以下では、問題ごとに「症状」「原因の切り分け」「対処」を整理する。
問題1:タブUI追加中に既存コードが削除され、画面が白紙化した
症状
タブUIの機能追加中に、既存コードの一部が削除され、/dev URLでカード一覧が表示されなくなった。
画面全体が期待通りに描画されず、実質的に白紙に近い状態になった。
この時点では、どのファイルが壊れたのかも明確ではなかった。
原因の切り分け
まず確認したのは、収集処理まで壊れているのか、それとも表示処理だけが壊れているのかだった。
スプレッドシート側の Data_Items にはデータが残っていた。
また、収集系の コード.gs、Sheets.gs、YouTube.gs などに大きな破損は見られなかった。
そのため、問題は表示系に絞られた。
最終的に、削除または破損していた主な対象は以下の2ファイルだと判断した。
index.htmlWebApp.gs
つまり、全面再建ではなく、表示系2ファイルの部分修理で対応できる可能性が高かった。
対処
最初は、GAS標準の「プロジェクト履歴」機能から復元できないか試した。
GASにはGitのような本格的なバージョン管理はないが、過去の変更履歴を確認できる機能はある。
ただし、UI操作がやや煩雑で、差分確認や完全復元の作業には向いていなかった。
結果として、プロジェクト履歴から完全復元するのではなく、壊れた表示系ファイルを再構成する方針にした。
今回の教訓は明確だった。
GASプロジェクトをAIエージェントに直接編集させる前には、必ず「概要 → コピーを作成」でプロジェクト複製を取るべき。
GASにはGitのような安心感がない。
そのため、GAS上で直接編集する場合は、プロジェクトコピーが最小限の防御策になる。
問題2:直接原因は未定義関数 formatSummaryText() の呼び出しだった
症状
画面のヘッダーやタブは表示される。
しかし、動画カード一覧だけが表示されない。
完全な白紙ではなく、「途中までは描画されるが、肝心の一覧が出ない」という症状だった。
原因の切り分け
フロントエンドの描画処理を確認すると、index.html 内で以下の関数が呼ばれていた。
formatSummaryText(item.summary)
しかし、この formatSummaryText() の定義が存在しなかった。
おそらく、前段の修正中に関数定義が削除されたか、別の構成に置き換える途中で参照だけが残ったと考えられる。
その結果、ブラウザ側で ReferenceError が発生し、カード一覧を描画する render() が途中で停止していた。
この症状が少し厄介だったのは、ページ全体が完全に落ちるのではなく、ヘッダーやタブなど一部は表示される点だった。
つまり、GAS側の doGet() が失敗しているのではなく、フロントエンドの描画途中で例外が起きていた。
対処
対処として、要約整形処理をサーバー側に寄せた。
WebApp.gs に summarizeItem_() を新設し、表示用データを作る段階でサマリーを整形する構成にした。
summarizeItem_() では、動画説明文から以下を除去・整理する。
- 宣伝文句
- URL
- ハッシュタグ
- 絵文字
- 旧フォーマットのラベル
【何が起きたか】のような見出し- 長すぎる説明文
最終的には、箇条書き最大3点程度の短い要約として表示する。
この関数は、現時点では簡易整形のための関数である。
ただし、将来的にはClaude APIによる要約生成に差し替えられるよう、意図的に関数として分離している。
ここでの教訓は、表示処理の中で呼ばれる関数が1つ欠けるだけで、カード一覧全体が止まるということだった。
GAS Web Appでは、サーバー側のエラーだけでなく、index.html 内のJavaScript例外も丁寧に見る必要がある。
問題3:「直った」というAIの報告と、実機動作が何度も食い違った
今回もっとも重要だったのが、この問題である。
AIエージェントは、コード上の整合性を見て「直った」と判断しがちだった。
しかし、実際にユーザーのブラウザで操作すると、まだ動かない。
この食い違いが複数回発生した。
症状
主な症状は以下だった。
- タブを押しても反応しない
- 日付の左右矢印を押すと白紙ページになる
- AIエージェント側では「修正済み」と報告される
- しかし、ユーザーの実機では動作しない
- 自動クリックによる検証結果と、人間の目視確認が一致しない
この時点で問題は、単なるコードミスだけではなく、検証環境そのものにある可能性が出てきた。
原因1:GAS Web Appはサンドボックスiframe内で動く
GAS Web Appは、通常のWebページとは少し違う。
script.google.com のURLで開いたとしても、実際のアプリ本体はGoogleが管理する script.googleusercontent.com 側のiframe内で実行される。
つまり、ユーザーから見ると1つのページに見えていても、内部的にはクロスオリジンiframeの中でアプリが動いている。
この構造が、ブラウザ操作型AIエージェントの検証を難しくしていた。
AIエージェントは、ブラウザ上のボタンをクリックして検証しようとする。
しかし、クロスオリジンiframeの内部には、外側ページから簡単にアクセスできない。
そのため、エージェントが「クリックできた」「確認できた」と思っていても、実際にはiframe内部のイベントに届いていない可能性があった。
この制約により、AIエージェント単独でのE2E検証は信頼しきれない状態になった。
切り分け方法
この問題に対しては、コードレビューだけではなく、実機での目視確認を重視した。
具体的には、一時的に以下のようなデバッグ表示を仕込んだ。
window.onerrorでエラー内容を画面上に表示する- クリックイベントが発火したかを画面上にログ表示する
- タブ切り替え時に状態変数が変化したかを表示する
- 日付移動時にどの関数が呼ばれたかを表示する
通常であれば、こうしたデバッグ表示は本番UIには不要である。
しかし今回は、AIエージェント側のブラウザ自動操作を完全には信用できないため、ユーザーの実機画面で何が起きているかを直接見えるようにした。
これはかなり有効だった。
「コード上は正しいはず」ではなく、
「クリックイベントが発火しているか」
「発火後にどの処理で止まっているか」
を画面上で確認できたからである。
問題箇所を特定した後、これらのデバッグ表示は撤去した。
原因2:DOMContentLoaded に依存していた
タブが反応しない原因の1つは、初期化処理を DOMContentLoaded に依存していたことだった。
通常のWebページでは、以下のような書き方はよく使われる。
document.addEventListener('DOMContentLoaded', init);
しかし、GASのHtmlService環境では、スクリプトの評価タイミングによっては、すでに DOMContentLoaded が発火済みになっている場合がある。
その場合、あとから addEventListener() で登録した init は呼ばれない。
結果として、タブやボタンにイベントリスナーが設定されず、UIが見えているのにクリックしても何も起きない状態になる。
対処
初期化処理を、document.readyState を確認する形に変更した。
考え方としては以下である。
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
これにより、まだ読み込み中であれば DOMContentLoaded を待ち、すでに読み込み済みであれば即座に初期化する。
GAS Web Appでは、この形の方が安全だった。
この修正後、タブのクリックイベントが安定して登録されるようになった。
原因3:日付移動を <a href> で実装していた
もう1つの大きな問題は、日付移動だった。
当初、日付の左右矢印は <a href> によるページ遷移として実装されていた。
つまり、前日や翌日に移動するたびに、URLを変えてページ全体を読み直す設計だった。
通常のWebアプリなら、この方式でも動くことがある。
しかし、GAS Web Appはiframe内で実行される。
そのため、iframe内で相対URL遷移を行うと、意図しないURLに飛んだり、空のページになったりする。
今回も、日付矢印を押すと白紙ページになる症状が発生した。
対処
日付移動は、ページ遷移ではなく google.script.run によるデータ差し替え方式に変更した。
つまり、矢印クリック時にページ全体を遷移させるのではなく、サーバー側関数を呼び出し、その日付のpayloadを取得して、同じ画面内で再描画する。
処理の考え方は以下。
google.script.run
.withSuccessHandler(function(payload) {
state.payload = payload;
render();
})
.withFailureHandler(function(error) {
showError(error);
})
.getDigestPayloadByDate(targetDate);
この方式にしたことで、iframe内でのURL遷移に依存しなくなった。
GAS Web Appでは、画面遷移よりも、google.script.run を使った部分更新の方が安定しやすい。
この問題から得た教訓
今回の最大の学びは、AIエージェントの「直った」という報告を、そのまま完了判定にしてはいけないということだった。
特にGAS Web Appでは、次のような制約がある。
- アプリ本体がクロスオリジンiframe内で動く
- ブラウザ自動操作がiframe内部に届かないことがある
DOMContentLoadedのタイミングが通常のWebページと違うことがある- iframe内の相対URL遷移が失敗しやすい
- 実機でしか確認できない不具合が残る
そのため、AIエージェントに実装を任せる場合でも、完了条件は「コード上よさそう」では足りない。
最低限、以下を確認する必要がある。
- 実際の
/devURLで表示されるか - ユーザー自身のブラウザでタブを押せるか
- ボタンや矢印のクリックイベントが発火するか
- ページ遷移で白紙にならないか
google.script.runの成功・失敗が画面上で確認できるか- 本番反映後の
/execURLでも同じ挙動か
GAS Web Appでは、AIエージェントによる論理レビューと、人間による実機確認を分けて考える必要がある。
問題4:本番デプロイが2重に存在していた
症状
修理が進んだあと、本番デプロイの状態を確認すると、/exec URLの本番デプロイが2つアクティブに残っていた。
どちらが実際に使われているのか分かりにくい状態だった。
これは、過去の作業で新規デプロイを追加した結果、古い本番URLと新しい本番URLが並存していたためだと考えられる。
対処
今回は、古いデプロイを完全削除するのではなく、アーカイブする方針にした。
削除ではなくアーカイブにした理由は、万一のときに戻せる余地を残すためである。
実機確認で旧バージョンと判断した方をアーカイブし、現在利用する方には「既存デプロイの編集 → 新バージョン」の手順で最新コードを反映した。
ここで重要なのは、新規デプロイを作らないことだった。
GAS Web Appでは、新規デプロイを作るとURLやデプロイIDが変わる。
すでに共有しているURLがある場合、むやみに新規デプロイを増やすと、どれが本番なのか分からなくなる。
今後は、次の運用に統一する。
- 開発確認は
/devURLで行う - 本番反映は既存デプロイを編集して新バージョンを割り当てる
- 新規デプロイは原則作らない
- 古いデプロイは削除ではなくアーカイブで整理する
使ってみた感想
今回の実験は、便利さよりも怖さが先に見えた回だった。
AIエージェントは、GASエディタを直接操作しながらかなり速く修正を進めてくれる。
関数の追加、表示ロジックの整理、既読処理の実装など、手作業なら時間がかかる部分を一気に進められる感覚はあった。
一方で、既存コードの一部削除や、実機では動かない状態での「直った」判定も起きた。
特にGAS Web Appのように、実行環境がGoogle側のiframeに包まれている場合、AIエージェントが見ている世界と、ユーザーが実際に操作している世界がずれることがある。
このずれは、かなり重要だと感じた。
AIエージェントはコードを書く相棒にはなる。
ただし、GAS Web Appの実機確認まで完全に任せるのはまだ危ない。
今回のツールは、完成版というより、前回作ったMVPを壊しながら直し、少しずつ運用に近づけている段階である。
手動で確認すべき部分はまだ多い。
特に、本番デプロイ前のクリック操作、日付移動、既読反映、スマホ表示は、人間が実際に触って確認する必要がある。
わかったこと
今回わかったのは、AIエージェントは「コード上の整合性」はかなり見られるが、「GAS Web Appとして実際に操作できるか」の検証には制約があるということだった。
特に、サンドボックスiframe内で動くUIは、ブラウザ自動操作型のAIエージェントにとって難所になる。
通常のWebアプリであれば、ボタンをクリックし、DOMを確認し、結果を検証する流れが取りやすい。
しかしGAS Web Appでは、アプリ本体が script.googleusercontent.com 側のiframe内に入るため、自動操作やDOM確認が思った通りに届かないことがある。
また、DOMContentLoaded への依存や、<a href> による相対URL遷移のように、普通のWeb制作では見逃しがちな実装が、GAS環境では不具合として表面化した。
今回の修理で、人間の判断が必要だったのは以下の部分だった。
- AIの「修正完了」報告をそのまま信じず、実機で確認すること
- 症状を「何も表示されない」ではなく、「ヘッダーは出るがカードだけ出ない」のように分解すること
- GASのプロジェクト履歴で戻すか、部分修理するかを判断すること
- 本番デプロイを削除せず、アーカイブに留めること
- 新規デプロイではなく、既存デプロイの更新を選ぶこと
「動くものを作る」ことと「運用できるものにする」ことは違う。
AIエージェントだけで、動くMVPに近いものは作れる。
しかし、壊れたときの復旧手順、デプロイ管理、実機検証、ユーザーが毎日使える安定性までは、人間側のレビューと判断がまだ必要だった。
次に改善したいこと
- GASプロジェクトを編集する前に、必ず「概要 → コピーを作成」でバックアップを取る
- 変更内容ごとに、修正前後のファイル内容を外部にも保存する
/devURLで確認する項目をチェックリスト化する- タブ切り替え、既読、日付移動、空データ時表示を毎回確認する
window.onerrorや画面上デバッグログを、必要なときだけ有効化できる仕組みにする- 日付移動はページ遷移ではなく
google.script.runの部分更新に統一する - 本番デプロイは新規作成せず、既存デプロイの編集で更新する
summarizeItem_()をClaude APIによる要約処理に差し替える- 重要度スコア
scoreを固定値ではなく、内容に応じた評価にする Config_Interestsシートを追加し、未登録チャンネルや海外動画の自動発見に進む- スマホ表示でカードUIが読みやすいかを確認する
今回の結論
AIエージェントだけで、GAS製YouTubeダイジェストツールの表示崩れを修理し、タブ切り替え・既読管理・日付移動の改善までは進められた。
ただし、GAS Web App特有のサンドボックスiframe環境では、AIエージェントのブラウザ自動操作だけでは実機動作を十分に検証できない。
今回の成果物は完成版ではなく、AIエージェントと人間の実機確認を組み合わせて、GASアプリを修理・運用改善できるかを検証するためのMVPとして公開する。
実験データ
- 実験番号:No.008
- テーマ:GAS製YouTubeダイジェストツールは、AIエージェントだけで修理・改善できるか?
- ステータス:修理完了、一部機能は次フェーズへ継続
- 使用AI:コーディング特化AIエージェント(ブラウザ操作型)
- 成果物:Google Apps Script製YouTube新着ダイジェストWeb App
- 使用技術:Google Apps Script、Google Sheets、HtmlService、YouTube RSSフィード、google.script.run
- GitHubリポジトリ:未使用、または公開後に追記
- 公開URL:伏せ字、または公開後に追記
- 今回の結論:AIエージェントでGASアプリの修理と改善は進められるが、サンドボックスiframe環境では人間の実機確認が必須

コメント