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-04-02 提交的版本,查看 最新版本

// ==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/
// ==/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 = `
            <p>${messages.title}</p>
            <p>${messages.current}: ${currentVersion}</p>
            <p>${messages.latest}: ${latestVersion}</p>
            <button id="dismissUpdatePopup">${messages.confirm}</button>
        `;
        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 = `
            <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; 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();

})();