// ==UserScript== // @name 多家大模型网页同时回答 // @namespace http://tampermonkey.net/ // @version 1.1.8 // @description 只需输入一次问题,就能自动同时给浏览器打开的各家大模型网页提问。免去手动拷贝粘贴到其他网页、并苦苦等待的麻烦。 // @author wz // @match https://www.kimi.com/* // @match https://chat.deepseek.com/* // @match https://www.tongyi.com/* // @match https://chatgpt.com/* // @match https://www.doubao.com/* // @match https://chat.zchat.tech/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @connect localhost // @connect www.ratetend.com // @license GPL-3.0-only // @downloadURL none // ==/UserScript== (function () { 'use strict'; console.log("ai script, start"); const T = "tool-"; const QUEUE = "tool-queue"; const LEN = "len"; const LAST_Q = "lastQ"; const UID_KEY = "uid"; const SPLIT_CHAR = ",,,"; // const DOMAIN = "https://www.ratetend.com:5001"; const DOMAIN = "http://localhost:8002"; const MAX_QUEUE = 3; const checkGap = 100; let maxRetries = 100; // 最多尝试10秒 const MAX_PLAIN = 50; // lastQuestion原文存储的极限长度 const HASH_LEN = 16; let MAIN_SITE = 0; let site = 0; let url = window.location.href; const keywords = { "kimi": 0, "deepseek": 1, "tongyi": 2, "chatgpt": 3, "doubao": 4, "zchat": 5 }; for (const keyword in keywords) { if (url.indexOf(keyword) > -1) { site = keywords[keyword]; break; } } const historySites = { 0: "https://www.kimi.com/chat/", 1: "https://chat.deepseek.com/a/chat/s/", 2: "https://www.tongyi.com/?sessionId=", 3: "https://chatgpt.com/c/", 4: "https://www.doubao.com/chat/", 5: "https://chat.zchat.tech/c/" } const newSites = { 0: "https://www.kimi.com/", 1: "https://chat.deepseek.com/", 2: "https://www.tongyi.com/", 3: "https://chatgpt.com/", 4: "https://www.doubao.com/chat", 5: "https://chat.zchat.tech/" } function getChatId(){ let url = getUrl(); let subStr = url.substring(url.lastIndexOf('/') + 1); // console.log("subStr: "+subStr); if(isEmpty(subStr)){ return ""; } if(site === 2){ let mark = 'sessionId='; if(url.indexOf(mark) === -1){ return ""; } let tmp = url.lastIndexOf(mark) + mark.length; return url.substring(tmp); }else if([3, 5].includes(site)){ if(subStr.indexOf("auto") > -1){ return ""; } return subStr; }else if(site === 4){ if(subStr.indexOf("local") > -1){ return ""; } return subStr; }else{ return subStr; } } function getUrl(){ return window.location.href; } // 队列头部添加元素 function enqueue(element) { let queue = JSON.parse(localStorage.getItem(QUEUE) || "[]"); if (queue.length > 0 && queue[0] === element) { return; } queue.unshift(element); localStorage.setItem(QUEUE, JSON.stringify(queue)); } // 当队列长度超过阈值,删除队尾元素 function dequeue() { let queue = JSON.parse(localStorage.getItem(QUEUE) || "[]"); let len = queue.length; if(len > MAX_QUEUE){ let chatIdKey = T + queue[len - 1]; let valJson = JSON.parse(getS(chatIdKey)); if(!isEmpty(valJson)){ let uid = valJson.uid; localStorage.removeItem("uid-" + uid); GM_deleteValue(uid); } localStorage.removeItem(chatIdKey); queue.pop(); localStorage.setItem(QUEUE, JSON.stringify(queue)); } } let hasChatId = false; let sendLock = false; // setInterval(function(){ // masterCheckNew(); // receiveNew(); // }, 3000); setTimeout(function(){ setInterval(function(){ masterCheckNew(); receiveNew(); }, 1000); }, 100); // SSE事件 const sseUrl = DOMAIN + "/create?sourceId=" + site; if (typeof(EventSource) === "undefined") { console.error("This browser does not support Server-Sent Events."); return; } const source = new EventSource(sseUrl); source.onopen = () => { console.log("[SSE] Connection established."); }; source.onmessage = (event) => { console.log("[SSE] Default message:", event.data); }; source.addEventListener("broadcast", (event) => { console.log("[SSE] Broadcast:", event.data); receiveNew(); }); source.onerror = (err) => { console.error("[SSE] Connection error:", err); }; function isEqual(latestQ, lastQ){ if(latestQ.length > MAX_PLAIN){ if(lastQ.length === HASH_LEN){ return dHash(latestQ) === lastQ; }else{ return false; } }else{ return latestQ === lastQ; } } function getQuesOrHash(ques){ return ques.length > MAX_PLAIN ? dHash(ques) : ques; } // 发送端 function masterCheckNew(){ if(sendLock){ return; } let masterId = getChatId(); if(isEmpty(masterId)){ return; } let questions = getQuestionList(); let lenNext = questions.length; if(lenNext > 0){ let len = hgetS(T + masterId, LEN) || 0; // console.log("lenNext: "+lenNext+", len: "+len); if(lenNext - len === 1){ let lastestQ = questions[lenNext - 1].textContent; let lastQuestion = hgetS(T + masterId, LAST_Q); if(!isEmpty(lastQuestion) && isEqual(lastestQ, lastQuestion)){ return; } masterReq(masterId, lastestQ); hasChatId = true; hsetS(T + masterId, LEN, lenNext); } } }; function masterReq(masterId, lastestQ){ let uid = hgetS(T + masterId, UID_KEY); if(isEmpty(uid)){ uid = guid(); hsetS(T + masterId, UID_KEY, uid); } let message = { uid: uid, question: lastestQ }; console.log(message); setGV("msg", message); hsetS(T + masterId, LAST_Q, getQuesOrHash(lastestQ)); let remoteUrl = DOMAIN + "/masterQ"; GM_xmlhttpRequest({ method: "POST", url: remoteUrl, data: null, headers: { "Content-Type": "application/json" }, onload: function(response) { console.log(response.responseText); }, onerror: function(error) { console.error('请求失败:', error); } }); let uidJson = getGV(uid); // 若json非空,则其中一定有首次提问的主节点的信息; // 故json若空则必为首次,只有首次会走如下逻辑 if(isEmpty(uidJson)){ uidJson = {}; uidJson[site] = masterId; console.log("master print uidJson: "+JSON.stringify(uidJson)); setGV(uid, uidJson); // 存储管理(删除与添加) dequeue(); enqueue(masterId); } } let receiveLock = false; function receiveNew(){ // console.log(new Date()+" receiveNew start"); if(receiveLock){ return; } let msg = getGV("msg"); if(isEmpty(msg)){ return; } receiveLock = true; if(sendLock){ receiveLock = false; return; } let curSlaveId = getChatId(); if(curSlaveId.length < 12){ curSlaveId = ""; } let question = msg.question; let lastQuestion = hgetS(T + curSlaveId, LAST_Q); let sameQuestion = false; if(!isEmpty(curSlaveId)){ sameQuestion = !isEmpty(lastQuestion) && isEqual(question, lastQuestion); console.log("question: "+question+", lastQuestion: "+lastQuestion); if(sameQuestion){ receiveLock = false; return; } } let questionBeforeJump = getS("questionBeforeJump"); // 如果是经跳转而来,无需处理主节点信息,直接从缓存取对话内容 if(!isEmpty(questionBeforeJump)){ console.log("questionBeforeJump: " + questionBeforeJump); let splits = questionBeforeJump.split(SPLIT_CHAR); let cachedQuestion = splits[0]; let cachedUid = splits[1]; let cachedSlaveId = ""; if(!isEmpty(curSlaveId)){ cachedSlaveId = splits[2]; if(curSlaveId !== cachedSlaveId){ receiveLock = false; return; } hsetS(T + curSlaveId, LAST_Q, getQuesOrHash(cachedQuestion)); } // 清空跳转用的缓存 setS("questionBeforeJump", ""); console.log("h1 send"); abstractSend(cachedQuestion, cachedSlaveId); if(isEmpty(curSlaveId)){ setUid(cachedUid, cachedQuestion); } receiveLock = false; return; } let uid = msg.uid; // 当前空,且之前chatId有值,则认为是手动打开的页面(若是从节点跟随跳转新页面的情况,前面已经拦截处理了) if(isEmpty(curSlaveId)){ if(hasChatId){ receiveLock = false; return; } }else{ hasChatId = true; } let targetUrl = ""; let slaveIdFlag = false; let slaveId = ""; let uidJson = getGV(uid); let lastQuestionOfComingSlaveId = ""; // 来者消息的uid,是否关联了从节点的chatId? if(!isEmpty(uidJson)){ console.log("uidJson " + JSON.stringify(uidJson)); slaveId = uidJson[site]; lastQuestionOfComingSlaveId = hgetS(T + slaveId, LAST_Q); console.log("lastQuestionOfComingSlaveId "+lastQuestionOfComingSlaveId); if(isEqual(question, lastQuestionOfComingSlaveId)){ receiveLock = false; return; } if(!isEmpty(slaveId)){ slaveIdFlag = true; } } let curIdFlag = !isEmpty(curSlaveId); // 从节点已进行过来者的uid对应的对话 if(slaveIdFlag){ // 当前页面有chatId if(curIdFlag){ // chatId相同则对话,不同则跳转 if(curSlaveId === slaveId){ hsetS(T + curSlaveId, LAST_Q, getQuesOrHash(question)); console.log(new Date() + " h2 send"); abstractSend(question, curSlaveId); }else{ targetUrl = historySites[site] + slaveId; } // 当前页面是空白,需跳转 }else{ targetUrl = historySites[site] + slaveId; } // 对从节点而言是新对话 }else{ // 当前页面有chatId,则跳转空白页 if(curIdFlag){ targetUrl = newSites[site]; // 当前页面已经是空白页 }else{ console.log("h3 send"); abstractSend(question, ""); setUid(uid, question); } } receiveLock = false; if(!isEmpty(targetUrl)){ setS("questionBeforeJump", question + SPLIT_CHAR + uid + SPLIT_CHAR + slaveId); window.location.href = targetUrl; } } function setUid(uid, question){ let intervalId; let lastUrl = getUrl(); let count = 0; let waitTime = 15000; if(site === 3){ waitTime *= 2; } console.log("ready to setUid"); intervalId = setInterval(function() { count ++; if(count > waitTime / checkGap){ console.log("setUid超时"); clearInterval(intervalId); } let chatId = getChatId(); if (!isEmpty(chatId)) { hasChatId = true; let uidJson = getGV(uid); if(!isEmpty(uidJson)){ if(isEmpty(uidJson[site])){ uidJson[site] = chatId; } }else{ uidJson = {}; uidJson[site] = chatId; } hsetS(T + chatId, LAST_Q, getQuesOrHash(question)); hsetS(T + chatId, LEN, 1); console.log("slave print uidJson: "+JSON.stringify(uidJson)); setGV(uid, uidJson); setS("uid-" + uid, JSON.stringify(uidJson)); sendLock = false; console.log("setUid finish"); hsetS(T + chatId, UID_KEY, uid); // 存储管理(删除与添加) dequeue(); enqueue(chatId); clearInterval(intervalId); } }, checkGap); } // ① 检查textArea存在 ② 检查sendBtn存在 ③ 检查问题列表长度是否加一 function abstractSend(content, chatId){ let intervalId; let count = 0; sendLock = true; intervalId = setInterval(function() { count ++; if(count > 5000 / checkGap){ clearInterval(intervalId); } const textarea = getTextArea(site); if (!isEmpty(textarea)) { clearInterval(intervalId); sendContent(textarea, content, chatId); } }, checkGap); } function sendContent(textarea, content, chatId){ textarea.focus(); document.execCommand('insertText', false, content); clickAndCheckLen(chatId); } function clickAndCheckLen(chatId) { let tryCount = 0; const checkBtnInterval = setInterval(() => { let quesFlag = false; if(isEmpty(chatId)){ quesFlag = true; }else{ let len = getQuestionList().length; if(len > 0){ quesFlag = true; } } let sendBtn = getBtn(site); if (quesFlag && !isEmpty(sendBtn)) { clearInterval(checkBtnInterval); setTimeout(function(){ // sendBtn存在不一定立即可以点击,最好延迟一下 sendBtn.click(); }, 200); checkQuesList(chatId); } else { tryCount++; if (tryCount > maxRetries) { clearInterval(checkBtnInterval); console.log("tryCount "+tryCount + ", quesFlag "+quesFlag+", sendBtn "+isEmpty(sendBtn)); console.warn("sendBtn或问题列表未找到,超时"); return; } } }, checkGap); } function checkQuesList(chatId) { let tryCount = 0; let cachedLen = hgetS(T + chatId, LEN); let newFlag = isEmpty(chatId) || isEmpty(cachedLen) || cachedLen === 0; const checkInterval = setInterval(() => { tryCount++; // 定时器:检查问题列表长度大于上次,则停止,并设置sendLock // 注意,若是chat首个问题,则只要求len=1 let len = getQuestionList().length; let questionDisplayFlag = false; if(newFlag){ if(len === 1){ questionDisplayFlag = true; } }else{ if(len > cachedLen){ questionDisplayFlag = true; } } if (questionDisplayFlag) { clearInterval(checkInterval); if(!isEmpty(chatId)){ hsetS(T + chatId, LEN, len); sendLock = false; // 解锁(如果chatId空,有setUid方法负责解锁) } } else if (tryCount > maxRetries) { console.log("tryCount "+tryCount + ", len "+len+", cachedLen "+cachedLen+", newFlag "+newFlag); clearInterval(checkInterval); console.warn("问题列表长度未符合判据,超时"); sendLock = false; let areaContent = getTextArea(site).textContent; if(!isEmpty(areaContent)){ location.reload(); } } }, checkGap); } function getQuestionList(){ let questions = []; if(site == 0){ questions = document.getElementsByClassName("user-content"); }else if(site === 1){ let scrollable = document.getElementsByClassName("scrollable")[1]; if(!isEmpty(scrollable)){ let list = scrollable.firstElementChild.firstElementChild.children let elementsArray = Array.from(list); questions = elementsArray.filter((item, index) => index % 2 === 0); } }else if(site === 2){ questions = document.querySelectorAll('[class^="bubble-"]'); }else if([3, 5].includes(site)){ questions = document.querySelectorAll('[data-message-author-role="user"]'); }else if(site === 4){ let list = document.querySelectorAll('[data-testid="message_text_content"]'); let elementsArray = Array.from(list); questions = elementsArray.filter((item, index) => index % 2 === 0); } return questions; } function getTextArea(site){ if(site == 0){ return document.getElementsByClassName('chat-input-editor')[0]; }else if(site === 1){ return document.getElementById('chat-input'); }else if([2, 4].includes(site)){ return document.getElementsByTagName('textarea')[0]; }else if([3, 5].includes(site)){ return document.getElementById('prompt-textarea'); } } function getBtn(site){ if(site == 0){ return document.getElementsByClassName('send-button')[0]; }else if(site === 1){ var btns = document.querySelectorAll('[role="button"]'); return btns[btns.length - 1]; }else if(site === 2){ return document.querySelectorAll('[class^="operateBtn-"], [class*=" operateBtn-"]')[0]; }else if([3, 5].includes(site)){ return document.getElementById('composer-submit-button'); }else if(site === 4){ return document.getElementById('flow-end-msg-send'); } } function hgetS(key, jsonKey){ let json = localStorage.getItem(key); if(isEmpty(json)){ return ""; } json = JSON.parse(json); return json[jsonKey]; } function hsetS(key, jsonKey, val){ let json = JSON.parse(localStorage.getItem(key) || "{}"); json[jsonKey] = val; localStorage.setItem(key, JSON.stringify(json)); } function getS(key){ return localStorage.getItem(key); } function setS(key, val){ localStorage.setItem(key, val); } function setGV(key, value){ GM_setValue(key, value); } function getGV(key){ return GM_getValue(key); } function isEmpty(item){ if(item===null || item===undefined || item.length===0 || item === "null"){ return true; }else{ return false; } } // 自定义哈希 function dHash(str, length = HASH_LEN) { let hash = 5381; for (let i = 0; i < str.length; i++) { hash = (hash * 33) ^ str.charCodeAt(i); } const chars = '0123456789abcdefghijklmnopqrstuvwxyz'; let result = ''; let h = hash >>> 0; // 转为无符号整数 // 简单的伪随机数生成器(带种子) function pseudoRandom(seed) { let value = seed; return () => { value = (value * 1664525 + 1013904223) >>> 0; // 常见的 LCG 参数 return value / 4294967296; // 返回 [0,1) 的浮点数 }; } const rand = pseudoRandom(hash); // 使用 hash 作为种子 for (let i = 0; i < length; i++) { if (h > 0) { result += chars[h % chars.length]; h = Math.floor(h / chars.length); } else { // 使用伪随机数生成字符 const randomIndex = Math.floor(rand() * chars.length); result += chars[randomIndex]; } } return result; } function guid() { return 'xxxxxxxx-xxxx-4xxx-yxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } })();