// ==UserScript== // @name Bing Plus // @version 2.0 // @description Link Bing search results directly to real URL, show Gemini search results on the right side (PC only), and highlight ad links in green. Gemini response is now cached across pages. // @author lanpod // @match https://www.bing.com/search* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @require https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js // @license MIT // @namespace http://tampermonkey.net/ // @downloadURL none // ==/UserScript== (function () { 'use strict'; /*** ๐Ÿ“Œ ์„ค์ • ์ƒ์ˆ˜ ***/ const MARKED_VERSION = '15.0.7'; // ์‚ฌ์šฉ ์ค‘์ธ marked.js ๋ฒ„์ „ const GEMINI_MODEL = 'gemini-2.0-flash'; // Gemini API ๋ชจ๋ธ ์ด๋ฆ„ // ํ˜„์žฌ ๋””๋ฐ”์ด์Šค๊ฐ€ ๋ฐ์Šคํฌํ†ฑ์ธ์ง€ ํŒ๋‹จ const isDesktop = () => window.innerWidth > 768 && !/Mobi|Android/i.test(navigator.userAgent); /*** ๐Ÿงฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋ชจ์Œ ***/ // ๋ฒ„์ „ ๋น„๊ต ํ•จ์ˆ˜ (ํ˜„์žฌ vs ์ตœ์‹  marked.js) const compareVersions = (v1, v2) => { const a = v1.split('.').map(Number), b = v2.split('.').map(Number); for (let i = 0; i < Math.max(a.length, b.length); i++) { if ((a[i] || 0) < (b[i] || 0)) return -1; if ((a[i] || 0) > (b[i] || 0)) return 1; } return 0; }; // ๋ธŒ๋ผ์šฐ์ € ์–ธ์–ด์— ๋”ฐ๋ผ Gemini์— ๋ณด๋‚ผ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ const getLocalizedPrompt = (query) => { const lang = navigator.language; if (lang.includes('ko')) return `"${query}"์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋งˆํฌ๋‹ค์šด ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•ด์ค˜`; if (lang.includes('zh')) return `่ฏทไปฅๆ ‡่ฎฐๆ ผๅผๅกซๅ†™ๆœ‰ๅ…ณ"${query}"็š„ไฟกๆฏใ€‚`; return `Please write information about "${query}" in markdown format`; }; // ์ถ”์  URL์˜ base64 ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋””์ฝ”๋”ฉํ•˜์—ฌ ์‹ค์ œ URL ๋ฐ˜ํ™˜ const decodeRedirectUrl = (url, key) => { const param = new URL(url).searchParams.get(key)?.replace(/^a1/, ''); if (!param) return null; try { const decoded = decodeURIComponent(atob(param.replace(/_/g, '/').replace(/-/g, '+'))); return decoded.startsWith('/') ? location.origin + decoded : decoded; } catch { return null; } }; // ํŠน์ • redirect URL ํŒจํ„ด์— ๋”ฐ๋ผ ์‹ค์ œ URL ๋ฐ˜ํ™˜ const resolveRealUrl = (url) => { const rules = [ { pattern: /bing\.com\/(ck\/a|aclick)/, key: 'u' }, { pattern: /so\.com\/search\/eclk/, key: 'aurl' } ]; for (const { pattern, key } of rules) { if (pattern.test(url)) { const real = decodeRedirectUrl(url, key); if (real && real !== url) return real; } } return url; }; // ๊ฒ€์ƒ‰๊ฒฐ๊ณผ ๋‚ด์˜ ๋ชจ๋“  ๋งํฌ๋ฅผ ์‹ค์ œ URL๋กœ ๋ณ€ํ™˜ const convertLinksToReal = (root) => { root.querySelectorAll('a[href]').forEach(a => { const realUrl = resolveRealUrl(a.href); if (realUrl && realUrl !== a.href) a.href = realUrl; }); }; /*** ๐ŸŽจ CSS ์Šคํƒ€์ผ ์ ์šฉ ***/ GM_addStyle(`#b_results > li.b_ad a { color: green !important; }`); // ๊ด‘๊ณ  ๋งํฌ ๊ฐ•์กฐ // Gemini ๋ฐ•์Šค ์Šคํƒ€์ผ GM_addStyle(` #gemini-box { max-width: 400px; background: #fff; border: 1px solid #e0e0e0; padding: 16px; margin-bottom: 20px; font-family: sans-serif; overflow-x: auto; position: relative; } #gemini-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } #gemini-title-wrap { display: flex; align-items: center; } #gemini-logo { width: 24px; height: 24px; margin-right: 8px; } #gemini-box h3 { margin: 0; font-size: 18px; color: #202124; } #gemini-refresh-btn { width: 20px; height: 20px; cursor: pointer; opacity: 0.6; transition: transform 0.5s ease; } #gemini-refresh-btn:hover { opacity: 1; transform: rotate(360deg); } #gemini-divider { height: 1px; background: #e0e0e0; margin: 8px 0; } #gemini-content { font-size: 14px; line-height: 1.6; color: #333; white-space: pre-wrap; word-wrap: break-word; } #gemini-content pre { background: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto; } `); /*** ๐Ÿ”‘ Gemini API ํ‚ค ๊ด€๋ฆฌ ***/ const getApiKey = () => { if (!isDesktop()) return null; let key = localStorage.getItem('geminiApiKey'); if (!key) { key = prompt('Gemini API ํ‚ค๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”:'); if (key) localStorage.setItem('geminiApiKey', key); } return key; }; /*** โš ๏ธ marked.js ์ตœ์‹  ๋ฒ„์ „ ํ™•์ธ ์•Œ๋ฆผ ***/ const checkMarkedJsVersion = () => { if (localStorage.getItem('markedUpdateDismissed') === MARKED_VERSION) return; GM_xmlhttpRequest({ method: 'GET', url: 'https://api.cdnjs.com/libraries/marked', onload({ responseText }) { try { const latest = JSON.parse(responseText).version; if (compareVersions(MARKED_VERSION, latest) < 0) { const warning = document.createElement('div'); warning.innerHTML = `

