Greasy Fork is available in English.

Zotero GPT Connector

Zotero GPT Pro, support ChatGPT Gemini Poe Kimi Coze Chatglm Yiyan Tongyi Claude Mytan ChanlderAi DeepSeek Doubao AIStudio

// ==UserScript==
// @name         Zotero GPT Connector
// @description  Zotero GPT Pro, support ChatGPT Gemini Poe Kimi Coze Chatglm Yiyan Tongyi Claude Mytan ChanlderAi DeepSeek Doubao AIStudio
// @namespace    http://tampermonkey.net/
// @icon         https://github.com/MuiseDestiny/zotero-gpt/blob/bootstrap/addon/chrome/content/icons/favicon.png?raw=true
// @version      3.5.0
// @author       Polygon
// @noframes
// @match        https://chatgpt.com/*
// @match        https://gemini.google.com/app*
// @match        https://poe.com/*
// @match        https://www.coze.com/*
// @match        https://kimi.moonshot.cn/*
// @match        https://chatglm.cn/*
// @match        https://yiyan.baidu.com/*
// @match        https://tongyi.aliyun.com/*
// @match        https://qianwen.aliyun.com/*
// @match        https://claude.ai/*
// @match        https://mytan.maiseed.com.cn/*
// @match        https://mychandler.bet/*
// @match        https://chat.deepseek.com/*
// @match        https://www.doubao.com/chat/*
// @match        https://*.chatshare.biz/*
// @match        https://chat.kelaode.ai/*
// @match        https://chat.rawchat.cn/*
// @match        https://chat.sharedchat.top/*
// @match        https://node.dawuai.buzz/*
// @match        https://aistudio.google.com/*
// @match        https://claude.ai0.cn/*
// @match        https://claude4.ai0.cn/*
// @match        https://www.zaiwen.top/chat/*
// @match        https://chat.aite.lol/*
// @match        https://yuanbao.tencent.com/chat/*
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_cookie
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(async function () {
  'use strict';
  let isRunning = true
  let AI = "ChatGPT"
  const host = location.host
  if (host == 'chatgpt.com') {
    AI = "ChatGPT"
  } else if (host == 'gemini.google.com') {
    AI = "Gemini"
  } else if (host == 'poe.com') {
    AI = "Poe"
  } else if (host == 'kimi.moonshot.cn') {
    AI = "Kimi"
  } else if (host == 'www.coze.com') {
    AI = "Coze"
  } else if (host == "chatglm.cn") {
    AI = "Chatglm"
  } else if (host == 'yiyan.baidu.com') {
    AI = "Yiyan"
  } else if (host == 'tongyi.aliyun.com' || host == 'qianwen.aliyun.com') {
    AI = "Tongyi"
  } else if (host == "claude.ai" || host == 'chat.kelaode.ai' || host.includes("claude")) {
    AI = "Claude"
  } else if (host == 'mytan.maiseed.com.cn') {
    AI = "MyTan"
  } else if (host == 'mychandler.bet') {
    localStorage.conversation_id = ""
    AI = "ChanlderAi"
  } else if (host == 'chat.deepseek.com') {
    AI = "DeepSeek"
  } else if (host == "www.doubao.com") {
    AI = "Doubao"
  } else if (host == 'aistudio.google.com') {
    AI = "AIStudio"
  } else if (host == "www.zaiwen.top") {
    AI = "Zaiwen"
  } else if (host == 'yuanbao.tencent.com') {
    AI = "Yuanbao"
  } else if (host == "www.tiangong.cn") {
    AI == "Tiangong"
  }
  const requestPatchArr = [
    {
      AI: "Kimi",
      regex: /https:\/\/kimi.moonshot.cn\/api\/chat\/.+\/completion\/stream/,
      extract: function (text) {
        for (let line of text.split("\n")) {
          if (line.startsWith("data")) {
            try { JSON.parse(line.split("data: ")[1]) } catch { continue }
            const data = JSON.parse(line.split("data: ")[1])
            if (data.event == "cmpl") {
              this.text += data.text
            }
          }
        }
      },
      text: ""
    },
    {
      AI: "AIStudio",
      regex: /GenerateContent$/,
      extract: function (text) {
        while (true) {
          try {
            JSON.parse(text)
            break
          } catch {
            text += "]"
          }
        }
        const data = JSON.parse(text)
        this.text += data[0].map(i => i[0][0][0][0][0][1]).join("")
      },
      text: ""
    },
    {
      AI: "ChatGPT",
      regex: /backend-api\/conversation$/,
      extract: function (text) {
        for (let line of text.split("\n")) {
          if (line.startsWith('data: {"message')) {
            try { JSON.parse(line.split("data: ")[1]) } catch { continue }
            const data = JSON.parse(line.split("data: ")[1])
            if (data.message.content.content_type == "text") {
              this.text = data.message.content.parts[0]
            }
          } else if (line.startsWith("data: {")) {
            try { JSON.parse(line.split("data: ")[1]) } catch { continue }
            const data = JSON.parse(line.split("data: ")[1])
            const streamPath = "/message/content/parts/0"
            if (Object.keys(data).length == 1 && typeof (data.v) == "string" && this.p == streamPath) {
              this.text += data.v
            } else if ((this.p == streamPath || data.p == streamPath)) {
              this.p = streamPath
              if (data.o && data.o == "add") {
                this.text = ""
              }
              if (typeof (data.v) == "string") {
                this.text += data.v
              } else if (Array.isArray(data.v)) {
                const d = data.v.find(i => i.p == streamPath)
                if (d && typeof (d.v) == "string") {
                  this.text += d.v
                }
              }
            } else {
              this.p = ""
            }
          }
        }
      },
      p: "",
      text: ""
    },
    {
      AI: "Claude",
      regex: /chat_conversations\/.+\/completion/,
      extract: function (text) {
        for (let line of text.split("\n")) {
          if (line.startsWith("data: {")) {
            try { JSON.parse(line.split("data: ")[1]) } catch { continue }
            const data = JSON.parse(line.split("data: ")[1])
            if (data.type && data.type == "completion") {
              this.text += data.completion
            } else if (data.type && data.type == "content_block_delta") {
              this.text += data.delta.text
            }
          }
        }
      },
      text: ""
    },
    {
      AI: "Chatglm",
      regex: /backend-api\/assistant\/stream/,
      extract: function (text) {
        for (let line of text.split("\n")) {
          if (line.startsWith("data: {")) {
            try { JSON.parse(line.split("data: ")[1]) } catch { continue }
            const data = JSON.parse(line.split("data: ")[1])
            try {
              if (data.parts && data.parts[0] && data.parts[0].content[0].type == "text") {
                this.text = data.parts[0].content[0].text
              }
            } catch (e) { console.log("extract", e) }
          }
        }
      },
      text: ""
    },
    {
      AI: "Zaiwen",
      regex: /admin\/chatbot$/,
      extract: function (text) {
        this.text = text

      },
      text: ""
    },
    {
      AI: "Yuanbao",
      regex: /api\/chat\/.+/,
      extract: function (text) {
        for (let line of text.split("\n")) {
          if (line.startsWith("data: {")) {
            try { JSON.parse(line.split("data: ")[1]) } catch { continue }
            const data = JSON.parse(line.split("data: ")[1])
            try {
              if (data.type == "text") {
                this.text += (data.msg || "")
              }
            } catch (e) { console.log("extract", e) }
          }
        }

      },
      text: ""
    },
    {
      AI: "DeepSeek",
      regex: /api\/v0\/chat\/completion$/,
      extract: function (text) {
        console.log(text)
        for (let line of text.split("\n")) {
          if (line.startsWith("data: {")) {
            try { JSON.parse(line.split("data: ")[1]) } catch { continue }
            const data = JSON.parse(line.split("data: ")[1])
            try {
              this.text += data.choices[0].delta.content
            } catch (e) { console.log("extract", e) }
          }
        }

      },
      text: ""
    },
    {
      AI: "ChanlderAi",
      regex: /api\/chat\/Chat$/,
      extract: function (text) {
        for (let line of text.split("\n")) {
          console.log("line", line)
          if (line.startsWith("data:{")) {
            try { JSON.parse(line.split("data:")[1]) } catch { continue }
            const data = JSON.parse(line.split("data:")[1])
            try {
              this.text += data.delta
            } catch (e) { console.log("extract", e) }
          }
        }

      },
      text: ""
    },
    {
      AI: "Yiyan",
      regex: /chat\/conversation\/v2$/,
      extract: function (text, allText) {
        let delta = ""
        for (let line of allText.split("\n\nevent:message\n")) {
          if (line.startsWith("data:{")) {
            try { JSON.parse(line.split("data:")[1]) } catch { continue }
            const data = JSON.parse(line.split("data:")[1])
            try {
              delta += data.data.text || ""
            } catch (e) { console.log("extract", e) }
          }
        }
        this.text = delta

      },
      text: "",
    },
  ]

  // 数据拦截,部分网站需要

  const originalXhrOpen = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function (method, url, async) {
    this.addEventListener('readystatechange', async function () {
      let requestPatch
      if ((requestPatch = requestPatchArr.find(i => i.AI == AI && i.regex.test(url)))) {
        execInZotero(`
            let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length - 1];
            task.responseText = ${JSON.stringify(requestPatch.text || "")};
            task.responseType = "markdown";
        `);
        if (this.readyState === 3) {
          requestPatch.text = ""
          // 请求返回数据的流式部分
          try {
            requestPatch.extract(this.responseText)
          } catch {
            console.log(this.responseText)
          }
          await execInZotero(`
              let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length-1]
              task.responseText = ${JSON.stringify(requestPatch.text || "")};
              task.type = "pending";
              task.responseType = "markdown"
            `)
        } else if ([0, 4].includes(this.readyState)) {
          await execInZotero(`
            let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length-1]
            task.responseText = ${JSON.stringify(requestPatch.text || "")};
            task.type = "done";
            task.responseType = "markdown"
          `)
          requestPatch.text = ""
        }
      }
    });
    originalXhrOpen.apply(this, arguments);
  };


  const originalFetch = window.fetch;
  unsafeWindow.fetch = function () {
    return originalFetch.apply(this, arguments)
      .then(response => {
        const url = response.url
        const requestPatch = requestPatchArr.find(i => i.AI == AI && i.regex.test(url))
        if (requestPatch) {
          requestPatch.text = ""
          execInZotero(`
                let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length - 1];
                task.responseText = ${JSON.stringify(requestPatch.text)};
                task.responseType = "markdown";
            `);
          const clonedResponse = response.clone();
          console.log("requestPatch", requestPatch)
          console.log(clonedResponse)
          const reader = clonedResponse.body.getReader();
          const decoder = new TextDecoder()
          let allText = ""
          function processStream() {
            reader.read().then(({ done, value }) => {
              if (done) {
                console.log(allText)
                execInZotero(`
                    let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length - 1];
                    task.responseText = ${JSON.stringify(requestPatch.text || "")};
                    task.type = "done";
                    task.responseType = "markdown";
                `);
                requestPatch.text = ""
                return;
              }

              // 将 Uint8Array 转为字符串
              const text = decoder.decode(value, { stream: true });
              allText += text
              try {
                requestPatch.extract(text, allText)
              } catch (e) { console.log("requestPatch.extract(text)", e) }
              execInZotero(`
                  let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length - 1];
                  task.responseText = ${JSON.stringify(requestPatch.text || "")};
                  task.responseType = "markdown";
              `);

              // 递归调用,继续读取流数据
              processStream();
            }).catch(error => {
              // 捕获所有错误,包括 AbortError
              console.log("Error when Patch", error)
              requestPatch.text = ""
              requestPatch.extract(allText)
              execInZotero(`
                  let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length - 1];
                  task.responseText = ${JSON.stringify(requestPatch.text || "")};
                  task.type = "done";
                  task.responseType = "markdown";
              `);
            });
          }

          // 开始处理流
          window.setTimeout(() => {
            processStream();
          })
        }
        return response;
      });
  };


  // 在Zotero中执行代码
  async function execInZotero(code) {
    try {
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "POST",
          url: "http://127.0.0.1:23119/zoterogpt",
          headers: {
            "Content-Type": "application/json",
          },
          responseType: "json",
          data: JSON.stringify({ code }),
          onload: function (response) {
            if (response.status >= 200 && response.status < 300) {
              resolve(response.response.result);
            } else {
              reject(new Error(`Request failed with status: ${response.status}`));
            }
          },
          onerror: function (error) {
            reject(new Error('Network error'));
          }
        });
      });
    } catch (e) {
      window.alert("execInZotero error: " + code);
      return ""
    }
  }

  // 设定ChatGPT输入框文本并发送
  const setText = async (text) => {
    const dispatchInput = (selector, type = "plain") => {
      // 获取 input 输入框的dom对象
      var inputNode = document.querySelector(selector);
      if (!inputNode) { return }
      // 修改input的值

      inputNode.value = text;
      // plus
      try {
        inputNode.innerHTML = text.split("\n").map(i => `<p>${i}</p>`).join("\n");
      } catch { }

      // 设置输入框的 input 事件
      var event = new InputEvent('input', {
        'bubbles': true,
        'cancelable': true,
      });
      inputNode.dispatchEvent(event);
    }
    const originalText = text
    if (AI == "ChatGPT") {
      dispatchInput('#prompt-textarea')
      await sleep(100)
      await send("article", () => {
        const button = document.querySelector('[data-testid="send-button"]');
        button.click()
      })
    } else if (AI == "Gemini") {
      // 获取 input 输入框的dom对象
      const element_input = window.document.querySelector('rich-textarea .textarea');
      // 修改input的值
      element_input.textContent = text;
      await send(".conversation-container", () => {
        const button = document.querySelector('.send-button');
        button.click()
      })
    } else if (AI == "Poe") {
      dispatchInput('textarea[class*=GrowingTextArea_textArea]')
      document.querySelector("button[class*=ChatMessageSendButton_sendButton]").click();
      setTimeout(() => {
        document.querySelector("button[class*=ChatMessageSendButton_sendButton]").click()
      }, 100)
    } else if (AI == "Kimi") {
      const node = document.querySelector("[class^=inputInner]")
      await node[Object.keys(node)[1]].children[1][1].ref.current.insertText(text)

      await send("[class^=segmentItem]", () => {
        const button = document.querySelector('[data-testid=msh-chatinput-send-button]');
        button.click()
      })

    } else if (AI == "Coze") {
      const node = document.querySelector(".b5gKALp6yXERRDn8TV4r")
      node[Object.keys(node)[0]].pendingProps.children[0].props.onSendMessage({ text, mentionList: [] })
    } else if (AI == "Chatglm") {
      dispatchInput(".input-box-inner textarea")
      await send(".item.conversation-item", () => {
        const button = document.querySelector('.enter img');
        if (button) {
          const mouseDownEvent = new MouseEvent('mousedown', {
            bubbles: true,
            cancelable: true
          });
          button.dispatchEvent(mouseDownEvent);
        }
      })
    } else if (AI == "Yiyan") {
      const node = document.querySelector(".oeNDrlEA")
      node[Object.keys(node)[1]].children[2].props.children[0].props.changeInput({ e: text })
      
      await send(".dialogue_card_item", () => {
        document.querySelector("#sendBtn").click()
      })

    } else if (AI == "Tongyi") {
      const node = document.querySelector(".chatInput--eJzBH8LP")
      await node[Object.keys(node)[1]].children[1].props.setText(text);
      await send("[class^=questionItem]", () => {
        const node2 = document.querySelector(".operateBtn--zFx6rSR0");
        node2[Object.keys(node2)[1]].onClick()
      })
    } else if (AI == "Claude") {
      const node = document.querySelector("fieldset")
      const props = node[Object.keys(node)[1]].children[0].props.children[0].props.children[0].props;
      await props.setInput(text);
      await sleep(100)
      document.querySelector("button[aria-label='Send Message']").click();
    } else if (AI == "MyTan") {
      const conversation_id = location.href.split("chat/")?.[1]
      const data = {
        "content": [
          { "type": "text", "text": text }
        ],
        "stream": true,
      }
      if (conversation_id) {
        data.conversation_id = conversation_id
      } else {
        data.conversation = { title: "新对话", model: JSON.parse(localStorage["chosen-model-obj"]).model }
      }
      requestStream({
        api: `https://mytan.maiseed.com.cn/api/v2/messages`,
        token: JSON.parse(localStorage["chat-tan-token"]).token,
        data,
        lineRegex: /data: .+/g,
        getContent: (data) => data.choices[0].delta.content,
      })
    } else if (AI == "ChanlderAi") {
      dispatchInput(".chandler-content_input-area")
      await sleep(100)
      await send(".chandler-ext-content_communication-group", () => {
        const button = document.querySelector('.send');
        button.click()
      })
    } else if (AI == "DeepSeek") {
      const node = document.querySelector("#chat-input")
      node[Object.keys(node)[1]].onChange({ currentTarget: { value: text } })
      await sleep(100)
      await send(".d7ad28f8 ", () => {
        const button = document.querySelector('.c968d517');
        button.click()
      })
    } else if (AI == "Doubao") {
      const node = document.querySelector("[class^=footer]")
      await node[Object.keys(node)[1]].children.ref.current.autoTransValue(text);
      await sleep(1e3)
      document.querySelector("button#flow-end-msg-send").click();
    } else if (AI == "AIStudio") {
      dispatchInput(".input-wrapper textarea")
      await sleep(100)
      await send("ms-chat-turn", () => {
        const button = document.querySelector('run-button button');
        button.click()
      })
    } else if (AI == "Zaiwen") {
      dispatchInput('textarea.arco-textarea')
      await sleep(100)
      await send(".sessions .item", () => {
        const button = document.querySelector('img.send');
        button.click()
      })
    } else if (AI == "Yuanbao") {
      dispatchInput('.chat-input-editor .ql-editor')
      await sleep(100)
      await send(".agent-chat__bubble__content", () => {
        const button = document.querySelector('.icon-send');
        button.click()
      })
    }
  }

  // 连续发送
  const send = async (selector, callback) => {
    const oldNumber = document.querySelectorAll(selector).length;
    callback();
    await sleep(100);
    while (document.querySelectorAll(selector).length == oldNumber) {
      callback();
      await sleep(100);
    }
  }

  const uploadFile = async (base64String, fileName) => {
    try {
      let fileType;
      if (fileName.endsWith("pdf")) {
        fileType = "application/pdf";
      } else if (fileName.endsWith("png")) {
        fileType = "image/png";
      }

      function base64ToArrayBuffer(base64) {
        const binaryString = atob(base64);
        const len = binaryString.length;
        const bytes = new Uint8Array(len);
        for (let i = 0; i < len; i++) {
          bytes[i] = binaryString.charCodeAt(i);
        }
        return bytes.buffer;
      }

      const AIData = {
        ChatGPT: {
          uploadMethod: "drag",
          selector: "form",
        },
        Tongyi: {
          uploadMethod: "drag",
          selector: "[class^=chatInput]",
        },
        Kimi: {
          uploadMethod: "input",
          selector: "input[type=file]"
        },
        Claude: {
          uploadMethod: "input",
          selector: "input[type=file]",
        },
        AIStudio: {
          uploadMethod: "drag",
          selector: ".input-wrapper",
          until: () => {
            return !!!document.querySelector(".upload-wrapper")
          }
        },
        Chatglm: {
          uploadMethod: "input",
          selector: "input[type=file]",
        },
        Doubao: {
          uploadMethod: "input",
          selector: "input[type=file]",
        },
        Zaiwen: {
          uploadMethod: "drag",
          selector: ".arco-upload-draggable",
        },
        DeepSeek: {
          uploadMethod: "drag",
          selector: ".c52c3154",
        },
        Yuanbao: {
          uploadMethod: "drag",
          selector: ".agent-chat__input-box"
        },
        ChanlderAi: {
          uploadMethod: "input",
          selector: "input[type=file]",
        },
        Yiyan: {
          uploadMethod: "drag",
          selector: ".UxLYHqhv",
        },
        Poe: {
          uploadMethod: "drag",
          selector: ".ChatDragDropTarget_dropTarget__1WrAL"
        }
      };
      if (AIData[AI]) {
        const { uploadMethod, selector, until } = AIData[AI];

        if (uploadMethod === "input") {
          const button = document.querySelector(selector);
          button && button.click();

          // 创建一个虚拟的文件对象
          const fileContent = base64ToArrayBuffer(base64String);
          const file = new File([fileContent], fileName, { type: fileType });

          // 创建一个DataTransfer对象,并添加文件
          const dataTransfer = new DataTransfer();
          dataTransfer.items.add(file);

          let fileInput = document.querySelector(selector);
          if (fileInput) {
            fileInput.files = dataTransfer.files;
            fileInput.dispatchEvent(new Event('change', { bubbles: true }));
          } else {
            window.alert(AI + "未获取到fileInput,请联系开发者修复")
          }
        } else if (uploadMethod === "drag") {
          // 创建一个虚拟的文件对象
          const fileContent = base64ToArrayBuffer(base64String);
          const file = new File([fileContent], fileName, { type: fileType });

          // 创建一个DataTransfer对象,并添加文件
          const dataTransfer = new DataTransfer();
          dataTransfer.items.add(file);

          // 查找可拖放的区域或上传区域
          const dropZone = document.querySelector(selector); // 使用提供的选择器查找拖放区域
          if (!dropZone) {
            window.alert(AI + "未获取到dropZone,请联系开发者修复")
          }

          // 创建dragenter, dragover, drop事件
          const dragStartEvent = new DragEvent("dragstart", {
            bubbles: true,
            dataTransfer: dataTransfer,
            cancelable: true
          });
          const dropEvent = new DragEvent("drop", {
            bubbles: true,
            dataTransfer: dataTransfer,
            cancelable: true
          });

          // 依次派发事件,模拟拖放过程
          dropZone.dispatchEvent(dragStartEvent);
          dropZone.dispatchEvent(dropEvent);
        }
        if (until) {
          await sleep(100)
          while (!until()) {
            await sleep(100)
          }
        }
      }
    } catch (e) {
      console.error(e);
    }
  };


  /**
   * {api, token, data, lineRegex, getContent, errorFunction, midFunction}
   * @param {*} data
   */
  const requestStream = async (params) => {
    fetch(params.api, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${params.token}`
      },
      body: JSON.stringify(params.data)
    })
      .then(response => {
        if (response.status == 200) {
          return response.body.getReader()
        } else if (response.status == 400) {
          throw new Error('频率过高');
        } {
          throw new Error('授权失败');
        }
      })
      .then(reader => {
        let text = ""
        const decoder = new TextDecoder();
        window.setTimeout(async () => {
          while (true) {
            const { done, value } = await reader.read();
            if (done) {
              await execInZotero(`
                  let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length-1]
                  task.responseText = ${JSON.stringify(text)};
                  task.type = "done";
                  task.responseType = "markdown"
                `)
              break
            }
            try {
              const newLines = decoder.decode(value, { stream: true })
              for (let line of newLines.match(params.lineRegex)) {
                try {
                  const data = JSON.parse(line.split("data:")[1].trim())
                  params.midFunction && params.midFunction(data)
                  text = params.isNotDelta ? params.getContent(data) : (text + params.getContent(data));
                } catch (e) {
                  if (String(e).includes("Stop")) { return }
                }
                execInZotero(`
                    let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length-1]
                    task.responseText = ${JSON.stringify(text)};
                    task.type = "pending";
                    task.responseType = "markdown"
                  `)
              }
            } catch (e) {
              console.log(e)
            }
          }
        }, 0)
      })
      .catch(e => {
        params.errorFunction && params.errorFunction()
      })
  }
  // 阻塞
  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 支持:多个联动页面打开
  const LOCK_KEY = 'gpt_connector_running';
  const TAB_ID = Math.random().toString(36).substr(2, 9);  // Unique ID for each tab

  GM_registerMenuCommand('⭐️ 优先', () => {
    isRunning = true
    releaseLock()
    acquireLock()
    window.alert("⭐️ 优先")
  });

  GM_registerMenuCommand('🔗 运行', () => {
    isRunning = true
    window.alert("🔗 已运行")
  });

  GM_registerMenuCommand('🎊 断开', () => {
    isRunning = false
    releaseLock()
    window.alert("🎊 断开")
  });


  function acquireLock() {
    let lockInfo = JSON.parse(GM_getValue(LOCK_KEY, "{}"));

    if (lockInfo && lockInfo.isLocked) {
      if (lockInfo.tabId === TAB_ID) {
        // The current tab already holds the lock
        console.log('This tab already holds the lock:', TAB_ID);
        return true;
      } else {
        // Lock is held by another tab
        console.log('Another tab is already running the script. Exiting...');
        return false;
      }
    } else {
      // Lock is not set, acquire it for this tab
      GM_setValue(LOCK_KEY, JSON.stringify({ isLocked: true, tabId: TAB_ID }));
      console.log('Lock acquired by tab:', TAB_ID);
      return true;
    }
  }

  function releaseLock() {
    GM_setValue(LOCK_KEY, JSON.stringify({ isLocked: false, tabId: null }));
  }

  // Add an event listener to release the lock when the page is unloaded
  window.addEventListener('beforeunload', releaseLock);
  window.addEventListener('reload', releaseLock);

  setInterval(async () => {
    await execInZotero(`
      async function getMD5(path) {
        function arrayBufferToBase64(buffer) {
          let binary = '';
          const bytes = new window.Uint8Array(buffer);
          const len = bytes.byteLength;
          for (let i = 0; i < len; i++) {
            binary += String.fromCharCode(bytes[i]);
          }
          return window.btoa(binary);
        }
        const res = await Zotero.HTTP.request("GET", path, { responseType: "arraybuffer" })
        return Zotero.Utilities.Internal.md5(arrayBufferToBase64(res.response))
      }
      (async () => {
        const md5 = await getMD5("chrome://zoterogpt/content/icons/favicon.png")
        if (md5 != "387f99eeaa7b0b6d9d3e311017098e37") {
          window.Meet = 0
        }
      })()
    `)
  }, 10e3)
  releaseLock()
  // 通信
  while (true) {
    if (!acquireLock()) {
      await sleep(1000)
      continue;
    }
    if (!isRunning) {
      await execInZotero(`
        window.Meet.Connector.time = 0;
      `)
      await sleep(1000)
      continue;
    }
    try {
      const tasks = (await execInZotero(`
        if (!window.Meet.Connector){
          window.Meet.Connector = ${JSON.stringify({
        AI, time: Date.now() / 1e3, tasks: []
      })};
        } else {
          window.Meet.Connector.time = ${Date.now() / 1e3};
          window.Meet.Connector.AI = "${AI}";
        }
        window.Meet.Connector
      `)).tasks
      if (!tasks || tasks.length == 0) {
        await sleep(500)
        continue
      }
      const task = tasks.slice(-1)[0]
      if (task.type == "pending") {
        if (task.file || task.files) {
          await execInZotero(`
            let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length-1]
            task.type = "done"
          `)
          if (task.file) {
            await uploadFile(task.file.base64String, task.file.name)
          } else if (task.files){
            for (let file of task.files) {
              await uploadFile(file.base64String, file.name)
            }
          }
        } else if (task.requestText) {
          await setText(task.requestText)
          // 操作浏览器提问
          await execInZotero(`
            let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length-1]
            task.requestText = "";
            task.responseText = "<p>Waiting ${AI}...</p>";
          `)
        } else {
          let isDone = false, text = "", type = "html"
          const setZoteroText = async () => {
            if (typeof (text) !== "string") { return }
            await execInZotero(`
              let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length-1]
              task.responseText = ${JSON.stringify(text)};
              task.type = ${isDone} ? "done" : "pending";
              task.responseType = "${type}"
            `)
            if (isDone) {
              await sleep(1000)
              await execInZotero(`
                let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length-1]
                task.responseText = ${JSON.stringify(text)};
            `)
            }
          }
          if (AI == "Gemini") {
            const outputEle = [...document.querySelectorAll('.conversation-container')].slice(-1)[0];
            const contentEle = outputEle.querySelector("model-response .response-content message-content")
            if (contentEle) {
              isDone = Boolean(outputEle.querySelector(".complete"))
              text = contentEle.querySelector(".markdown").innerHTML
              await setZoteroText()
            }
          } else if (AI == "Poe") {
            type = "markdown"
            const lastNode = [...document.querySelectorAll("[class^=ChatMessagesView_messagePair]")].slice(-1)[0]
            const props = lastNode[Object.keys(lastNode)[0]].child.memoizedProps
            text = props.pairedMessage.text
            isDone = props.pairedMessage.state == "complete"
            await setZoteroText()
          } else if (AI == "Coze") {
            const outputEle = document.querySelector(".message-group-wrapper");
            const contentEle = outputEle.querySelector("[data-testid='bot.ide.chat_area.message_box'] .flow-markdown-body")
            isDone = Boolean(outputEle.querySelector(".chat-uikit-message-box-container__message__message-box__footer").childNodes.length != 0)
            text = contentEle.innerHTML.replace(/<br .+?>/g, "").replace(/<hr .+?>/g, "<hr/>")
            await setZoteroText()
          } else if (AI == "Tongyi") {
            const lastAnwser = [...document.querySelectorAll("[class^=answerItem]")].slice(-1)[0]
            type = "markdown"
            const message = lastAnwser[Object.keys(lastAnwser)[0]].memoizedProps.children.find(i => { try { return i.props.children[2].props.message } catch { } }).props.children[2].props.message
            isDone = message.contents[message.contents.length - 1].status == "finished"
            text = message.contents[message.contents.length - 1].content
            await setZoteroText()
          } else if (AI == "Doubao") {
            const nodes = [...document.querySelectorAll("[class^=message-box-content-wrapper]")]
            const node = nodes.slice(-1)[0]
            const data = node[Object.keys(node)[0]].child.child.child.child.child.child.memoizedState.memoizedState.current.value
            type = "markdown"
            text = data.content_obj.text;
            isDone = data.ext.is_finish == "1";
            await setZoteroText()
          }
        }
      }
    } catch (e) {
      console.log(e)
      await sleep(1000)
    }
    await sleep(100)
  }
})();