// ==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}