├── .gitignore ├── readme.md ├── Baidu-Search-Font-Size++ └── user.css ├── Chrome-Scrollbar-Beautify └── user.css ├── Disable-SendBeacon └── user.js ├── Rename-Zhuhu-Daily-Title └── user.js ├── Douban-Book-to-UJS-Library └── user.js ├── GitHub-File-History └── user.js ├── Ask-AI-Anywhere ├── README_zh.md ├── README.md └── user.js ├── FT-Auto-Full-Article └── user.js ├── Qireader-Keymap └── user.js ├── Video-Auto-Fullscreen └── user.js ├── RSS-to-Inoreader └── user.js ├── RSS-to-Clipboard └── user.js └── GitHub-Star-History └── user.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # UserJS 2 | 3 | 自己写的一些脚本收集 -------------------------------------------------------------------------------- /Baidu-Search-Font-Size++/user.css: -------------------------------------------------------------------------------- 1 | .t { 2 | font-size: 18px; 3 | } 4 | 5 | .c-abstract { 6 | font-size: 15px; 7 | } -------------------------------------------------------------------------------- /Chrome-Scrollbar-Beautify/user.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 7px; 3 | height: 7px; 4 | } 5 | 6 | ::-webkit-scrollbar-thumb:vertical { 7 | background-color: #CCC; 8 | -webkit-border-radius: 6px; 9 | } 10 | 11 | ::-webkit-scrollbar-thumb:horizontal { 12 | background-color: #CCC; 13 | -webkit-border-radius: 6px; 14 | } 15 | 16 | ::-webkit-scrollbar-thumb:active { 17 | background-color: #666; 18 | } -------------------------------------------------------------------------------- /Disable-SendBeacon/user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 禁用sendBeacon 3 | // @namespace https://blog.xlab.app/ 4 | // @more https://github.com/ttttmr/UserJS 5 | // @version 0.2 6 | // @description 禁用navigator.sendBeacon 7 | // @author tmr 8 | // @include http://* 9 | // @include https://* 10 | // @grant none 11 | // ==/UserScript== 12 | 13 | (function () { 14 | 'use strict'; 15 | navigator.sendBeacon = function (url, data) { 16 | // console.log('fake sendBeacon: ', url, data); 17 | return true; 18 | }; 19 | })(); 20 | -------------------------------------------------------------------------------- /Rename-Zhuhu-Daily-Title/user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 修改知乎日报标题 3 | // @namespace https://blog.xlab.app/ 4 | // @more https://github.com/ttttmr/UserJS 5 | // @version 0.6 6 | // @description 修改知乎日报标题为文章标题 7 | // @author tmr 8 | // @match *://daily.zhihu.com/story/* 9 | // @grant none 10 | // ==/UserScript== 11 | 12 | (function () { 13 | 'use strict'; 14 | function rename() { 15 | let zh_title = document.querySelector("p[class=DailyHeader-title]").innerText; 16 | let new_title = zh_title + "-知乎日报"; 17 | if (document.title !== new_title) { 18 | document.title = new_title; 19 | } 20 | setTimeout(rename, 1000); 21 | } 22 | rename(); 23 | })(); -------------------------------------------------------------------------------- /Douban-Book-to-UJS-Library/user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 豆瓣读书直达江苏大学图书馆搜索 3 | // @namespace https://blog.xlab.app/ 4 | // @more https://github.com/ttttmr/UserJS 5 | // @version 0.3 6 | // @description 豆瓣读书直达江苏大学图书馆搜索,方便找书 7 | // @author tmr 8 | // @include https://book.douban.com/* 9 | // @grant none 10 | // ==/UserScript== 11 | 12 | (function () { 13 | 'use strict'; 14 | let title = document.getElementsByTagName('h1')[0].children[0].innerText; 15 | let info = document.getElementById('info'); 16 | let liblink = document.createElement('a'); 17 | let isbn = info.innerText.match(/\d{13}|\d{10}/)[0]; 18 | liblink.href = 19 | 'http://huiwen.ujs.edu.cn:8080/opac/openlink.php?strSearchType=isbn&match_flag=full&historyCount=1&strText=' + 20 | isbn + 21 | '&doctype=ALL&with_ebook=on&displaypg=20&showmode=list&sort=CATA_DATE&orderby=desc&location=ALL'; 22 | liblink.target = '_blank'; 23 | liblink.innerText = '去江苏大学图书馆搜索'; 24 | info.appendChild(liblink); 25 | })(); 26 | -------------------------------------------------------------------------------- /GitHub-File-History/user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name GitHub File History 3 | // @namespace https://blog.xlab.app/ 4 | // @more https://github.com/ttttmr/UserJS 5 | // @version 0.7 6 | // @description GitHub File History 快速跳转到 https://github.githistory.xyz/ 7 | // @author tmr 8 | // @match https://github.com/*/* 9 | // @grant none 10 | // ==/UserScript== 11 | 12 | (async function () { 13 | "use strict"; 14 | let count = 0; 15 | function run() { 16 | if (document.readyState == "complete" && count > 5) { 17 | return; 18 | } 19 | count++; 20 | let n = document.querySelector("svg.octicon-history").parentElement 21 | .parentElement.parentElement.parentElement; 22 | if (n == null) { 23 | setTimeout(run, 500); 24 | return; 25 | } 26 | 27 | let cn = n.cloneNode(true); 28 | cn.firstChild.textContent = "Git History"; 29 | cn.querySelector("a").href = cn 30 | .querySelector("a") 31 | .href.replace("github.com", "github.githistory.xyz"); 32 | n.parentElement.append(cn); 33 | } 34 | run(); 35 | })(); 36 | -------------------------------------------------------------------------------- /Ask-AI-Anywhere/README_zh.md: -------------------------------------------------------------------------------- 1 | # Ask AI Anywhere (一键发送到 AI) 2 | 3 | [English](README.md) 4 | 5 | 这是一个用户脚本,允许你快速将网页内容发送到 AI (Gemini, ChatGPT, AI Studio, DeepSeek) 进行总结或对话。 6 | 7 | - **Markdown 格式**:网页内容会自动转换为 Markdown 格式。 8 | - **支持图片**:如果包含图片,图片也会被一起发送。 9 | 10 | ## 安装 11 | 12 | 1. 安装用户脚本管理器,如 [Tampermonkey](https://www.tampermonkey.net/) (篡改猴)、[Violentmonkey](https://violentmonkey.github.io/) (暴力猴) 或 [ScriptCat](https://docs.scriptcat.org/) (脚本猫)。 13 | 2. 从 [GreasyFork](https://greasyfork.org/zh-CN/scripts/556649-%E4%B8%80%E9%94%AE%E5%8F%91%E9%80%81%E5%88%B0gemini) 安装此脚本。 14 | 15 | ## 支持的 AI 服务商 16 | 17 | - **Gemini** (默认) 18 | - **ChatGPT** 19 | - **Google AI Studio** 20 | - **DeepSeek** 21 | 22 | 你可以通过 Tampermonkey 菜单中的 "Switch Provider" (切换服务商) 选项在这些服务商之间切换。脚本会记住你的选择。 23 | 24 | ## 使用方法 25 | 26 | ### 1. 总结模式 27 | 28 | - 按下 `Alt + 2` (Windows) 或 `Option + 2` (Mac) 激活元素选择器。 29 | - 点击页面上的任意元素(例如文章正文)。 30 | - AI 将总结你点击的元素内容。 31 | 32 | ### 2. 问答模式 33 | 34 | - 先用鼠标划选页面上的特定文本。 35 | - 按下 `Alt + 2` / `Option + 2`。 36 | - 点击包含该文本的元素(作为上下文)。 37 | - AI 将结合你点击的上下文来解释你划选的文本。 38 | 39 | ### 3. 自定义搜索引擎支持 40 | 41 | - 脚本支持 `q` 查询参数,这主要是为了适配浏览器的自定义搜索引擎功能。 42 | - 你可以在浏览器设置中添加一个新的搜索引擎,URL 填写为:`https://gemini.google.com/app?q=%s`。 43 | - 之后,你就可以在地址栏输入你设置的关键字(例如 `g`)加上你的问题,直接发送给当前选定的 AI 服务商。 44 | -------------------------------------------------------------------------------- /FT-Auto-Full-Article/user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name FT中文网自动加载全文 3 | // @namespace https://blog.xlab.app/ 4 | // @more https://github.com/ttttmr/UserJS 5 | // @version 1.0 6 | // @description FT中文网自动加载全文,并修改所有FT中文网链接,增加全文参数 7 | // @author tmr 8 | // @match http://*/* 9 | // @match https://*/* 10 | // @grant none 11 | // ==/UserScript== 12 | 13 | (function () { 14 | "use strict"; 15 | function fullft(link) { 16 | try { 17 | let u = new URL(link); 18 | if ( 19 | (u.host == "chineseft.com" || 20 | u.host == "www.chineseft.com" || 21 | u.host == "ftchinese.com" || 22 | u.host == "www.ftchinese.com" || 23 | u.host == "m.ftchinese.com" || 24 | u.host == "cn.ft.com") && 25 | (u.pathname.startsWith("/story/") || 26 | u.pathname.startsWith("/premium/") || 27 | u.pathname.startsWith("/interactive/")) && 28 | u.searchParams.get("full") == null 29 | ) { 30 | u.searchParams.set("full", "y"); 31 | return u.toString(); 32 | } else { 33 | return false; 34 | } 35 | } catch (e) { 36 | console.error(e); 37 | return false; 38 | } 39 | } 40 | // 替换页面ft链接 41 | function replace(u) { 42 | let n = fullft(location.href); 43 | if (n) { 44 | location.href = n; 45 | return; 46 | } 47 | let aTagList = document.querySelectorAll("a"); 48 | aTagList.forEach(function (ele) { 49 | let n = fullft(ele.href); 50 | if (n) { 51 | ele.href = n; 52 | } 53 | }); 54 | } 55 | replace(); 56 | })(); 57 | -------------------------------------------------------------------------------- /Ask-AI-Anywhere/README.md: -------------------------------------------------------------------------------- 1 | # Ask AI Anywhere 2 | 3 | [中文](README_zh.md) 4 | 5 | A userscript that allows you to quickly send page content (text & images) to AI (Gemini, ChatGPT, AI Studio, DeepSeek) for summary or conversation. 6 | 7 | - **Markdown Support**: Web content is automatically converted to Markdown format. 8 | - **Image Support**: Images within the selected content are also sent to the AI. 9 | 10 | ## Installation 11 | 12 | 1. Install a userscript manager like [Tampermonkey](https://www.tampermonkey.net/), [Violentmonkey](https://violentmonkey.github.io/), or [ScriptCat](https://docs.scriptcat.org/). 13 | 2. Install this script from [GreasyFork](https://greasyfork.org/en/scripts/556649-%E4%B8%80%E9%94%AE%E5%8F%91%E9%80%81%E5%88%B0gemini). 14 | 15 | ## Supported Providers 16 | 17 | - **Gemini** (Default) 18 | - **ChatGPT** 19 | - **Google AI Studio** 20 | - **DeepSeek** 21 | 22 | You can switch between providers using the Tampermonkey menu command "Switch Provider". The script will remember your choice. 23 | 24 | ## Usage 25 | 26 | ### 1. Summarize Mode 27 | 28 | - Press `Alt + 2` (Windows) or `Option + 2` (Mac) to activate the element selector. 29 | - Click on any element (e.g., the main article content). 30 | - The AI will summarize the content of the clicked element. 31 | 32 | ### 2. Q&A Mode 33 | 34 | - First, select specific text on the page with your mouse. 35 | - Press `Alt + 2` / `Option + 2`. 36 | - Click on the surrounding element (context). 37 | - The AI will explain the selected text based on the context you clicked. 38 | 39 | ### 3. Custom Search Engine Support 40 | 41 | - The script supports a `q` query parameter, designed to work with browser custom search engines. 42 | - You can add a new search engine in your browser settings with the URL: `https://gemini.google.com/app?q=%s`. 43 | - Then you can type your keyword (e.g., `g`) followed by your query in the address bar to directly send it to the current AI provider. 44 | -------------------------------------------------------------------------------- /Qireader-Keymap/user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Qireader keymap 3 | // @namespace https://blog.xlab.app/ 4 | // @more https://github.com/ttttmr/UserJS 5 | // @match https://www.qireader.com.cn/* 6 | // @match https://www.qireader.com/* 7 | // @grant none 8 | // @version 0.3 9 | // @author tmr 10 | // @grant GM_openInTab 11 | // @inject-into content 12 | // ==/UserScript== 13 | 14 | (function () { 15 | const keyMap = { 16 | b: OpenBGEntry, 17 | s: StarEntry, 18 | }; 19 | 20 | function onKeyDownEvent(event) { 21 | if (keyMap[event.key]) { 22 | console.log("key", event.key); 23 | if (keyMap[event.key]()) { 24 | event.preventDefault(); 25 | } 26 | } 27 | } 28 | document.addEventListener("keydown", onKeyDownEvent); 29 | 30 | let currEntry = null; 31 | document.addEventListener("mouseover", (event) => { 32 | currEntry = event.target.closest("div[data-is-entry]"); 33 | if (!currEntry) { 34 | currEntry = document.querySelector("article") 35 | } 36 | }); 37 | 38 | function EntryLink() { 39 | if (!currEntry) { 40 | return false; 41 | } 42 | if (currEntry.tagName == "DIV") { 43 | return currEntry.querySelector('a[data-is-entry-title="true"]').href; 44 | } 45 | if (currEntry.tagName == "ARTICLE") { 46 | return currEntry.dataset.articleUrl; 47 | } 48 | } 49 | 50 | function EntryNav() { 51 | if (!currEntry) { 52 | return []; 53 | } 54 | if (currEntry.tagName == "ARTICLE") { 55 | let nav; 56 | let parent = currEntry.parentElement; 57 | while (parent) { 58 | nav = Array.from(parent.children).find((e) => e.tagName === "NAV"); 59 | if (nav) { 60 | break; 61 | } 62 | parent = parent.parentElement; 63 | } 64 | if (nav) { 65 | return Array.from(nav.children); 66 | } 67 | } 68 | return []; 69 | } 70 | 71 | function StarEntry() { 72 | if (!currEntry) { 73 | return false; 74 | } 75 | if (currEntry.tagName == "DIV") { 76 | const menuEvent = new MouseEvent("contextmenu", { 77 | button: 2, 78 | bubbles: true, 79 | cancelable: true, 80 | }); 81 | currEntry.dispatchEvent(menuEvent); 82 | const menu = document.querySelector('div[data-is-menu="true"]'); 83 | menu 84 | .querySelectorAll("div>span") 85 | .forEach((e) => e.textContent === "稍后阅读" && e.click()); 86 | menu.remove(); 87 | return true; 88 | } 89 | if (currEntry.tagName == "ARTICLE") { 90 | EntryNav().forEach((e) => e.title === "稍后阅读" && e.click()); 91 | } 92 | } 93 | 94 | function ReadEntry(force = false) { 95 | if (!currEntry) { 96 | return false; 97 | } 98 | if (currEntry.tagName == "DIV") { 99 | const menuEvent = new MouseEvent("contextmenu", { 100 | button: 2, 101 | bubbles: true, 102 | cancelable: true, 103 | }); 104 | currEntry.dispatchEvent(menuEvent); 105 | const menu = document.querySelector('div[data-is-menu="true"]'); 106 | menu 107 | .querySelectorAll("div>span") 108 | .forEach( 109 | (e) => 110 | (e.textContent === "标记为已读" || (!force && e.textContent === "标记为未读") ) && 111 | e.click() 112 | ); 113 | menu.remove(); 114 | return true; 115 | } 116 | if (currEntry.tagName == "ARTICLE") { 117 | EntryNav().forEach((e) => (e.title === "标记为已读" || (!force && e.title === "标记为未读")) && e.click()); 118 | } 119 | } 120 | 121 | function OpenBGEntry() { 122 | if (!currEntry) { 123 | return false; 124 | } 125 | ReadEntry(true); 126 | GM_openInTab(EntryLink(), { 127 | active: false, 128 | }); 129 | return true; 130 | } 131 | })(); 132 | -------------------------------------------------------------------------------- /Video-Auto-Fullscreen/user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 自动网页全屏播放 3 | // @namespace https://blog.xlab.app/ 4 | // @more https://github.com/ttttmr/UserJS 5 | // @version 0.9 6 | // @description 自动网页全屏播放,已支持Bilibili,腾讯视频 7 | // @author tmr 8 | // @match https://www.bilibili.com/video/av* 9 | // @match https://www.bilibili.com/bangumi/play/ss* 10 | // @match https://www.bilibili.com/bangumi/play/ep* 11 | // @match https://v.qq.com/x/page/* 12 | // @match https://v.qq.com/x/cover/* 13 | // @grant none 14 | // ==/UserScript== 15 | 16 | (function () { 17 | 'use strict'; 18 | let counter = 0; 19 | function fullscreen() { 20 | console.log('web fullscreen start'); 21 | webFull(); 22 | function webFull() { 23 | console.log('web fullscreen ing ' + counter); 24 | counter++; 25 | let fullscreenClass; 26 | if (location.host == 'www.bilibili.com') { 27 | fullscreenClass = '.bilibili-player-video-web-fullscreen'; 28 | } else if (location.host == 'v.qq.com') { 29 | fullscreenClass = '.txp_btn_fake'; 30 | } 31 | if (fullscreenClass) { 32 | // 尝试全屏 33 | if (document.querySelector(fullscreenClass)) { 34 | // 网页全屏 35 | document.querySelector(fullscreenClass).click(); 36 | console.log('web fullscreen success'); 37 | // 重置计数 38 | counter = 0; 39 | // 移除监听 40 | document.removeEventListener('visibilitychange', fullscreen); 41 | } 42 | // 失败并重试 43 | else { 44 | // 超过30次就退出 45 | if (counter > 30) { 46 | console.log('web fullscreen fail'); 47 | return; 48 | } 49 | // 延迟0.5秒重试 50 | setTimeout(webFull, 500); 51 | } 52 | } 53 | } 54 | } 55 | clickVideoLink(); 56 | function clickVideoLink() { 57 | window.onclick = function (mClick) { 58 | let mClickElement = mClick.target; 59 | // 视频链接 60 | let videoUrlList; 61 | // 视频Class 62 | let videoClassList; 63 | if (location.host == 'www.bilibili.com') { 64 | videoUrlList = [ 65 | 'https://www.bilibili.com/video/av', 66 | 'https://www.bilibili.com/bangumi/play/ss', 67 | 'https://www.bilibili.com/bangumi/play/ep', 68 | ]; 69 | videoClassList = [ 70 | 'bilibili-player-ending-panel-box-recommend-cover', 71 | 'bilibili-player-ending-panel-box-recommend', 72 | 'ep-title', 73 | 'ep-item', 74 | ]; 75 | } else if (location.host == 'v.qq.com') { 76 | videoUrlList = ['https://v.qq.com/x/page/']; 77 | videoClassList = []; 78 | } 79 | // 优先Class处理 80 | videoClassList.forEach(function (videoClass) { 81 | if (mClickElement.classList.contains(videoClass)) { 82 | fullscreen(); 83 | return; 84 | } 85 | }); 86 | // 链接处理 87 | let mClickElementTmp = mClickElement; 88 | // 判断是否是a标签的子元素 89 | while (mClickElementTmp) { 90 | // 元素是a标签 91 | if (mClickElementTmp.tagName == 'A') { 92 | // 新tab打开不处理 93 | if (mClickElementTmp.target == '_blank') { 94 | break; 95 | } else { 96 | // 循环判断链接 97 | videoUrlList.some(function (videoUrl) { 98 | if (String(mClickElementTmp.href).indexOf(videoUrl) == 0) { 99 | fullscreen(); 100 | return true; 101 | } 102 | }); 103 | break; 104 | } 105 | } 106 | // 不是a标签就循环父级元素 107 | else { 108 | mClickElementTmp = mClickElementTmp.parentElement; 109 | } 110 | } 111 | }; 112 | } 113 | window.addEventListener('load', function () { 114 | // 判断后台打开 115 | if (document.visibilityState == 'hidden') { 116 | console.log('now hidden, wait visible'); 117 | document.addEventListener('visibilitychange', fullscreen); 118 | } 119 | // 前台打开,直接直行 120 | else { 121 | fullscreen(); 122 | } 123 | }); 124 | })(); 125 | -------------------------------------------------------------------------------- /RSS-to-Inoreader/user.js: -------------------------------------------------------------------------------- 1 | javascript: (function () { 2 | let rsshub_host = 'https://rsshub.app'; 3 | 4 | let cnblog = 'https://www.cnblogs.com/'; 5 | let csdn = 'https://blog.csdn.net/'; 6 | let jianshu_user = '/jianshu/user/'; 7 | let zhihu_user = '/zhihu/people/activities/'; 8 | let zhihu_collection = '/zhihu/collection/'; 9 | let bilibili_user = '/bilibili/user/video/'; 10 | let jike_topic = '/jike/topic/'; 11 | let jike_square = '/jike/topic/square/'; 12 | let jike_user = '/jike/user/'; 13 | let twitter_user = '/twitter/user/'; 14 | let weibo_user = '/weibo/user/'; 15 | let instagram_user = '/instagram/user/'; 16 | let youtube_channel = '/youtube/channel/'; 17 | let github_issues = '/github/issue/'; 18 | 19 | let w = 800; 20 | let h = 600; 21 | let feedurl = ''; 22 | let domain = location.host; 23 | let path = location.pathname.split('/'); 24 | 25 | if (domain == 'www.cnblogs.com') { 26 | feedurl = cnblog + path[1] + '/rss'; 27 | } else if (domain == 'blog.csdn.net') { 28 | feedurl = csdn + path[1] + '/rss/list'; 29 | } 30 | if (feedurl != '') { 31 | console.log('RSS found in Website'); 32 | } else { 33 | console.log('RSS not found in Website'); 34 | console.log('Trying RSSHub ... '); 35 | let rsshub_path = ''; 36 | if (domain == 'www.jianshu.com') { 37 | if (path[1] == 'u') { 38 | rsshub_path = jianshu_user + path[2]; 39 | } else { 40 | alert('Use it in Jianshu user page'); 41 | return; 42 | } 43 | } else if (domain == 'www.zhihu.com') { 44 | if (path[1] == 'people' || path[1] == 'org') { 45 | rsshub_path = zhihu_user + path[2]; 46 | } else if (path[1] == 'collection') { 47 | rsshub_path = zhihu_collection + path[2]; 48 | } else { 49 | alert('Use it in Zhihu user page'); 50 | return; 51 | } 52 | } else if (domain == 'space.bilibili.com') { 53 | rsshub_path = bilibili_user + path[1]; 54 | } else if (domain == 'web.okjike.com') { 55 | if (path[1] == 'topic') { 56 | if (path[3] == 'official') { 57 | rsshub_path = jike_topic + path[2]; 58 | } else if (path[3] == 'user') { 59 | rsshub_path = jike_square + path[2]; 60 | } 61 | } else if (path[1] == 'user') { 62 | rsshub_path = jike_user + path[2]; 63 | } else { 64 | alert('Use it in Jike user or topic page'); 65 | return; 66 | } 67 | } else if (domain == 'twitter.com') { 68 | rsshub_path = twitter_user + path[1]; 69 | } else if (domain == 'm.weibo.cn') { 70 | if (path[1] == 'profile') { 71 | rsshub_path = weibo_user + path[2]; 72 | } else { 73 | alert('Use it in Weibo user home page'); 74 | return; 75 | } 76 | } else if (domain == 'weibo.com' || domain == 'www.weibo.com') { 77 | rsshub_path = weibo_user + $CONFIG.oid; 78 | } else if (domain == 'www.instagram.com') { 79 | if (path[1] == 'p') { 80 | alert('Use it in Instagram user home page'); 81 | return; 82 | } else { 83 | rsshub_path = instagram_user + path[1]; 84 | } 85 | } else if (domain == 'www.youtube.com') { 86 | if (path[1] == 'channel') { 87 | rsshub_path = youtube_channel + path[2]; 88 | } else { 89 | alert('Use it in YouTube channel page'); 90 | return; 91 | } 92 | } else if (domain == 'github.com') { 93 | if (path[3] == 'issues') { 94 | rsshub_path = github_issues + path[1] + '/' + path[2]; 95 | } else { 96 | alert('Use it in GitHub Issues page'); 97 | return; 98 | } 99 | } 100 | if (rsshub_path == '') { 101 | console.log('RSS not found'); 102 | } else { 103 | console.log('RSS found in RSSHub'); 104 | feedurl = rsshub_host + rsshub_path; 105 | } 106 | } 107 | if (feedurl) { 108 | console.log(feedurl); 109 | feedurl = 'https://www.inoreader.com/?add_feed=' + feedurl; 110 | } else { 111 | feedurl = 112 | 'https://www.inoreader.com/bookmarklet/subscribe/' + 113 | encodeURIComponent(location.href); 114 | } 115 | console.log(feedurl); 116 | let b = window.screenLeft != undefined ? window.screenLeft : screen.left; 117 | let c = window.screenTop != undefined ? window.screenTop : screen.top; 118 | let width = window.innerWidth 119 | ? window.innerWidth 120 | : document.documentElement.clientWidth 121 | ? document.documentElement.clientWidth 122 | : screen.width; 123 | let height = window.innerHeight 124 | ? window.innerHeight 125 | : document.documentElement.clientHeight 126 | ? document.documentElement.clientHeight 127 | : screen.height; 128 | let d = width / 2 - w / 2 + b; 129 | let e = height / 2 - h / 2 + c; 130 | let f = window.open( 131 | feedurl, 132 | new Date().getTime(), 133 | 'width=' + 134 | w + 135 | ', height=' + 136 | h + 137 | ', top=' + 138 | e + 139 | ', left=' + 140 | d + 141 | 'location=yes,resizable=yes,status=no,scrollbars=no,personalbar=no,toolbar=no,menubar=no' 142 | ); 143 | if (window.focus) { 144 | f.focus(); 145 | } 146 | })(); 147 | -------------------------------------------------------------------------------- /RSS-to-Clipboard/user.js: -------------------------------------------------------------------------------- 1 | javascript: (function () { 2 | let rsshub_host = 'https://rsshub.app'; 3 | let lilydjwg_host = 'https://rss.lilydjwg.me'; 4 | 5 | let cnblog = 'https://www.cnblogs.com/'; 6 | let csdn = 'https://blog.csdn.net/'; 7 | let feed43 = 'https://feed43.com'; 8 | let jianshu_user = '/jianshu/user/'; 9 | let zhihu_user = '/zhihu/people/activities/'; 10 | let zhihu_zhuanlan = '/zhihu/zhuanlan/'; 11 | let zhihu_collection = '/zhihu/collection/'; 12 | let bilibili_user = '/bilibili/user/video/'; 13 | let jike_topic = '/jike/topic/'; 14 | let jike_square = '/jike/topic/square/'; 15 | let jike_user = '/jike/user/'; 16 | let twitter_user = '/twitter/user/'; 17 | let weibo_user = '/weibo/user/'; 18 | let instagram_user = '/instagram/user/'; 19 | let youtube_channel = '/youtube/channel/'; 20 | let github_issues = '/github/issue/'; 21 | 22 | let feedurl = ''; 23 | let domain = location.host; 24 | let path = location.pathname.split('/'); 25 | let urlparam = new URLSearchParams(location.href); 26 | 27 | if (domain == 'www.cnblogs.com') { 28 | feedurl = cnblog + path[1] + '/rss'; 29 | } else if (domain == 'blog.csdn.net') { 30 | feedurl = csdn + path[1] + '/rss/list'; 31 | } else if (domain == 'feed43.com') { 32 | if (path[1].length - path[1].indexOf('.xml') == 4) { 33 | feedurl = location.href; 34 | } else if (path[1] == 'feed.html') { 35 | if (urlparam.has('name')) { 36 | feedurl = feed43 + '/' + urlparam.get('name') + '.xml'; 37 | } else { 38 | alert('Use it in Feed43 feed edit Page'); 39 | } 40 | } else { 41 | alert('Use it in Feed43 feed edit Page'); 42 | } 43 | } else if (domain == 'zhuanlan.zhihu.com') { 44 | if (path[1] == 'p') { 45 | alert('Use it in ZhihuZhuanlan home page'); 46 | } else { 47 | feedurl = lilydjwg_host + '/zhihuzhuanlan/' + path[1]; 48 | } 49 | } else if (domain == 'www.zhihu.com') { 50 | if (path[1] == 'people' || path[1] == 'org') { 51 | feedurl = lilydjwg_host + '/zhihu/' + path[2]; 52 | } else { 53 | alert('Use it in Zhihu user page'); 54 | return; 55 | } 56 | } 57 | if (feedurl != '') { 58 | console.log('RSS found in Website'); 59 | } else { 60 | console.log('RSS not found in Website'); 61 | console.log('Trying RSSHub ... '); 62 | let rsshub_path = ''; 63 | if (domain == 'www.jianshu.com') { 64 | if (path[1] == 'u') { 65 | rsshub_path = jianshu_user + path[2]; 66 | } else { 67 | alert('Use it in Jianshu user page'); 68 | return; 69 | } 70 | } else if (domain == 'www.zhihu.com') { 71 | if (path[1] == 'people' || path[1] == 'org') { 72 | rsshub_path = zhihu_user + path[2]; 73 | } else if (path[1] == 'collection') { 74 | rsshub_path = zhihu_collection + path[2]; 75 | } else { 76 | alert('Use it in Zhihu user page'); 77 | return; 78 | } 79 | } else if (domain == 'zhuanlan.zhihu.com') { 80 | if (path[1] == 'p') { 81 | alert('Use it in ZhihuZhuanlan home page'); 82 | return; 83 | } else { 84 | rsshub_path = zhihu_zhuanlan + path[1]; 85 | } 86 | } else if (domain == 'space.bilibili.com') { 87 | rsshub_path = bilibili_user + path[1]; 88 | } else if (domain == 'web.okjike.com') { 89 | if (path[1] == 'topic') { 90 | if (path[3] == 'official') { 91 | rsshub_path = jike_topic + path[2]; 92 | } else if (path[3] == 'user') { 93 | rsshub_path = jike_square + path[2]; 94 | } 95 | } else if (path[1] == 'user') { 96 | rsshub_path = jike_user + path[2]; 97 | } else { 98 | alert('Use it in Jike user or topic page'); 99 | return; 100 | } 101 | } else if (domain == 'twitter.com') { 102 | rsshub_path = twitter_user + path[1]; 103 | } else if (domain == 'm.weibo.cn') { 104 | if (path[1] == 'profile') { 105 | rsshub_path = weibo_user + path[2]; 106 | } else { 107 | alert('Use it in Weibo user home page'); 108 | return; 109 | } 110 | } else if (domain == 'weibo.com' || domain == 'www.weibo.com') { 111 | rsshub_path = weibo_user + $CONFIG.oid; 112 | } else if (domain == 'www.instagram.com') { 113 | if (path[1] == 'p') { 114 | alert('Use it in Instagram user home page'); 115 | return; 116 | } else { 117 | rsshub_path = instagram_user + path[1]; 118 | } 119 | } else if (domain == 'www.youtube.com') { 120 | if (path[1] == 'channel') { 121 | rsshub_path = youtube_channel + path[2]; 122 | } else { 123 | alert('Use it in YouTube channel page'); 124 | return; 125 | } 126 | } else if (domain == 'github.com') { 127 | if (path[3] == 'issues') { 128 | rsshub_path = github_issues + path[1] + '/' + path[2]; 129 | } else { 130 | alert('Use it in GitHub Issues page'); 131 | return; 132 | } 133 | } 134 | if (rsshub_path == '') { 135 | console.log( 136 | 'Rss not found, if rsshub supports this website, please contact me' 137 | ); 138 | console.log('https://blog.xlab.app/'); 139 | alert( 140 | 'Rss not found, if rsshub supports this website, please contact me' 141 | ); 142 | return; 143 | } else { 144 | console.log('RSS found in RSSHub'); 145 | feedurl = rsshub_host + rsshub_path; 146 | } 147 | } 148 | if (feedurl) { 149 | console.log(feedurl); 150 | const input = document.createElement('input'); 151 | document.body.appendChild(input); 152 | input.setAttribute('value', feedurl); 153 | input.select(); 154 | if (document.execCommand('copy')) { 155 | document.execCommand('copy'); 156 | console.log('Copy to clipboard'); 157 | alert('RSS copied to clipboard'); 158 | } 159 | document.body.removeChild(input); 160 | } 161 | })(); 162 | -------------------------------------------------------------------------------- /GitHub-Star-History/user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name GitHub Star History 3 | // @namespace https://blog.xlab.app/ 4 | // @more https://github.com/ttttmr/UserJS 5 | // @version 0.2 6 | // @description GitHub Star History Link 7 | // @author tmr 8 | // @match https://github.com/*/* 9 | // @grant none 10 | // ==/UserScript== 11 | 12 | (function () { 13 | "use strict"; 14 | let paths = location.pathname.split("/"); 15 | if (paths.length < 3) { 16 | return; 17 | } 18 | let repo = paths[1] + "/" + paths[2]; 19 | 20 | let link = document.createElement("a"); 21 | link.href = "https://star-history.com/#" + repo; 22 | link.className = "btn btn-sm tooltipped tooltipped-s"; 23 | link.title = "Open Star History"; 24 | link.target = "_blank"; 25 | link.rel = "noopener noreferrer"; 26 | link.setAttribute("aria-label", "Open Star History"); 27 | 28 | let logo = document.createElement("img"); 29 | logo.src = 30 | "data:image/png;base64,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"; 31 | logo.width = 16; 32 | logo.height = 16; 33 | logo.classList.add("v-align-text-bottom"); 34 | link.appendChild(logo); 35 | 36 | let l = document.createElement("li"); 37 | l.appendChild(link); 38 | 39 | let count = 0; 40 | function run() { 41 | if (document.readyState == "complete" && count > 5) { 42 | return; 43 | } 44 | count++; 45 | let n = document.querySelector(".pagehead-actions"); 46 | if (n == null) { 47 | setTimeout(run, 500); 48 | return; 49 | } 50 | n.appendChild(l); 51 | } 52 | run(); 53 | })(); 54 | -------------------------------------------------------------------------------- /Ask-AI-Anywhere/user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 一键发送到AI(支持图文) 3 | // @name:en Ask AI Anywhere (Support Image) 4 | // @namespace https://blog.xlab.app/ 5 | // @more https://github.com/ttttmr/UserJS 6 | // @version 0.11 7 | // @description 按快捷键选择页面元素,快速发送到Gemini/ChatGPT/AI Studio/DeepSeek 8 | // @description:en Quickly send web content (text & images) to AI (Gemini, ChatGPT, AI Studio, DeepSeek) with a shortcut 9 | // @author tmr 10 | // @match http://*/* 11 | // @match https://*/* 12 | // @grant GM_setValue 13 | // @grant GM_getValue 14 | // @grant GM_deleteValue 15 | // @grant GM_registerMenuCommand 16 | // @grant GM_unregisterMenuCommand 17 | // @grant GM_addStyle 18 | // @grant GM_xmlhttpRequest 19 | // ==/UserScript== 20 | 21 | const CONFIG = { 22 | SHORTCUT_TRIGGER: (e) => e.altKey && e.code === "Digit2", 23 | PROVIDERS: { 24 | gemini: { 25 | name: "Gemini", 26 | url: "https://gemini.google.com/app", 27 | inputSelector: 'div[contenteditable="true"], textarea', 28 | sendButtonSelector: "button.submit", 29 | }, 30 | chatgpt: { 31 | name: "ChatGPT", 32 | url: "https://chatgpt.com/", 33 | inputSelector: "#prompt-textarea", 34 | sendButtonSelector: 'button[data-testid="send-button"]', 35 | }, 36 | aistudio: { 37 | name: "AI Studio", 38 | url: "https://aistudio.google.com/prompts/new_chat", 39 | inputSelector: "ms-autosize-textarea textarea", 40 | sendButtonSelector: 'button[aria-label="Run"]', 41 | }, 42 | deepseek: { 43 | name: "DeepSeek", 44 | url: "https://chat.deepseek.com/", 45 | inputSelector: 'textarea[placeholder*="DeepSeek"]', 46 | sendButtonSelector: 'div[role="button"].ds-icon-button', 47 | }, 48 | }, 49 | GENERATE_PROMPT: (data) => { 50 | const { title, url, selection, content, images } = data; 51 | const zh = navigator.language.toLowerCase().startsWith("zh"); 52 | const prompts = []; 53 | if (zh) { 54 | prompts.push(`我正在阅读:${title}`); 55 | } else { 56 | prompts.push(`I'm reading: ${title}`); 57 | } 58 | if (content) { 59 | if (zh) { 60 | prompts.push("内容:"); 61 | } else { 62 | prompts.push("Content:"); 63 | } 64 | prompts.push("```markdown"); 65 | prompts.push(content); 66 | prompts.push("```"); 67 | } 68 | if (selection) { 69 | console.log("[Ask] found selection"); 70 | if (zh) { 71 | prompts.push(`其中${selection}如何理解?`); 72 | } else { 73 | prompts.push(`How to understand ${selection}?`); 74 | } 75 | } else if (content) { 76 | console.log("[Ask] found content"); 77 | if (zh) { 78 | prompts.push("使用通俗的语言总结这篇文章"); 79 | } else { 80 | prompts.push("Summarize this article in plain language"); 81 | } 82 | } else if (images) { 83 | console.log("[Ask] found images"); 84 | if (zh) { 85 | prompts.push("解释这个图片"); 86 | } else { 87 | prompts.push("Explain this image"); 88 | } 89 | } 90 | return prompts.join("\n"); 91 | }, 92 | }; 93 | 94 | // Helper to wait for element using MutationObserver 95 | function waitForElement(selector, checkFn = (el) => true) { 96 | return new Promise((resolve) => { 97 | const element = document.querySelector(selector); 98 | if (element && checkFn(element)) { 99 | return resolve(element); 100 | } 101 | 102 | const observer = new MutationObserver(() => { 103 | const element = document.querySelector(selector); 104 | if (element && checkFn(element)) { 105 | resolve(element); 106 | observer.disconnect(); 107 | } 108 | }); 109 | 110 | observer.observe(document.body, { 111 | childList: true, 112 | subtree: true, 113 | }); 114 | }); 115 | } 116 | 117 | // Helper to get valid image source, handling lazy loading and relative URLs 118 | function getImageSrc(imgNode) { 119 | const candidates = [imgNode.src, imgNode.getAttribute("data-src")]; 120 | 121 | for (const src of candidates) { 122 | if (src && !src.startsWith("data:")) { 123 | try { 124 | return new URL(src, location.href).href; 125 | } catch {} 126 | } 127 | } 128 | return null; 129 | } 130 | 131 | // Helper to check if image should be included (filters out icons, avatars, etc.) 132 | function shouldIncludeImage(imgNode) { 133 | // Filter by keywords 134 | const keywords = ["avatar", "icon", "logo", "profile"]; 135 | const checkStr = `${imgNode.className || ""} ${imgNode.alt || ""} ${ 136 | imgNode.id || "" 137 | }`.toLowerCase(); 138 | if (keywords.some((k) => checkStr.includes(k))) return false; 139 | 140 | return true; 141 | } 142 | 143 | // Helper to extract text (Markdown) and images from element or fragment 144 | function extractContent(elementOrFragment) { 145 | if (!elementOrFragment) return { text: "", images: [] }; 146 | 147 | let text = ""; 148 | const images = []; 149 | 150 | function traverse(node) { 151 | if (node.nodeType === Node.TEXT_NODE) { 152 | // Escape Markdown characters in text 153 | return node.textContent.replace(/([*_`[\]])/g, "\\$1"); 154 | } 155 | 156 | if (node.nodeType !== Node.ELEMENT_NODE) return ""; 157 | 158 | const tagName = node.tagName; 159 | if (tagName === "SCRIPT" || tagName === "STYLE" || tagName === "NOSCRIPT") 160 | return ""; 161 | 162 | const parts = []; 163 | for (const child of node.childNodes) { 164 | parts.push(traverse(child)); 165 | } 166 | const content = parts.join(""); 167 | 168 | // Handle specific tags 169 | switch (tagName) { 170 | case "IMG": { 171 | const src = getImageSrc(node); 172 | if (src && shouldIncludeImage(node)) { 173 | const filename = `img_${images.length + 1}`; 174 | images.push({ url: src, filename }); 175 | return `\n![${filename}]\n`; 176 | } 177 | return ""; 178 | } 179 | case "BR": 180 | return "\n"; 181 | case "P": 182 | case "DIV": 183 | return `\n${content}\n`; 184 | case "H1": 185 | return `\n# ${content}\n`; 186 | case "H2": 187 | return `\n## ${content}\n`; 188 | case "H3": 189 | return `\n### ${content}\n`; 190 | case "H4": 191 | return `\n#### ${content}\n`; 192 | case "H5": 193 | return `\n##### ${content}\n`; 194 | case "H6": 195 | return `\n###### ${content}\n`; 196 | case "STRONG": 197 | case "B": 198 | return `**${content}**`; 199 | case "EM": 200 | case "I": 201 | return `*${content}*`; 202 | case "A": { 203 | const href = node.href; 204 | if (href) { 205 | try { 206 | const url = new URL(href, location.href); 207 | const isImage = 208 | ["http:", "https:"].includes(url.protocol) && 209 | /\.(jpeg|jpg|gif|png|webp|svg|bmp)$/i.test(url.pathname); 210 | 211 | if (isImage) { 212 | const filename = `img_${images.length + 1}`; 213 | images.push({ url: href, filename }); 214 | return `\n![${filename}]\n`; 215 | } else { 216 | return `[${content}](${href})`; 217 | } 218 | } catch {} 219 | } 220 | } 221 | case "CODE": 222 | return `\`${content}\``; 223 | case "PRE": 224 | return `\n\`\`\`\n${content}\n\`\`\`\n`; 225 | case "BLOCKQUOTE": 226 | return `\n> ${content}\n`; 227 | case "LI": 228 | return `\n- ${content}`; 229 | case "UL": 230 | case "OL": 231 | return `\n${content}\n`; 232 | case "TR": 233 | return `\n${content}`; 234 | case "TD": 235 | case "TH": 236 | return ` ${content} |`; 237 | default: 238 | return content; 239 | } 240 | } 241 | 242 | text = traverse(elementOrFragment); 243 | 244 | // Clean up whitespace 245 | text = text.replace(/\n(\s*\n)+/g, "\n").trim(); 246 | 247 | // Deduplicate images 248 | const uniqueImages = []; 249 | const seenUrls = new Set(); 250 | for (const img of images) { 251 | if (!seenUrls.has(img.url)) { 252 | seenUrls.add(img.url); 253 | uniqueImages.push(img); 254 | } 255 | } 256 | 257 | return { text, images: uniqueImages }; 258 | } 259 | 260 | // Helper to fetch image as File object 261 | function fetchImageAsFile(url, filename, referrer, timeout = 5000) { 262 | return new Promise((resolve, reject) => { 263 | GM_xmlhttpRequest({ 264 | method: "GET", 265 | url: url, 266 | headers: { 267 | Referer: referrer, 268 | }, 269 | timeout: timeout, 270 | responseType: "blob", 271 | onload: (response) => { 272 | if (response.status === 200) { 273 | const blob = response.response; 274 | const file = new File([blob], filename, { type: blob.type }); 275 | resolve(file); 276 | } else { 277 | reject(new Error(`Failed to fetch image: ${response.status}`)); 278 | } 279 | }, 280 | ontimeout: () => reject(new Error("Timeout fetching image")), 281 | onerror: (err) => reject(err), 282 | }); 283 | }); 284 | } 285 | 286 | // DOM Selector Class 287 | class DomSelector { 288 | constructor() { 289 | this.state = { 290 | active: false, 291 | overlay: null, 292 | currentElement: null, 293 | onSelect: null, 294 | }; 295 | this.boundHandleMouseMove = this.handleMouseMove.bind(this); 296 | this.boundHandleClick = this.handleClick.bind(this); 297 | this.boundHandleKeydown = this.handleKeydown.bind(this); 298 | } 299 | 300 | injectStyles() { 301 | if (document.getElementById("ask-ai-anywhere-selector-styles")) return; 302 | 303 | const css = ` 304 | .ask-ai-anywhere-selector-overlay { 305 | position: absolute; 306 | border: 3px solid #4285f4; 307 | background: rgba(66, 133, 244, 0.1); 308 | pointer-events: none; 309 | z-index: 2147483647; 310 | transition: all 0.1s ease; 311 | box-shadow: 0 0 0 1px rgba(66, 133, 244, 0.3); 312 | } 313 | .ask-ai-anywhere-selector-active { 314 | cursor: crosshair !important; 315 | } 316 | .ask-ai-anywhere-selector-active * { 317 | cursor: crosshair !important; 318 | } 319 | `; 320 | const style = GM_addStyle(css); 321 | if (style) { 322 | style.id = "ask-ai-anywhere-selector-styles"; 323 | } 324 | } 325 | 326 | createOverlay() { 327 | const overlay = document.createElement("div"); 328 | overlay.className = "ask-ai-anywhere-selector-overlay"; 329 | overlay.style.display = "none"; 330 | document.body.appendChild(overlay); 331 | return overlay; 332 | } 333 | 334 | highlight(element) { 335 | if (!this.state.overlay) return; 336 | 337 | if (!element || element === document.documentElement) { 338 | this.state.overlay.style.display = "none"; 339 | this.state.currentElement = null; 340 | return; 341 | } 342 | 343 | const rect = element.getBoundingClientRect(); 344 | const overlay = this.state.overlay; 345 | 346 | overlay.style.display = "block"; 347 | overlay.style.left = `${rect.left + window.scrollX}px`; 348 | overlay.style.top = `${rect.top + window.scrollY}px`; 349 | overlay.style.width = `${rect.width}px`; 350 | overlay.style.height = `${rect.height}px`; 351 | 352 | this.state.currentElement = element; 353 | } 354 | 355 | handleMouseMove(e) { 356 | if (!this.state.active) return; 357 | 358 | e.stopPropagation(); 359 | const element = document.elementFromPoint(e.clientX, e.clientY); 360 | this.highlight(element); 361 | } 362 | 363 | handleClick(e) { 364 | if (!this.state.active) return; 365 | 366 | e.preventDefault(); 367 | e.stopPropagation(); 368 | 369 | const element = this.state.currentElement; 370 | if (element && this.state.onSelect) { 371 | const content = element; // Pass the whole element 372 | this.state.onSelect(content); 373 | this.deactivate(); 374 | } 375 | } 376 | 377 | handleKeydown(e) { 378 | if (!this.state.active) return; 379 | 380 | if (e.key === "Escape") { 381 | e.preventDefault(); 382 | e.stopPropagation(); 383 | console.log("[Selector] Canceled by user"); 384 | this.deactivate(); 385 | } 386 | } 387 | 388 | activate(onSelect) { 389 | if (this.state.active) return; 390 | 391 | console.log("[Selector] Activating DOM selector"); 392 | 393 | this.injectStyles(); 394 | this.state.overlay = this.createOverlay(); 395 | this.state.active = true; 396 | this.state.onSelect = onSelect; 397 | 398 | document.body.classList.add("ask-ai-anywhere-selector-active"); 399 | 400 | // Add event listeners with capture to intercept all events 401 | document.addEventListener("mousemove", this.boundHandleMouseMove, true); 402 | document.addEventListener("click", this.boundHandleClick, true); 403 | document.addEventListener("keydown", this.boundHandleKeydown, true); 404 | } 405 | 406 | deactivate() { 407 | if (!this.state.active) return; 408 | 409 | console.log("[Selector] Deactivating DOM selector"); 410 | 411 | document.body.classList.remove("ask-ai-anywhere-selector-active"); 412 | 413 | // Remove event listeners 414 | document.removeEventListener("mousemove", this.boundHandleMouseMove, true); 415 | document.removeEventListener("click", this.boundHandleClick, true); 416 | document.removeEventListener("keydown", this.boundHandleKeydown, true); 417 | 418 | // Clean up overlay 419 | if (this.state.overlay) { 420 | this.state.overlay.remove(); 421 | this.state.overlay = null; 422 | } 423 | 424 | this.state.active = false; 425 | this.state.currentElement = null; 426 | this.state.onSelect = null; 427 | } 428 | } 429 | 430 | const domSelector = new DomSelector(); 431 | 432 | // Initialize Provider page to receive prompts 433 | async function initProviderPage(providerConfig) { 434 | console.log(`[Ask] Initializing ${providerConfig.name} page`); 435 | 436 | // Check for prompt from URL param or storage 437 | const urlParams = new URLSearchParams(window.location.search); 438 | const urlPrompt = urlParams.get("q"); 439 | const prompt = urlPrompt || GM_getValue("ask_prompt"); 440 | 441 | // Check for images in storage 442 | const storedImagesJson = GM_getValue("ask_images"); 443 | let images = []; 444 | let referrer = ""; 445 | if (storedImagesJson) { 446 | try { 447 | const data = JSON.parse(storedImagesJson); 448 | images = Array.isArray(data) ? data : data.urls; 449 | referrer = Array.isArray(data) ? "" : data.referrer; 450 | } catch (e) { 451 | console.error("[Ask] Failed to parse stored images", e); 452 | } 453 | } 454 | 455 | if (!prompt && (!images || images.length === 0)) return; 456 | 457 | console.log("[Ask] Found content to process"); 458 | 459 | // Start fetching images immediately if any 460 | const imageFetchPromise = 461 | images && images.length > 0 462 | ? Promise.all( 463 | images.map((img) => { 464 | return fetchImageAsFile(img.url, img.filename, referrer).catch( 465 | (err) => { 466 | console.error(`[Ask] Failed to fetch image ${img.url}`, err); 467 | return null; 468 | } 469 | ); 470 | }) 471 | ) 472 | : Promise.resolve([]); 473 | 474 | if (document.readyState !== "complete") { 475 | await new Promise((resolve) => window.addEventListener("load", resolve)); 476 | } 477 | 478 | try { 479 | const inputBox = await waitForElement(providerConfig.inputSelector); 480 | console.log("[Ask] Input box found"); 481 | inputBox.focus(); 482 | 483 | // 1. Paste Images 484 | const rawFiles = await imageFetchPromise; 485 | const files = rawFiles.filter((f) => f !== null); 486 | if (files.length > 0) { 487 | console.log( 488 | `[Ask] Waiting for window load to paste ${files.length} images...` 489 | ); 490 | 491 | console.log(`[Ask] Window loaded, pasting images`); 492 | const dataTransfer = new DataTransfer(); 493 | files.forEach((file) => { 494 | console.log( 495 | `[Ask] Adding file to DataTransfer: ${file.name} (${file.type}, ${file.size} bytes)` 496 | ); 497 | dataTransfer.items.add(file); 498 | }); 499 | 500 | const pasteEvent = new ClipboardEvent("paste", { 501 | bubbles: true, 502 | cancelable: true, 503 | clipboardData: dataTransfer, 504 | }); 505 | 506 | // Fallback for some browsers/environments where constructor doesn't set clipboardData correctly 507 | if (!pasteEvent.clipboardData) { 508 | Object.defineProperty(pasteEvent, "clipboardData", { 509 | value: dataTransfer, 510 | writable: false, 511 | }); 512 | } 513 | 514 | inputBox.dispatchEvent(pasteEvent); 515 | } 516 | 517 | // 2. Fill Text 518 | if (prompt) { 519 | console.log("[Ask] Filling text prompt"); 520 | inputBox.focus(); // Ensure focus is back on input 521 | if (inputBox.tagName === "TEXTAREA") { 522 | const valueSetter = Object.getOwnPropertyDescriptor( 523 | window.HTMLTextAreaElement.prototype, 524 | "value" 525 | ).set; 526 | valueSetter.call(inputBox, prompt); 527 | } else { 528 | // Safe text insertion for contenteditable 529 | // If we just pasted images, we don't want to wipe them out with textContent = ... 530 | // So we append a text node. 531 | const textNode = document.createTextNode(prompt); 532 | inputBox.appendChild(textNode); 533 | } 534 | inputBox.dispatchEvent(new Event("input", { bubbles: true })); 535 | inputBox.dispatchEvent(new Event("change", { bubbles: true })); 536 | } 537 | 538 | // 3. Send 539 | const btn = await waitForElement( 540 | providerConfig.sendButtonSelector, 541 | (btn) => { 542 | return !btn.disabled && btn.getAttribute("aria-disabled") !== "true"; 543 | } 544 | ); 545 | 546 | if (btn) { 547 | console.log("[Ask] Send button ready, clicking"); 548 | btn.click(); 549 | // Cleanup 550 | console.log("[Ask] Cleanup"); 551 | GM_deleteValue("ask_prompt"); 552 | GM_deleteValue("ask_images"); 553 | } 554 | } catch (err) { 555 | console.error("[Ask] Error processing content", err); 556 | } 557 | } 558 | 559 | // Handle shortcut trigger 560 | function handleShortcut(e) { 561 | if (!CONFIG.SHORTCUT_TRIGGER(e)) return; 562 | 563 | console.log("[Source] Shortcut triggered"); 564 | e.preventDefault(); 565 | e.stopPropagation(); 566 | e.stopImmediatePropagation(); 567 | 568 | const selection = window.getSelection(); 569 | let selectionText = ""; 570 | let selectionImages = []; 571 | 572 | if (selection.rangeCount > 0) { 573 | const container = document.createElement("div"); 574 | for (let i = 0; i < selection.rangeCount; i++) { 575 | container.appendChild(selection.getRangeAt(i).cloneContents()); 576 | } 577 | const result = extractContent(container); 578 | selectionText = result.text; 579 | selectionImages = result.images; 580 | } 581 | 582 | domSelector.activate((element) => { 583 | const { text: content, images: elementImages } = extractContent(element); 584 | 585 | // Combine images and deduplicate 586 | const includeImages = GM_getValue("include_images", true); 587 | const allImages = includeImages ? [...selectionImages, ...elementImages] : []; 588 | // Deduplicate again based on URL 589 | const uniqueImages = []; 590 | const seenUrls = new Set(); 591 | for (const img of allImages) { 592 | if (!seenUrls.has(img.url)) { 593 | seenUrls.add(img.url); 594 | uniqueImages.push(img); 595 | } 596 | } 597 | 598 | const promptText = CONFIG.GENERATE_PROMPT({ 599 | title: document.title, 600 | url: location.href, 601 | selection: selectionText, 602 | content, 603 | images: uniqueImages.length ? uniqueImages : null, 604 | }); 605 | console.log( 606 | "[Source] Generated prompt from element, length:", 607 | promptText.length 608 | ); 609 | 610 | GM_setValue("ask_prompt", promptText); 611 | if (uniqueImages.length > 0) { 612 | console.log(`[Source] Saving ${uniqueImages.length} images to storage`); 613 | GM_setValue( 614 | "ask_images", 615 | JSON.stringify({ 616 | urls: uniqueImages, 617 | referrer: location.href, 618 | }) 619 | ); 620 | } 621 | 622 | const currentProviderKey = GM_getValue("provider", "gemini"); 623 | const provider = CONFIG.PROVIDERS[currentProviderKey]; 624 | 625 | const win = window.open(provider.url, "_blank"); 626 | if (!win) { 627 | console.log("[Source] Failed to open window"); 628 | return; 629 | } 630 | console.log(`[Source] ${provider.name} window opened`); 631 | }); 632 | } 633 | 634 | let menuIds = []; 635 | // Register menu command to switch provider 636 | function registerMenuCommands() { 637 | // Unregister existing commands 638 | for (const id of menuIds) { 639 | GM_unregisterMenuCommand(id); 640 | } 641 | menuIds = []; 642 | 643 | const currentProviderKey = GM_getValue("provider", "gemini"); 644 | 645 | Object.entries(CONFIG.PROVIDERS).forEach(([key, config]) => { 646 | const isCurrent = currentProviderKey === key; 647 | const title = isCurrent ? `✅ ${config.name}` : `⬜ ${config.name}`; 648 | 649 | const id = GM_registerMenuCommand(title, () => { 650 | GM_setValue("provider", key); 651 | registerMenuCommands(); // Re-register to update checkmarks 652 | }); 653 | menuIds.push(id); 654 | }); 655 | 656 | const includeImages = GM_getValue("include_images", true); 657 | const imageStatus = includeImages ? "✅" : "⬜"; 658 | menuIds.push(GM_registerMenuCommand(`${imageStatus} 包含图片 (Include Images)`, () => { 659 | GM_setValue("include_images", !includeImages); 660 | registerMenuCommands(); 661 | })); 662 | } 663 | 664 | (async function () { 665 | "use strict"; 666 | 667 | // Check if we are on a provider page 668 | const currentUrl = location.href; 669 | for (const [key, config] of Object.entries(CONFIG.PROVIDERS)) { 670 | if (currentUrl.startsWith(config.url)) { 671 | await initProviderPage(config); 672 | return; // Exit if we are on a provider page 673 | } 674 | } 675 | 676 | // Otherwise, we are on a source page 677 | registerMenuCommands(); 678 | window.addEventListener("keydown", handleShortcut, true); 679 | })(); 680 | --------------------------------------------------------------------------------