GreasyFork AI Safety Checker

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

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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