GreasyFork AI Safety Checker

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

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

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