Apps Script ダイアログを本人の Google 承認に依存させない — 「承認が必要です」全滅をWebアプリfetchフォールバックで塞ぐ
bound script のダイアログ内 google.script.run が複数アカウント同時ログインで承認エラーになる構造問題を、文脈のテンプレ埋め込み+開発者権限 Web アプリへの fetch フォールバック(dedup・CORS・外部オリジン Playwright 検証)で解く
約15.5万トークンの節約 (API料金換算で約230円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「Apps Script ダイアログを本人の Google 承認に依存させない — 「承認が必要です」全滅をWebアプリfetchフォールバックで塞ぐ」は、Google WorkspaceカテゴリのAI指示書(MDファイル)です。bound script のダイアログ内 google.script.run が複数アカウント同時ログインで承認エラーになる構造問題を、文脈のテンプレ埋め込み+開発者権限 Web アプリへの fetch フォールバック(dedup・CORS・外部オリジン Playwright 検証)で解くこの巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約15.5万トークン(API料金換算で約230円)・86%のトークンを節約できます。
- カテゴリ
- Google Workspace
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約18万トークン
- この巻物使用時
- 約2.5万トークン
- 節約量
- 約15.5万トークン (約230円)
- 更新日
- 2026-09-20
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/apps-script-google-web-fetch/raw を読み込んで、この指示書どおりに実装して"
中身
Apps Script のダイアログを「開いた本人の Google 承認」に依存させない — 「承認が必要です」で全滅する構造を塞ぐ
症状(この指示書が効く条件)
- スプレッドシートにバインドした Apps Script のカスタムメニューからダイアログ(HtmlService)を開ける。
- ところがダイアログ内の
google.script.run.xxx()が 全部その操作を実行するには承認が必要です(英:Authorization is required to perform that action)で落ちる。起動時の文脈取得も、送信も、再送も同じ。 - 開発者のアカウントでは絶対に再現しない。特定の利用者だけで起きる。コードは変えていない。権限(oauthScopes)も変えていない。
原因
ダイアログの iframe から呼ぶ google.script.run は「メニューを押した人の承認」で走るとは限らない。ブラウザに複数の Google アカウント(個人 Gmail と会社アカウント等)を同時ログインしていると、既定アカウント側で実行され、そのアカウントはスクリプトを承認していないので全部落ちる。メニュー項目の実行は承認済みの本人で通っているため「ダイアログは開くのに中身が動かない」という形になる。
つまりコードのバグではなく、ダイアログの機能を利用者本人の承認状態に依存させている構造が原因。利用者に「アカウントを切り替えて」と頼む解決は毎回の手作業になるので採らない。
対策の型(2 本立て)
1. 文脈はダイアログを開く関数の中で取り、テンプレートに埋め込む
showXxxDialog()(メニューから呼ばれる=承認済みの実行)の中で、ダイアログが必要とする値(アクティブシート名・記録先 URL・利用者メール等)を全部取り、JSON にして HTML テンプレートへ渡す。起動後に google.script.run で取りに行かない。
function showFeedbackDialog() {
const context = { sheetName: '', logSheetUrl: '', reporter: '', submitUrl: FALLBACK_WEBAPP_URL };
try {
const ss = SpreadsheetApp.getActive();
context.sheetName = ss.getActiveSheet().getName();
context.logSheetUrl = ss.getUrl();
} catch (e) { /* 文脈が取れなくても報告手段そのものは開く */ }
try { context.reporter = Session.getActiveUser().getEmail() || ''; } catch (e) {}
const t = HtmlService.createTemplateFromFile('FeedbackDialog');
t.contextJson = JSON.stringify(context);
SpreadsheetApp.getUi().showModalDialog(t.evaluate().setWidth(560).setHeight(700), '報告');
}
HTML 側は data 属性経由で受ける(<?!= ?> で <script> 内に直書きしない。シート名に " や </script> が入っても壊れないように)。
<div id="dialog-context" data-context="<?= contextJson ?>" hidden></div>
<script>
let ctx = { sheetName: '', logSheetUrl: '', reporter: '', submitUrl: '' };
try { ctx = Object.assign(ctx, JSON.parse(document.getElementById('dialog-context').dataset.context || '{}')); } catch (e) {}
</script>
2. 送信は google.script.run を一次、失敗したら「開発者権限で動く Web アプリ」へ fetch でフォールバック
同じスクリプト(または別スクリプト)を Web アプリとして 実行するユーザー: 自分(デプロイした人) / アクセスできるユーザー: 全員(匿名含む) でデプロイし、doPost に token 不要の受け口を 1 つ作る。利用者の承認が一切要らない経路になる。
function submitPayload(payload) {
google.script.run
.withSuccessHandler(function (r) {
if (!r) { showError('応答を受け取れませんでした。記録済みの可能性があるので記録先を確認してください'); return; }
if (!r.ok) { showError(r.error); return; }
showSuccess(r.row);
})
.withFailureHandler(function (err) { submitViaWebApp(payload, err); }) // ← ここだけがフォールバック
.Server_submit(payload);
}
function submitViaWebApp(payload, primaryError) {
if (!ctx.submitUrl) { showError(String(primaryError)); return; }
const controller = new AbortController();
const timer = setTimeout(function () { controller.abort(); }, 180000);
return fetch(ctx.submitUrl, {
method: 'POST',
body: JSON.stringify(Object.assign({ action: 'submitFromDialog' }, payload)), // Content-Type ヘッダは付けない
redirect: 'follow',
signal: controller.signal
}).then(function (res) { if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); })
.then(function (r) { if (!r || !r.ok) { showError(r && r.error); return; } showSuccess(r.row); })
.catch(function (e) {
let reason = '通常の送信: ' + primaryError + ' / 別経路の送信: ' + e;
if (/承認|Authorization/i.test(String(primaryError))) reason += '(複数の Google アカウントで同時ログインしていると起きます)';
showError(reason);
}).finally(function () { clearTimeout(timer); });
}
サーバ側(Web アプリの doPost):
function doPost(e) {
let body = {};
try { body = JSON.parse(e.postData.contents); } catch (err) { return json({ ok: false, error: 'bad json' }); }
if (body.action === 'submitFromDialog') return json(submitFromDialog(body)); // token 検証より前に置く
if (body.token !== expectedToken()) return json({ ok: false, error: 'unauthorized' });
// …既存の認証付き API
}
必ず守る設計上のルール
- フォールバックするのは
withFailureHandlerに入った時だけ。成功ハンドラで falsy やok:falseを受けた時はサーバが動いている(記録済みの可能性)ので再送しない。 - 二重登録の防止: 送信ごとにクライアントで
submitId(文字列)を作り、両経路とも記録行の同じ列に書く。Web アプリ側は追記前に末尾 30 行程度を見て同じsubmitIdがあれば追記せず既存行を返す(dedup:true)。 - fetch に独自ヘッダを付けない。文字列 body の既定
text/plainのままなら CORS の preflight が発生せず、Apps Script Web アプリは 302 応答にAccess-Control-Allow-Origin: <Origin>を返して通る(実測)。Content-Type: application/jsonを付けると落ちる。 - 匿名で叩ける受け口なので レート制限(例: 10 分 5 件、
CacheServiceに固定ウィンドウでカウント)と 入力の切り詰め・画像サイズ上限 を入れる。 - 送信元の記録に経路の印(例:
/ webapp)を付けて、フォールバックの発火数を後から数えられるようにする。0 でなければ利用者環境の問題が続いている合図。 - 戻り値に
Dateを入れない(google.script.runは Date を運べず、成功ハンドラが falsy を受ける別障害になる)。文字列に整形して返す。
検証のしかた(利用者のアカウント無しでできる)
利用者本人の環境は再現できないので、Web アプリ経路そのものを外部オリジンから叩いて確かめる。Playwright で https://example.com/ を開き、page.evaluate 内で上と同じ fetch(POST) を実行する。ログイン不要で、実ブラウザの CORS を含めた検証になる。
確認する 3 点:
{ok:true, row:N}が返り、記録先に N 行目が入っている(画像を付けたなら保存 URL が入っている)。- 同じ
submitIdで再送すると{ok:true, dedup:true, row:N}で行が増えない。 - token 必須の他の action を token 無しで叩くと従来どおり
unauthorized。
検証行は後で状態を完了にし、保存した検証画像はゴミ箱へ。
同じ型で直せる仲間
同じスクリプトに他のダイアログ(ファイルアップロード・登録フォーム等)があれば構造は同じなので、同症状が出たら同じ 2 本立てで直す。先に全部直す必要はないが、「文脈はテンプレ埋め込み」だけは新規ダイアログの既定にしておくと安い。
よくある質問
+「Apps Script ダイアログを本人の Google 承認に依存させない — 「承認が必要です」全滅をWebアプリfetchフォールバックで塞ぐ」とは何ですか?
bound script のダイアログ内 google.script.run が複数アカウント同時ログインで承認エラーになる構造問題を、文脈のテンプレ埋め込み+開発者権限 Web アプリへの fetch フォールバック(dedup・CORS・外部オリジン Playwright 検証)で解く
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約18万トークンかかりますが、この巻物を使えば約2.5万トークンで済みます。差し引き約15.5万トークン(API料金換算で約230円)・86%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
GAS完全自動化テンプレ — Driveコマンドキュー方式
Google Apps Script の「毎回エディタで▶実行」を根絶。Drive 経由のコマンドキューで、初回1クリック以降は AI がすべての GAS 関数をリモート実行できるようになるテンプレート指示書。
人間の手入力台帳を壊さずに自動更新する — GAS Web App upsert 設計
各PC/各拠点の点検結果を、人間が手運用しているスプレッドシート台帳へ自動反映する。手入力列とコメントを絶対に壊さない突合設計、タブ/列の解決、並行POST対策、配布シークレットの落とし穴まで。
数式まみれの業務スプレッドシートを、Webアプリから壊さずに編集させる型
ArrayFormula と per-row 数式が混在する台帳を、セル単位 allowlist・dry-run 既定・適用前バックアップ・触っていないセルの数式不変検査で安全に書き換える設計手順。列ごとの数式復元と、テストが緑のまま壊れる典型例つき。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア