GPT Auto task

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

// ==UserScript==
// @namespace         https://greasyfork.org/zh-CN/users/1106595-ikkem-lin
// @name              GPT Auto task
// @author            Mark
// @description       根据缓存中的数据自动在网页上与chat gpt对话
// @homepageURL       https://github.com/IKKEM-Lin/gpt-auto-task
// @version           0.2.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),
    config: idbKeyval.createStore("config", tableName),
    skipSnippet: idbKeyval.createStore("skipSnippet", tableName),
    response1: idbKeyval.createStore("response1", tableName),
    response2: idbKeyval.createStore("response2", tableName),
    responseProcessed: idbKeyval.createStore("responseProcessed", tableName),
  };

  const locationReload = () => {
    location.href = "https://chat.openai.com/?model=gpt-4";
  };

  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;
  }

  function readFile(accept = "", multiple = false) {
    const inputEl = document.createElement("input");
    inputEl.setAttribute("type", "file");
    inputEl.setAttribute("accept", accept);
    inputEl.setAttribute("multiple", !!multiple);
    return new Promise((resolve, reject) => {
      inputEl.addEventListener("change", (e) => {
        resolve(multiple ? inputEl.files : inputEl.files[0]);
        window.removeEventListener("click", onWindowClick, true);
      });
      inputEl.click();

      const onWindowClick = () => {
        if (!inputEl.value) {
          reject(new Error("用户取消选择"));
        }
        window.removeEventListener("click", onWindowClick, true);
      };
      setTimeout(() => {
        window.addEventListener("click", onWindowClick, true);
      }, 100);
    });
  }

  class GPT_ASK_LOOP {
    queue = [];
    abstract = [];
    responds = [];
    checkInterval = 20000;
    account = "";
    downloadBtn = null;
    retrying = false;
    lastSaveTime = 0;
    prompt1 = "";
    prompt2 = "";
    modelNum = 1;

    INPUT_SELECTOR = "#prompt-textarea";
    SUBMIT_BTN_SELECTOR = 'button[data-testid="send-button"]';
    RESPOND_SELECTOR = 'main div[data-message-author-role="assistant"]';
    NEW_CHART_BTN_SELECTOR = "nav div.flex-col a[href='/']";
    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);

        const btnWrapBackup = document.createElement("div");
        btnWrapBackup.innerHTML = `<button style="padding: 4px 8px;position: fixed;bottom: 30%;right: 8px;border-radius: 4px;background-color: #224466;color: #fff;">备份</button>`;
        const backupBtn = btnWrapBackup.querySelector("button");
        backupBtn.onclick = this.backUp.bind(this);
        document.body.appendChild(btnWrapBackup);

        const btnWrapImport = document.createElement("div");
        btnWrapImport.innerHTML = `<button style="padding: 4px 8px;position: fixed;bottom: 40%;right: 8px;border-radius: 4px;background-color: #224466;color: #fff;">导入</button>`;
        const importBtn = btnWrapImport.querySelector("button");
        importBtn.onclick = async () => {
          if (
            !window.confirm(
              "The data in browser will be clear up. Please make sure you have to do this !!!"
            )
          ) {
            return;
          }
          const file = await readFile(".json");
          const reader = new FileReader();

          reader.onload = (event) => {
            const json = JSON.parse(event.target.result);
            // console.log({json}, 'json')
            this.importFromBackUp.bind(this)(json);
          };

          reader.readAsText(file);
        };
        document.body.appendChild(btnWrapImport);
        this.main();
      });
    }

    async initData() {
      await this.legacyTaskInit();
      const skipSnippetKeys = await idbKeyval.keys(dbTable.skipSnippet);
      const responseKeys = await idbKeyval.keys(dbTable.responseProcessed);
      const responseValues = await idbKeyval.values(dbTable.responseProcessed);
      this.responds = responseValues.map((item) => ({
        articleId: item.articleId,
        snippetId: item.snippetId,
        createdTime: item.createdTime,
      }));
      const indexDBConfig = (await idbKeyval.entries(dbTable.config)) || [];
      if (indexDBConfig.length) {
        this.prompt1 = indexDBConfig.find((item) => item[0] === "prompt1");
        this.prompt2 = indexDBConfig.find((item) => item[0] === "prompt2");
        this.modelNum = indexDBConfig.find((item) => item[0] === "modelNum");
        this.prompt1 = (this.prompt1 && this.prompt1[1]) || "";
        this.prompt2 = (this.prompt2 && this.prompt2[1]) || "";
        this.modelNum = (this.modelNum && this.modelNum[1]) || 1;
      }
      const snippetSourceData = (await idbKeyval.values(dbTable.tasks)) || [];
      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}`) &&
          !(skipSnippetKeys || []).includes(`${item.article_id}-${item.id}`)
      );
      if (this.queue.length !== 0) {
        return;
      }
      this.queue = paragraphs.filter((item) =>
        (skipSnippetKeys || []).includes(`${item.article_id}-${item.id}`)
      );
      const skipSnippetEntries = await idbKeyval.entries(dbTable.skipSnippet);
      this.queue.sort((a,b) => {
        const aItem = skipSnippetEntries.find(item => item[0] === `${a.article_id}-${a.id}`)
        const bItem = skipSnippetEntries.find(item => item[0] === `${b.article_id}-${b.id}`)
        return aItem[1] < bItem[1] ? -1 : 1
      })
    }

    async legacyTaskInit() {
      const indexDBTasks = (await idbKeyval.entries(dbTable.tasks)) || [];
      const indexDBConfig = (await idbKeyval.entries(dbTable.config)) || [];
      if (indexDBConfig.length === 0) {
        const prompt1 = localStorage.getItem("mock_prompt1");
        const prompt2 = localStorage.getItem("mock_prompt2");
        const modelNum = +localStorage.getItem("model_number") || 1;
        if (!prompt1 || !prompt2) {
          return;
        }
        await idbKeyval.setMany(
          [
            ["prompt1", prompt1],
            ["prompt2", prompt2],
            ["modelNum", modelNum],
          ],
          dbTable.config
        );
      }
      if (indexDBTasks.length === 0) {
        const snippetSourceData = JSON.parse(
          localStorage.getItem("snippetSourceData") || "[]"
        );
        if (!snippetSourceData.length) {
          return;
        }
        const snippetSourceDataEntries = snippetSourceData.map((item) => [
          `${item.article_id}-${item.id}`,
          item,
        ]);
        await idbKeyval.setMany(snippetSourceDataEntries, dbTable.tasks);
      }
    }

    async importFromBackUp(data) {
      const {
        response1,
        response2,
        responseProcessed,
        skipSnippet,
        config,
        tasks,
      } = data;
      if (
        !(
          response1 &&
          response2 &&
          responseProcessed &&
          skipSnippet &&
          config &&
          tasks
        )
      ) {
        alert(
          `[ "response1", "response2", "responseProcessed", "skipSnippet", "config", "tasks" ], all of them are required`
        );
        return;
      }
      await idbKeyval.clear(dbTable.response1);
      await idbKeyval.clear(dbTable.response2);
      await idbKeyval.clear(dbTable.responseProcessed);
      await idbKeyval.clear(dbTable.skipSnippet);
      await idbKeyval.clear(dbTable.config);
      await idbKeyval.clear(dbTable.tasks);
      await idbKeyval.setMany(response1, dbTable.response1);
      await idbKeyval.setMany(response2, dbTable.response2);
      await idbKeyval.setMany(responseProcessed, dbTable.responseProcessed);
      await idbKeyval.setMany(skipSnippet, dbTable.skipSnippet);
      await idbKeyval.setMany(config, dbTable.config);
      await idbKeyval.setMany(tasks, dbTable.tasks);
      locationReload();
    }

    async backUp() {
      const response1 = (await idbKeyval.entries(dbTable.response1)) || [];
      const response2 = (await idbKeyval.entries(dbTable.response2)) || [];
      const responseProcessed =
        (await idbKeyval.entries(dbTable.responseProcessed)) || [];
      const skipSnippet = (await idbKeyval.entries(dbTable.skipSnippet)) || [];
      const config = (await idbKeyval.entries(dbTable.config)) || [];
      const tasks = (await idbKeyval.entries(dbTable.tasks)) || [];

      const paragraphs = tasks.filter((item) => item[1].type != "abstract");

      const articleIds = tasks.map((item) => item[1].article_id).sort();

      const now = new Date();
      const current = `${now.getFullYear()}-${
        now.getMonth() + 1
      }-${now.getDate()}-${now.getHours()}${now.getMinutes()}${now.getSeconds()}`;
      downloadFile(
        JSON.stringify({
          response1,
          response2,
          responseProcessed,
          skipSnippet,
          config,
          tasks,
        }),
        `Article_${articleIds[0]}_${
          articleIds[articleIds.length - 1]
        }-progress_${paragraphs.length}_${
          responseProcessed.length
        }-${current}.backup.json`
      );
    }

    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),
        `${this.account}-${now.getFullYear()}-${
          now.getMonth() + 1
        }-${now.getDate()}-${now.getHours()}${now.getMinutes()}${now.getSeconds()}-${
          reactions.length
        }.json`
      );
    }

    async report(tip = "") {
      await fetch("http://localhost:3000", {
        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
        ? `${this.prompt1}

              ''' ${content} ''' `
        : this.prompt2;
    }

    async _updateDownloadBtnText() {
      if (this.downloadBtn) {
        const snippetSourceData = (await idbKeyval.values(dbTable.tasks)) || [];
        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 skipSnippetHandler(articleId, snippetId) {
      const oldVal = await idbKeyval.get(
        `${articleId}-${snippetId}`,
        dbTable.skipSnippet
      );
      await idbKeyval.set(
        `${articleId}-${snippetId}`,
        (oldVal || 0) + 1,
        dbTable.skipSnippet
      );
      this.queue = this.queue.filter((item) => item.id !== snippetId);
    }

    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, 即将跳过`);
        await this.skipSnippetHandler(articleId, 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
      );
      try {
        await idbKeyval.del(`${articleId}-${snippetId}`, dbTable.skipSnippet);
      } catch (err) {}
      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
      );
      const feedbackBtns = document.querySelectorAll('main .final-completion button[class*="final-completion"]')
      const regenerateBtn = feedbackBtns[feedbackBtns.length - 1];
      // 如果触发gpt-4 3小时25次限制
      if (!regenerateBtn && !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结构,重试一次");
        regenerateBtn.click();
        this.retrying = true;
        return false;
      }
      this.retrying = false;
      // 如果还未完全输出
      if (
        buttons.length > 1 &&
        !buttons[buttons.length - 1].innerText.includes("Regenerate")
      ) {
        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}
        const modelNum = this.modelNum;
        // 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);
        //   locationReload();
        // }
        const currentModel = document.querySelector('main [aria-haspopup="menu"]')?.innerText;
        const isGPT4 = currentModel.trim() === "ChatGPT 4"
        await this.sleep(sleepTime / 2);
        if (
          modelNum === 1 && !isGPT4
        ) {
          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);
            locationReload();
            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);
            locationReload();
            break;
          }
          emptyCount++;
          await this.sleep(5 * 60 * 1000);
          continue;
        }

        const result = await task();
        if (result) {
          this.saveRespond(result);
          emptyCount = 0;
        } else {
          if (emptyCount > 0) {
            const task = this.queue[0];
            const { article_id, id } = task;
            console.warn(
              `${article_id}-${id}连续两次未获取到任务值,2分钟后刷新`
            );
            await this.skipSnippetHandler(article_id, id);
            await this.sleep(2 * 60 * 1000);
            locationReload();
            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...");
    const sleep = (duration) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(true);
        }, duration);
      });
    };
    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 sleep(2 * 60 * 1000);
        locationReload();
      }
    }, 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();
})();