Faster Bing

将 Bing 的重定向 url 转换为真实 url

  1. // ==UserScript==
  2. // @name Faster Bing
  3. // @name:en Faster Bing
  4. // @namespace CoderJiang
  5. // @version 1.1.4
  6. // @description 将 Bing 的重定向 url 转换为真实 url
  7. // @description:en Convert Bing's redirect url to a real url
  8. // @author CoderJiang
  9. // @match *://*.bing.com/*
  10. // @icon https://cdn.coderjiang.com/pic-go/2024/faster-bing-logo-v1.png!pure
  11. // @license MIT
  12. // @grant none
  13. // @note 2024-12-26 v1.1.4
  14. // - 功能:修复一些动态创建的链接(例如第一条人工智能生成的解答)无法解析的问题,同时解决在 v1.1.2 中无法解析的问题
  15. // @note 2024-07-19 v1.1.3
  16. // - 功能:对于 e.so.com/search/eclk 的链接,需要再次解析(示例:https://cn.bing.com/search?q=ui%E8%AE%BE%E8%AE%A1%E7%BD%91%E7%AB%99&first=1&FORM=PERE)
  17. // @note 2024-07-18 v1.1.2
  18. // - 修复:修复在 Edge 浏览器中第二次之后搜索无法解析的问题
  19. // - 功能:提供对 aclick 的中转链接解析支持
  20. // @note 2024-06-24 v1.1.1
  21. // - 修复:修复 Base64 解码问题,提升链接解析准确性。
  22. // - 优化:针对脚本运行两遍的问题,限制脚本在主页面上运行,无需在 iframes 再运行一次。
  23. // @note 2024-06-24 v1.1.0
  24. // - 优化:识别并处理站内相对 URL,确保这些 URL 被正确解析并转换为绝对形式。
  25. // - 优化:添加 LOGO
  26. // @noframes
  27. // ==/UserScript==
  28.  
  29. (function () {
  30. 'use strict';
  31.  
  32. const Config = {
  33. /**
  34. * 日志配置
  35. * log.enable 是否打印日志
  36. * log.success.icon 成功的图标
  37. * log.fail.icon 失败的图标
  38. * log.success.originalLink.maxLength 原始链接最大长度,如果为-1则不截断
  39. * log.success.originalLink.frontChars 原始链接前面保留的字符数
  40. * log.success.realLink.maxLength 真实链接最大长度,如果为-1则不截断
  41. * log.success.realLink.frontChars 真实链接前面保留的字符数
  42. * log.fail.originalLink.maxLength 原始链接最大长度,如果为-1则不截断
  43. * log.fail.originalLink.frontChars 原始链接前面保留的字符数
  44. * log.fail.realLink.display 失败时不显示真实链接,用这个字符串代替
  45. */
  46. log: {
  47. enable: true,
  48. success: {
  49. icon: '✅',
  50. originalLink: {
  51. maxLength: 30,
  52. frontChars: 10,
  53. },
  54. realLink: {
  55. maxLength: 30,
  56. frontChars: 20,
  57. }
  58. },
  59. fail: {
  60. icon: '❎',
  61. originalLink: {
  62. maxLength: -1,
  63. frontChars: 20,
  64. },
  65. realLink: {
  66. display: '------',
  67. }
  68. }
  69. }
  70. }
  71. const logo = `
  72. ________) ______
  73. (, / (, / ) ,
  74. /___, _ _ _/_ _ __ /---( __ _
  75. ) / (_(_/_)_(___(/_/ (_) / ____)_(_/ (_(_/_
  76. (_/ (_/ ( .-/
  77. (_/`;
  78.  
  79. /**
  80. * 获取url中的参数
  81. *
  82. * @param url url
  83. * @param key 参数名
  84. * @returns {string} 参数值,如果没有则返回null
  85. */
  86. function getUrlParameter(url, key) {
  87. const parsedUrl = new URL(url);
  88. const queryParams = parsedUrl.searchParams;
  89. return queryParams.get(key);
  90. }
  91.  
  92. /**
  93. * 判断是否是bing的重定向url
  94. *
  95. * @param url url
  96. * @returns {{valid: boolean, key: (string|null)}} 是否是bing的重定向url
  97. */
  98. function checkBingRedirectUrl(url) {
  99. const patterns = [
  100. // eg: https://www.bing.com/ck/a...
  101. {pattern: /^https?:\/\/(.*\.)?bing\.com\/(ck\/a|aclick)/, key: 'u'},
  102. // eg: https://e.so.com/search/eclk
  103. {pattern: /^https?:\/\/e\.so\.com\/search\/eclk/, key: 'aurl'},
  104. ];
  105. for (const {pattern, key} of patterns) {
  106. if (pattern.test(url)) {
  107. return {valid: true, key: key};
  108. }
  109. }
  110. return {valid: false, key: void 0};
  111. }
  112.  
  113. /**
  114. * 判断url是否有效
  115. * @param urlString url字符串
  116. * @returns {boolean} 是否是有效的url
  117. */
  118. function isValidUrl(urlString) {
  119. try {
  120. new URL(urlString);
  121. return true; // 没有抛出异常,URL有效
  122. } catch (error) {
  123. return false; // 抛出异常,URL无效
  124. }
  125. }
  126.  
  127. /**
  128. * 将重定向url转换为真实url
  129. * @param url Bing的重定向url,必须包含 key 指定的链接参数(eg: u)
  130. * @param key 参数名
  131. * @returns {string|null} 真实url,转换失败则返回null
  132. */
  133. function redirect2RealUrl(url, key) {
  134. let urlBase64 = getUrlParameter(url, key)
  135. urlBase64 = urlBase64.replace(/^a1/, '');
  136. let realUrl = ''
  137. try {
  138. // 还原Base64 URL编码中的特殊字符以便解码
  139. realUrl = atob(urlBase64.replace(/_/g, '/').replace(/-/g, '+'));
  140. } catch (error) {
  141. return null;
  142. }
  143. // 解码后的 URL 可能包含特殊字符,例如 http%3a%2f%2fpuaai.net
  144. realUrl = decodeURIComponent(realUrl);
  145. // 检查 realUrl 是否是有效的相对路径(以 '/' 开头)
  146. if (realUrl.startsWith('/')) {
  147. // 获取当前协议和域
  148. let currentUrl = window.location.origin; // e.g., "https://www.bing.com"
  149.  
  150. // 将相对路径追加到当前 URL
  151. realUrl = currentUrl + realUrl;
  152. }
  153.  
  154. if (!isValidUrl(realUrl)) {
  155. return null;
  156. }
  157. return realUrl;
  158. }
  159.  
  160. /**
  161. * 找到所有的链接并转换为真实url
  162. * @returns {{failedUrls: *[], processedUrls: *[]}}
  163. */
  164. function convertBingRedirectUrls() {
  165. const failedUrls = [];
  166. const processedUrls = [];
  167. const links = document.querySelectorAll("a");
  168.  
  169. for (const link of links) {
  170. const {realUrl, originalUrl} = convertBingRedirectUrl(link);
  171. if (realUrl) {
  172. processedUrls.push({
  173. realUrl,
  174. originalUrl
  175. });
  176. } else {
  177. failedUrls.push(originalUrl);
  178. }
  179. }
  180. return {
  181. processedUrls,
  182. failedUrls
  183. };
  184. }
  185.  
  186. /**
  187. * 将bing的重定向url转换为真实url
  188. * @param aElement a标签元素
  189. * @returns {{realUrl: null, originalUrl: null}}
  190. */
  191. function convertBingRedirectUrl(aElement) {
  192. // v1.1.3: 对于 e.so.com/search/eclk 的链接,需要再次解析,因此使用递归解析
  193. const resolve = (href) => {
  194. const checkResult = checkBingRedirectUrl(href)
  195. if (!checkResult.valid) return href
  196. const realUrl = redirect2RealUrl(href, checkResult.key);
  197. return realUrl ? resolve(realUrl) : null;
  198. }
  199. const originalUrl = aElement.href;
  200. const checkResult = checkBingRedirectUrl(originalUrl)
  201. let realUrl = null;
  202. if (checkResult.valid) {
  203. realUrl = resolve(originalUrl);
  204. if (realUrl) {
  205. aElement.href = realUrl;
  206. }
  207. }
  208. return {realUrl, originalUrl}
  209. }
  210.  
  211. /**
  212. * 可视化结果
  213. *
  214. * @param result 结果
  215. */
  216. function log(result) {
  217. function middleTruncate(str, maxLength, frontChars) {
  218. if (maxLength < 0) return str;
  219. if (str.length > maxLength) {
  220. const backChars = maxLength - frontChars - 3; // 确保总长度不超过maxLength
  221. return str.substring(0, frontChars) + '...' + str.substring(str.length - backChars);
  222. } else {
  223. return str;
  224. }
  225. }
  226.  
  227. const overview = {
  228. "Success": result.processedUrls.length,
  229. "Failed": result.failedUrls.length,
  230. }
  231. const details = [];
  232. const successSample = {
  233. "Status": Config.log.success.icon,
  234. "Original": "",
  235. "Real": "",
  236. }
  237. const failedSample = {
  238. "Status": Config.log.fail.icon,
  239. "Original": "",
  240. "Real": Config.log.fail.realLink.display,
  241. }
  242. for (const processedUrl of result.processedUrls) {
  243. const success = {...successSample};
  244. success.Original = middleTruncate(
  245. processedUrl.originalUrl,
  246. Config.log.success.originalLink.maxLength,
  247. Config.log.success.originalLink.frontChars);
  248. success.Real = middleTruncate(
  249. processedUrl.realUrl,
  250. Config.log.success.realLink.maxLength,
  251. Config.log.success.realLink.frontChars);
  252. details.push(success);
  253. }
  254. for (const failedUrl of result.failedUrls) {
  255. const failed = {...failedSample};
  256. failed.Original = middleTruncate(failedUrl,
  257. Config.log.fail.originalLink.maxLength,
  258. Config.log.fail.originalLink.frontChars);
  259. details.push(failed);
  260. }
  261.  
  262. console.table(overview);
  263. console.table(details);
  264. }
  265.  
  266. console.log(logo);
  267.  
  268. const result = convertBingRedirectUrls();
  269. if (Config.log.enable) {
  270. log(result)
  271. }
  272.  
  273. // v1.1.3: 修复一些动态创建的链接无法解析的问题,同时解决在 v1.1.2 中无法解析的问题
  274. new MutationObserver((mutationsList) => {
  275. for (const mutation of mutationsList) {
  276. // 检测到新的 a 标签被添加
  277. if (mutation.type === 'childList') {
  278. mutation.addedNodes.forEach((node) => {
  279. if (node.nodeType === 1) { // 元素节点
  280. // 如果是 a 标签
  281. if (node.tagName === 'A') {
  282. convertBingRedirectUrl(node);
  283. }
  284. // 如果子节点中包含 a 标签
  285. node.querySelectorAll?.('a').forEach((a) => {
  286. convertBingRedirectUrl(a);
  287. });
  288. }
  289. });
  290. }
  291. // 检测到 a 标签的 href 属性被修改
  292. if (mutation.type === 'attributes' && mutation.attributeName === 'href') {
  293. const target = mutation.target;
  294. if (target.tagName === 'A') {
  295. convertBingRedirectUrl(target);
  296. }
  297. }
  298. }
  299. }).observe(document.body, {
  300. childList: true, // 监听子节点的添加或删除
  301. subtree: true, // 监听所有后代节点
  302. attributes: true, // 监听属性变化
  303. attributeFilter: ['href'] // 仅监听 href 属性的变化
  304. });
  305.  
  306. })();