// ==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
// @downloadURL none
// ==/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 = `
配置 Google Indexing API
请输入您的服务账号凭据 JSON:
示例格式:
{
"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": "your-service-account@project.iam.gserviceaccount.com",
...
}
`;
// 添加调试模式开关
const debugModeDiv = document.createElement('div');
debugModeDiv.style.marginTop = '10px';
debugModeDiv.innerHTML = `
`;
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);
})();