// ==UserScript==
// @name Faster Bing
// @name:en Faster Bing
// @namespace CoderJiang
// @version 1.1.3
// @description 将 Bing 的重定向 url 转换为真实 url
// @description:en Convert Bing's redirect url to a real url
// @author CoderJiang
// @match *://*.bing.com/*
// @icon https://cdn.coderjiang.com/pic-go/2024/faster-bing-logo-v1.png!pure
// @license MIT
// @grant none
// @note 2024-07-19 v1.1.3
// - 功能:对于 e.so.com/search/eclk 的链接,需要再次解析(示例:Firefox 访问 https://cn.bing.com/search?q=ui%E8%AE%BE%E8%AE%A1%E7%BD%91%E7%AB%99&first=1&FORM=PERE 时)
// @note 2024-07-18 v1.1.2
// - 修复:修复在 Edge 浏览器中第二次之后搜索无法解析的问题
// - 功能:提供对 aclick 的中转链接解析支持
// @note 2024-06-24 v1.1.1
// - 修复:修复 Base64 解码问题,提升链接解析准确性。
// - 优化:针对脚本运行两遍的问题,限制脚本在主页面上运行,无需在 iframes 再运行一次。
// @note 2024-06-24 v1.1.0
// - 优化:识别并处理站内相对 URL,确保这些 URL 被正确解析并转换为绝对形式。
// - 优化:添加 LOGO
// @noframes
// ==/UserScript==
(function () {
'use strict';
const Config = {
/**
* 日志配置
* log.enable 是否打印日志
* log.success.icon 成功的图标
* log.fail.icon 失败的图标
* log.success.originalLink.maxLength 原始链接最大长度,如果为-1则不截断
* log.success.originalLink.frontChars 原始链接前面保留的字符数
* log.success.realLink.maxLength 真实链接最大长度,如果为-1则不截断
* log.success.realLink.frontChars 真实链接前面保留的字符数
* log.fail.originalLink.maxLength 原始链接最大长度,如果为-1则不截断
* log.fail.originalLink.frontChars 原始链接前面保留的字符数
* log.fail.realLink.display 失败时不显示真实链接,用这个字符串代替
*/
log: {
enable: true,
success: {
icon: '✅',
originalLink: {
maxLength: 30,
frontChars: 10,
},
realLink: {
maxLength: 30,
frontChars: 20,
}
},
fail: {
icon: '❎',
originalLink: {
maxLength: -1,
frontChars: 20,
},
realLink: {
display: '------',
}
}
}
}
const logo = `
________) ______
(, / (, / ) ,
/___, _ _ _/_ _ __ /---( __ _
) / (_(_/_)_(___(/_/ (_) / ____)_(_/ (_(_/_
(_/ (_/ ( .-/
(_/`;
/**
* 获取url中的参数
*
* @param url url
* @param key 参数名
* @returns {string} 参数值,如果没有则返回null
*/
function getUrlParameter(url, key) {
const parsedUrl = new URL(url);
const queryParams = parsedUrl.searchParams;
return queryParams.get(key);
}
/**
* 判断是否是bing的重定向url
*
* @param url url
* @returns {{valid: boolean, key: (string|null)}} 是否是bing的重定向url
*/
function checkBingRedirectUrl(url) {
const patterns = [
// eg: https://www.bing.com/ck/a...
{pattern: /^https?:\/\/(.*\.)?bing\.com\/(ck\/a|aclick)/, key: 'u'},
// eg: https://e.so.com/search/eclk
{pattern: /^https?:\/\/e\.so\.com\/search\/eclk/, key: 'aurl'},
];
for (const {pattern, key} of patterns) {
if (pattern.test(url)) {
return {valid: true, key: key};
}
}
return {valid: false, key: void 0};
}
/**
* 判断url是否有效
* @param urlString url字符串
* @returns {boolean} 是否是有效的url
*/
function isValidUrl(urlString) {
try {
new URL(urlString);
return true; // 没有抛出异常,URL有效
} catch (error) {
return false; // 抛出异常,URL无效
}
}
/**
* 将重定向url转换为真实url
* @param url Bing的重定向url,必须包含 key 指定的链接参数(eg: u)
* @param key 参数名
* @returns {string|null} 真实url,转换失败则返回null
*/
function redirect2RealUrl(url, key) {
let urlBase64 = getUrlParameter(url, key)
urlBase64 = urlBase64.replace(/^a1/, '');
let realUrl = ''
try {
// 还原Base64 URL编码中的特殊字符以便解码
realUrl = atob(urlBase64.replace(/_/g, '/').replace(/-/g, '+'));
} catch (error) {
return null;
}
// 解码后的 URL 可能包含特殊字符,例如 http%3a%2f%2fpuaai.net
realUrl = decodeURIComponent(realUrl);
// 检查 realUrl 是否是有效的相对路径(以 '/' 开头)
if (realUrl.startsWith('/')) {
// 获取当前协议和域
let currentUrl = window.location.origin; // e.g., "https://www.bing.com"
// 将相对路径追加到当前 URL
realUrl = currentUrl + realUrl;
}
if (!isValidUrl(realUrl)) {
return null;
}
return realUrl;
}
/**
* 找到所有的链接并转换为真实url
* @returns {{failedUrls: *[], processedUrls: *[]}}
*/
function convertBingRedirectUrls() {
const failedUrls = [];
const processedUrls = [];
const links = document.querySelectorAll("a");
// v1.1.3: 对于 e.so.com/search/eclk 的链接,需要再次解析,因此使用递归解析
const resolve = (href) => {
const checkResult = checkBingRedirectUrl(href)
if (!checkResult.valid) return href
const realUrl = redirect2RealUrl(href, checkResult.key);
return realUrl ? resolve(realUrl) : null;
}
for (const link of links) {
const href = link.href;
const checkResult = checkBingRedirectUrl(href)
if (checkResult.valid) {
const realUrl = resolve(href);
if (realUrl) {
link.href = realUrl;
processedUrls.push({
realUrl,
originalUrl: href
});
} else {
failedUrls.push(href);
}
}
}
return {
processedUrls,
failedUrls
};
}
/**
* 可视化结果
*
* @param result 结果
*/
function log(result) {
function middleTruncate(str, maxLength, frontChars) {
if (maxLength < 0) return str;
if (str.length > maxLength) {
const backChars = maxLength - frontChars - 3; // 确保总长度不超过maxLength
return str.substring(0, frontChars) + '...' + str.substring(str.length - backChars);
} else {
return str;
}
}
const overview = {
"Success": result.processedUrls.length,
"Failed": result.failedUrls.length,
}
const details = [];
const successSample = {
"Status": Config.log.success.icon,
"Original": "",
"Real": "",
}
const failedSample = {
"Status": Config.log.fail.icon,
"Original": "",
"Real": Config.log.fail.realLink.display,
}
for (const processedUrl of result.processedUrls) {
const success = {...successSample};
success.Original = middleTruncate(
processedUrl.originalUrl,
Config.log.success.originalLink.maxLength,
Config.log.success.originalLink.frontChars);
success.Real = middleTruncate(
processedUrl.realUrl,
Config.log.success.realLink.maxLength,
Config.log.success.realLink.frontChars);
details.push(success);
}
for (const failedUrl of result.failedUrls) {
const failed = {...failedSample};
failed.Original = middleTruncate(failedUrl,
Config.log.fail.originalLink.maxLength,
Config.log.fail.originalLink.frontChars);
details.push(failed);
}
console.table(overview);
console.table(details);
}
console.log(logo);
const result = convertBingRedirectUrls();
if (Config.log.enable) {
log(result)
}
// v1.1.2: 兼容 Edge 浏览器,解决在第二次搜索时无法解析的问题
new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach(node => {
if (node.id === 'b_content') {
const result = convertBingRedirectUrls();
if (Config.log.enable) {
log(result)
}
}
});
});
}).observe(document.body, {
childList: true,
subtree: false,
});
})();