// ==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();
})();