Skip to content

Commit

Permalink
feat: add QianWen (通义千问) support
Browse files Browse the repository at this point in the history
Known issue: Login status lost after relaunching ChatALL
  • Loading branch information
sunner committed May 31, 2023
1 parent 49a3953 commit efe2b76
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 0 deletions.
Binary file added src/assets/bots/qianwen-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
161 changes: 161 additions & 0 deletions src/bots/QianWenBot.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions src/bots/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -31,6 +32,7 @@ const all = [
ClaudeBot.getInstance(),
BardBot.getInstance(),
WenxinQianfanBot.getInstance(),
QianWenBot.getInstance(),
SparkBot.getInstance(),
HuggingChatBot.getInstance(),
VicunaBot.getInstance(),
Expand Down
33 changes: 33 additions & 0 deletions src/components/BotSettings/QianWenBotSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<template>
<v-list-subheader>{{ bot.getBrandName() }}</v-list-subheader>
<login-setting :bot="bot"></login-setting>
</template>

<script>
const electron = window.require("electron");
const ipcRenderer = electron.ipcRenderer;
import { mapMutations } from "vuex";
import Bot from "@/bots/QianWenBot";
import LoginSetting from "@/components/BotSettings/LoginSetting.vue";
export default {
components: {
LoginSetting,
},
data() {
return {
bot: Bot.getInstance(),
};
},
mounted() {
// Listen for the QIANWEN-XSRF-TOKEN message from background.js
ipcRenderer.on("QIANWEN-XSRF-TOKEN", (event, token) => {
this.setQianWenToken(token);
});
},
methods: {
...mapMutations(["setQianWenToken"]),
},
};
</script>
2 changes: 2 additions & 0 deletions src/components/SettingsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -72,6 +73,7 @@ const settings = [
HuggingChatBotSettings,
LMSYSBotSettings,
MOSSBotSettings,
QianWenBotSettings,
SparkBotSettings,
];
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@
"temperature2": "More random",
"apiKey": "API Key"
},
"qianWen": {
"name": "QianWen"
},
"spark": {
"name": "iFlytek Spark"
},
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@
"temperature2": "更具随机性",
"apiKey": "API 密钥"
},
"qianWen": {
"name": "通义千问"
},
"spark": {
"name": "讯飞星火"
},
Expand Down
6 changes: 6 additions & 0 deletions src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export default createStore({
moss: {
token: "",
},
qianWen: {
xsrfToken: "",
},
wenxinQianfan: {
apiKey: "",
secretKey: "",
Expand Down Expand Up @@ -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 };
},
Expand Down

0 comments on commit efe2b76

Please sign in to comment.