GreasyFork AI Safety Checker

在 GreasyFork 主頁面顯示 AI 安全檢查提示,支援多語言與網域檢查,可選啟用政治檢查。

目前為 2025-03-14 提交的版本,檢視 最新版本

// ==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);
        });
})();