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