GreasyFork AI Safety Checker (Multi API)

在 GreasyFork 主頁面顯示簡約的 AI 安全檢查提示,整合多重 API(Google, VirusTotal, AbuseIPDB),支援多國語系

目前为 2025-03-14 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name GreasyFork AI Safety Checker (Multi API)
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.0.8
  5. // @description 在 GreasyFork 主頁面顯示簡約的 AI 安全檢查提示,整合多重 API(Google, VirusTotal, AbuseIPDB),支援多國語系
  6. // @match https://greasyfork.org/*scripts/*
  7. // @exclude https://greasyfork.org/*scripts/*/code*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_addStyle
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_info
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. let userLang = GM_getValue('userSelectedLanguage', '');
  21. if (!userLang) {
  22. userLang = navigator.language === 'zh-TW' ? 'zh-TW' :
  23. navigator.language.startsWith('zh') ? 'zh-CN' :
  24. navigator.language.startsWith('ja') ? 'ja' : 'en';
  25. }
  26.  
  27. const translations = {
  28. 'zh-TW': {
  29. safetyNotice: 'AI 安全提示:${grant} - 靜態檢查: 檢查 ${count} 個域名,檢測到 ${riskCount} 個高風險域名${apiSummary}${politicalWarning}, 建議在沙盒環境中測試此腳本。',
  30. safetyNoticeNoRisk: 'AI 安全提示:${grant} - 靜態檢查: 檢查 ${count} 個域名,未發現匹配${apiSummary}${politicalWarning}.',
  31. staticRiskNotice: 'AI 安全提示:靜態檢查發現風險域名 - 檢查 ${count} 個域名,檢測到 ${riskCount} 個高風險域名${apiSummary}${politicalWarning}, 建議在沙盒環境中測試此腳本。',
  32. noHighRiskGrant: '未檢測到高風險權限',
  33. fetchCodeFailed: 'AI 安全提示:無法獲取腳本代碼,請手動檢查。',
  34. gamblingRelated: '可能與博彩相關',
  35. ptcSite: 'PTC(點擊賺錢)網站',
  36. urlShortening: '縮短連結服務',
  37. fileSharing: '文件分享網站'
  38. },
  39. 'zh-CN': {
  40. safetyNotice: 'AI 安全提示:${grant} - 静态检查: 检查 ${count} 个域名,检测到 ${riskCount} 个高风险域名${apiSummary}${politicalWarning}, 建议在沙盒环境中测试此脚本。',
  41. safetyNoticeNoRisk: 'AI 安全提示:${grant} - 静态检查: 检查 ${count} 个域名,未发现匹配${apiSummary}${politicalWarning}.',
  42. staticRiskNotice: 'AI 安全提示:静态检查发现风险域名 - 检查 ${count} 个域名,检测到 ${riskCount} 个高风险域名${apiSummary}${politicalWarning}, 建议在沙盒环境中测试此脚本。',
  43. noHighRiskGrant: '未检测到高风险权限',
  44. fetchCodeFailed: 'AI 安全提示:无法获取脚本代码,请手动检查。',
  45. gamblingRelated: '可能与博彩相关',
  46. ptcSite: 'PTC(点击赚钱)网站',
  47. urlShortening: '缩短链接服务',
  48. fileSharing: '文件分享网站'
  49. },
  50. 'en': {
  51. safetyNotice: 'AI Safety Notice: ${grant} - Static check: checked ${count} domains, detected ${riskCount} high-risk domains${apiSummary}${politicalWarning}, test in a sandbox environment.',
  52. safetyNoticeNoRisk: 'AI Safety Notice: ${grant} - Static check: checked ${count} domains, no matches${apiSummary}${politicalWarning}.',
  53. staticRiskNotice: 'AI Safety Notice: Static check detected risks - checked ${count} domains, detected ${riskCount} high-risk domains${apiSummary}${politicalWarning}, test in a sandbox environment.',
  54. noHighRiskGrant: 'No high-risk permissions detected',
  55. fetchCodeFailed: 'AI Safety Notice: Unable to fetch script code, please check manually.',
  56. gamblingRelated: 'Possibly gambling-related',
  57. ptcSite: 'PTC (Pay-to-Click) site',
  58. urlShortening: 'URL shortening service',
  59. fileSharing: 'File sharing site'
  60. },
  61. 'ja': {
  62. safetyNotice: 'AI安全通知:${grant} - 静的チェック:${count}ドメインをチェックし、${riskCount}個の高リスクドメインを検出しました${apiSummary}${politicalWarning}。サンドボックス環境でテストすることをお勧めします。',
  63. safetyNoticeNoRisk: 'AI安全通知:${grant} - 静的チェック:${count}ドメインをチェックしましたが、一致するものはありません${apiSummary}${politicalWarning}。',
  64. staticRiskNotice: 'AI安全通知:静的チェックでリスクを検出 - ${count}ドメインをチェックし、${riskCount}個の高リスクドメインを検出しました${apiSummary}${politicalWarning}。サンドボックス環境でテストすることをお勧めします。',
  65. noHighRiskGrant: '高リスク権限は検出されませんでした',
  66. fetchCodeFailed: 'AI安全通知:スクリプトコードを取得できません。手動で確認してください。',
  67. gamblingRelated: 'ギャンブル関連の可能性',
  68. ptcSite: 'PTC(クリックで稼ぐ)サイト',
  69. urlShortening: 'URL短縮サービス',
  70. fileSharing: 'ファイル共有サイト'
  71. }
  72. };
  73.  
  74. function t(key, params = {}) {
  75. let text = translations[userLang][key] || translations['en'][key];
  76. for (const [param, value] of Object.entries(params)) {
  77. text = text.replace(`\${${param}}`, value);
  78. }
  79. return text;
  80. }
  81.  
  82. GM_registerMenuCommand('切換語言 / Switch Language', () => {
  83. const languages = ['zh-TW', 'zh-CN', 'en', 'ja'];
  84. const currentIndex = languages.indexOf(userLang);
  85. const nextIndex = (currentIndex + 1) % languages.length;
  86. userLang = languages[nextIndex];
  87. GM_setValue('userSelectedLanguage', userLang);
  88. alert('語言已切換 / Language switched to: ' + userLang);
  89. location.reload();
  90. });
  91.  
  92. if (/\/code/.test(window.location.href)) return;
  93.  
  94. const fetchScriptCode = () => {
  95. const scriptId = window.location.pathname.match(/scripts\/(\d+)/)?.[1];
  96. if (!scriptId) throw new Error('無法提取腳本 ID');
  97. const codeUrl = `${window.location.origin}${window.location.pathname}/code`;
  98. return new Promise((resolve, reject) => {
  99. GM_xmlhttpRequest({
  100. method: 'GET', url: codeUrl, onload: (response) => {
  101. const parser = new DOMParser();
  102. const doc = parser.parseFromString(response.responseText, 'text/html');
  103. const code = doc.querySelector('pre')?.textContent;
  104. if (code) resolve(code);
  105. else reject('未找到代碼');
  106. }, onerror: () => reject('獲取代碼失敗')
  107. });
  108. });
  109. };
  110.  
  111. const analyzeScript = async (code) => {
  112. const lines = code.split('\n');
  113. const matches = lines
  114. .filter(line => line.trim().startsWith('// @match'))
  115. .map(line => line.replace('// @match', '').trim().replace(/^\*?:\/\//, '').replace(/\/.*/, ''))
  116. .filter(url => url && !url.includes('greasyfork.org'));
  117.  
  118. const highRiskDomains = [];
  119. const riskReasons = {};
  120. const knownSafe = ['google.com', 'youtube.com', 'facebook.com', 'instagram.com', 'twitter.com', 'wikipedia.org'];
  121. const riskyPatterns = [
  122. /vn88\..*/, /fb88\..*/, /m88\..*/, /bet88li\.com/, /yeumoney\.com/, /165\.22\.63\.250/, /188\.166\.185\.213/,
  123. /aylink\.co/, /gplinks\.co/, /v2links\.me/, /coinclix\.co/, /cutyion\.com/, /upfion\.com/,
  124. /modsfire\.com/, /dropbox\.com/, /drive\.google\.com/, /mega\.nz/,
  125. /ourcoincash\.xyz/, /bitcotasks\.com/, /freepayz\.com/, /gwaher\.com/, /geekgrove\.net/, /cricketlegacy\.com/,
  126. /.*\.techyuth\.xyz/, /.*\.idblogmarket\.com/, /.*\.phonesparrow\.com/, /.*\.wikijankari\.com/,
  127. /financewada\.com/, /financenova\.online/, /utkarshonlinetest\.com/, /rajasthantopnews\.com/
  128. ];
  129.  
  130. let staticCheckedCount = 0;
  131. matches.forEach(domain => {
  132. staticCheckedCount++;
  133. if (knownSafe.some(safe => domain.includes(safe))) return;
  134. let reason = '未知風險';
  135. if (riskyPatterns.some(pattern => pattern.test(domain))) {
  136. if (/vn88\..*|fb88\..*|m88\..*|bet88li\.com|yeumoney\.com/.test(domain)) reason = t('gamblingRelated');
  137. else if (/aylink\.co|gplinks\.co|v2links\.me|coinclix\.co|cutyion\.com|upfion\.com/.test(domain)) reason = t('urlShortening');
  138. else if (/modsfire\.com|dropbox\.com|drive\.google\.com|mega\.nz/.test(domain)) reason = t('fileSharing');
  139. else if (/ourcoincash\.xyz|bitcotasks\.com|freepayz\.com|gwaher\.com|geekgrove\.net|cricketlegacy\.com/.test(domain)) reason = t('ptcSite');
  140. highRiskDomains.push(domain);
  141. riskReasons[domain] = reason;
  142. }
  143. });
  144.  
  145. const highRiskGrants = ['GM_xmlhttpRequest', 'unsafeWindow', 'GM_setValue', 'GM_openInTab'];
  146. let firstRiskyGrant = '';
  147. for (let i = 0; i < lines.length; i++) {
  148. const line = lines[i].trim();
  149. if (line.includes('@grant GM_xmlhttpRequest')) {
  150. firstRiskyGrant = userLang === 'en' ? 'GM_xmlhttpRequest' :
  151. userLang === 'zh-TW' ? 'GM 跨站請求' :
  152. userLang === 'zh-CN' ? 'GM 跨站请求' : 'GM_xmlhttpRequest';
  153. break;
  154. }
  155. if (!firstRiskyGrant && highRiskGrants.some(grant => line.includes(`@grant ${grant}`))) {
  156. firstRiskyGrant = highRiskGrants.find(grant => line.includes(`@grant ${grant}`));
  157. break;
  158. }
  159. }
  160. if (!firstRiskyGrant) firstRiskyGrant = t('noHighRiskGrant');
  161.  
  162. let warning;
  163. if (highRiskDomains.length > 0) {
  164. warning = firstRiskyGrant === t('noHighRiskGrant')
  165. ? t('staticRiskNotice', { count: staticCheckedCount, riskCount: highRiskDomains.length, apiSummary: '', politicalWarning: '' })
  166. : t('safetyNotice', { grant: firstRiskyGrant, count: staticCheckedCount, riskCount: highRiskDomains.length, apiSummary: '', politicalWarning: '' });
  167. } else {
  168. warning = t('safetyNoticeNoRisk', { grant: firstRiskyGrant, count: staticCheckedCount, apiSummary: '', politicalWarning: '' });
  169. }
  170.  
  171. return { warning, highRiskDomains, riskReasons };
  172. };
  173.  
  174. const waitForElement = (selector) => new Promise((resolve) => {
  175. const element = document.querySelector(selector);
  176. if (element) return resolve(element);
  177. const observer = new MutationObserver(() => {
  178. const el = document.querySelector(selector);
  179. if (el) {
  180. observer.disconnect();
  181. resolve(el);
  182. }
  183. });
  184. observer.observe(document.body, { childList: true, subtree: true });
  185. });
  186.  
  187. GM_addStyle('.grok-ai-safety-notice { background: #fff3f3; border: 2px solid #ff4d4d; padding: 10px; margin-bottom: 15px; border-radius: 5px; color: #333; }');
  188.  
  189. waitForElement('.install-link').then((installButton) => {
  190. fetchScriptCode().then(async (code) => {
  191. const { warning } = await analyzeScript(code);
  192. const notice = document.createElement('div');
  193. notice.className = 'grok-ai-safety-notice';
  194. notice.textContent = warning;
  195. installButton.parentNode.insertBefore(notice, installButton);
  196. console.log('提示已插入 / Notice inserted');
  197. }).catch(() => {
  198. const notice = document.createElement('div');
  199. notice.className = 'grok-ai-safety-notice';
  200. notice.textContent = t('fetchCodeFailed');
  201. installButton.parentNode.insertBefore(notice, installButton);
  202. });
  203. });
  204. })();