// ==UserScript==
// @name GreasyFork AI Safety Checker
// @namespace http://tampermonkey.net/
// @version 2.0.5
// @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} - 靜態檢查: 檢查 ${total} 個域名,檢測到 ${risk} 個高風險域名${political}, 建議在沙盒環境中測試此腳本。',
safetyNoticeNoRisk: 'AI 安全提示:${grant} - 靜態檢查: 檢查 ${total} 個域名,未發現高風險域名${political}。',
fetchFailed: '抓取失敗:${error}',
switchLanguage: '切換語言',
enablePoliticalCheck: '啟用政治正確檢查',
disablePoliticalCheck: '禁用政治正確檢查',
viewDetails: '查看詳情',
hideDetails: '收起詳情',
politicalWarning: '警告:從 ${sensitive} 轉向 ${china}',
matchDomains: '提取的 @match 域名:${domains}',
details: '檢查詳情'
},
'zh-CN': {
safetyNotice: 'AI 安全提示:${grant} - 静态检查: 检查 ${total} 个域名,检测到 ${risk} 个高风险域名${political}, 建议在沙盒环境中测试此脚本。',
safetyNoticeNoRisk: 'AI 安全提示:${grant} - 静态检查: 检查 ${total} 个域名,未发现高风险域名${political}。',
fetchFailed: '抓取失败:${error}',
switchLanguage: '切换语言',
enablePoliticalCheck: '启用政治正确检查',
disablePoliticalCheck: '禁用政治正确检查',
viewDetails: '查看详情',
hideDetails: '收起详情',
politicalWarning: '警告:从 ${sensitive} 转向 ${china}',
matchDomains: '提取的 @match 域名:${domains}',
details: '检查详情'
},
'en': {
safetyNotice: 'AI Safety Notice: ${grant} - Static check: Checked ${total} domains, detected ${risk} high-risk domains${political}, test in a sandbox environment.',
safetyNoticeNoRisk: 'AI Safety Notice: ${grant} - Static check: Checked ${total} domains, no high-risk domains found${political}.',
fetchFailed: 'Fetch failed: ${error}',
switchLanguage: 'Switch Language',
enablePoliticalCheck: 'Enable Political Check',
disablePoliticalCheck: 'Disable Political Check',
viewDetails: 'View Details',
hideDetails: 'Hide Details',
politicalWarning: 'Warning: Redirect from ${sensitive} to ${china}',
matchDomains: 'Extracted @match domains: ${domains}',
details: 'Check Details'
},
'ja': {
safetyNotice: 'AI安全通知:${grant} - 静的チェック:${total}ドメインをチェックし、${risk}個の高リスクドメインを検出しました${political}。サンドボックス環境でテストすることをお勧めします。',
safetyNoticeNoRisk: 'AI安全通知:${grant} - 静的チェック:${total}ドメインをチェックしましたが、高リスクドメインは見つかりませんでした${political}。',
fetchFailed: '取得失敗:${error}',
switchLanguage: '言語を切り替え',
enablePoliticalCheck: '政治的チェックを有効にする',
disablePoliticalCheck: '政治的チェックを無効にする',
viewDetails: '詳細を表示',
hideDetails: '詳細を非表示',
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-notice a { color: #ff4d4d; text-decoration: underline; cursor: pointer; margin-left: 10px; }
.grok-ai-safety-details { display: none; 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.length ? `; ${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', 'GM_setValue', 'GM_openInTab'];
let grant = 'No high-risk grant';
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
for (const g of highRiskGrants) {
if (line.includes(`@grant ${g}`)) {
grant = `${g} (行 ${i + 1})`;
break;
}
}
if (grant !== 'No high-risk grant') break;
}
// 靜態檢查高風險域名
const riskyPatterns = [/bit\.ly/, /tinyurl\.com/, /\.cn$/, /\.ru$/];
const highRiskDomains = matches.filter(domain => riskyPatterns.some(pattern => pattern.test(domain)));
const totalDomains = matches.length;
const riskCount = highRiskDomains.length;
// 政治檢查
const politicalResult = await checkPoliticalRedirection(code);
// 生成警告訊息
const warning = riskCount > 0
? t('safetyNotice', { grant, total: totalDomains, risk: riskCount, political: politicalResult })
: t('safetyNoticeNoRisk', { grant, total: totalDomains, political: politicalResult });
// 詳細資訊
const details = `<div class="grok-ai-safety-details">${t('details')}<br>${t('matchDomains', { domains: matches.join(', ') || 'None' })}<br>${politicalResult || 'No political issues'}</div>`;
return { warning, details, hasRisk: riskCount > 0 };
};
// 主邏輯
const installButton = document.querySelector('.install-link');
if (!installButton) return console.error('Install button not found');
fetchScriptCode()
.then(analyzeScript)
.then(({ warning, details, hasRisk }) => {
const notice = document.createElement('div');
notice.className = 'grok-ai-safety-notice';
notice.innerHTML = `${warning}${hasRisk ? `<a href="#" id="toggle-details">${t('viewDetails')}</a>` : ''}${details}`;
// 展開/收起詳情
if (hasRisk) {
const toggleLink = notice.querySelector('#toggle-details');
const detailsDiv = notice.querySelector('.grok-ai-safety-details');
toggleLink.addEventListener('click', (e) => {
e.preventDefault();
if (detailsDiv.style.display === 'block') {
detailsDiv.style.display = 'none';
toggleLink.textContent = t('viewDetails');
} else {
detailsDiv.style.display = 'block';
toggleLink.textContent = t('hideDetails');
}
});
}
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);
});
})();