Bing Plus

Link Bing search results directly to real URL, show Gemini search results on the right side (PC only), and highlight ad links in green.

目前為 2025-03-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Bing Plus
// @version      1.1
// @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
// @license      MIT
// @namespace    http://tampermonkey.net/
// ==/UserScript==

(function () {
    'use strict';

    /*** 공통 유틸 함수 ***/
    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 = () => {
        // PC 환경 감지: 화면 크기와 User-Agent를 기반으로 판단
        return window.innerWidth > 768 && !/Mobi|Android|iPhone|iPad|iPod/.test(navigator.userAgent);
    };

    /*** Gemini 검색 결과 박스 생성 및 API 호출 로직 ***/
    let apiKey; // API 키 초기화

    if (isPCEnvironment()) {
        // PC 환경에서만 API 키를 요청
        apiKey = localStorage.getItem('geminiApiKey') || prompt('Gemini API 키를 입력하세요:');
        if (apiKey) localStorage.setItem('geminiApiKey', apiKey);
    }

    const markedParse = text => text
        .replace(/^### (.*$)/gm, '<h3>$1</h3>')
        .replace(/^## (.*$)/gm, '<h2>$1</h2>')
        .replace(/^# (.*$)/gm, '<h1>$1</h1>')
        .replace(/^\* (.*$)/gm, '<li>$1</li>')
        .replace(/^- (.*$)/gm, '<li>$1</li>')
        .replace(/``````/gs, '<pre><code>$1</code></pre>')
        .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
        .replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/g, '<em>$1</em>')
        .replace(/\n/g, '<br>');

    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 = `
            <div id="gemini-header">
                <img id="gemini-logo" src="https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg" alt="Gemini Logo">
                <h3>Gemini Search Results</h3>
            </div>
            <hr id="gemini-divider">
            <div id="gemini-content">Loading...</div>
        `;
        return box;
    };

    GM_addStyle(`
      #gemini-box { max-width:400px; background:#fff; border:1px solid #e0e0e0; padding:16px; margin-bottom:20px; font-family:sans-serif; }
      #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; }
    `);

    let currentQuery;
    let geminiResponseCache;

    const fetchGeminiResult = query => {
        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) }] }] }),
            onload({ responseText }) {
                if (currentQuery !== query) return;
                try {
                    geminiResponseCache = JSON.parse(responseText)?.candidates?.[0]?.content?.parts?.[0]?.text || 'No response';
                    document.getElementById('gemini-content').innerHTML = markedParse(geminiResponseCache);
                } catch {
                    document.getElementById('gemini-content').innerText = 'Error parsing response';
                }
            },
            onerror() { document.getElementById('gemini-content').innerText = 'API request failed'; }
        });
    };

    const ensureGeminiBox = () => {
        if (!isPCEnvironment()) return; // 모바일 환경에서는 실행하지 않음

        const queryParam = new URLSearchParams(location.search).get('q');
        if (!queryParam) return;

        let contextEl = document.getElementById('b_context');
        if (!contextEl) return;

        let geminiBoxEl = document.getElementById('gemini-box');

        if (!geminiBoxEl) contextEl.prepend(createGeminiBox());

        if (queryParam === currentQuery && geminiResponseCache) {
            // 캐시된 응답이 있으면 표시
            document.getElementById('gemini-content').innerHTML = markedParse(geminiResponseCache);
        } else {
            // 새로운 쿼리일 경우 API 호출
            currentQuery = queryParam;
            fetchGeminiResult(queryParam);
        }
    };

    // URL 변경 감지 및 Gemini 박스 유지
    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);

    // PC 환경일 경우에만 Gemini 박스 초기화
    if (isPCEnvironment()) ensureGeminiBox();

})();