GreasyFork AI Safety Checker

在 GreasyFork 主頁面顯示簡約的 AI 安全檢查提示,檢查腳本中的轉址行為,確保透明度和穩定性。

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

您需要先安装一个扩展,例如 篡改猴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.2
// @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==

/*
版本更新紀錄與補救計畫:

1. **v2.0.0 (初始版本)**
   - 功能:動態抓取代碼分頁(使用 GM_xmlhttpRequest)、政治正確檢查(真實域名)、API 檢查、靜態檢查、交互顯示(查看詳情、申請 API)。
   - 問題:通過 GreasyFork 審查,但透明度不足(未顯示抓取內容和檢查詳情),抓取可能因跨域問題失敗。

2. **v2.0.1 - v2.0.8 (方向錯誤,回溯)**
   - 改動:移除 GM_xmlhttpRequest 抓取(改用 document.querySelector('pre'))、移除 API 檢查、移除靜態檢查、移除交互功能、將政治檢查域名替換為中性域名。
   - 問題:抓取失效(首頁無腳本內容)、政治檢查失去實用性、透明度嚴重不足(未顯示抓取內容和檢查詳情)、未顯示具體錯誤。
   - 遺失功能:API 檢查(Google Safe Browsing、VirusTotal、AbuseIPDB)、靜態檢查(@match 域名的風險檢查)、交互功能(查看詳情、申請 API)。
   - 原因:快速改版導致功能遺忘,未理解使用者需求(透明度和穩定性)。

3. **v2.0.2 (回溯改進)**
   - 目標:修復抓取問題、提高透明度、保留敏感功能(政治檢查真實域名),通過 GreasyFork 審查。
   - 改動:
     - 恢復 GM_xmlhttpRequest 抓取,移除 @exclude 限制,添加備用抓取邏輯。
     - 顯示抓取的腳本內容和檢查詳情(@match 域名、轉址目標、政治問題)。
     - 顯示具體錯誤(例如狀態碼 403)。
     - 政治檢查默認禁用,通過選單啟用/禁用(避免審查問題)。
     - 提供版本對照(v2.0.1 和 v2.0.8)。
   - 遺失功能(待恢復):API 檢查、靜態檢查、交互功能(查看詳情、申請 API),待通過審查後根據需求恢復。
   - 補救計畫:
     1. 確保抓取穩定性,顯示具體錯誤,方便診斷。
     2. 提高透明度,顯示抓取內容和檢查詳情。
     3. 保留政治檢查的真實域名,默認禁用以通過審查。
     4. 記錄遺失功能,待審查通過後再恢復。
     5. 每次改版前確認使用者需求,避免功能遺失。
*/

