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 @@
+
+ {{ bot.getBrandName() }}
+
+
+
+
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 };
},