GreasyFork AI Safety Checker

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

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
        });
})();