Проверка кода скриптов GreasyFork на безопасность (Локально + ИИ)
// ==UserScript==
// @name GF Code Scanner [Local + AI]
// @name:en GF Code Scanner [Local + AI]
// @namespace http://tampermonkey.net/
// @version 1.2.0
// @description Проверка кода скриптов GreasyFork на безопасность (Локально + ИИ)
// @description:en Security check for GreasyFork scripts (Local scan + AI analysis)
// @author Kimi, Claude, DeepSeek on base script GreasyFork Code Safety Scanner
// @match https://greatest.deepsurf.us/*/scripts/*/code*
// @match https://greatest.deepsurf.us/*/scripts/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM.xmlHttpRequest
// @grant GM.getValue
// @grant GM.setValue
// @grant unsafeWindow
// @connect api.groq.com
// @connect api.deepseek.com
// @connect api.openai.com
// @connect api.x.ai
// @connect generativelanguage.googleapis.com
// @connect openrouter.ai
// @connect js.puter.com
// @require https://update.greatest.deepsurf.us/scripts/34138/223779/markedjs.js
// @run-at document-end
// @compatible firefox 78+ TamperMonkey 4.18+
// @compatible firefox 78+ GreasyMonkey 4.11+
// @compatible chrome 88+ TamperMonkey 4.18+
// @compatible chrome 88+ ScriptCat 0.9+
// @compatible edge 88+ TamperMonkey 4.18+
// @compatible opera 74+ TamperMonkey 4.18+
// @compatible firefox 78+ ViolentMonkey 2.12+ (не полностью протестирован)
// @compatible chrome 88+ ViolentMonkey 2.12+ (не полностью протестирован)
// @license MIT
// ==/UserScript==
// ╔═══════════════════════════════════════════════════════════════════════════╗
// ║ СОВМЕСТИМОСТЬ БРАУЗЕРОВ И МЕНЕДЖЕРОВ СКРИПТОВ ║
// ╠═══════════════════════════════════════════════════════════════════════════╣
// ║ ✅ Chrome 88+ + TamperMonkey 4.18+ — полная поддержка ║
// ║ ✅ Chrome 88+ + ScriptCat 0.9+ — полная поддержка ║
// ║ ✅ Firefox 78+ + TamperMonkey 4.18+ — полная поддержка ║
// ║ ✅ Firefox 78+ + GreasyMonkey 4.11+ — полная поддержка ║
// ║ ✅ Edge 88+ + TamperMonkey 4.18+ — полная поддержка ║
// ║ ✅ Opera 74+ + TamperMonkey 4.18+ — полная поддержка ║
// ║ ✅ Vivaldi 3.6+ + TamperMonkey 4.18+ — полная поддержка ║
// ║ ✅ Llama [Puter.js]: Chrome/Edge 88+, Firefox 78+ (TM 4.18+) ║
// ║ Firefox: исправлено через cloneInto() без правки настроек ║
// ║ ⚠️ Любой браузер + ViolentMonkey 2.12+ — работает, не тестировалось ║
// ║ ⚠️ Via Browser (Android) — частично: ИИ работает, ключ может ║
// ║ не сохраняться между сессиями ║
// ║ ❌ Safari (любой) + Userscripts — GM XHR API несовместим ║
// ║ ❌ Firefox + GreasyMonkey 3.x — устаревший API, не поддерживается ║
// ╠═══════════════════════════════════════════════════════════════════════════╣
// ║ ИСТОРИЯ ИЗМЕНЕНИЙ ║
// ║ v1.2.0 Добавлен чекбокс «Расскажи о скрипте»: ║
// ║ • При активации ИИ дополнительно запрашивает краткое описание ║
// ║ скрипта и его алгоритма работы ║
// ║ • Добавляет юмористический вывод о безопасности использования ║
// ║ с точки зрения конфиденциальности данных пользователя ║
// ║ на основе локально найденных параметров ║
// ║ • Состояние чекбокса НЕ сохраняется между страницами ║
// ║ v1.1.0 Оптимизация производительности (по анализу Nemotron): ║
// ║ • MutationObserver вместо setInterval в init() — убирает ║
// ║ тикающий таймер, меньше пробуждений CPU ║
// ║ • pre-compile regex в RULES[i].re — new RegExp только 1 раз ║
// ║ • URL_RE.lastIndex=0 при каждом вызове extractUrls ║
// ║ • aiInProgress флаг — предотвращает параллельные XHR ║
// ║ • GM-тип определяется один раз при старте (hasNewGM) ║
// ║ • parts[]+join вместо h+= в buildWidget ║
// ║ • requestIdleCallback для запуска скана (деградирует к setTimeout)║
// ║ • cleanup() при Turbolinks — явная отвязка observer ║
// ║ • Именованные константы: RETRY_MS, MAX_RETRIES, MAX_CODE_LEN ║
// ║ v1.0.8 Firefox fix для Puter.js: cloneInto(); подсказка о ключе; ║
// ║ nVidia→OpenRouter; BrowserAI отключён; комментарии на RU ║
// ║ v1.0.7 Флаг CLEAR_ALL_API_KEYS; Qwen через Groq; Puter.js; OpenRouter ║
// ║ v1.0.6 Точные версии @compatible; изолированные CSS для ответа ИИ ║
// ║ v1.0.5 Исправление window.prompt() в Firefox (TDZ) ║
// ║ v1.0.4 Универсальный GM API; выбор языка ответа; i18n UI ║
// ║ v1.0.3 Первый публичный релиз ║
// ╚═══════════════════════════════════════════════════════════════════════════╝
// ╔═══════════════════════════════════════════════════════════════════════════╗
// ║ НАСТРОЙКИ ПОЛЬЗОВАТЕЛЯ — редактировать только этот раздел ║
// ╠═══════════════════════════════════════════════════════════════════════════╣
// Принудительная очистка всех сохранённых API-ключей.
// Установить в true → перезагрузить страницу → вернуть в false.
const CLEAR_ALL_API_KEYS = false;
// Диагностика: вывести в консоль (F12) список доступных моделей провайдеров.
// Нужны сохранённые ключи. Установить true → открыть консоль → вернуть false.
const TEST_MODELS_AVAILABLE = false;
// ╚═══════════════════════════════════════════════════════════════════════════╝
(async function () {
'use strict';
console.log('[GF-Scanner-v1.2.0] Start');
// ══════════════════════════════════════════════════════════════════════════
// ИМЕНОВАННЫЕ КОНСТАНТЫ (вместо «магических чисел» в коде)
// ══════════════════════════════════════════════════════════════════════════
const RETRY_MS = 400; // интервал ожидания <pre> в MutationObserver-таймауте
const MAX_RETRIES = 20; // максимум попыток найти <pre> (fallback для старых браузеров)
const MAX_CODE_LEN = 15000; // лимит символов кода в промпте для ИИ
const MAX_FIND_LINES = 5; // максимум строк кода для каждой найденной угрозы
// ══════════════════════════════════════════════════════════════════════════
// CSS ДЛЯ ОТОБРАЖЕНИЯ ОТВЕТА ИИ (markdown)
// Инжектируется один раз. Все правила изолированы на #gfr.
// ══════════════════════════════════════════════════════════════════════════
if (!document.getElementById('gfs-md-style')) {
const style = document.createElement('style');
style.id = 'gfs-md-style';
style.textContent = `
#gfr { background:#0d1117 !important; color:#c9d1d9 !important; }
#gfr p { margin:4px 0; }
#gfr ul, #gfr ol { padding-left:20px; margin:4px 0; }
#gfr li { margin:2px 0; }
#gfr code {
background:rgba(110,118,129,0.2); color:#c9d1d9;
border:1px solid rgba(110,118,129,0.3); border-radius:3px;
padding:1px 5px; font-family:'Consolas','Courier New',monospace;
font-size:11px; word-break:break-word; }
#gfr pre { background:rgba(0,0,0,0.4); border:1px solid rgba(110,118,129,0.2);
border-radius:4px; padding:8px 10px; overflow-x:auto; margin:6px 0; }
#gfr pre code { background:none !important; border:none !important; padding:0 !important; }
#gfr h1,#gfr h2 { color:#79b8ff; font-size:14px; margin:10px 0 4px;
border-bottom:1px solid #30363d; padding-bottom:3px; }
#gfr h3,#gfr h4 { color:#79b8ff; font-size:13px; margin:8px 0 3px; }
#gfr strong,#gfr b { color:#f0f0f0; font-weight:600; }
#gfr em,#gfr i { color:#d0d0d0; font-style:italic; }
#gfr a { color:#58a6ff; text-decoration:none; }
#gfr a:hover { text-decoration:underline; }
#gfr blockquote { border-left:3px solid #444; padding:4px 8px; color:#8b949e;
margin:4px 0; background:rgba(255,255,255,0.03); border-radius:0 4px 4px 0; }
#gfr hr { border:none; border-top:1px solid #30363d; margin:8px 0; }
#gfr table { border-collapse:collapse; width:100%; margin:6px 0; font-size:12px; }
#gfr th { background:rgba(110,118,129,0.15); color:#c9d1d9;
padding:4px 8px; border:1px solid #30363d; }
#gfr td { padding:4px 8px; border:1px solid #30363d; color:#c9d1d9; }
#gfr tr:nth-child(even) td { background:rgba(110,118,129,0.05); }`;
document.head.appendChild(style);
}
// ══════════════════════════════════════════════════════════════════════════
// GM API — ОПРЕДЕЛЕНИЕ ТИПА ОДИН РАЗ ПРИ СТАРТЕ
//
// TamperMonkey → GM_getValue / GM_setValue / GM_xmlhttpRequest (синхр.)
// GreasyMonkey4 → GM.getValue / GM.setValue / GM.xmlHttpRequest (Promise)
// ══════════════════════════════════════════════════════════════════════════
const hasNewGM = typeof GM !== 'undefined' && typeof GM.getValue === 'function';
const gmGet = async (key, def) => {
try {
if (hasNewGM) return await GM.getValue(key, def);
if (typeof GM_getValue === 'function') {
const v = GM_getValue(key, def);
return (v && typeof v.then === 'function') ? await v : v;
}
} catch (e) { console.warn('[GF-Scanner] gmGet:', e); }
return def;
};
const gmSet = async (key, val) => {
try {
if (hasNewGM) return await GM.setValue(key, val);
if (typeof GM_setValue === 'function') return GM_setValue(key, val);
} catch (e) { console.warn('[GF-Scanner] gmSet:', e); }
};
const gmXhr = (opts) => {
try {
if (hasNewGM && typeof GM.xmlHttpRequest === 'function') return GM.xmlHttpRequest(opts);
if (typeof GM_xmlhttpRequest === 'function') return GM_xmlhttpRequest(opts);
throw new Error('GM XHR недоступен — проверьте @grant в заголовке скрипта');
} catch (e) {
console.error('[GF-Scanner] gmXhr:', e);
opts.onerror && opts.onerror({ message: e.message });
}
};
// ══════════════════════════════════════════════════════════════════════════
// СПИСОК МОДЕЛЕЙ ИИ
// ══════════════════════════════════════════════════════════════════════════
const MODELS = {
groq: { n: 'Groq',
u: 'https://api.groq.com/openai/v1/chat/completions',
m: 'llama-3.3-70b-versatile' },
deepseek: { n: 'DeepSeek 💰',
u: 'https://api.deepseek.com/chat/completions',
m: 'deepseek-chat' },
gemini: { n: 'Gemini',
u: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent',
m: 'gemini-2.5-flash',
g: true },
grok: { n: 'Grok 💰',
u: 'https://api.x.ai/v1/chat/completions',
m: 'grok-beta' },
qwen: { n: 'Qwen 🡒 Groq',
u: 'https://api.groq.com/openai/v1/chat/completions',
m: 'qwen/qwen3-32b',
sharedKey: 'groq' },
openai: { n: 'ChatGPT 💰',
u: 'https://api.openai.com/v1/chat/completions',
m: 'gpt-3.5-turbo' },
mistral_or: { n: 'nVidia 🡒 OpenRouter',
u: 'https://openrouter.ai/api/v1/chat/completions',
m: 'nvidia/nemotron-3-super-120b-a12b:free',
keyLabel: 'OpenRouter (openrouter.ai/keys)',
orModel: true },
llama_puter: { n: 'Llama 🡒 Puter.js',
noKey: true,
puter: true },
};
const KEY_URLS = {
groq: 'https://console.groq.com/keys',
deepseek: 'https://platform.deepseek.com/api_keys',
gemini: 'https://aistudio.google.com/app/apikey',
grok: 'https://console.x.ai/',
openai: 'https://platform.openai.com/api-keys',
mistral_or: 'https://openrouter.ai/keys'
};
// ══════════════════════════════════════════════════════════════════════════
// ЯЗЫКИ ОТВЕТА ИИ
// ══════════════════════════════════════════════════════════════════════════
const LANGS = {
ru: { label: '🇷🇺 Русский', name: 'Russian' },
en: { label: '🇬🇧 English', name: 'English' },
de: { label: '🇩🇪 Deutsch', name: 'German' },
fr: { label: '🇫🇷 Français', name: 'French' },
es: { label: '🇪🇸 Español', name: 'Spanish' },
zh: { label: '🇨🇳 中文', name: 'Chinese' },
ja: { label: '🇯🇵 日本語', name: 'Japanese' },
ko: { label: '🇰🇷 한국어', name: 'Korean' },
pt: { label: '🇵🇹 Português', name: 'Portuguese' }
};
// ══════════════════════════════════════════════════════════════════════════
// ПЕРЕВОД ИНТЕРФЕЙСА (i18n)
// ══════════════════════════════════════════════════════════════════════════
const I18N = {
ru: {
report:'Отчёт', safe:'Безопасно', low:'Низкий риск', med:'Средний риск', danger:'ОПАСНО',
issues:'Угроз', domains:'Домены', showUrls:'Показать URL', showLines:'Показать строки',
noThreats:'Угроз не обнаружено.', retryLocal:'Пересканировать', checkAi:'Проверить ИИ',
retryAi:'Снова ИИ', sending:'Отправка данных в', thinking:'Думает...',
noKey:'Ключ не введён', enterKey:'Введите API-ключ для', parseErr:'Ошибка разбора ответа',
netErr:'Ошибка сети. Проверьте консоль (F12).', emptyResp:'Пустой ответ от ИИ.',
modelLabel:'Модель ИИ:', langLabel:'Язык ответа:',
keyHint:'🔑 API-ключ вводится один раз и сохраняется в браузере',
loadingPuter:'Загрузка Puter.js, подключение к Llama...',
puterErr:'Ошибка Puter.js',
keysCleared:'✅ Все API-ключи очищены (CLEAR_ALL_API_KEYS=true).',
describeScript:'Расскажи о скрипте'
},
en: {
report:'Report', safe:'Safe', low:'Low Risk', med:'Medium Risk', danger:'DANGER',
issues:'Issues', domains:'Domains', showUrls:'Show URLs', showLines:'Show Lines',
noThreats:'No threats found.', retryLocal:'Retry Local', checkAi:'Check AI',
retryAi:'Retry AI', sending:'Sending data to', thinking:'Thinking...',
noKey:'No key provided', enterKey:'Enter API key for', parseErr:'Parse error',
netErr:'Network error. Check console (F12).', emptyResp:'Empty response from AI.',
modelLabel:'AI Model:', langLabel:'Response language:',
keyHint:'🔑 API key is entered once and saved in your browser',
loadingPuter:'Loading Puter.js, connecting to Llama...',
puterErr:'Puter.js error',
keysCleared:'✅ All API keys cleared (CLEAR_ALL_API_KEYS=true).',
describeScript:'Describe script'
}
};
const t = (k) => (I18N[L] || I18N.en)[k] ?? I18N.en[k] ?? k;
// ══════════════════════════════════════════════════════════════════════════
// ПРАВИЛА ЛОКАЛЬНОГО СКАНИРОВАНИЯ — PRE-COMPILED REGEX
// ══════════════════════════════════════════════════════════════════════════
const RULES = [
{ p:/\beval\s*\(/g, v:4, en:'eval()', ru:'eval()', den:'Code Execution', dru:'Выполнение кода' },
{ p:/\bnew\s+Function\s*\(/g, v:4, en:'new Function()', ru:'new Function()', den:'Dynamic Code', dru:'Динамика' },
{ p:/coinhive|cryptonight|monero|minero\.js/i, v:4, en:'Miner', ru:'Майнер', den:'Crypto Mining', dru:'Криптомайнинг' },
{ p:/\batob\s*\(/g, v:3, en:'atob()', ru:'atob()', den:'Hidden Base64', dru:'Скрытый Base64' },
{ p:/_0x[a-f0-9]{4,}/i, v:3, en:'Obfuscation', ru:'Обфускация', den:'Obfuscated Code', dru:'Запутанный код' },
{ p:/String\.fromCharCode/i, v:3, en:'CharCode', ru:'CharCode', den:'Obfuscation Technique', dru:'Техника обфускации' },
{ p:/\bnavigator\.geolocation\b/g, v:3, en:'Geolocation', ru:'Геолокация', den:'GPS Tracking', dru:'Отслеживание GPS' },
{ p:/\bnavigator\.mediaDevices\b/g, v:3, en:'Cam/Mic', ru:'Кам/Мик', den:'Camera/Microphone', dru:'Камера/Микрофон' },
{ p:/\bGM_xmlhttpRequest\b/g, v:3, en:'GM_XHR', ru:'GM_XHR', den:'Hidden XHR Requests', dru:'Скрытые запросы' },
{ p:/\bdocument\.cookie\b/g, v:2, en:'Cookies', ru:'Cookies', den:'Cookie Access', dru:'Доступ к куки' },
{ p:/\blocalStorage\b/g, v:2, en:'LocalStorage', ru:'LocalStorage', den:'Persistent Storage', dru:'Локальное хранение' },
{ p:/\bsessionStorage\b/g, v:2, en:'SessionStorage', ru:'SessionStorage', den:'Session Storage', dru:'Сессионное хранение' },
{ p:/\bnavigator\.clipboard\b/g, v:2, en:'Clipboard', ru:'Буфер обмена', den:'Clipboard Access', dru:'Буфер обмена' },
{ p:/\bGM_getValue\b|\bGM_setValue\b/g, v:2, en:'GM_Store', ru:'GM_Store', den:'TM/GM Storage API', dru:'Хранилище TM/GM' },
{ p:/\bfetch\s*\(/g, v:1, en:'Fetch', ru:'Fetch', den:'Network Request', dru:'Сетевой запрос' },
{ p:/\bXMLHttpRequest\b/g, v:1, en:'XHR', ru:'XHR', den:'Network Request', dru:'Сетевой запрос' },
{ p:/\bdocument\.write\s*\(/g, v:2, en:'DocWrite', ru:'DocWrite', den:'Page Overwrite', dru:'Перезапись страницы' }
].map(r => ({ ...r, re: new RegExp(r.p.source, r.p.flags) }));
const URL_RE = /https?:\/\/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)/gi;
const esc = s => s ? String(s)
.replace(/&/g,'&').replace(/</g,'<')
.replace(/>/g,'>').replace(/"/g,'"') : '';
const findLines = (code, re) => {
const localRe = new RegExp(re.source, re.flags);
const out = [];
code.split('\n').forEach((l, i) => {
if (localRe.test(l)) out.push(`L${i+1}: ${l.trim().substring(0, 70)}`);
localRe.lastIndex = 0;
});
return out.slice(0, MAX_FIND_LINES);
};
const extractUrls = code => {
URL_RE.lastIndex = 0;
const u = [...new Set(code.match(URL_RE) || [])], d = new Set();
u.forEach(x => { try { d.add(new URL(x).hostname); } catch(e) {} });
return { u, d: [...d] };
};
// ══════════════════════════════════════════════════════════════════════════
// ЗАГРУЗКА СОХРАНЁННЫХ НАСТРОЕК
// ══════════════════════════════════════════════════════════════════════════
let K = await gmGet('k', {});
if (typeof K !== 'object' || K === null || Array.isArray(K)) K = {};
let S = await gmGet('s', 'groq'); if (!MODELS[S]) S = 'groq';
let L = await gmGet('l', 'ru'); if (!LANGS[L]) L = 'ru';
// v1.2.0: чекбокс «Расскажи о скрипте» — локально, не сохраняется между страницами
let D = false;
// ══════════════════════════════════════════════════════════════════════════
// ДИАГНОСТИКА
// ══════════════════════════════════════════════════════════════════════════
if (TEST_MODELS_AVAILABLE) {
console.log('[GF-Scanner] === ДИАГНОСТИКА: проверка доступных моделей ===');
const fetchModels = (url, apiKey, name) => new Promise(resolve => {
if (!apiKey) { console.warn(`[${name}] Нет ключа`); return resolve(null); }
gmXhr({ method:'GET', url, headers:{'Authorization':`Bearer ${apiKey}`,'Content-Type':'application/json'},
onload: r => {
try {
const d = JSON.parse(r.responseText);
const ids = (d.data||d.models||[]).map(m => m.id);
console.log(`[${name}] Доступно (${ids.length}):`, ids); resolve(ids);
} catch(e) { console.error(`[${name}] Ошибка:`, e.message); resolve(null); }
},
onerror: e => { console.error(`[${name}] Сеть:`, e); resolve(null); }
});
});
if (K.groq) await fetchModels('https://api.groq.com/openai/v1/models', K.groq, 'Groq');
if (K.deepseek) await fetchModels('https://api.deepseek.com/v1/models', K.deepseek, 'DeepSeek');
gmXhr({ method:'GET', url:'https://openrouter.ai/api/v1/models',
onload: r => { try { const d=JSON.parse(r.responseText);
const free=(d.data||[]).filter(m=>m.id.includes(':free')).map(m=>m.id);
console.log(`[OpenRouter] Бесплатных (${free.length}):`, free);
} catch(e) { console.error('[OpenRouter]', e.message); }},
onerror: e => console.error('[OpenRouter]', e)
});
console.log('[GF-Scanner] === Диагностика завершена. Верните TEST_MODELS_AVAILABLE=false ===');
}
if (CLEAR_ALL_API_KEYS) {
K = {};
await gmSet('k', {});
console.warn('[GF-Scanner] Все API-ключи очищены. Верните CLEAR_ALL_API_KEYS=false.');
}
// ══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
// ══════════════════════════════════════════════════════════════════════════
const renderAnswer = (el, ans) => {
try {
el.innerHTML = (typeof marked !== 'undefined' && marked.parse)
? marked.parse(ans)
: ans.replace(/\n/g, '<br>');
} catch(e) { el.textContent = ans; }
};
const buildPrompt = (findings, net, code, langName, describe) => {
const localSummary = findings.length
? 'LOCAL SCAN FOUND:\n' + findings.map(x =>
`- ${x.en}: ${x.den} (×${x.count}${x.lines.length ? '; Lines: ' + x.lines.join('; ') : ''})`
).join('\n')
: 'Local scan found no obvious threats.';
const domainSummary = net.d.length
? `DOMAINS FOUND: ${net.d.join(', ')}`
: 'No external domains found.';
let prompt = `YOU ARE A JAVASCRIPT SECURITY EXPERT.\n`
+ `ANSWER STRICTLY IN ${langName.toUpperCase()} — NO OTHER LANGUAGE.\n\n`
+ `=== LOCAL SCAN RESULTS ===\n${localSummary}\n\n`
+ `=== NETWORK ===\n${domainSummary}\n\n`
+ `=== FULL SCRIPT CODE (truncated to ${MAX_CODE_LEN} chars) ===\n${code.substring(0, MAX_CODE_LEN)}\n\n`
+ `=== YOUR TASK ===\n`
+ `1. Confirm or dismiss each locally-found threat (Confirmed / False Positive).\n`
+ `2. Identify any hidden or missed threats.\n`
+ `3. Analyse all external domains for malicious patterns.\n`
+ `4. Final verdict: SAFE or DANGEROUS with justification.\n`
+ `5. ALL RESPONSES MUST BE IN ${langName.toUpperCase()} ONLY.`;
if (describe) {
prompt += `\n\n=== ADDITIONAL REQUEST ===\n`
+ `6. Provide a brief, clear description of what this script does and its main algorithm/workflow.\n`
+ `7. Based on the locally found parameters and your analysis, give a humorous conclusion about the script's safety from a user data privacy perspective. `
+ `Make it funny and entertaining, but keep it informative and grounded in the actual findings.`;
}
return prompt;
};
// ══════════════════════════════════════════════════════════════════════════
// PUTER.JS — LLAMA БЕЗ API-КЛЮЧА
// ══════════════════════════════════════════════════════════════════════════
const RW = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
const PUTER_OPTS = { model: 'meta-llama/llama-4-scout' };
const loadPuter = () => new Promise((resolve, reject) => {
if (RW.puter && RW.puter.ai) return resolve();
const s = document.createElement('script');
s.src = 'https://js.puter.com/v2/';
s.onload = () => {
const puterReady = new Promise(res => {
let tries = 0;
const poll = setInterval(() => {
if (RW.puter && RW.puter.ai) { clearInterval(poll); res(true); }
else if (++tries >= 40) { clearInterval(poll); res(false); }
}, 200);
});
puterReady.then(resolve);
};
s.onerror = () => reject(new Error('Не удалось загрузить Puter.js с js.puter.com'));
(document.head || document.documentElement).appendChild(s);
});
const callPuter = async (promptText) => {
await loadPuter();
if (!RW.puter?.ai?.chat) throw new Error('puter.ai.chat недоступен после загрузки Puter.js.');
const safeOpts = typeof cloneInto !== 'undefined' ? cloneInto(PUTER_OPTS, RW) : PUTER_OPTS;
const response = await RW.puter.ai.chat(promptText, safeOpts);
return response?.message?.content
|| response?.content
|| (typeof response === 'string' ? response : '')
|| 'Empty response from Puter.js / Llama.';
};
// ══════════════════════════════════════════════════════════════════════════
// ФЛАГ ЗАЩИТЫ ОТ ПАРАЛЛЕЛЬНЫХ XHR
// ══════════════════════════════════════════════════════════════════════════
let aiInProgress = false;
// ══════════════════════════════════════════════════════════════════════════
// ОСНОВНАЯ ФУНКЦИЯ: СКАН + РЕНДЕР ВИДЖЕТА
// ══════════════════════════════════════════════════════════════════════════
const scan = () => {
document.getElementById('gfs')?.remove();
const blk = document.querySelector('pre code') || document.querySelector('pre');
if (!blk) return false;
const code = blk.innerText || blk.textContent || '';
// ── ЛОКАЛЬНЫЙ СКАН ───────────────────────────────────────────────────
const findings = []; let maxV = 0;
RULES.forEach(r => {
r.re.lastIndex = 0;
const m = code.match(r.re);
if (m) {
findings.push({ ...r, count: m.length, lines: findLines(code, r.re) });
if (r.v > maxV) maxV = r.v;
}
});
const net = extractUrls(code);
// ── ОЦЕНКА УРОВНЯ РИСКА ──────────────────────────────────────────────
let col = '#4caf50', status = t('safe');
if (maxV === 1) { col = '#ffd700'; status = t('low'); }
if (maxV === 2) { col = '#ff9800'; status = t('med'); }
if (maxV >= 3) { col = '#f44336'; status = t('danger'); }
// ══════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ HTML
// ══════════════════════════════════════════════════════════════════════
const p = [];
p.push(`<h3 style="margin:0 0 10px;color:${col};font-size:15px">🔍 ${t('report')}: <span style="font-size:16px">${status}</span></h3>`);
p.push(`<p style="margin:0 0 10px;color:#9e9e9e;font-size:12px">${t('issues')}: <b style="color:${col}">${findings.length}</b> | ${t('domains')}: <b style="color:#4fc3f7">${net.d.length}</b></p>`);
if (net.d.length) {
p.push(`<div style="background:#16213e;padding:8px 10px;margin:6px 0;border-radius:4px;border-left:3px solid #4fc3f7">`);
p.push(`<b style="color:#4fc3f7">🌐 ${t('domains')}:</b> <span style="word-break:break-all">${net.d.map(esc).join(', ')}</span></div>`);
if (net.u.length <= 20) {
p.push(`<details style="margin:4px 0 8px"><summary style="cursor:pointer;color:#666;font-size:11px;user-select:none">${t('showUrls')} (${net.u.length})</summary>`);
p.push(`<div style="background:#0d1117;padding:6px;border-radius:4px;word-break:break-all;font-size:10px;color:#888;margin-top:4px">${net.u.map(esc).join('<br>')}</div></details>`);
}
}
if (findings.length) {
findings.forEach(x => {
const fc = x.v >= 4 ? '#f44336' : x.v === 3 ? '#ff9800' : '#ffd700';
const nm = L === 'ru' ? x.ru : x.en;
const ds = L === 'ru' ? x.dru : x.den;
p.push(`<div style="border-left:3px solid ${fc};padding:6px 8px;margin:5px 0;background:#16213e;border-radius:0 4px 4px 0">`);
p.push(`<b style="color:${fc}">⚠ ${esc(nm)}</b>: <span style="color:#ccc">${esc(ds)}</span> <span style="color:#666;font-size:11px">(×${x.count})</span>`);
if (x.lines.length) {
p.push(`<details><summary style="cursor:pointer;font-size:10px;color:#666;margin-top:3px;user-select:none">${t('showLines')}</summary>`);
p.push(`<div style="background:#0d1117;padding:4px 6px;font-family:monospace;font-size:10px;color:#888;border-radius:4px;margin-top:3px">${x.lines.map(esc).join('<br>')}</div></details>`);
}
p.push(`</div>`);
});
} else if (!net.d.length) {
p.push(`<p style="color:#4caf50;margin:8px 0">✅ ${t('noThreats')}</p>`);
}
if (CLEAR_ALL_API_KEYS) {
p.push(`<p style="color:#ffd700;font-size:11px;margin:6px 0;background:#2a1e00;padding:6px 8px;border-radius:4px;border-left:3px solid #ffd700">⚠ ${t('keysCleared')}</p>`);
}
// Кнопки действий
p.push(`<div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap">`);
p.push(`<button id="gfb-local" style="flex:1;min-width:130px;padding:8px 12px;background:#1565c0;color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:bold;font-size:13px">🔄 ${t('retryLocal')}</button>`);
p.push(`<button id="gfb-ai" style="flex:1;min-width:130px;padding:8px 12px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:bold;font-size:13px">🤖 ${t('checkAi')}</button>`);
p.push(`</div>`);
// Чекбокс «Расскажи о скрипте» (v1.2.0) — локально, без сохранения
p.push(`<div style="margin-top:8px;display:flex;align-items:center;gap:6px">`);
p.push(`<input type="checkbox" id="gfs-describe" style="cursor:pointer"${D ? ' checked' : ''}>`);
p.push(`<label for="gfs-describe" style="color:#aaa;font-size:12px;cursor:pointer;user-select:none">${t('describeScript')}</label>`);
p.push(`</div>`);
// Строка выборов: модель + язык + подсказка о ключе
p.push(`<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap;align-items:center">`);
let mopts = ''; Object.keys(MODELS).forEach(k => { mopts += `<option value="${k}"${k === S ? ' selected' : ''}>${esc(MODELS[k].n)}</option>`; });
p.push(`<div style="display:flex;align-items:center;gap:6px">`);
p.push(`<label for="gfs-sel-model" style="color:#888;font-size:12px;white-space:nowrap">${t('modelLabel')}</label>`);
p.push(`<select id="gfs-sel-model" style="background:#2a2a3e;color:#fff;border:1px solid #444;padding:4px 8px;border-radius:4px;font-size:12px">${mopts}</select>`);
p.push(`</div>`);
let lopts = ''; Object.keys(LANGS).forEach(k => { lopts += `<option value="${k}"${k === L ? ' selected' : ''}>${esc(LANGS[k].label)}</option>`; });
p.push(`<div style="display:flex;align-items:center;gap:6px">`);
p.push(`<label for="gfs-sel-lang" style="color:#888;font-size:12px;white-space:nowrap">${t('langLabel')}</label>`);
p.push(`<select id="gfs-sel-lang" style="background:#2a2a3e;color:#fff;border:1px solid #444;padding:4px 8px;border-radius:4px;font-size:12px">${lopts}</select>`);
p.push(`</div>`);
p.push(`<div id="gfs-key-hint" style="font-size:11px;color:#8b949e;font-style:italic;display:${MODELS[S].noKey ? 'none' : 'block'}">${t('keyHint')}</div>`);
p.push(`</div>`);
// Область отображения ответа ИИ
p.push(`<div id="gfr" style="margin-top:10px;display:none;padding:12px;border:1px solid #333;border-radius:4px;max-height:500px;overflow-y:auto;font-size:12px;line-height:1.7;"></div>`);
// ── Вставка виджета ──────────────────────────────────────────────────
const box = document.createElement('div');
box.id = 'gfs';
box.style.cssText = `background:#1a1a2e;color:#e0e0e0;border:2px solid ${col};padding:16px;margin:12px 0;font-family:system-ui,Arial,Helvetica,sans-serif;font-size:13px;border-radius:8px;line-height:1.5;box-sizing:border-box;`;
box.innerHTML = p.join('');
const anchor = document.querySelector('pre');
if (anchor) anchor.parentNode.insertBefore(box, anchor);
else document.body.prepend(box);
// ── ОБРАБОТЧИКИ СОБЫТИЙ ──────────────────────────────────────────────
document.getElementById('gfs-sel-model').addEventListener('change', async e => {
S = e.target.value;
await gmSet('s', S);
const hint = document.getElementById('gfs-key-hint');
if (hint) { hint.textContent = t('keyHint'); hint.style.display = MODELS[S].noKey ? 'none' : 'block'; }
});
document.getElementById('gfs-sel-lang').addEventListener('change', async e => {
L = e.target.value; await gmSet('l', L); scan();
});
// Чекбокс «Расскажи о скрипте» — только локальная переменная (v1.2.0)
document.getElementById('gfs-describe').addEventListener('change', e => {
D = e.target.checked;
});
document.getElementById('gfb-local').addEventListener('click', () => scan());
document.getElementById('gfb-ai').addEventListener('click', async () => {
if (aiInProgress) return;
aiInProgress = true;
const btn = document.getElementById('gfb-ai');
const res = document.getElementById('gfr');
const cfg = MODELS[S];
const langName = LANGS[L].name;
// Состояние чекбокса берём прямо из DOM (на случай если scan() перерисовался)
const describeScript = document.getElementById('gfs-describe')?.checked ?? false;
const promptText = buildPrompt(findings, net, code, langName, describeScript);
btn.disabled = true;
res.style.display = 'block';
// ── ВЕТКА A: Puter.js ───────────────────────────────────────────
if (cfg.puter) {
btn.textContent = '⏳ Puter.js…';
res.textContent = t('loadingPuter');
try {
const ans = await callPuter(promptText);
renderAnswer(res, ans);
} catch(e) {
res.textContent = `${t('puterErr')}: ${e.message}`;
}
btn.disabled = false;
btn.textContent = `🤖 ${t('retryAi')}`;
aiInProgress = false;
return;
}
// ── ВЕТКА B: модели с API-ключом (GM XHR) ───────────────────────
const keySlot = cfg.sharedKey || S;
const keyOwner = cfg.keyLabel || (cfg.sharedKey ? MODELS[cfg.sharedKey]?.n || cfg.sharedKey : cfg.n);
const keyUrl = KEY_URLS[keySlot] || '';
let key = K[keySlot];
if (!key) {
key = window.prompt(`${t('enterKey')} ${keyOwner}\n(${keyUrl}):`, '');
if (key && key.length > 5) { K[keySlot] = key; await gmSet('k', K); }
else {
window.alert(t('noKey'));
btn.disabled = false; btn.textContent = `🤖 ${t('checkAi')}`;
aiInProgress = false; return;
}
}
btn.textContent = `⏳ ${t('thinking')}`;
res.textContent = `${t('sending')} ${cfg.n}…`;
let body, headers = { 'Content-Type': 'application/json' }, url = cfg.u;
if (cfg.g) {
const gemUrl = new URL(cfg.u);
gemUrl.searchParams.set('key', key);
url = gemUrl.toString();
body = JSON.stringify({ contents: [{ parts: [{ text: promptText }] }] });
} else {
const reqBody = {
model: cfg.m,
messages: [
{ role: 'system', content: `You are a security expert. Answer strictly in ${langName}.` },
{ role: 'user', content: promptText }
],
temperature: 0.2
};
if (cfg.orModel) reqBody.tool_choice = 'none';
body = JSON.stringify(reqBody);
headers['Authorization'] = 'Bearer ' + key;
}
gmXhr({
method: 'POST', url, headers, data: body,
onload: r => {
try {
const j = JSON.parse(r.responseText);
let ans = cfg.g
? j.candidates?.[0]?.content?.parts?.[0]?.text || (j.error ? `API Error: ${j.error.message}` : '')
: j.choices?.[0]?.message?.content || (j.error ? `API Error: ${j.error.message}` : '');
if (!ans) ans = t('emptyResp');
renderAnswer(res, ans);
} catch(e) {
res.textContent = `${t('parseErr')}: ${e.message}\n${r.responseText.substring(0, 300)}`;
}
btn.disabled = false; btn.textContent = `🤖 ${t('retryAi')}`;
aiInProgress = false;
},
onerror: () => {
res.textContent = t('netErr');
btn.disabled = false; btn.textContent = `🤖 ${t('checkAi')}`;
aiInProgress = false;
}
});
});
return true;
};
// ══════════════════════════════════════════════════════════════════════════
// ИНИЦИАЛИЗАЦИЯ С MutationObserver
// ══════════════════════════════════════════════════════════════════════════
let _observer = null;
const cleanup = () => {
if (_observer) { _observer.disconnect(); _observer = null; }
document.getElementById('gfs')?.remove();
};
const init = () => {
cleanup();
if (scan()) return;
const scheduleIdle = (fn) => {
if (typeof requestIdleCallback !== 'undefined') {
requestIdleCallback(fn, { timeout: 2000 });
} else {
setTimeout(fn, RETRY_MS);
}
};
if (typeof MutationObserver !== 'undefined') {
let tries = 0;
_observer = new MutationObserver(() => {
scheduleIdle(() => {
if (scan()) {
_observer?.disconnect(); _observer = null;
} else if (++tries >= MAX_RETRIES) {
_observer?.disconnect(); _observer = null;
console.warn('[GF-Scanner] <pre> не найден после', MAX_RETRIES, 'попыток');
}
});
});
_observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
if (_observer) { _observer.disconnect(); _observer = null; }
}, MAX_RETRIES * RETRY_MS);
} else {
let tries = 0;
const id = setInterval(() => { if (scan() || ++tries >= MAX_RETRIES) clearInterval(id); }, RETRY_MS);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => init());
} else {
init();
}
document.addEventListener('turbolinks:load', () => init());
document.addEventListener('turbo:load', () => init());
})();