GreasyFork AI Safety Checker

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

当前为 2025-03-14 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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