// ==UserScript==
// @name 多家大模型网页同时回答
// @namespace http://tampermonkey.net/
// @version 1.1.9
// @description 只需输入一次问题,就能自动同时给浏览器打开的各家大模型网页提问。免去手动拷贝粘贴到其他网页、并苦苦等待的麻烦。支持范围:Deepseek, ChatGPT(官网版、zchat原生界面版),Kimi,通义千问,豆包
// @author interest2
// @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
// ==/UserScript==
(function () {
'use strict';
console.log("ai script, start");
let newFollow = false; // 新开一家大模型网页,是否自动同步之前其他大模型的问题,默认不同步
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
};
// 根据当前网址关键词,设置site值
for (const keyword in keywords) {
if (url.indexOf(keyword) > -1) {
site = keywords[keyword];
break;
}
}
// 各家大模型的网址(新对话,历史对话的前缀)
const webSites = {
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 = Object.fromEntries(
Object.entries(webSites).map(([key, [baseUrl]]) => [key, baseUrl])
);
const historySites = Object.fromEntries(
Object.entries(webSites).map(([key, [baseUrl, suffix]]) => [key, baseUrl + suffix])
);
let pat0 = "[0-9a-z]{20}";
let pat1 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
let pat2 = "[0-9a-f]{32}";
let pat3 = "[0-9a-f]{16}";
const pattern ={
0: pat0,
1: pat1,
2: pat2,
3: pat1,
4: pat3,
5: pat1
}
// 从url提取各大模型网站给一场对话设置的唯一标识
function getChatId(){
let url = getUrl();
if(isEmpty(url)){
return "";
}
if(site === 4 && url.indexOf("local") > -1){
return "";
}
const regex = new RegExp(pattern[site]);
let ret = url.match(regex);
if(isEmpty(ret)){
return "";
}
return ret[0];
}
function getUrl(){
return window.location.href;
}
// // 用于辅助判断的情形:该网页此前位于历史对话(hasChatId为true),而现在手动打开了新对话
// let hasChatId = false;
// 给receive方法整体加锁
let receiveLock = false;
// 给发送环节加锁。能否不要这个锁?不能,因为send环节是异步轮询,receive解锁时send未必轮询结束
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);
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 msg = {
uid: uid,
question: lastestQ
};
console.log(msg);
setGV("msg", msg);
hsetS(T + masterId, LAST_Q, getQuesOrHash(lastestQ));
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 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);
}
});
}
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();
let question = msg.question;
let sameQuestion = false;
if(!isEmpty(curSlaveId)){
let lastQuestion = hgetS(T + curSlaveId, LAST_Q);
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;
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];
if(!isEmpty(slaveId)){
lastQuestionOfComingSlaveId = hgetS(T + slaveId, LAST_Q);
console.log("lastQuestionOfComingSlaveId "+lastQuestionOfComingSlaveId);
if(isEqual(question, lastQuestionOfComingSlaveId)){
receiveLock = false;
return;
}
slaveIdFlag = true;
}
}
let curIdFlag = !isEmpty(curSlaveId);
// 从节点已进行过来者的uid对应的对话
if(slaveIdFlag){
// 当前页面有chatId
if(curIdFlag){
// chatId相同则对话,不同则跳转
if(curSlaveId === slaveId){
console.log(new Date() + " h2 send");
hsetS(T + curSlaveId, LAST_Q, getQuesOrHash(question));
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)) {
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 newChatFlag = isEmpty(chatId) || isEmpty(cachedLen) || cachedLen === 0;
const checkInterval = setInterval(() => {
tryCount++;
// 定时器:检查问题列表长度大于上次,则停止,并设置sendLock
// 注意,若是chat首个问题,则只要求len=1
let len = getQuestionList().length;
let questionDisplayFlag = false;
if(newChatFlag){
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+", newChatFlag "+newChatFlag);
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 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));
}
}
// localStorage读写json(hashMap)
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);
});
}
})();