Greasy Fork

来自缓存

Greasy Fork is available in English.

Google SEO API索引提交插件

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Google SEO API索引提交插件
// @namespace    http://tampermonkey.net/
// @version      0.9.1
// @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==

/* jshint esversion: 8 */
/* eslint-env es8, browser */
/* global GM_getValue, GM_setValue, GM_xmlhttpRequest, GM_registerMenuCommand, KJUR */

(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;
        }
    }
    
    // 创建批量提交面板
    function createBatchPanel() {
        const batchPanel = document.createElement('div');
        batchPanel.id = 'googleIndexingBatch';
        batchPanel.style.cssText = `
            display: none;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 800px;
            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;
        `;
        
        batchPanel.innerHTML = `
            <h3 style="margin-top: 0;">批量提交 URL</h3>
            <div style="margin-bottom: 15px;">
                <p style="margin-bottom: 10px; color: #666;">请输入要提交的 URL(每行一个):</p>
                <textarea id="batchUrlInput" 
                    style="width: 100%; height: 150px; padding: 8px; margin-bottom: 10px; box-sizing: border-box; font-family: monospace;" 
                    placeholder="https://example.com/page1&#10;https://example.com/page2"></textarea>
                <div style="margin-bottom: 10px;">
                    <label>
                        <input type="checkbox" id="randomDelay" checked>
                        随机延迟(1-8秒)
                    </label>
                </div>
                <button id="startBatch" style="
                    padding: 8px 15px;
                    background: #4285f4;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    margin-right: 10px;
                ">开始提交</button>
                <button id="copyFailed" style="
                    padding: 8px 15px;
                    background: #dc3545;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    margin-right: 10px;
                    display: none;
                ">复制失败的 URL</button>
                <button id="closeBatch" style="
                    padding: 8px 15px;
                    background: #666;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                ">关闭</button>
            </div>
            <div id="batchProgress" style="
                max-height: 300px;
                overflow-y: auto;
                border: 1px solid #eee;
                padding: 10px;
                display: none;
            ">
                <table style="width: 100%; border-collapse: collapse;">
                    <thead>
                        <tr>
                            <th style="text-align: left; padding: 8px; border-bottom: 2px solid #ddd;">URL</th>
                            <th style="text-align: center; padding: 8px; border-bottom: 2px solid #ddd;">状态</th>
                            <th style="text-align: left; padding: 8px; border-bottom: 2px solid #ddd;">结果</th>
                        </tr>
                    </thead>
                    <tbody id="batchProgressList"></tbody>
                </table>
            </div>
        `;
        
        document.body.appendChild(batchPanel);
        
        // 批量提交处理
        let isProcessing = false;
        let urlList = [];
        
        // 更新进度列表
        function updateProgress(url, status, result = '') {
            const progressList = document.getElementById('batchProgressList');
            const existingRow = progressList.querySelector(`[data-url="${url}"]`);
            
            const statusColors = {
                '待提交': '#666',
                '提交中': '#007bff',
                '已提交': '#28a745',
                '提交失败': '#dc3545'
            };
            
            if (existingRow) {
                existingRow.children[1].innerHTML = `<span style="color: ${statusColors[status]}">${status}</span>`;
                existingRow.children[2].textContent = result;
            } else {
                const row = document.createElement('tr');
                row.setAttribute('data-url', url);
                row.innerHTML = `
                    <td style="padding: 8px; border-bottom: 1px solid #ddd;">${url}</td>
                    <td style="text-align: center; padding: 8px; border-bottom: 1px solid #ddd;">
                        <span style="color: ${statusColors[status]}">${status}</span>
                    </td>
                    <td style="padding: 8px; border-bottom: 1px solid #ddd;">${result}</td>
                `;
                progressList.appendChild(row);
            }
        }
        
        // 提交单个 URL
        async function submitUrl(url) {
            try {
                if (!validateServiceAccount()) {
                    updateProgress(url, '提交失败', '请先配置服务账号');
                    return false;
                }

                updateProgress(url, '提交中');
                const accessToken = await getAccessToken();
                
                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: url,
                            type: 'URL_UPDATED'
                        }),
                        onload: resolve,
                        onerror: reject
                    });
                });

                const data = JSON.parse(response.responseText);
                
                if (data.error) {
                    // 如果是权限错误,清除令牌缓存
                    if (data.error.status === 'PERMISSION_DENIED') {
                        accessTokenCache = null;
                        accessTokenExpiry = 0;
                        updateProgress(url, '提交失败', '没有权限提交此 URL,请确保服务账号已验证网站所有权');
                    } else {
                        updateProgress(url, '提交失败', data.error.message);
                    }
                    return false;
                }
                
                if (data.urlNotificationMetadata) {
                    updateProgress(url, '已提交', '成功');
                    return true;
                }
                
                updateProgress(url, '提交失败', '未知错误');
                return false;
            } catch (error) {
                console.error('提交失败:', error);
                updateProgress(url, '提交失败', error.message);
                return false;
            }
        }
        
        // 修改 setTimeout 的使用方式
        function delay(ms) {
            return new Promise(resolve => {
                setTimeout(resolve, ms);
            });
        }
        
        // 开始批量提交
        async function startBatchSubmit() {
            if (!validateServiceAccount()) {
                alert('请先配置服务账号!');
                const configPanel = document.getElementById('googleApiKeyConfig') || createConfigPanel();
                configPanel.style.display = 'block';
                return;
            }

            if (isProcessing) return;
            
            const urls = document.getElementById('batchUrlInput').value
                .split('\n')
                .map(url => url.trim())
                .filter(url => {
                    try {
                        const urlObj = new URL(url);
                        return urlObj.protocol.startsWith('http');
                    } catch (e) {
                        return false;
                    }
                });
                
            if (urls.length === 0) {
                alert('请输入有效的 URL(必须以 http:// 或 https:// 开头)');
                return;
            }
            
            isProcessing = true;
            document.getElementById('startBatch').disabled = true;
            document.getElementById('batchProgress').style.display = 'block';
            document.getElementById('copyFailed').style.display = 'none';
            
            // 初始化进度列表
            document.getElementById('batchProgressList').innerHTML = '';
            urls.forEach(url => updateProgress(url, '待提交'));
            
            const useRandomDelay = document.getElementById('randomDelay').checked;
            let successCount = 0;
            let failureCount = 0;
            
            for (const url of urls) {
                if (useRandomDelay) {
                    // 使用 delay 函数替代直接使用 setTimeout
                    await delay(Math.random() * 7000 + 1000);
                }
                const success = await submitUrl(url);
                if (success) {
                    successCount++;
                } else {
                    failureCount++;
                }
            }
            
            isProcessing = false;
            document.getElementById('startBatch').disabled = false;
            document.getElementById('copyFailed').style.display = 'block';
            
            // 显示提交统计
            alert(`批量提交完成!\n成功:${successCount} 个\n失败:${failureCount} 个\n\n如有失败的 URL,可以点击"复制失败的 URL"按钮重新提交。`);
        }
        
        // 复制失败的 URL
        function copyFailedUrls() {
            const failedUrls = Array.from(document.getElementById('batchProgressList').querySelectorAll('tr'))
                .filter(row => row.querySelector('td:nth-child(2)').textContent.includes('失败'))
                .map(row => row.querySelector('td:first-child').textContent)
                .join('\n');
                
            if (failedUrls) {
                navigator.clipboard.writeText(failedUrls);
                alert('已复制失败的 URL 到剪贴板');
            } else {
                alert('没有失败的 URL');
            }
        }
        
        // 添加事件监听
        document.getElementById('startBatch').addEventListener('click', startBatchSubmit);
        document.getElementById('copyFailed').addEventListener('click', copyFailedUrls);
        document.getElementById('closeBatch').addEventListener('click', () => {
            if (!isProcessing || confirm('正在提交中,确定要关闭吗?')) {
                batchPanel.style.display = 'none';
            }
        });
        
        return batchPanel;
    }
    
    // 显示批量提交面板
    function showBatchPanel() {
        const batchPanel = document.getElementById('googleIndexingBatch') || createBatchPanel();
        batchPanel.style.display = 'block';
    }
    
    // 注册菜单命令
    GM_registerMenuCommand('配置 Google Indexing API', showConfigPanel);
    GM_registerMenuCommand('提交到 Google 索引', submitToGoogleIndex);
    GM_registerMenuCommand('批量提交 URL', showBatchPanel);
})();