// ==UserScript== // @name Bing Plus // @version 1.7 // @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_Name = 'gemini-2.0-flash' const isDesktop = () => window.innerWidth > 768 && !/Mobi|Android/i.test(navigator.userAgent); // 데스크탑 환경 여부 판별 함수 /*** ───────────────────────────────────────────── * 📦 유틸리티 함수 모음 * ───────────────────────────────────────────── */ // 버전 비교 함수 (v1이 더 낮으면 -1, 같으면 0, 높으면 1) 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의 파라미터 값을 디코딩하여 실제 링크를 추출하는 함수 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; } }; // 특정 규칙에 따라 실제 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; }); }; /*** ───────────────────────────────────────────── * 🎨 스타일 추가 (광고 강조, Gemini 박스 디자인) * ───────────────────────────────────────────── */ 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; } #gemini-header { display:flex; align-items:center; margin-bottom:8px; } #gemini-logo { width:24px; height:24px; margin-right:8px; } #gemini-box h3 { margin:0; font-size:18px; color:#202124; } #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; } `); /*** ───────────────────────────────────────────── * 🔐 API 키 처리 및 marked.js 버전 체크 * ───────────────────────────────────────────── */ // 사용자가 입력한 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) => { const cacheKey = `gemini_cache_${query}`; const cached = sessionStorage.getItem(cacheKey); // 세션 캐시가 있으면 바로 표시 if (cached) { container.innerHTML = marked.parse(cached); return; } checkMarkedJsVersion(); // 버전 체크 // Gemini API 호출 (Flash 모델 사용) GM_xmlhttpRequest({ method: 'POST', url: `https://generativelanguage.googleapis.com/v1beta/models/${Gemini_Model_Name}: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 = '⚠️ 응답에 유효한 내용이 없습니다.'; } } catch (e) { container.textContent = `❌ 응답 파싱 중 오류 발생: ${e.message}`; } }, onerror(err) { container.textContent = `❌ Gemini 요청 중 네트워크 오류 발생\n\n🔗 ${err.finalUrl}`; }, ontimeout() { container.textContent = '❌ 요청 시간이 초과되었습니다.'; } }); }; /*** ───────────────────────────────────────────── * 🧠 Gemini 박스 UI 생성 및 삽입 * ───────────────────────────────────────────── */ // Gemini 박스를 DOM 요소로 생성 const createGeminiBox = () => { const wrapper = document.createElement('div'); wrapper.id = 'gemini-box'; wrapper.innerHTML = `

Gemini Search Results


Loading...
`; 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) return; let geminiBox = document.getElementById('gemini-box'); if (!geminiBox) { geminiBox = createGeminiBox(); 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) { const apiKey = getApiKey(); if (apiKey) fetchGeminiResponse(query, contentDiv, apiKey); } }; /*** ───────────────────────────────────────────── * 🚀 초기화 및 URL 변경 감지 * ───────────────────────────────────────────── */ const init = () => { convertLinksToReal(document); // 모든 링크를 실제 URL로 변환 renderGeminiOnSearch(); // 초기 페이지 로드 시 Gemini 박스 표시 // 주소(URL)가 변경될 경우에도 Gemini 박스를 다시 렌더링 let lastUrl = location.href; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; renderGeminiOnSearch(); convertLinksToReal(document); } }).observe(document.body, { childList: true, subtree: true }); }; init(); // 스크립트 실행 시작 })();