AI Studio Auth Extractor

一键提取 Google AI Studio 完整认证信息,下载或直接上传到 studiogw / AIStudioToAPI 服务端

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         AI Studio Auth Extractor
// @author       xjetry
// @namespace    https://github.com/iBUHub/AIStudioToAPI
// @version      3.0.0
// @description  一键提取 Google AI Studio 完整认证信息,下载或直接上传到 studiogw / AIStudioToAPI 服务端
// @match        https://aistudio.google.com/*
// @grant        GM_cookie
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @connect      *
// @run-at       document-idle
// @license      MIT
// @noframes
// ==/UserScript==

/**
 * 使用前请在 Tampermonkey 中完成以下设置(仅需一次):
 *
 * 1. 点击 Tampermonkey 图标 → 管理面板 → 设置
 * 2. 将「配置模式」切换为「高级」
 * 3. 找到「安全」区域 → 将「允许脚本访问 Cookie」设置为「All」
 * 4. 保存设置并刷新 AI Studio 页面
 *
 * 原因:核心认证 Cookie(如 __Secure-1PSID)标记了 httpOnly,
 *       浏览器禁止 JS 直接读取。上述设置授权 Tampermonkey 的
 *       GM_cookie API 读取这些 httpOnly Cookie。
 *
 * v3 变更:
 *  - 补齐认证 cookie 集(含 3P 变体 / APISID / NID / SIDCC 三件套 / accounts 登录态),
 *    解决旧版只抓部分 cookie 导致会话很快过期的问题。
 *  - 支持直接上传到服务端的 POST /admin/auth(菜单里配置地址 + API Key),
 *    免去手动下载再上传。未配置时回退为下载 {email}.json。
 */

