diff --git a/src/assets/bots/qianwen-logo.png b/src/assets/bots/qianwen-logo.png new file mode 100644 index 000000000..fc044ca6c Binary files /dev/null and b/src/assets/bots/qianwen-logo.png differ diff --git a/src/background.js b/src/background.js index 07a0c1400..fc9132f70 100644 --- a/src/background.js +++ b/src/background.js @@ -145,6 +145,22 @@ function createNewWindow(url, userAgent = "") { newWin.destroy(); // Destroy the window manually }); } + + // Get QianWen bot's XSRF-TOKEN + if (url.includes("tongyi.aliyun.com")) { + newWin.on("close", async (e) => { + try { + e.preventDefault(); // Prevent the window from closing + const token = await newWin.webContents.executeJavaScript( + 'document.cookie.split("; ").find((cookie) => cookie.startsWith("XSRF-TOKEN="))?.split("=")[1];', + ); + mainWindow.webContents.send("QIANWEN-XSRF-TOKEN", token); + } catch (error) { + console.error(error); + } + newWin.destroy(); // Destroy the window manually + }); + } } ipcMain.handle("create-new-window", (event, url, userAgent) => { diff --git a/src/bots/QianWenBot.js b/src/bots/QianWenBot.js new file mode 100644 index 000000000..66a1ee8eb --- /dev/null +++ b/src/bots/QianWenBot.js @@ -0,0 +1,161 @@ +import AsyncLock from "async-lock"; +import Bot from "@/bots/Bot"; +import axios from "axios"; +import store from "@/store"; +import { SSE } from "sse.js"; + +function generateRandomId() { + let randomStr = ""; + for (let i = 0; i < 32; i++) { + randomStr += Math.floor(Math.random() * 16).toString(16); + } + return randomStr; +} + +export default class QianWenBot extends Bot { + static _brandId = "qianWen"; // Brand id of the bot, should be unique. Used in i18n. + static _className = "QianWenBot"; // Class name of the bot + static _logoFilename = "qianwen-logo.png"; // Place it in assets/bots/ + static _loginUrl = "https://tongyi.aliyun.com/"; + static _lock = new AsyncLock(); // AsyncLock for prompt requests + + constructor() { + super(); + } + + getRequestHeaders() { + return { + "x-xsrf-token": store.state.qianWen?.xsrfToken, + }; + } + + /** + * Check whether the bot is logged in, settings are correct, etc. + * @returns {boolean} - true if the bot is available, false otherwise. + * @sideeffect - Set this.constructor._isAvailable + */ + async checkAvailability() { + await axios + .post( + "https://tongyi.aliyun.com/qianwen/querySign", + {}, + { headers: this.getRequestHeaders() }, + ) + .then((resp) => { + this.constructor._isAvailable = resp.data?.success; + if (!resp.data?.success) { + console.error("Error QianWen check login:", resp.data); + } + }) + .catch((error) => { + console.error("Error QianWen check login:", error); + this.constructor._isAvailable = false; + }); + + return this.isAvailable(); // Always return like this + } + + /** + * Send a prompt to the bot and call onResponse(response, callbackParam) + * when the response is ready. + * @param {string} prompt + * @param {function} onUpdateResponse params: callbackParam, Object {content, done} + * @param {object} callbackParam - Just pass it to onUpdateResponse() as is + */ + // eslint-disable-next-line + async _sendPrompt(prompt, onUpdateResponse, callbackParam) { + const context = await this.getChatContext(); + const headers = { + ...this.getRequestHeaders(), + accept: "text/event-stream", + "content-type": "application/json", + }; + const payload = JSON.stringify({ + action: "next", + msgId: generateRandomId(), + parentMsgId: context.parentMessageId || "0", + contents: [ + { + contentType: "text", + content: prompt, + }, + ], + timeout: 17, + openSearch: false, + sessionId: context.sessionId, + model: "", + }); + + return new Promise((resolve, reject) => { + try { + const source = new SSE( + "https://tongyi.aliyun.com/qianwen/conversation", + { + headers, + payload, + withCredentials: true, + }, + ); + + source.addEventListener("message", (event) => { + if (event.data === "") { + // Empty message usually means error + const resp = JSON.parse(source.chunk); + if (resp?.failed) { + reject(new Error(`${resp?.errorCode} ${resp?.errorMsg}`)); + return; + } + } + const data = JSON.parse(event.data); + onUpdateResponse(callbackParam, { + content: data.content[0], + done: false, + }); + if (data.stopReason === undefined || data.stopReason === "stop") { + onUpdateResponse(callbackParam, { done: true }); + context.parentMessageId = data.msgId; + this.setChatContext(context); + resolve(); + } + }); + + source.addEventListener("error", (event) => { + console.error(event); + reject(new Error(event)); + }); + + source.stream(); + } catch (err) { + reject(err); + } + }); + } + + /** + * Should implement this method if the bot supports conversation. + * The conversation structure is defined by the subclass. + * @param null + * @returns {any} - Conversation structure. null if not supported. + */ + async createChatContext() { + let context = null; + await axios + .post( + "https://tongyi.aliyun.com/qianwen/addSession", + { firstQuery: "ChatALL" }, // A hack to set session name + { headers: this.getRequestHeaders() }, + ) + .then((resp) => { + if (resp.data?.success) { + const sessionId = resp.data?.data?.sessionId; + const userId = resp.data?.data?.userId; + const parentMsgId = "0"; + context = { sessionId, parentMsgId, userId }; + } + }) + .catch((err) => { + console.error("Error QianWen adding sesion:", err); + }); + return context; + } +} diff --git a/src/bots/index.js b/src/bots/index.js index c5ee7fd6d..70efec1ea 100644 --- a/src/bots/index.js +++ b/src/bots/index.js @@ -18,6 +18,7 @@ import ClaudeBot from "@/bots/lmsys/ClaudeBot"; import DevBot from "@/bots/DevBot"; import GradioAppBot from "@/bots/huggingface/GradioAppBot"; import HuggingChatBot from "@/bots/huggingface/HuggingChatBot"; +import QianWenBot from "./QianWenBot"; const all = [ ChatGPT35Bot.getInstance(), @@ -31,6 +32,7 @@ const all = [ ClaudeBot.getInstance(), BardBot.getInstance(), WenxinQianfanBot.getInstance(), + QianWenBot.getInstance(), SparkBot.getInstance(), HuggingChatBot.getInstance(), VicunaBot.getInstance(), diff --git a/src/components/BotSettings/QianWenBotSettings.vue b/src/components/BotSettings/QianWenBotSettings.vue new file mode 100644 index 000000000..03db49f2f --- /dev/null +++ b/src/components/BotSettings/QianWenBotSettings.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index c06ec84ef..006408513 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -55,6 +55,7 @@ import WenxinQianfanBotSettings from "@/components/BotSettings/WenxinQianfanBotS import GradioAppBotSettings from "@/components/BotSettings/GradioAppBotSettings.vue"; import LMSYSBotSettings from "@/components/BotSettings/LMSYSBotSettings.vue"; import HuggingChatBotSettings from "@/components/BotSettings/HuggingChatBotSettings.vue"; +import QianWenBotSettings from "@/components/BotSettings/QianWenBotSettings.vue"; const { t: $t, locale } = useI18n(); const store = useStore(); @@ -72,6 +73,7 @@ const settings = [ HuggingChatBotSettings, LMSYSBotSettings, MOSSBotSettings, + QianWenBotSettings, SparkBotSettings, ]; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 120dbb335..04f1dc0d8 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -101,6 +101,9 @@ "temperature2": "More random", "apiKey": "API Key" }, + "qianWen": { + "name": "QianWen" + }, "spark": { "name": "iFlytek Spark" }, diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index c8bc32d6d..6df315378 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -101,6 +101,9 @@ "temperature2": "更具随机性", "apiKey": "API 密钥" }, + "qianWen": { + "name": "通义千问" + }, "spark": { "name": "讯飞星火" }, diff --git a/src/store/index.js b/src/store/index.js index 2bd025e41..8649b7e3a 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -49,6 +49,9 @@ export default createStore({ moss: { token: "", }, + qianWen: { + xsrfToken: "", + }, wenxinQianfan: { apiKey: "", secretKey: "", @@ -83,6 +86,9 @@ export default createStore({ setMoss(state, token) { state.moss.token = token; }, + setQianWenToken(state, token) { + state.qianWen.xsrfToken = token; + }, setWenxinQianfan(state, values) { state.wenxinQianfan = { ...state.wenxinQianfan, ...values }; },