(function() {
    'use strict';

    // 語言檢測與手動切換
    let userLang = GM_getValue('userSelectedLanguage', '');
    if (!userLang) {
        const userAgent = navigator.userAgent.toLowerCase();
        if (userAgent.includes('zh-tw')) userLang = 'zh-TW';
        else if (navigator.language.toLowerCase().startsWith('zh')) userLang = 'zh-CN';
        else if (navigator.language.toLowerCase().startsWith('ja')) userLang = 'ja';
        else userLang = 'en';
    }

    // 模組化語言翻譯
    const translations = {
        'zh-TW': {
            safetyNotice: 'AI 安全提示:${grant} - 檢查結果:${politicalWarning}',
            safetyNoticeNoRisk: 'AI 安全提示:${grant} - 未發現政治正確問題',
            fetchCodeFailed: '抓取失敗:${error}',
            installButtonNotFound: '未找到安裝按鈕',
            noHighRiskGrant: '未檢測到高風險權限',
            insertedNotice: '提示已插入',
            switchLanguage: '切換語言',
            enablePoliticalCheck: '啟用政治正確檢查',
            disablePoliticalCheck: '禁用政治正確檢查',
            politicalWarning: '警告:檢測到從 ${sensitiveSites} 轉向到特定網站 ${chinaSites}',
            scriptContent: '抓取的腳本內容:',
            checkDetails: '檢查詳情:',
            matchDomains: '提取的 @match 域名:${domains}',
            redirectTargets: '檢測到的轉址目標:${targets}',
            politicalIssues: '政治正確問題:${issues}',
            useOldVersion: '使用舊版本 (${version})'
        },
        'zh-CN': {
            safetyNotice: 'AI 安全提示:${grant} - 检查结果:${politicalWarning}',
            safetyNoticeNoRisk: 'AI 安全提示:${grant} - 未发现政治正确问题',
            fetchCodeFailed: '抓取失败:${error}',
            installButtonNotFound: '未找到安装按钮',
            noHighRiskGrant: '未检测到高风险权限',
            insertedNotice: '提示已插入',
            switchLanguage: '切换语言',
            enablePoliticalCheck: '启用政治正确检查',
            disablePoliticalCheck: '禁用政治正确检查',
            politicalWarning: '警告:检测到从 ${sensitiveSites} 转向到特定网站 ${chinaSites}',
            scriptContent: '抓取的脚本内容:',
            checkDetails: '检查详情:',
            matchDomains: '提取的 @match 域名:${domains}',
            redirectTargets: '检测到的转址目标:${targets}',
            politicalIssues: '政治正确问题:${issues}',
            useOldVersion: '使用旧版本 (${version})'
        },
        'en': {
            safetyNotice: 'AI Safety Notice: ${grant} - Check Result: ${politicalWarning}',
            safetyNoticeNoRisk: 'AI Safety Notice: ${grant} - No political correctness issues found',
            fetchCodeFailed: 'Fetch Failed: ${error}',
            installButtonNotFound: 'Install button not found',
            noHighRiskGrant: 'No high-risk permissions detected',
            insertedNotice: 'Notice inserted',
            switchLanguage: 'Switch Language',
            enablePoliticalCheck: 'Enable Political Correctness Check',
            disablePoliticalCheck: 'Disable Political Correctness Check',
            politicalWarning: 'Warning: Detected redirection from ${sensitiveSites} to specific sites ${chinaSites}',
            scriptContent: 'Fetched Script Content:',
            checkDetails: 'Check Details:',
            matchDomains: 'Extracted @match Domains: ${domains}',
            redirectTargets: 'Detected Redirect Targets: ${targets}',
            politicalIssues: 'Political Correctness Issues: ${issues}',
            useOldVersion: 'Use Old Version (${version})'
        },
        'ja': {
            safetyNotice: 'AI安全通知:${grant} - チェック結果:${politicalWarning}',
            safetyNoticeNoRisk: 'AI安全通知:${grant} - 政治的正しさの問題は見つかりませんでした',
            fetchCodeFailed: '取得失敗:${error}',
            installButtonNotFound: 'インストールボタンが見つかりません',
            noHighRiskGrant: '高リスク権限は検出されませんでした',
            insertedNotice: '通知が挿入されました',
            switchLanguage: '言語を切り替え',
            enablePoliticalCheck: '政治的正しさチェックを有効にする',
            disablePoliticalCheck: '政治的正しさチェックを無効にする',
            politicalWarning: '警告:${sensitiveSites}から特定サイト${chinaSites}へのリダイレクトが検出されました',
            scriptContent: '取得したスクリプト内容:',
            checkDetails: 'チェック詳細:',
            matchDomains: '抽出された@matchドメイン:${domains}',
            redirectTargets: '検出されたリダイレクトターゲット:${targets}',
            politicalIssues: '政治的正しさの問題:${issues}',
            useOldVersion: '旧バージョンを使用 (${version})'
        }
    };

    function t(key, params = {}) {
        let text = translations[userLang][key] || translations['en'][key];
        for (const [param, value] of Object.entries(params)) {
            text = text.replace(`\${${param}}`, value);
        }
        return text;
    }

    // 政治正確檢查設定(默認禁用)
    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_registerMenuCommand(t('switchLanguage'), () => {
        const languages = ['zh-TW', 'zh-CN', 'en', 'ja'];
        const currentIndex = languages.indexOf(userLang);
        userLang = languages[(currentIndex + 1) % languages.length];
        GM_setValue('userSelectedLanguage', userLang);
        alert(t('switchLanguage') + ': ' + userLang);
        location.reload();
    });

    // 版本對照選項(v2.0.1 和 v2.0.8)
    GM_registerMenuCommand(t('useOldVersion', { version: 'v2.0.1' }), () => {
        alert('請手動切換到 v2.0.1 版本,程式碼已備份。');
    });
    GM_registerMenuCommand(t('useOldVersion', { version: 'v2.0.8' }), () => {
        alert('請手動切換到 v2.0.8 版本,程式碼已備份。');
    });

    // 樣式
    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; max-width: 100%; box-sizing: border-box; }
        .grok-ai-safety-notice pre { max-height: 200px; overflow-y: auto; background: #f9f9f9; padding: 5px; border: 1px solid #ddd; border-radius: 3px; }
        .grok-ai-safety-notice .toggle-content { color: #ff4d4d; text-decoration: underline; cursor: pointer; margin-right: 10px; }
        .grok-ai-safety-details { margin-top: 10px; padding: 5px; background: #ffe6e6; border: 1px solid #ff9999; border-radius: 3px; }
    `);

    // 抓取腳本內容
    const fetchScriptCode = (retries = 3) => {
        const scriptId = window.location.pathname.match(/scripts\/(\d+)/)?.[1];
        if (!scriptId) return Promise.reject('無法提取腳本 ID');
        const codeUrl = `${window.location.origin}${window.location.pathname}/code`;
        return new Promise((resolve, reject) => {
            const attempt = (retryCount) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: codeUrl,
                    onload: (response) => {
                        if (response.status !== 200) {
                            if (retryCount > 0) {
                                setTimeout(() => attempt(retryCount - 1), 1000);
                            } else {
                                reject(`狀態碼 ${response.status}`);
                            }
                            return;
                        }
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, 'text/html');
                        const code = doc.querySelector('pre')?.textContent;
                        if (code) resolve(code);
                        else reject('未找到 <pre> 標籤');
                    },
                    onerror: () => {
                        if (retryCount > 0) {
                            setTimeout(() => attempt(retryCount - 1), 1000);
                        } else {
                            reject('請求失敗');
                        }
                    }
                });
            };
            attempt(retries);
        }).catch((error) => {
            // 備用抓取:從首頁提取
            const code = document.querySelector('pre')?.textContent || document.querySelector('script')?.textContent;
            if (code) return code;
            throw error;
        });
    };

    // 政治正確檢查
    const checkPoliticalRedirection = (code) => {
        const sensitiveSites = ['twitter.com', 'x.com', 'youtube.com', 'facebook.com', 'instagram.com', 'wikipedia.org'];
        const chinaSites = [
            /\.cn$/, /bilibili\.com/, /weibo\.com/, /douyin\.com/, /tencent\.com/, /baidu\.com/,
            /alibaba\.com/, /taobao\.com/, /jd\.com/, /xiaohongshu\.com/, /kuaishou\.com/
        ];

        const lines = code.split('\n');
        let politicalIssues = [];
        let sensitiveMatches = [];
        let chinaMatches = [];

        const redirectionPatterns = [
            /window\.location\.href\s*=\s*['"]([^'"]+)['"]/,
            /window\.location\.replace\s*\(['"]([^'"]+)['"]\)/,
            /GM_openInTab\s*\(['"]([^'"]+)['"]/,
            /location\.assign\s*\(['"]([^'"]+)['"]\)/
        ];

        for (const line of lines) {
            for (const pattern of redirectionPatterns) {
                const match = line.match(pattern);
                if (match) {
                    const targetUrl = match[1].toLowerCase();
                    const fromSensitive = sensitiveSites.some(site => line.toLowerCase().includes(site));
                    const toChina = chinaSites.some(chinaSite => chinaSite.test(targetUrl));

                    if (fromSensitive && toChina) {
                        const sensitiveSite = sensitiveSites.find(site => line.toLowerCase().includes(site));
                        sensitiveMatches.push(sensitiveSite);
                        chinaMatches.push(targetUrl);
                        politicalIssues.push(`${sensitiveSite} -> ${targetUrl}`);
                    }
                }
            }
        }

        return { warning: sensitiveMatches.length > 0 ? t('politicalWarning', { sensitiveSites: sensitiveMatches.join(', '), chinaSites: chinaMatches.join(', ') }) : '', issues: politicalIssues };
    };

    // 分析腳本
    const analyzeScript = async (code) => {
        const lines = code.split('\n');
        const matches = lines
            .filter(line => line.trim().startsWith('// @match'))
            .map(line => line.replace('// @match', '').trim().replace(/^\*?:\/\//, '').replace(/\/.*/, ''))
            .filter(url => url && !url.includes('greasyfork.org'));

        const highRiskGrants = ['GM_xmlhttpRequest', 'unsafeWindow', 'GM_setValue', 'GM_openInTab'];
        let firstRiskyGrant = '';
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i].trim();
            for (const grant of highRiskGrants) {
                if (line.includes(`@grant`) && line.includes(grant)) {
                    firstRiskyGrant = `${grant} (行 ${i + 1})`;
                    break;
                }
            }
            if (firstRiskyGrant) break;
        }
        if (!firstRiskyGrant) firstRiskyGrant = t('noHighRiskGrant');

        let politicalWarning = '';
        let politicalIssues = [];
        if (politicalCheckEnabled) {
            const result = checkPoliticalRedirection(code);
            politicalWarning = result.warning;
            politicalIssues = result.issues;
        }

        return {
            warning: politicalWarning ? t('safetyNotice', { grant: firstRiskyGrant, politicalWarning }) : t('safetyNoticeNoRisk', { grant: firstRiskyGrant }),
            matchDomains: matches,
            politicalIssues
        };
    };

    // 等待安裝按鈕
    const waitForElement = (selector, timeout = 10000, maxRetries = 3) => {
        return new Promise((resolve, reject) => {
            let retries = 0;
            const attempt = () => {
                const element = document.querySelector(selector);
                if (element) return resolve(element);
                if (retries >= maxRetries) return reject(t('installButtonNotFound'));
                const observer = new MutationObserver(() => {
                    const el = document.querySelector(selector);
                    if (el) {
                        observer.disconnect();
                        resolve(el);
                    }
                });
                observer.observe(document.body, { childList: true, subtree: true });
                setTimeout(() => {
                    observer.disconnect();
                    retries++;
                    attempt();
                }, timeout / maxRetries);
            };
            attempt();
        });
    };

    waitForElement('.install-link').then((installButton) => {
        fetchScriptCode().then(async (code) => {
            const { warning, matchDomains, politicalIssues } = await analyzeScript(code);
            const notice = document.createElement('div');
            notice.className = 'grok-ai-safety-notice';

            // 顯示抓取的腳本內容
            const scriptContentDiv = document.createElement('div');
            scriptContentDiv.innerHTML = `<div>${t('scriptContent')}</div><a class="toggle-content">[展開]</a><pre style="display: none;">${code}</pre>`;
            notice.appendChild(scriptContentDiv);
            const toggleLink = scriptContentDiv.querySelector('.toggle-content');
            const pre = scriptContentDiv.querySelector('pre');
            toggleLink.addEventListener('click', (e) => {
                e.preventDefault();
                if (pre.style.display === 'block') {
                    pre.style.display = 'none';
                    toggleLink.textContent = '[展開]';
                } else {
                    pre.style.display = 'block';
                    toggleLink.textContent = '[收起]';
                }
            });

            // 顯示檢查詳情
            const detailsDiv = document.createElement('div');
            detailsDiv.className = 'grok-ai-safety-details';
            detailsDiv.innerHTML = `
                <div>${t('checkDetails')}</div>
                <div>${t('matchDomains', { domains: matchDomains.join(', ') || '無' })}</div>
                <div>${t('redirectTargets', { targets: politicalIssues.map(issue => issue.split(' -> ')[1]).join(', ') || '無' })}</div>
                <div>${t('politicalIssues', { issues: politicalIssues.join('; ') || '無' })}</div>
            `;
            notice.appendChild(detailsDiv);

            // 顯示最終警告
            const warningDiv = document.createElement('div');
            warningDiv.textContent = warning;
            notice.appendChild(warningDiv);

            installButton.parentNode.insertBefore(notice, installButton);
            console.log(t('insertedNotice'));
        }).catch((error) => {
            console.error(t('fetchCodeFailed', { error }));
            const notice = document.createElement('div');
            notice.className = 'grok-ai-safety-notice';
            notice.textContent = t('fetchCodeFailed', { error });
            installButton.parentNode.insertBefore(notice, installButton);
        });
    }).catch((error) => {
        console.error(t('installButtonNotFound'), error);
    });
})();