Zotero GPT Connector

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

// ==UserScript==
// @name         Zotero GPT Connector
// @description  Zotero GPT Pro, support ChatGPT Gemini Poe Kimi Coze Chatglm Yiyan Tongyi Claude Mytan ChanlderAi DeepSeek Doubao
// @namespace    http://tampermonkey.net/
// @icon         https://github.com/MuiseDestiny/zotero-gpt/blob/bootstrap/addon/chrome/content/icons/favicon.png?raw=true
// @version      2.9.9
// @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://chatglm.cn/*
// @match        https://yiyan.baidu.com/*
// @match        https://tongyi.aliyun.com/qianwen/*
// @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/*
// @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") {
    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"
  }
  // 在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 originalText = text
    if (AI == "ChatGPT") {
      // 获取 input 输入框的dom对象
      var element_input = window.document.querySelector('#prompt-textarea');
      // 修改input的值
      element_input.value = text;
      // plus
      element_input.innerText = text;
      // 设置输入框的 input 事件
      var event = new InputEvent('input', {
        'bubbles': true,
        'cancelable': true,
      });
      element_input.dispatchEvent(event);
      await sleep(1000)
      const button = document.querySelector('[data-testid="send-button"]')
      button.click()
    } else if (AI == "Gemini") {
      // 获取 input 输入框的dom对象
      var element_input = window.document.querySelector('rich-textarea .textarea');
      // 修改input的值
      element_input.textContent = text;
      document.querySelector(".send-button").click();
      setTimeout(() => {
        document.querySelector(".send-button").click()
      }, 100)
    } else if (AI == "Poe") {
      var element_input = window.document.querySelector('textarea[class*=GrowingTextArea_textArea]');
      element_input.value = text;
      // 设置输入框的 input 事件
      var event = new InputEvent('input', {
        'bubbles': true,
        'cancelable': true,
      });
      element_input.dispatchEvent(event);
      document.querySelector("button[class*=ChatMessageSendButton_sendButton]").click();
      setTimeout(() => {
        document.querySelector("button[class*=ChatMessageSendButton_sendButton]").click()
      }, 100)
    } else if (AI == "Kimi") {
      if (location.href == 'https://kimi.moonshot.cn/') {
        document.querySelector("[data-testid='msh-sidebar-new']").click()
      }
      requestStream({
        api: `https://kimi.moonshot.cn/api/chat/${location.href.match(/chat\/([0-9a-z]+)/)[1]}/completion/stream`,
        token: localStorage.access_token,
        data: { "messages": [{ "role": "user", "content": text }], "refs": [], "use_search": false },
        lineRegex: /data: .+/g,
        getContent: (data) => {
          if (data.event && data.event == "cmpl") {
            return data.text
          } else {
            return ""
          }
        },
        midFunction: (data) => {
          if (data.error) {
            document.querySelector("[data-testid=msh-sidebar-new]").click();
            window.setTimeout(() => {
              setText(originalText)
            }, 1000)
            throw Error("Stop")
          }
        },
        errorFunction: () => {
          window.setTimeout(async () => {
            const res = await fetch("https://kimi.moonshot.cn/api/auth/token/refresh", {
              headers: {
                "Authorization": `Bearer ${localStorage.refresh_token}`
              }
            })
            const data = await res.json()
            localStorage.access_token = data.access_token
            localStorage.refresh_token = data.refresh_token
            await setText(text)
          })
        }
      })
    } 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") {
      const token = document.cookie.match(/chatglm_token=([^;]+);/)[1];
      requestStream({
        api: `https://chatglm.cn/chatglm/backend-api/assistant/stream`,
        token,
        data: {
          "assistant_id": "65940acff94777010aa6b796",
          "conversation_id": localStorage.conversation_id || "",
          "meta_data": {
            "mention_conversation_id": "",
            "is_test": false,
            "input_question_type": "xxxx",
            "channel": "",
            "draft_id": ""
          }, "messages": [
            { "role": "user", "content": [{ "type": "text", "text": text }] }]
        },
        lineRegex: /event:message\ndata: .+/g,
        getContent: (data) => data.parts[0].content[0].text,
        midFunction: (data) => {
          if (!localStorage.conversation_id || localStorage.conversation_id.length == 0) {
            localStorage.conversation_id = data.conversation_id
          }
        },
        isNotDelta: true
      })
    } 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")
      node[Object.keys(node)[1]].children[0].props.setText(text);
      window.setTimeout(async () => {
        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();
    }
  }
  /**
   * {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 {
          console.log("授权失败", response)
        }
      })
      .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()
        console.log(e)
      })
  }
  // 阻塞
  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

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


  // 通信
  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.requestText) {
          // 操作浏览器提问
          if (AI == "ChatGPT") {
            let getUserQuestionNum = () => document.querySelectorAll("[data-message-author-role=user]").length
            const questionNum = getUserQuestionNum()
            await setText(task.requestText)
            while (getUserQuestionNum() == questionNum) {
              await sleep(100)
            }
          } else if (AI == "Gemini") {
            let getUserQuestionNum = () => document.querySelectorAll("user-query").length
            const questionNum = getUserQuestionNum()
            await setText(task.requestText)
            while (getUserQuestionNum() == questionNum) {
              await sleep(100)
            }
            while (document.querySelectorAll('model-response').length != getUserQuestionNum()) {
              await sleep(100)
            }
          } else {
            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 () => {
            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 == "ChatGPT") {
            type = "markdown"
            const outputEle = [...document.querySelectorAll('[data-testid^=conversation-turn]')].slice(-1)[0];
            const node = outputEle.querySelector("[data-message-author-role=assistant]")
            isDone = Boolean(outputEle.querySelector("span[data-state=closed]"))
            text = node[Object.keys(node)[0]].alternate.alternate.pendingProps.children[2].props.messages[0].message.content.parts[0]
            await setZoteroText()
          } else if (AI == "Gemini") {
            const outputEle = [...document.querySelectorAll('model-response')].slice(-1)[0];
            const contentEle = outputEle.querySelector(".response-content message-content")
            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 == "Kimi") {
            // 无需这一步
          } 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 == "Kimi") {
            // 无需这一步
          } 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
            console.log(message)
            isDone = message.contents[0].status == "finished"
            text = message.contents[0].content
            await setZoteroText()
          } else if (AI == "Claude") {
            const node = [...document.querySelectorAll("div[data-test-render-count]")].slice(-1)[0].querySelector("[data-is-streaming]");
            type = "markdown"
            text = node[Object.keys(node)[1]].children[0].props.children.props.text;
            isDone = node.getAttribute("data-is-streaming") == "false";
            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)
  }
})();