マキモノ
Google Workspace無料✅ 公式検証済みv1.0.0 / 更新

ログイン必須の社内ページ(Google Sites/社内Wiki)をAIに自力で読ませる

APIが無く通常のHTTP取得では302でログイン画面に飛ぶ社内ページを、Playwrightの永続プロファイル1本でAIエージェントが自力取得できるようにする。実測で踏んだWindows固有の致命バグ4件と回避策つき。

出品者: nishi@orgiast.jp2 DL📖 読込 約3,255トークン (約5円)💰 コスパ 5
トークン節約メーター86%節約
ゼロからAIに作らせた場合1.8万トークン
このMDを読ませた場合2,600トークン

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 のターミナルに貼るだけです。

⬇ .md をダウンロード
claude "https://makimono-md.vercel.app/api/v1/files/google-sites-wiki-ai/raw を読み込んで、この指示書どおりに実装して"
claude-codecursorcodex-cliライセンス: 商用利用可 (再販不可)

中身

ログイン必須の社内ページ(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();

実測で踏んだ罠(ここが本体)

  1. **import.meta.url === \file://${process.argv[1]}`は Windows で必ず false になる。**import.meta.urlfile:///C:/...argv[1]C:...なので一致せず、**CLI が無言で何もしない**。fileURLToPath+fs.realpathSync.native` + 小文字化して比較する(junction やドライブ表記の揺れも吸収できる)。

  2. process.exit()finally の前に呼ぶと、ログインCookie がディスクに保存されない。 永続プロファイルの意味が消えて「ログインしたのに毎回未ログイン」になる。終了コードは変数に貯め、context.close() の後で process.exit する。

  3. Chromium の配置はバージョンで変わる。 新しめの Playwright は chrome-win64/chrome.exe(旧 chrome-win 決め打ちだと見つからない)。候補を総当たりする。

  4. readdirSync(dir, { withFileTypes: true })isDirectory は関数。 .filter(e => e.isDirectory && ...) と書くと常に true になり、ファイルまで拾う。e.isDirectory() と呼ぶ。

  5. ハブページは [role=main] に見出ししか入っていない。 サイトのトップや章の入口は本文が別要素にあることが多い。取得結果が異常に短い(100文字未満)なら document.body.innerText に落とす。

  6. --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で頼むだけで、仕事が完成」。

AI代行堂を見る →

関連する巻物

この巻物、誰かのトークンも救えます

𝕏 で節約レシートをシェア