(function () {
    "use strict";

    // 抓取所有"与 Google 登录态相关"的 cookie,而不是固定子集——缺 cookie 是会话
    // 很快失效的根因。保留:任意 *.google.com(含 accounts 登录态、3P 变体)+ Canvas
    // 预览域 *.run.app 的 token。排除 youtube 等无关服务。
    function isRelevantDomain(domain) {
        const d = (domain || "").toLowerCase();
        if (d.includes("youtube")) return false;
        return d.includes("google.com") || d.includes(".run.app");
    }

    function mapSameSite(v) {
        if (v === "lax") return "Lax";
        if (v === "strict") return "Strict";
        return "None";
    }

    function normalizeCookie(c) {
        return {
            name: c.name,
            value: c.value,
            domain: c.domain,
            path: c.path || "/",
            expires: c.expirationDate != null ? c.expirationDate : -1,
            httpOnly: !!c.httpOnly,
            secure: !!c.secure,
            sameSite: mapSameSite(c.sameSite),
        };
    }

    function extractEmail() {
        const re = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
        for (const el of document.querySelectorAll('script[type="application/json"]')) {
            const m = (el.textContent || "").match(re);
            if (m) return m[0];
        }
        return null;
    }

    function listCookies(filter) {
        return new Promise(resolve => {
            if (typeof GM_cookie === "undefined" || !GM_cookie || !GM_cookie.list) {
                return resolve([]);
            }
            GM_cookie.list(filter || {}, (cookies, error) => {
                resolve(error ? [] : cookies || []);
            });
        });
    }

    // accounts.google.com 的登录 cookie 通常是 host-only,在 aistudio 页面用空 filter
    // 可能拿不到,所以额外按域显式查询一次,再按 (name|domain|path) 去重合并。
    async function gatherCookies() {
        const groups = await Promise.all([
            listCookies({}),
            listCookies({ domain: "google.com" }),
            listCookies({ domain: "accounts.google.com" }),
        ]);
        const seen = new Set();
        const merged = [];
        for (const group of groups) {
            for (const c of group) {
                if (!isRelevantDomain(c.domain)) continue;
                const key = `${c.name}|${c.domain}|${c.path || "/"}`;
                if (seen.has(key)) continue;
                seen.add(key);
                merged.push(normalizeCookie(c));
            }
        }
        return merged;
    }

    function downloadJSON(data, filename) {
        const a = document.createElement("a");
        a.href = URL.createObjectURL(new Blob([JSON.stringify(data)], { type: "application/json" }));
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        a.remove();
        URL.revokeObjectURL(a.href);
    }

    function uploadToServer(state, baseUrl, apiKey) {
        const url = baseUrl.replace(/\/+$/, "") + "/admin/auth";
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url,
                headers: { Authorization: "Bearer " + apiKey, "Content-Type": "application/json" },
                data: JSON.stringify(state),
                onload: res => {
                    if (res.status >= 200 && res.status < 300) {
                        try {
                            resolve(JSON.parse(res.responseText));
                        } catch {
                            resolve({});
                        }
                    } else {
                        reject(new Error(`HTTP ${res.status}: ${(res.responseText || "").slice(0, 200)}`));
                    }
                },
                onerror: () => reject(new Error("网络错误(检查服务器地址 / @connect / CORS)")),
                ontimeout: () => reject(new Error("请求超时")),
            });
        });
    }

    async function extract() {
        const cookies = await gatherCookies();
        if (!cookies.some(c => c.name === "SAPISID")) {
            throw new Error("缺少 SAPISID,请确保已登录 AI Studio(并已开启 Cookie 访问=All)。");
        }
        if (!cookies.some(c => c.name === "__Secure-1PSID")) {
            throw new Error("缺少 __Secure-1PSID(httpOnly)。请在 Tampermonkey 设置里把 Cookie 访问设为 All。");
        }
        let email = extractEmail();
        if (!email) {
            email = prompt("未检测到邮箱,请输入账号标签:");
            if (!email) return null;
        }
        return {
            email,
            state: {
                accountName: email,
                cookies,
                origins: [{ origin: "https://aistudio.google.com", localStorage: [] }],
            },
            count: cookies.length,
        };
    }

    // ---- 服务端配置(持久化)----

    function getServerConfig() {
        return {
            url: GM_getValue("studiogw_url", ""),
            key: GM_getValue("studiogw_key", ""),
        };
    }

    function configureServer() {
        const cur = getServerConfig();
        const url = prompt("studiogw 服务端地址(如 http://127.0.0.1:7860)。留空则清除并改用下载模式:", cur.url);
        if (url === null) return;
        if (!url.trim()) {
            GM_setValue("studiogw_url", "");
            GM_setValue("studiogw_key", "");
            alert("已清除服务端配置,将改用下载模式。");
            return;
        }
        const key = prompt("API Key(对应服务端 API_KEYS 之一):", cur.key);
        if (key === null) return;
        GM_setValue("studiogw_url", url.trim());
        GM_setValue("studiogw_key", key.trim());
        alert("已保存。点击「Extract Auth」将直接上传到服务端。");
    }

    if (typeof GM_registerMenuCommand !== "undefined") {
        GM_registerMenuCommand("⚙️ 配置 studiogw 上传地址 / API Key", configureServer);
    }

    // ---- UI ----

    const btn = document.createElement("button");
    btn.textContent = "\u{1F4E6} Extract Auth";
    Object.assign(btn.style, {
        position: "fixed",
        bottom: "20px",
        right: "20px",
        zIndex: "99999",
        padding: "10px 18px",
        background: "#1a73e8",
        color: "#fff",
        border: "none",
        borderRadius: "24px",
        fontSize: "14px",
        fontWeight: "500",
        cursor: "pointer",
        boxShadow: "0 2px 8px rgba(0,0,0,0.25)",
        fontFamily: "Google Sans, Roboto, Arial, sans-serif",
        transition: "all 0.2s",
    });

    btn.onmouseenter = () => ((btn.style.background = "#1557b0"), (btn.style.transform = "translateY(-1px)"));
    btn.onmouseleave = () => ((btn.style.background = "#1a73e8"), (btn.style.transform = ""));

    btn.onclick = async () => {
        if (btn.disabled) return;
        btn.disabled = true;
        btn.textContent = "⏳ 提取中...";
        try {
            const r = await extract();
            if (!r) {
                btn.textContent = "❌ 取消";
            } else {
                const server = getServerConfig();
                if (server.url && server.key) {
                    btn.textContent = "⬆️ 上传中...";
                    const resp = await uploadToServer(r.state, server.url, server.key);
                    btn.textContent = `✅ 已上传 (${r.count})`;
                    console.log(`[Auth Extractor] uploaded ${r.email}: ${r.count} cookies →`, resp);
                } else {
                    downloadJSON(r.state, `${r.email}.json`);
                    btn.textContent = `✅ ${r.count} cookies`;
                    console.log(`[Auth Extractor] downloaded ${r.email}: ${r.count} cookies (配置服务端可直传)`);
                }
            }
        } catch (e) {
            btn.textContent = "❌ 失败";
            alert(e.message);
        }
        setTimeout(() => {
            btn.textContent = "\u{1F4E6} Extract Auth";
            btn.disabled = false;
        }, 2500);
    };

    document.body.appendChild(btn);
})();