ログイン必須の社内ページ(Google Sites/社内Wiki)をAIに自力で読ませる
APIが無く通常のHTTP取得では302でログイン画面に飛ぶ社内ページを、Playwrightの永続プロファイル1本でAIエージェントが自力取得できるようにする。実測で踏んだWindows固有の致命バグ4件と回避策つき。
約1.5万トークンの節約 (API料金換算で約23円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「ログイン必須の社内ページ(Google Sites/社内Wiki)をAIに自力で読ませる」は、Google WorkspaceカテゴリのAI指示書(MDファイル)です。APIが無く通常のHTTP取得では302でログイン画面に飛ぶ社内ページを、Playwrightの永続プロファイル1本でAIエージェントが自力取得できるようにする。実測で踏んだWindows固有の致命バグ4件と回避策つき。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約1.5万トークン(API料金換算で約23円)・86%のトークンを節約できます。
- カテゴリ
- Google Workspace
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約1.8万トークン
- この巻物使用時
- 約2,600トークン
- 節約量
- 約1.5万トークン (約23円)
- 更新日
- 2026-08-30
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/google-sites-wiki-ai/raw を読み込んで、この指示書どおりに実装して"
中身
ログイン必須の社内ページ(Google Sites / 社内Wiki)を AI に自力で読ませる
これは何の問題を解くか
AI エージェントに「社内マニュアルを見て実装して」と頼むと、たいてい次で詰まる。
- 通常の HTTP 取得(WebFetch 相当)は
302 → accounts.google.comに飛ばされる。エージェントは Google のログインセッションを持てない - Google Sites には本文を返す API が存在しない。Drive API の export も「head revision に中身がない」で失敗する
- ドメイン全員に共有されていても同じ。権限の問題ではなく、セッションを運べないことが原因
結果、毎回人間が本文をコピペする運用になる。マニュアルが更新されるたび、別ページを扱うたびに再発する。
解決方針
ログイン済みのブラウザプロファイルを1つ用意し、そこからページを取得する CLI を1本置く。 初回だけ人間がログインすれば、以後はエージェントが任意の社内ページを自力で読める。Google Sites に限らず、認証付きの社内 Wiki 全般に効く。
- 依存は
playwright-coreのみ(ブラウザは既存キャッシュを流用) - プロファイルは
<ホーム>/.<エージェント設定ディレクトリ>/google-profileに永続化 - 追加費用ゼロ・ローカル完結・外部送信なし
実装
// tools/gpage-fetch.mjs
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { chromium } from "playwright-core";
const PROFILE = path.join(os.homedir(), ".agent", "google-profile");
export function isLoginUrl(u) {
return typeof u === "string" &&
(u.includes("accounts.google.com") || u.includes("ServiceLogin"));
}
function browsersBaseDir() {
if (process.env.PLAYWRIGHT_BROWSERS_PATH) return process.env.PLAYWRIGHT_BROWSERS_PATH;
if (process.platform === "win32") return path.join(process.env.LOCALAPPDATA, "ms-playwright");
if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Caches", "ms-playwright");
return path.join(os.homedir(), ".cache", "ms-playwright");
}
export function findChromium(baseDir = browsersBaseDir()) {
let entries = [];
try { entries = fs.readdirSync(baseDir, { withFileTypes: true }); } catch { return null; }
const dirs = entries
.filter((e) => e.isDirectory() && e.name.startsWith("chromium-"))
.map((e) => e.name)
.sort((a, b) => b.localeCompare(a));
for (const d of dirs) {
for (const c of [
path.join(baseDir, d, "chrome-win64", "chrome.exe"),
path.join(baseDir, d, "chrome-win", "chrome.exe"),
path.join(baseDir, d, "chrome-mac-arm64", "Chromium.app", "Contents", "MacOS", "Chromium"),
path.join(baseDir, d, "chrome-linux64", "chrome"),
path.join(baseDir, d, "chrome-linux", "chrome"),
]) if (fs.existsSync(c)) return c;
}
return null;
}
async function extract(page, wantLinks) {
const text = await page.evaluate(() => {
const main = document.querySelector("[role=main]");
const mainText = main ? main.innerText.trim() : "";
// 見出しだけのハブページでは [role=main] が数十文字しか返らないので body に落とす
const raw = mainText.length >= 100 ? mainText : document.body.innerText;
return raw.replace(/\n{3,}/g, "\n\n").trim();
});
const links = wantLinks
? await page.evaluate(() =>
Array.from(document.querySelectorAll("a[href]"))
.map((a) => ({ text: a.innerText.trim(), href: a.href }))
.filter((l) => l.href.startsWith("http")))
: null;
return { text, links };
}
async function main() {
const args = process.argv.slice(2);
const login = args.includes("--login");
const wantLinks = args.includes("--links");
const url = args.find((a) => !a.startsWith("-"));
const exe = findChromium();
if (!exe) { console.error("Chromium が無い。npx playwright install chromium"); process.exit(4); }
let code = 0;
let ctx = null;
try {
ctx = await chromium.launchPersistentContext(PROFILE, { headless: !login, executablePath: exe });
const page = ctx.pages()[0] || (await ctx.newPage());
if (login) {
await page.goto("https://accounts.google.com/", { waitUntil: "networkidle" });
const start = Date.now();
while (Date.now() - start < 15 * 60 * 1000) {
await new Promise((r) => setTimeout(r, 30000));
if (!isLoginUrl(page.url())) { console.log("ログインを保存しました"); return; }
await page.goto("https://myaccount.google.com/", { waitUntil: "networkidle" }).catch(() => {});
}
code = 5;
return;
}
await page.goto(url, { waitUntil: "networkidle", timeout: 60000 });
if (isLoginUrl(page.url())) {
console.error("未ログイン。先に --login を実行すること");
code = 3;
return;
}
const { text, links } = await extract(page, wantLinks);
console.log(wantLinks ? links.map((l) => l.text + "\t" + l.href).join("\n") : text);
} catch (e) {
console.error("エラー: " + e.message);
code = 1;
} finally {
if (ctx) await ctx.close().catch(() => {});
}
process.exit(code); // ← close の後に呼ぶこと(理由は下記)
}
main();
実測で踏んだ罠(ここが本体)
-
**
import.meta.url === \file://${process.argv[1]}`は Windows で必ず false になる。**import.meta.urlはfile:///C:/...、argv[1]はC:...なので一致せず、**CLI が無言で何もしない**。fileURLToPath+fs.realpathSync.native` + 小文字化して比較する(junction やドライブ表記の揺れも吸収できる)。 -
process.exit()をfinallyの前に呼ぶと、ログインCookie がディスクに保存されない。 永続プロファイルの意味が消えて「ログインしたのに毎回未ログイン」になる。終了コードは変数に貯め、context.close()の後でprocess.exitする。 -
Chromium の配置はバージョンで変わる。 新しめの Playwright は
chrome-win64/chrome.exe(旧chrome-win決め打ちだと見つからない)。候補を総当たりする。 -
readdirSync(dir, { withFileTypes: true })のisDirectoryは関数。.filter(e => e.isDirectory && ...)と書くと常に true になり、ファイルまで拾う。e.isDirectory()と呼ぶ。 -
ハブページは
[role=main]に見出ししか入っていない。 サイトのトップや章の入口は本文が別要素にあることが多い。取得結果が異常に短い(100文字未満)ならdocument.body.innerTextに落とす。 -
--linksを先に作ると調査が一気に楽になる。 サイト内リンクを全列挙できれば、URL を推測せずに全ページを辿れる。実際これで対象サイトの全ページを把握できた。
運用上の注意
- 初回の
--loginは人間が実行する。 ログイン画面つきブラウザの起動は、エージェントの安全機構がブロックすることがある。これは正常な挙動なので回避しようとせず、1行のコマンドを人間に渡す。 - プロファイルはローカルにのみ保存され、外部へは送信しない。普段使いのブラウザに Google セッションが保存されているのと同じ性質のもの。
- 取得したマニュアル本文に暗証番号・パスワードが含まれていることがある。 そのままリポジトリや生成物に入れない。該当箇所は「暗証番号はマニュアル参照」等に置き換える。
- マニュアルが一次ソースである以上、生成物(チェックリスト定義など)は取り直して差し替えられる形にしておく。取得コマンドを README に書いておくと、更新のたびに再現できる。
検証の型
ブラウザを起動しないユニットテストを必ず用意する(引数パース / ログイン画面判定 / 実行ファイル探索)。依存を引数で注入できる形にしておけばスタブで検証できる。
そのうえでログイン前に一度実行し、「未ログイン」を正しく検出して終了コード3で落ちることを確認する。これだけで「起動する・遷移する・判定が効く」の3点がまとめて検証でき、人間にログインを頼む前に実装の正しさを確かめられる。
よくある質問
+「ログイン必須の社内ページ(Google Sites/社内Wiki)をAIに自力で読ませる」とは何ですか?
APIが無く通常のHTTP取得では302でログイン画面に飛ぶ社内ページを、Playwrightの永続プロファイル1本でAIエージェントが自力取得できるようにする。実測で踏んだWindows固有の致命バグ4件と回避策つき。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約1.8万トークンかかりますが、この巻物を使えば約2,600トークンで済みます。差し引き約1.5万トークン(API料金換算で約23円)・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 既定・適用前バックアップ・触っていないセルの数式不変検査で安全に書き換える設計手順。列ごとの数式復元と、テストが緑のまま壊れる典型例つき。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア