// ==UserScript==
// @name GreasyFork AI Safety Checker
// @namespace http://tampermonkey.net/
// @version 2.0.4
// @description 在 GreasyFork 主頁面顯示 AI 安全檢查提示,支援多語言與網域檢查,可選啟用政治檢查。
// @match https://greasyfork.org/*scripts/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_info
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Tampermonkey 版本檢查
const MINIMUM_TAMPERMONKEY_VERSION = '5.0.0';
if (GM_info.version < MINIMUM_TAMPERMONKEY_VERSION) {
console.warn(`Warning: Tampermonkey ${GM_info.version} is outdated, recommended: ${MINIMUM_TAMPERMONKEY_VERSION}`);
}
// 語言設定與切換
let userLang = GM_getValue('userSelectedLanguage', navigator.language.startsWith('zh') ? 'zh-CN' : 'en');
const translations = {
'zh-TW': {
safetyNotice: 'AI 安全提示:${grant} - 檢查結果:${details}',
noRisk: '未發現風險',
fetchFailed: '抓取失敗:${error}',
switchLanguage: '切換語言',
enablePoliticalCheck: '啟用政治正確檢查',
disablePoliticalCheck: '禁用政治正確檢查',
politicalWarning: '警告:從 ${sensitive} 轉向 ${china}',
matchDomains: '提取的 @match 域名:${domains}',
details: '檢查詳情'
},
'zh-CN': {
safetyNotice: 'AI 安全提示:${grant} - 检查结果:${details}',
noRisk: '未发现风险',
fetchFailed: '抓取失败:${error}',
switchLanguage: '切换语言',
enablePoliticalCheck: '启用政治正确检查',
disablePoliticalCheck: '禁用政治正确检查',
politicalWarning: '警告:从 ${sensitive} 转向 ${china}',
matchDomains: '提取的 @match 域名:${domains}',
details: '检查详情'
},
'en': {
safetyNotice: 'AI Safety Notice: ${grant} - Check Result: ${details}',
noRisk: 'No risks detected',
fetchFailed: 'Fetch failed: ${error}',
switchLanguage: 'Switch Language',
enablePoliticalCheck: 'Enable Political Check',
disablePoliticalCheck: 'Disable Political Check',
politicalWarning: 'Warning: Redirect from ${sensitive} to ${china}',
matchDomains: 'Extracted @match domains: ${domains}',
details: 'Check Details'
},
'ja': {
safetyNotice: 'AI安全通知:${grant} - チェック結果:${details}',
noRisk: 'リスクは検出されませんでした',
fetchFailed: '取得失敗:${error}',
switchLanguage: '言語を切り替え',
enablePoliticalCheck: '政治的チェックを有効にする',
disablePoliticalCheck: '政治的チェックを無効にする',
politicalWarning: '警告:${sensitive}から${china}へのリダイレクト',
matchDomains: '抽出された @match ドメイン:${domains}',
details: 'チェック詳細'
}
};
function t(key, params = {}) {
let text = translations[userLang][key] || translations['en'][key];
for (const [p, v] of Object.entries(params)) text = text.replace(`\${${p}}`, v);
return text;
}
// 語言切換選單
GM_registerMenuCommand(t('switchLanguage'), () => {
const languages = ['zh-TW', 'zh-CN', 'en', 'ja'];
userLang = languages[(languages.indexOf(userLang) + 1) % languages.length];
GM_setValue('userSelectedLanguage', userLang);
alert(t('switchLanguage') + ': ' + userLang);
location.reload();
});
// 政治檢查選單
let politicalCheckEnabled = GM_getValue('politicalCheckEnabled', false);
GM_registerMenuCommand(politicalCheckEnabled ? t('disablePoliticalCheck') : t('enablePoliticalCheck'), () => {
politicalCheckEnabled = !politicalCheckEnabled;
GM_setValue('politicalCheckEnabled', politicalCheckEnabled);
alert(politicalCheckEnabled ? t('enablePoliticalCheck') : t('disablePoliticalCheck'));
location.reload();
});
// 樣式
GM_addStyle(`
.grok-ai-safety-notice { background: #fff3f3; border: 2px solid #ff4d4d; padding: 10px; margin-bottom: 15px; border-radius: 5px; color: #333; font-size: 14px; line-height: 1.5; }
.grok-ai-safety-details { margin-top: 10px; padding: 5px; background: #ffe6e6; border: 1px solid #ff9999; border-radius: 3px; }
`);
// 抓取代碼
const fetchScriptCode = () => {
const codeUrl = `${window.location.origin}${window.location.pathname}/code`;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: codeUrl,
onload: (res) => {
const code = new DOMParser().parseFromString(res.responseText, 'text/html').querySelector('pre')?.textContent;
code ? resolve(code) : reject('No code found');
},
onerror: () => reject('Request failed')
});
});
};
// 政治正確檢查
const checkPoliticalRedirection = (code) => {
if (!politicalCheckEnabled) return '';
const sensitiveSites = ['twitter.com', 'youtube.com', 'facebook.com'];
const chinaSites = ['bilibili.com', 'weibo.com', 'baidu.com'];
const redirectPatterns = [/window\.location\.href\s*=\s*['"]([^'"]+)['"]/];
const lines = code.split('\n');
let issues = [];
for (const line of lines) {
for (const pattern of redirectPatterns) {
const match = line.match(pattern);
if (match) {
const target = match[1].toLowerCase();
if (sensitiveSites.some(s => line.includes(s)) && chinaSites.some(c => target.includes(c))) {
issues.push(t('politicalWarning', {
sensitive: sensitiveSites.find(s => line.includes(s)),
china: target
}));
}
}
}
}
return issues.join('; ') || '';
};
// 分析腳本
const analyzeScript = async (code) => {
const lines = code.split('\n');
const matches = lines
.filter(l => l.startsWith('// @match'))
.map(l => l.replace('// @match', '').trim().replace(/^\*?:\/\//, '').replace(/\/.*/, ''))
.filter(d => d && !d.includes('greasyfork.org'));
const highRiskGrants = ['GM_xmlhttpRequest', 'unsafeWindow'];
let grant = lines.find(l => highRiskGrants.some(g => l.includes(`@grant ${g}`)))?.match(/@grant\s+(\S+)/)?.[1] || 'No high-risk grant';
let details = [];
details.push(t('matchDomains', { domains: matches.join(', ') || 'None' }));
const politicalResult = await checkPoliticalRedirection(code);
if (politicalResult) details.push(politicalResult);
return {
warning: t('safetyNotice', { grant, details: details.length ? '' : t('noRisk') }),
details: details.length ? `<div class="grok-ai-safety-details">${t('details')}<br>${details.join('<br>')}</div>` : ''
};
};
// 主邏輯
const installButton = document.querySelector('.install-link');
if (!installButton) return console.error('Install button not found');
fetchScriptCode()
.then(analyzeScript)
.then(({ warning, details }) => {
const notice = document.createElement('div');
notice.className = 'grok-ai-safety-notice';
notice.innerHTML = `${warning}${details}`;
installButton.parentNode.insertBefore(notice, installButton);
})
.catch(error => {
const notice = document.createElement('div');
notice.className = 'grok-ai-safety-notice';
notice.textContent = t('fetchFailed', { error });
installButton.parentNode.insertBefore(notice, installButton);
});
})();