marked.min.js ์—…๋ฐ์ดํŠธ ํ•„์š”

ํ˜„์žฌ: ${MARKED_VERSION}
์ตœ์‹ : ${latest}

`; warning.querySelector('button').onclick = () => { localStorage.setItem('markedUpdateDismissed', MARKED_VERSION); warning.remove(); }; document.body.appendChild(warning); } } catch {} } }); }; /*** ๐Ÿ“ก Gemini ์‘๋‹ต ์š”์ฒญ (์บ์‹œ ํ™œ์šฉ ํฌํ•จ) ***/ const fetchGeminiResponse = (query, container, apiKey, force = false) => { const cacheKey = `gemini_cache_${query}`; if (!force) { const cached = sessionStorage.getItem(cacheKey); if (cached) { container.innerHTML = marked.parse(cached); return; } } container.textContent = 'Loading...'; checkMarkedJsVersion(); GM_xmlhttpRequest({ method: 'POST', url: `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${apiKey}`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ contents: [{ parts: [{ text: getLocalizedPrompt(query) }] }] }), onload({ responseText }) { try { const text = JSON.parse(responseText)?.candidates?.[0]?.content?.parts?.[0]?.text; if (text) { sessionStorage.setItem(cacheKey, text); container.innerHTML = marked.parse(text); } else { container.textContent = 'โš ๏ธ Gemini response is empty.'; } } catch (e) { container.textContent = `โŒ Parsing error: ${e.message}`; } }, onerror(err) { container.textContent = `โŒ Network error: ${err.finalUrl}`; }, ontimeout() { container.textContent = 'โŒ Request timeout'; } }); }; /*** ๐Ÿงฑ Gemini UI ์š”์†Œ ์ƒ์„ฑ ๋ฐ ์ดˆ๊ธฐํ™” ***/ const createGeminiBox = (query, apiKey) => { const wrapper = document.createElement('div'); wrapper.id = 'gemini-box'; wrapper.innerHTML = `

Gemini Search Results


Loading...
`; const refreshBtn = wrapper.querySelector('#gemini-refresh-btn'); const content = wrapper.querySelector('#gemini-content'); refreshBtn.onclick = () => fetchGeminiResponse(query, content, apiKey, true); // ์ƒˆ๋กœ๊ณ ์นจ ํด๋ฆญ ์‹œ ๊ฐ•์ œ ์š”์ฒญ return wrapper; }; /*** ๐Ÿ”„ Gemini ๊ฒฐ๊ณผ ๋ฐ•์Šค ๋ Œ๋”๋ง ***/ const renderGeminiOnSearch = () => { if (!isDesktop()) return; const query = new URLSearchParams(location.search).get('q'); if (!query) return; const sidebar = document.getElementById('b_context'); if (!sidebar || document.getElementById('gemini-box')) return; const apiKey = getApiKey(); if (!apiKey) return; const geminiBox = createGeminiBox(query, apiKey); sidebar.prepend(geminiBox); const contentDiv = geminiBox.querySelector('#gemini-content'); const cache = sessionStorage.getItem(`gemini_cache_${query}`); contentDiv.innerHTML = cache ? marked.parse(cache) : 'Loading...'; if (!cache) fetchGeminiResponse(query, contentDiv, apiKey); }; /*** ๐Ÿš€ ์ดˆ๊ธฐ ์‹คํ–‰ ๋ฐ URL ๋ณ€๊ฒฝ ๊ฐ์ง€ ํ•ธ๋“ค๋ง ***/ const init = () => { convertLinksToReal(document); // ๋งํฌ ์‹ค์ฃผ์†Œ ๋ณ€ํ™˜ renderGeminiOnSearch(); // Gemini ๋ฐ•์Šค ๋ Œ๋”๋ง // SPA ๋Œ€์‘: URL ๋ณ€๊ฒฝ ๊ฐ์ง€ํ•˜์—ฌ ๋‹ค์‹œ ์ฒ˜๋ฆฌ let lastUrl = location.href; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; renderGeminiOnSearch(); convertLinksToReal(document); } }).observe(document.body, { childList: true, subtree: true }); }; init(); // ์Šคํฌ๋ฆฝํŠธ ์‹œ์ž‘ })();