Greasy Fork

Greasy Fork is available in English.

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.2
// @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();

})();