// ==UserScript== // @name Bing Plus // @version 1.4 // @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. // @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'; /*** marked 버전 동적 추출 ***/ const REQUIRE_URL = 'https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js'; // @require와 수동 동기화 필요 const CURRENT_MARKED_VERSION = REQUIRE_URL.match(/marked\/([\d.]+)\/marked\.min\.js/)[1]; /*** 버전 확인 및 커스텀 팝업 로직 ***/ let hasCheckedVersion = false; const checkMarkedVersion = () => { if (hasCheckedVersion || localStorage.getItem('markedUpdateDismissed') === CURRENT_MARKED_VERSION) return; hasCheckedVersion = true; GM_xmlhttpRequest({ method: 'GET', url: 'https://api.cdnjs.com/libraries/marked', onload({ responseText }) { try { const data = JSON.parse(responseText); const latestVersion = data.version; if (compareVersions(CURRENT_MARKED_VERSION, latestVersion) < 0) { showUpdatePopup(CURRENT_MARKED_VERSION, latestVersion); } } catch (e) { console.error('Failed to check marked version:', e); } }, onerror() { console.error('Failed to fetch marked version from cdnjs API'); } }); }; // 버전 비교 함수 const compareVersions = (v1, v2) => { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const n1 = parts1[i] || 0; const n2 = parts2[i] || 0; if (n1 < n2) return -1; if (n1 > n2) return 1; } return 0; }; // 언어별 메시지 함수 const getUpdateMessage = () => { const lang = navigator.language; if (lang.includes('ko')) { return { title: 'marked.min.js 의 업데이트가 필요합니다', current: '현재 버전', latest: '최신 버전', confirm: '확인' }; } else if (lang.includes('zh')) { return { title: '需要更新 marked.min.js', current: '当前版本', latest: '最新版本', confirm: '确认' }; } else { return { title: 'marked.min.js needs an update', current: 'Current version', latest: 'Latest version', confirm: 'OK' }; } }; // 커스텀 팝업 const showUpdatePopup = (currentVersion, latestVersion) => { const messages = getUpdateMessage(); const popup = document.createElement('div'); popup.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 0 10px rgba(0,0,0,0.3); z-index: 10000; font-family: Arial, sans-serif; text-align: center; `; popup.innerHTML = `

${messages.title}

${messages.current}: ${currentVersion}

${messages.latest}: ${latestVersion}

`; document.body.appendChild(popup); document.getElementById('dismissUpdatePopup').addEventListener('click', () => { localStorage.setItem('markedUpdateDismissed', currentVersion); popup.remove(); }); }; /*** 공통 유틸 함수 ***/ const getUrlParam = (url, key) => new URL(url).searchParams.get(key); const patterns = [ { pattern: /^https?:\/\/(.*\.)?bing\.com\/(ck\/a|aclick)/, key: 'u' }, { pattern: /^https?:\/\/e\.so\.com\/search\/eclk/, key: 'aurl' }, ]; const isRedirectUrl = url => patterns.find(p => p.pattern.test(url)); const decodeRedirectUrl = (url, key) => { let encodedUrl = getUrlParam(url, key)?.replace(/^a1/, ''); if (!encodedUrl) return null; try { let decodedUrl = decodeURIComponent(atob(encodedUrl.replace(/_/g, '/').replace(/-/g, '+'))); return decodedUrl.startsWith('/') ? window.location.origin + decodedUrl : decodedUrl; } catch { return null; } }; const resolveRealUrl = url => { let match; while ((match = isRedirectUrl(url))) { const realUrl = decodeRedirectUrl(url, match.key); if (!realUrl || realUrl === url) break; url = realUrl; } return url; }; /*** 링크 URL 변환 로직 ***/ const convertLinks = root => { root.querySelectorAll('a[href]').forEach(a => { const realUrl = resolveRealUrl(a.href); if (realUrl && realUrl !== a.href) a.href = realUrl; }); }; /*** 광고 링크 스타일 적용 (초록색) ***/ GM_addStyle(`#b_results > li.b_ad a { color: green !important; }`); /*** PC 환경 확인 함수 ***/ const isPCEnvironment = () => window.innerWidth > 768 && !/Mobi|Android|iPhone|iPad|iPod/.test(navigator.userAgent); /*** Gemini 검색 결과 박스 생성 및 API 호출 로직 ***/ let apiKey; if (isPCEnvironment()) { apiKey = localStorage.getItem('geminiApiKey') || prompt('Gemini API 키를 입력하세요:'); if (apiKey) localStorage.setItem('geminiApiKey', apiKey); } const markedParse = text => marked.parse(text); const getPromptQuery = 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`; }; const createGeminiBox = () => { const box = document.createElement('div'); box.id = 'gemini-box'; box.innerHTML = `

Gemini Search Results


Loading...
`; return box; }; 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; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; } #gemini-content pre { background:#f5f5f5; padding:10px; border-radius:5px; overflow-x: auto; } `); let currentQuery; let geminiResponseCache; const fetchGeminiResult = query => { if (!apiKey) { document.getElementById('gemini-content').innerText = 'Error: No API key provided'; return; } checkMarkedVersion(); GM_xmlhttpRequest({ method: 'POST', url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ "contents": [{ "parts": [{"text": getPromptQuery(query)}] }] }), timeout: 10000, onload({ responseText }) { if (currentQuery !== query) return; try { const response = JSON.parse(responseText); console.log('Gemini API Response:', response); if (!response || !response.candidates || response.candidates.length === 0) { document.getElementById('gemini-content').innerText = 'No content available: API returned empty response'; return; } geminiResponseCache = response.candidates[0]?.content?.parts?.[0]?.text; if (!geminiResponseCache) { document.getElementById('gemini-content').innerText = 'No content available: Response lacks valid text'; return; } document.getElementById('gemini-content').innerHTML = markedParse(geminiResponseCache); } catch (e) { document.getElementById('gemini-content').innerText = `Error parsing response: ${e.message}`; } }, onerror() { document.getElementById('gemini-content').innerText = 'API request failed: Network error'; }, ontimeout() { document.getElementById('gemini-content').innerText = 'API request failed: Timeout (response took too long)'; } }); }; const ensureGeminiBox = () => { if (!isPCEnvironment()) return; let contextEl = document.getElementById('b_context'); if (!contextEl) return; let geminiBoxEl = document.getElementById('gemini-box'); if (!geminiBoxEl) { geminiBoxEl = createGeminiBox(); contextEl.prepend(geminiBoxEl); } const queryParam = new URLSearchParams(location.search).get('q'); if (queryParam !== currentQuery) { currentQuery = queryParam; fetchGeminiResult(queryParam); } }; let lastHref = location.href; new MutationObserver(() => { if (location.href !== lastHref) { lastHref = location.href; ensureGeminiBox(); convertLinks(document); } }).observe(document.body, { childList: true, subtree: true }); // 초기 실행 convertLinks(document); if (isPCEnvironment()) ensureGeminiBox(); })();