// ==UserScript== // @name Bing Plus // @version 1.6 // @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 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}