Greasy Fork

Google SEO API索引提交插件

向 Google Indexing API 提交当前页面网址进行索引

目前为 2025-02-25 提交的版本。查看 最新版本

// ==UserScript==
// @name         Google SEO API索引提交插件
// @namespace    http://tampermonkey.net/
// @version      0.9.0
// @description  向 Google Indexing API 提交当前页面网址进行索引
// @license      GPL License
// @author       Benson
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @require      https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/8.0.20/jsrsasign-all-min.js
// ==/UserScript==

(function() {
    'use strict';

    // 检查是否在 iframe 中
    if (window !== window.top) {
        return;
    }

    // 配置
    let SERVICE_ACCOUNT = GM_getValue('SERVICE_ACCOUNT', null);
    const ENDPOINT = 'https://indexing.googleapis.com/v3/urlNotifications:publish';
    const DISCOVERY_DOC = 'https://indexing.googleapis.com/$discovery/rest?version=v3';
    const SCOPE = 'https://www.googleapis.com/auth/indexing';
    let accessTokenCache = null;
    let accessTokenExpiry = 0;
    let isSubmitting = false; // 添加提交状态标志

    // 创建配置面板
    function createConfigPanel() {
        const configPanel = document.createElement('div');
        configPanel.id = 'googleApiKeyConfig';
        configPanel.style.cssText = `
            display: none;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 500px;
            background: white;
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 10000;
        `;

        configPanel.innerHTML = `
            <h3 style="margin-top: 0;">配置 Google Indexing API</h3>
            <div style="margin-bottom: 15px;">
                <p style="margin-bottom: 10px; color: #666;">请输入您的服务账号凭据 JSON:</p>
                <textarea id="serviceAccountInput"
                    style="width: 100%; height: 200px; padding: 8px; margin-bottom: 10px; box-sizing: border-box; font-family: monospace;"
                    placeholder="请粘贴您的服务账号凭据 JSON 文件内容">${SERVICE_ACCOUNT ? JSON.stringify(SERVICE_ACCOUNT, null, 2) : ''}</textarea>
                <div style="color: #666; font-size: 12px; margin-bottom: 10px;">
                    <p>示例格式:</p>
                    <pre style="background: #f5f5f5; padding: 8px; border-radius: 4px;">
{
  "type": "service_account",
  "project_id": "your-project-id",
  "private_key_id": "key-id",
  "private_key": "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n",
  "client_email": "[email protected]",
  ...
}</pre>
                </div>
                <button id="saveConfig" style="
                    padding: 8px 15px;
                    background: #4285f4;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    margin-right: 10px;
                ">保存</button>
                <button id="clearConfig" style="
                    padding: 8px 15px;
                    background: #dc3545;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    margin-right: 10px;
                ">清除配置</button>
                <button id="closeConfig" style="
                    padding: 8px 15px;
                    background: #666;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                ">关闭</button>
            </div>
        `;

        // 添加调试模式开关
        const debugModeDiv = document.createElement('div');
        debugModeDiv.style.marginTop = '10px';
        debugModeDiv.innerHTML = `
            <label>
                <input type="checkbox" id="debugMode" ${localStorage.getItem('DEBUG_MODE') === 'true' ? 'checked' : ''}>
                调试模式(在控制台显示详细信息)
            </label>
        `;
        configPanel.querySelector('div').appendChild(debugModeDiv);

        document.body.appendChild(configPanel);

        // 添加事件监听
        document.getElementById('saveConfig').addEventListener('click', () => {
            try {
                const jsonStr = document.getElementById('serviceAccountInput').value.trim();
                if (!jsonStr) {
                    throw new Error('请输入服务账号凭据');
                }

                const config = JSON.parse(jsonStr);
                if (!config.private_key || !config.client_email) {
                    throw new Error('无效的服务账号凭据,请确保包含 private_key 和 client_email');
                }

                SERVICE_ACCOUNT = config;
                GM_setValue('SERVICE_ACCOUNT', config);
                accessTokenCache = null; // 清除访问令牌缓存
                accessTokenExpiry = 0;

                alert('服务账号凭据已保存!');
                configPanel.style.display = 'none';
            } catch (error) {
                alert('保存失败:' + error.message);
            }
        });

        // 添加清除配置按钮事件
        document.getElementById('clearConfig').addEventListener('click', () => {
            if (confirm('确定要清除服务账号凭据吗?')) {
                SERVICE_ACCOUNT = null;
                GM_setValue('SERVICE_ACCOUNT', null);
                accessTokenCache = null;
                accessTokenExpiry = 0;
                document.getElementById('serviceAccountInput').value = '';
                alert('服务账号凭据已清除!');
            }
        });

        document.getElementById('closeConfig').addEventListener('click', () => {
            configPanel.style.display = 'none';
        });

        // 添加调试模式切换事件
        document.getElementById('debugMode').addEventListener('change', function(e) {
            localStorage.setItem('DEBUG_MODE', e.target.checked);
        });

        return configPanel;
    }

    // 显示配置面板
    function showConfigPanel() {
        const configPanel = document.getElementById('googleApiKeyConfig') || createConfigPanel();
        configPanel.style.display = 'block';
    }

    // 初始化 Google API 客户端
    async function initClient() {
        if (!SERVICE_ACCOUNT) {
            console.log('服务账号未配置');
            return;
        }

        try {
            await gapi.client.init({
                apiKey: SERVICE_ACCOUNT.private_key,
                discoveryDocs: [DISCOVERY_DOC],
                clientId: SERVICE_ACCOUNT.client_id,
                scope: SCOPE
            });
            console.log('Google API 客户端初始化成功');
        } catch (error) {
            console.error('初始化失败:', error);
        }
    }

    // 获取访问令牌
    async function getAccessToken() {
        if (!SERVICE_ACCOUNT) {
            throw new Error('未配置服务账号');
        }

        // 检查缓存的令牌是否还有效
        const now = Math.floor(Date.now() / 1000);
        if (accessTokenCache && now < accessTokenExpiry - 300) { // 提前5分钟刷新
            return accessTokenCache;
        }

        try {
            const expiry = now + 3600;

            // 准备 JWT 头部和载荷
            const header = {
                alg: 'RS256',
                typ: 'JWT'
            };

            const payload = {
                iss: SERVICE_ACCOUNT.client_email,
                scope: SCOPE,
                aud: 'https://oauth2.googleapis.com/token',
                exp: expiry,
                iat: now
            };

            // 使用 jsrsasign 创建 JWT
            const sHeader = JSON.stringify(header);
            const sPayload = JSON.stringify(payload);
            const privateKey = SERVICE_ACCOUNT.private_key.replace(/\\n/g, '\n');

            // 使用 KJUR.jws.JWS 签名
            const jwt = KJUR.jws.JWS.sign(null, sHeader, sPayload, privateKey);

            // 获取访问令牌
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: 'https://oauth2.googleapis.com/token',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded'
                    },
                    data: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`,
                    onload: resolve,
                    onerror: reject
                });
            });

            const data = JSON.parse(response.responseText);
            if (data.access_token) {
                accessTokenCache = data.access_token;
                accessTokenExpiry = now + (data.expires_in || 3600);
                return accessTokenCache;
            } else {
                throw new Error(`获取访问令牌失败: ${response.responseText}`);
            }
        } catch (error) {
            accessTokenCache = null;
            accessTokenExpiry = 0;
            throw new Error(`生成访问令牌失败: ${error.message}`);
        }
    }

    // 验证服务账号配置
    function validateServiceAccount() {
        if (!SERVICE_ACCOUNT) {
            return false;
        }

        try {
            if (!SERVICE_ACCOUNT.private_key || !SERVICE_ACCOUNT.client_email) {
                SERVICE_ACCOUNT = null;
                GM_setValue('SERVICE_ACCOUNT', null);
                return false;
            }
            return true;
        } catch (error) {
            console.error('验证服务账号配置失败:', error);
            return false;
        }
    }

    // 提交 URL 到 Google 索引
    async function submitToGoogleIndex() {
        if (!validateServiceAccount()) {
            alert('请先配置服务账号!');
            const configPanel = document.getElementById('googleApiKeyConfig') || createConfigPanel();
            configPanel.style.display = 'block';
            return;
        }

        if (isSubmitting) {
            console.log('正在提交中,请等待...');
            return;
        }

        // 获取当前页面的真实 URL
        const currentUrl = window.top.location.href;

        // 检查 URL 是否有效
        try {
            const url = new URL(currentUrl);
            if (!url.protocol.startsWith('http')) {
                alert('只能提交 HTTP/HTTPS 协议的 URL');
                return;
            }
        } catch (e) {
            alert('无效的 URL');
            return;
        }

        isSubmitting = true;

        try {
            // 先获取访问令牌
            const accessToken = await getAccessToken();

            // 使用访问令牌调用 Indexing API
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: ENDPOINT,
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${accessToken}`
                    },
                    data: JSON.stringify({
                        url: currentUrl,
                        type: 'URL_UPDATED'
                    }),
                    onload: resolve,
                    onerror: reject
                });
            });

            const data = JSON.parse(response.responseText);

            // 只在开发模式下打印结果
            if (localStorage.getItem('DEBUG_MODE') === 'true') {
                console.log('提交结果:', data);
            }

            if (data.error) {
                // 如果是权限错误,清除令牌缓存
                if (data.error.status === 'PERMISSION_DENIED') {
                    accessTokenCache = null;
                    accessTokenExpiry = 0;
                    alert(`提交失败:您没有权限提交此 URL。\n请确保您的服务账号已在 Google Search Console 中验证了该网站的所有权。\n当前页面:${currentUrl}`);
                } else {
                    alert(`提交失败:${data.error.message}\n当前页面:${currentUrl}`);
                }
                return;
            }

            if (data.urlNotificationMetadata) {
                const metadata = data.urlNotificationMetadata;
                const successMessage = [
                    'URL 已成功提交到 Google 索引!',
                    `当前页面:${currentUrl}`,
                    '',
                    '提交详情:',
                    `- 提交时间:${new Date().toLocaleString()}`,
                    `- 提交类型:URL_UPDATED`,
                    `- 响应状态:成功`,
                    '',
                    '后续步骤:',
                    '1. 您可以在 Google Search Console 中查看索引状态',
                    '2. Google 可能需要一些时间来处理您的请求',
                    '3. 建议使用 Google Search Console 的"检查网址"功能验证索引状态'
                ].join('\n');

                alert(successMessage);

                // 在控制台显示更多技术细节
                if (localStorage.getItem('DEBUG_MODE') === 'true') {
                    console.log('提交详情:', {
                        url: currentUrl,
                        timestamp: new Date().toISOString(),
                        type: 'URL_UPDATED',
                        response: data
                    });
                }
            } else {
                alert([
                    '提交状态未知',
                    `当前页面:${currentUrl}`,
                    '',
                    '建议操作:',
                    '1. 开启调试模式查看详细信息',
                    '2. 检查 Google Search Console 验证索引状态',
                    '3. 如果问题持续,请稍后重试'
                ].join('\n'));
            }
        } catch (error) {
            console.error('提交失败:', error);
            alert('提交失败:' + error.message);
        } finally {
            isSubmitting = false;
        }
    }

    // 注册菜单命令
    GM_registerMenuCommand('配置 Google Indexing API', showConfigPanel);
    GM_registerMenuCommand('提交到 Google 索引', submitToGoogleIndex);
})();