Zotero GPT Connector

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

当前为 2024-10-24 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.2.8
// @author       Polygon
// @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/*
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @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") {
    localStorage.conversation_id = ""
    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"
  }
  const requestPatchArr = [
    {
      AI: "Kimi",
      regex: /https:\/\/kimi.moonshot.cn\/api\/chat\/.+\/completion\/stream/,
      extract: function (text) {
        // console.log(text)
        for (let line of text.split("\n")) {
          if (line.startsWith("data")) {
            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])
            if (line.includes("/message/metadata/citations")) { continue }
            if (data.o && data.o == "patch") { this.text += "\n" }
            if (typeof (data.v) == "string") {
              console.log("=>", data.v, text)
              this.text += data.v
            } else if (data.v instanceof Array) {
              if (data.v[0].o == "append") {
                this.text += data.v[0].v
                console.log("=>", data.v[0].v, text)

              }
            }
          }
        }
      },
      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
            }
          }
        }
      },
      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: ""
    }
  ]

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

  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.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 (this.readyState == 0) {
          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.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) {
                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";
                `);
                return;
              }

              // 将 Uint8Array 转为字符串
              const text = decoder.decode(value, { stream: true });
              allText += text
              try {
                requestPatch.extract(text)
              } 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) => {
      // 获取 input 输入框的dom对象
      var inputNode = document.querySelector(selector);
      if (!inputNode) { return }
      // 修改input的值
      inputNode.value = text;
      // plus
      inputNode.innerText = text;
      // 设置输入框的 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("#eb_model_footer")
      node[Object.keys(node)[1]].children[3].props.children[2].props.children[0].props.setText(text);
      document.querySelector(".VAtmtpqL").click()
    } else if (AI == "Tongyi") {
      const node = document.querySelector(".chatInput--eJzBH8LP")
      await node[Object.keys(node)[1]].children[0].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;
      props.setInput(text);
      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") {
      // 更新id
      if (!localStorage.conversation_id || localStorage.conversation_id == "") {
        async function readStream(stream) {
          const reader = stream.getReader();
          let result = '';
          const decoder = new TextDecoder();
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;

            result += decoder.decode(value, { stream: true });
          }
          return result
        }
        const res = await fetch("https://api.chandler.bet/api/chat/chatHistory", {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            "Authorization": `Bearer ${localStorage.token}`,
            "accept": "application/json, text/plain, */*"
          },
          body: JSON.stringify({ "keywords": "", "model_names": [], "page_size": 10, "page_num": 1 }),
        })
        const data = JSON.parse(await readStream(res.body)).data[0]
        localStorage.conversation_id = data.conversation_id
        localStorage.model_name = data.model_name
        localStorage.parent_message_id = data.parent_message_id
      }
      const appData = JSON.parse(localStorage.appLocalStorage)
      requestStream({
        api: `https://api.chandler.bet/api/chat/Chat`,
        token: localStorage.token,
        data: {
          "uid": appData.email,
          "prompt": text,
          "model_name": localStorage.model_name,
          "request_timeout": 30,
          "global_timeout": 100,
          "max_retries": 1,
          "attachment_list": [],
          "parent_message_id": localStorage.parent_message_id,
          "conversation_id": localStorage.conversation_id,
          "answer_again": false,
          "aireply": "",
          "timestamp": (new Date()).getTime(),
          "status": "",
          "app_name": "",
          "web_url": "https://api.chandler.bet"
        },
        lineRegex: /event:message\ndata:.+/g,
        getContent: (data) => data.delta
      })
    } else if (AI == "DeepSeek") {
      requestStream({
        api: `https://chat.deepseek.com/api/v0/chat/completions`,
        token: JSON.parse(localStorage.userToken).value,
        data: {
          "message": text,
          "stream": true,
          "model_preference": null,
          "model_class":
            "deepseek_chat",
          "temperature": 0
        },
        lineRegex: /data: .+/g,
        getContent: (data) => data.choices[0].delta.content || ""
      })
    } 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()
      })
    }
  }

  // 连续发送
  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 type
      if (fileName.endsWith("pdf")) {
        type = "application/pdf"
      } else if (fileName.endsWith("png")) {
        type = "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;
      }
      if (AI == "AIStudio") {
        const button = document.querySelector("ms-add-chunk-menu button")
        button && button.click()
      } else if (AI == "Tongyi") {
        const button = document.querySelector(".optionBtn--xj4hyzKT")
        button && button.click()
      }
      // 创建一个虚拟的 PDF 文件对象
      const fileContent = base64ToArrayBuffer(base64String);
      const file = new File([fileContent], fileName, { type });

      // 创建一个DataTransfer对象
      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(file);
      const fileInput = [...document.querySelectorAll("input[type=file]")].find(i => i.accept.includes(type.split("/")[1])) ||
        document.querySelector('[data-testid="file-upload"]') // Claude

      fileInput.files = dataTransfer.files
      fileInput.dispatchEvent(new Event('change', { bubbles: true }));
      // 需要特殊处理的
      if (AI == "AIStudio") {
        const button = document.querySelector("ms-add-chunk-menu button")
        button && button.click()
      }
    } catch (e) {
      // window.alert(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));
  }

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

  const waitSend = async (selector) => {
    let getUserQuestionNum = () => document.querySelectorAll(selector).length
    const questionNum = getUserQuestionNum()
    while (getUserQuestionNum() == questionNum) {
      await sleep(100)
    }
  }

  // 通信
  await sleep(3000)
  while (true) {
    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) {
          // document.querySelector("[data-testid='create-new-chat-button']").click();
          // await sleep(1e3)
          await execInZotero(`
            let task = window.Meet.Connector.tasks[window.Meet.Connector.tasks.length-1]
            task.type = "done"
          `)
          await uploadFile(task.file.base64String, task.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 == "Yiyan") {
            const outputEle = document.querySelector(".ErXhAgf5 .RmHagX8t");
            const contentEle = outputEle.querySelector(".custom-html.md-stream-desktop")
            isDone = Boolean(outputEle.querySelector(".fXxD0Rtx"))
            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.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)
  }
})();