Greasy Fork

多家大模型网页同时回答

只需输入一次问题,就能自动同时给浏览器打开的各家大模型网页提问。免去手动拷贝粘贴到其他网页、并苦苦等待的麻烦。

当前为 2025-06-02 提交的版本,查看 最新版本

// ==UserScript==
// @name         多家大模型网页同时回答
// @namespace    http://tampermonkey.net/
// @version      1.1.7
// @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
// ==/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);
        });
    }

})();