GPT Auto task

根据缓存中的数据自动在网页上与chat gpt对话

当前为 2023-07-20 提交的版本,查看 最新版本

// ==UserScript==
// @namespace         https://greasyfork.org/zh-CN/users/1106595-ikkem-lin
// @name              GPT Auto task
// @author            Mark
// @description       根据缓存中的数据自动在网页上与chat gpt对话
// @description       "snippetSourceData", "mock_prompt1", "mock_prompt2", "model_number" 四个localStorage变量用于存储数据
// @homepageURL       https://github.com/IKKEM-Lin/gpt-auto-task
// @version           0.1.0
// @match             *chat.openai.com/*
// @run-at            document-idle
// @require           https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js
// @require           https://cdn.jsdelivr.net/npm/idb-keyval@6/dist/umd.js
// ==/UserScript==
(function () {
    "use strict";

    const tableName = "data";

    const dbTable = {
        // tasks: idbKeyval.createStore("tasks", tableName),
        response1: idbKeyval.createStore("response1", tableName),
        response2: idbKeyval.createStore("response2", tableName),
        responseProcessed: idbKeyval.createStore("responseProcessed", tableName),
    };

    const downloadFile = (data, fileName) => {
        const a = document.createElement("a");
        document.body.appendChild(a);
        a.style = "display: none";
        const blob = new Blob([data], {
            type: "application/octet-stream",
        });
        const url = window.URL.createObjectURL(blob);
        a.href = url;
        a.download = fileName;
        a.click();
        window.URL.revokeObjectURL(url);
    };

    const yaml2object = (yamlStr) => {
        try {
            return jsyaml.load(yamlStr);
        } catch (error) {
            return null;
        }
    };

    function hashFnv32a(str, asString = true, seed = undefined) {
        /*jshint bitwise:false */
        var i,
            l,
            hval = seed === undefined ? 0x811c9dc5 : seed;

        for (i = 0, l = str.length; i < l; i++) {
            hval ^= str.charCodeAt(i);
            hval +=
                (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
        }
        if (asString) {
            // Convert to 8 digit hex string
            return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
        }
        return hval >>> 0;
    }

    function reactionObjHandler(input) {
        let result = [];
        const validateKeys = ["reactants", "products", "condition", "catalysts"];
        function test(reaction) {
            // if (!reaction) {
            //     return
            // }
            if (reaction instanceof Array) {
                return reaction.map((item) => {
                    return test(item);
                });
            } else {
                try {
                    var keys = Object.keys(reaction);
                } catch (error) {
                    //   debugger;
                    console.error(error);
                    throw new Error();
                }
                if (validateKeys.some((key) => keys.includes(key))) {
                    result.push(reaction);
                    return;
                }
                keys.forEach((key) => {
                    if (reaction[key] && typeof reaction[key] === "object") {
                        test(reaction[key]);
                    }
                });
            }
        }
        test(input);
        return result;
    }

    class GPT_ASK_LOOP {
        queue = [];
        abstract = [];
        responds = [];
        checkInterval = 20000;
        account = "";
        downloadBtn = null;
        retrying = false;
        defaultMode = 2;
        lastSaveTime = 0;

        INPUT_SELECTOR = "#prompt-textarea";
        SUBMIT_BTN_SELECTOR = "#prompt-textarea + button";
        RESPOND_SELECTOR = "main .group";
        NEW_CHART_BTN_SELECTOR = "nav>div.mb-1>a:first-child";
        NORMAL_RESPOND_BTN_SELECTOR = "form div button.btn-neutral";
        ERROR_RESPOND_BTN_SELECTOR = "form div button.btn-primary";

        sleep(duration) {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve(true);
                }, duration);
            });
        }

        constructor(account) {
            this.initData().then(() => {
                this.account = account || Math.ceil(Math.random() * 1e10).toString(32);
                const btnWrap = document.createElement("div");
                btnWrap.innerHTML = `<button style="padding: 4px 8px;position: fixed;bottom: 20%;right: 8px;border-radius: 4px;background-color: #224466;color: #fff;">下载已生成结果(queue: ${this.queue.length}, res: ${this.responds.length})</button>`;
                this.downloadBtn = btnWrap.querySelector("button");
                this.downloadBtn.onclick = this.handleDownload.bind(this);
                document.body.appendChild(btnWrap);
                this.main();
            });
        }

        async initData() {
            const responseKeys = await idbKeyval.keys(dbTable.responseProcessed);
            this.responds = responseKeys;
            const snippetSourceData = JSON.parse(
                localStorage.getItem("snippetSourceData")
            );
            this.abstract = snippetSourceData.filter(
                (item) => item.type == "abstract"
            );
            const paragraphs = snippetSourceData.filter(
                (item) => item.type != "abstract"
            );
            this.queue = paragraphs.filter(
                (item) =>
                    !(responseKeys || []).includes(`${item.article_id}-${item.id}`)
            );
        }

        async handleDownload() {
            const reactionGroups = await idbKeyval.values(dbTable.responseProcessed);
            if (!reactionGroups.length) {
                return;
            }
            const reactions = [];
            reactionGroups.forEach((item) => {
                const { articleId, snippetId, reaction } = item;
                const uniqReaction = Array.from(
                    new Set(reaction.map((v) => JSON.stringify(v)))
                ).map((v) => JSON.parse(v));
                uniqReaction.forEach((data) => {
                    const name = hashFnv32a(JSON.stringify(data));
                    reactions.push({ articleId, snippetId, data, name });
                });
            });

            const now = new Date();
            downloadFile(
                JSON.stringify(reactions),
                `${now.getFullYear()}-${now.getMonth() + 1
                }-${now.getDate()}-${now.getHours()}${now.getMinutes()}${now.getSeconds()}-${reactions.length
                }.json`
            );
        }

        async report(tip = "") {
            await fetch("https://gpt-hit.deno.dev/api/update", {
                method: "POST",
                body: JSON.stringify({
                    account: this.account,
                    reaction_count: this.responds.length,
                    queue_count: this.queue.length,
                    tip: tip,
                }),
            }).catch((err) => {
                console.error({ err });
            });
        }

        genPrompt(content, step = 1) {
            return step === 1
                ? `${localStorage.getItem("mock_prompt" + step)}
  
              ''' ${content} ''' `
                : localStorage.getItem("mock_prompt" + step);
        }

        _updateDownloadBtnText() {
            if (this.downloadBtn) {
                const snippetSourceData = JSON.parse(
                    localStorage.getItem("snippetSourceData") || "[]"
                );
                const paragraphs = snippetSourceData.filter(
                    (item) => item.type != "abstract"
                );
                this.downloadBtn.innerText = `下载已生成结果(queue: ${this.queue.length
                    }, res: ${this.responds.length}, skip: ${paragraphs.length - this.queue.length - this.responds.length
                    })`;
            }
        }

        _getLastRespondTime() {
            return Math.max.apply(
                null,
                this.responds
                    .map((item) => item.createdTime)
                    .filter((item) => item)
                    .concat([0])
            );
        }

        getTask() {
            const task = this.queue[0];
            const maxTime = this._getLastRespondTime();
            this.report(
                (task &&
                    `Working on articleId: ${task.article_id}, snippetId: ${task.id
                    }, last-update-time: ${new Date(maxTime).toLocaleString()}`) ||
                ""
            );
            if (!task) {
                console.log("任务队列为空");
                return async () => null;
            }
            return async () => {
                const { article_id, id, content } = task;
                const relatedAbstract =
                    this.abstract.find((item) => item.article_id === article_id)
                        ?.content || "";
                console.log(
                    `开始触发 ${article_id}-${id}, ${new Date().toTimeString()}`
                );
                const promptContent = `
                  ${relatedAbstract}
  
                  ${content}
                  `;
                const prompt1 = this.genPrompt(promptContent, 1);
                const prompt2 = this.genPrompt(promptContent, 2);
                const result1 = await this.trigger(prompt1).catch((err) => {
                    return null;
                });
                if (!result1) {
                    return null;
                }
                await idbKeyval.set(
                    `${article_id}-${id}`,
                    {
                        articleId: article_id,
                        snippetId: id,
                        reaction: result1,
                        createdTime: new Date().valueOf(),
                    },
                    dbTable.response1
                );
                await this.sleep(3 * 1000);
                const result2 = await this.trigger(prompt2).catch((err) => {
                    return null;
                });
                if (!result2) {
                    return { articleId: article_id, snippetId: id, reaction: result1 };
                }
                await idbKeyval.set(
                    `${article_id}-${id}`,
                    {
                        articleId: article_id,
                        snippetId: id,
                        reaction: result2,
                        createdTime: new Date().valueOf(),
                    },
                    dbTable.response2
                );
                return { articleId: article_id, snippetId: id, reaction: result2 };
            };
        }

        async rawReactionProcess(rawReactionHTML) {
            const ele = document.createElement("div");
            ele.innerHTML = rawReactionHTML;
            const res = Array.from(ele.querySelectorAll("code"))
                .map((el) => el.innerText)
                .map((yml) => yaml2object(yml));

            if (res && res.length > 0 && res.every((s) => s !== null)) {
                const result = reactionObjHandler(res);
                return result.length > 0 ? result : null;
            }
            return null;
        }

        async saveRespond(respond) {
            const { articleId, snippetId } = respond;
            const currentTimeStamp = new Date().valueOf();
            const reactionProcessed = await this.rawReactionProcess(respond.reaction);
            if (!reactionProcessed) {
                console.warn(`${articleId}-${snippetId} 无法解析出 reaction, 即将跳过`);
                this.queue = this.queue.filter((item) => item.id !== snippetId);
                return;
            }
            this.responds.push({
                articleId,
                snippetId,
                createdTime: currentTimeStamp,
            });
            this.queue = this.queue.filter((item) => item.id !== snippetId);

            await idbKeyval.set(
                `${articleId}-${snippetId}`,
                {
                    ...respond,
                    reaction: reactionProcessed,
                    createdTime: new Date().valueOf(),
                },
                dbTable.responseProcessed
            );
            if (this.responds.length && this.responds.length % 50 === 0) {
                this.handleDownload.bind(this)();
            }
        }

        trigger(prompt, checkInterval = this.checkInterval) {
            return new Promise((resolve, reject) => {
                const textEl = document.querySelector(this.INPUT_SELECTOR);
                const submitEl = document.querySelector(this.SUBMIT_BTN_SELECTOR);
                textEl.value = prompt;
                textEl.dispatchEvent(new Event("input", { bubbles: true }));
                setTimeout(() => {
                    submitEl.click();

                    let resCache = null;
                    let checkOutputCount = 0;
                    (async () => {
                        while (true) {
                            await this.sleep(checkInterval);
                            const result = Array.from(
                                document.querySelectorAll(this.RESPOND_SELECTOR)
                            );
                            const temp = result[result.length - 1];
                            if (!temp) {
                                if (checkOutputCount > 0) {
                                    console.log("检查结果超时");
                                    reject(null);
                                    break;
                                }
                                checkOutputCount++;
                                continue;
                            }
                            if (resCache === temp.innerHTML) {
                                // console.log("匹配,resCache:", resCache);
                                const validateResult = await this.validate(resCache).catch(
                                    (err) => {
                                        reject(null);
                                        return;
                                    }
                                );
                                if (validateResult === true) {
                                    resolve(resCache);
                                    break;
                                } else if (validateResult === false) {
                                    continue;
                                }
                                reject(null);
                                break;
                            }
                            resCache = temp.innerHTML;
                            console.log(`${checkInterval / 1000}s后再次检查结果`);
                        }
                    })();
                }, 4000);
            });
        }

        async validate(innerHTML) {
            const buttons = document.querySelectorAll(
                this.NORMAL_RESPOND_BTN_SELECTOR
            );
            const errorBtn = document.querySelectorAll(
                this.ERROR_RESPOND_BTN_SELECTOR
            );
            // 如果触发gpt-4 3小时25次限制
            if (!buttons[0] && !errorBtn[0] && innerHTML.includes("usage cap")) {
                console.error("触发gpt-4 3小时25次限制,等待10min后重试");
                await this.sleep(10 * 60 * 1000);
                throw new Error("触发gpt-4 3小时25次限制");
            }
            // 如果openAI服务器报错未返回结果
            if (errorBtn[0]) {
                // && innerHTML.includes("wrong")) {
                if (this.retrying) {
                    this.retrying = false;
                    return true;
                }
                errorBtn[0].click();
                this.retrying = true;
                return false;
            }
            // 如果输出结果未包含code标签
            if (!innerHTML.includes("</code>")) {
                if (this.retrying) {
                    this.retrying = false;
                    console.error("第二次还是未输出yaml结构");
                    throw new Error("未返回yaml结构");
                }
                console.error("未输出yaml结构,重试一次");
                buttons[0].click();
                this.retrying = true;
                return false;
            }
            this.retrying = false;
            // 如果还未完全输出
            if (buttons.length > 1) {
                buttons[buttons.length - 1].click();
                return false;
            }
            return true;
        }

        async main(sleepTime = 5000) {
            let emptyCount = 0;
            while (true) {
                // {0: gpt-3.5, 1: gpt-4, 2: gpt-4 mobile}
                const modelNum =
                    +localStorage.getItem("model_number") || this.defaultMode;
                const gpt4btn = document.querySelectorAll(
                    "ul > li > button.cursor-pointer"
                )[modelNum];

                if (gpt4btn) {
                    console.log(`当前模型为:${gpt4btn.innerText}`);
                    gpt4btn.firstChild.click();
                } else {
                    console.warn(`无法选择模型,2分钟后刷新`);
                    await this.sleep(2 * 60 * 1000);
                    location.reload();
                }
                await this.sleep(sleepTime / 2);
                if (modelNum === 1 && !location.href.endsWith("gpt-4")) {
                    console.log("未切换到gpt-4模式, 5分钟后重试");
                    const maxTime = this._getLastRespondTime();
                    const diff = new Date().valueOf() - maxTime;
                    if (maxTime && diff > 1.5 * 60 * 60 * 1000) {
                        console.warn("超时未刷新, 5分钟后刷新页面");
                        await this.sleep(5 * 60 * 1000);
                        location.reload();
                        break;
                    }
                    this.report(
                        `触发gpt-4 3小时25次限制,上次运行时间:${new Date(
                            maxTime
                        ).toLocaleString()}`
                    );
                    await this.sleep(5 * 60 * 1000);
                    const newChatBtn = document.querySelector(
                        this.NEW_CHART_BTN_SELECTOR
                    );
                    newChatBtn.click();
                    continue;
                }
                const task = this.getTask();
                if (!task) {
                    if (emptyCount > 0) {
                        console.warn("连续两次未获取到任务,2分钟后刷新");
                        await this.sleep(2 * 60 * 1000);
                        location.reload();
                        break;
                    }
                    emptyCount++;
                    await this.sleep(5 * 60 * 1000);
                    continue;
                }

                const result = await task();
                if (result) {
                    this.saveRespond(result);
                    emptyCount = 0;
                } else {
                    if (emptyCount > 0) {
                        console.warn("连续两次未获取到任务,2分钟后刷新");
                        await this.sleep(2 * 60 * 1000);
                        location.reload();
                        break;
                    }
                    emptyCount += 1;
                }
                console.log(`${sleepTime / 1000}s后将再次触发`);
                const newChatBtn = document.querySelector(this.NEW_CHART_BTN_SELECTOR);
                newChatBtn.click();
                await this.sleep(sleepTime / 2);
                this._updateDownloadBtnText();
            }
        }
    }

    function secondInterval() {
        console.log("start secondInterval...");
        setInterval(async () => {
            const responds = await idbKeyval.values(dbTable.responseProcessed);
            const maxTime = Math.max.apply(
                null,
                responds
                    .map((item) => item.createdTime)
                    .filter((item) => item)
                    .concat([0])
            );
            const diff = new Date().valueOf() - maxTime;

            console.log(`last updated at: ${maxTime}, diff is ${diff}`);
            if (maxTime && diff > 30 * 60 * 1000) {
                console.warn("超时未刷新, 2分钟后刷新页面");
                await this.sleep(2 * 60 * 1000);
                location.reload();
            }
        }, 10 * 60 * 1000);
    }

    function start() {
        const ACCOUNT_NAME_SELECTOR = "nav > div:last-child > div:last-child";
        const nameEl = document.querySelector(ACCOUNT_NAME_SELECTOR);
        const name = nameEl && nameEl.innerText;
        if (name) {
            new GPT_ASK_LOOP(name);
            secondInterval();
        } else {
            setTimeout(() => {
                start();
            }, 5000);
        }
    }
    start();
})();