diff --git a/package.json b/package.json index bd610ea887..48e66dc16b 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "md5": "^2.3.0", "prettier": "^3.1.1", "prettier-plugin-vue": "^1.1.6", + "raw-loader": "^4.0.2", "sse.js": "^2.2.0" }, "eslintConfig": { diff --git a/src/background.js b/src/background.js index 48bb937569..6baaf98164 100644 --- a/src/background.js +++ b/src/background.js @@ -1,16 +1,16 @@ "use strict"; import { BrowserWindow, app, ipcMain, nativeTheme, protocol } from "electron"; -import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; import installExtension, { VUEJS3_DEVTOOLS } from "electron-devtools-installer"; import fs from "fs"; import path from "path"; +import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; import { setMenuItems } from "./menu"; import updateApp from "./update"; +import { sendPhind } from "./windows/phind/phind"; const isDevelopment = process.env.NODE_ENV !== "production"; const DEFAULT_USER_AGENT = ""; // Empty string to use the Electron default -let mainWindow = null; // start - makes application a Single Instance Application const singleInstanceLock = app.requestSingleInstanceLock(); @@ -19,9 +19,9 @@ if (!singleInstanceLock) { } else { app.on("second-instance", () => { // Someone tried to run a second instance, we should focus our window. - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.focus(); + if (global.mainWindow) { + if (global.mainWindow.isMinimized()) global.mainWindow.restore(); + global.mainWindow.focus(); } }); } @@ -157,7 +157,7 @@ async function createWindow() { }, }); - mainWindow = win; + global.mainWindow = win; // Force the SameSite attribute to None for all cookies // This is required for the cross-origin request to work @@ -295,34 +295,37 @@ function createNewWindow(url, userAgent = "") { if (url.startsWith("https://moss.fastnlp.top/")) { // Get the secret of MOSS const secret = await getLocalStorage("flutter.token"); - mainWindow.webContents.send("moss-secret", secret); + global.mainWindow.webContents.send("moss-secret", secret); } else if (url.startsWith("https://qianwen.aliyun.com/")) { // Get QianWen bot's XSRF-TOKEN const token = await getCookie("XSRF-TOKEN"); - mainWindow.webContents.send("QIANWEN-XSRF-TOKEN", token); + global.mainWindow.webContents.send("QIANWEN-XSRF-TOKEN", token); } else if (url.startsWith("https://chat.tiangong.cn/")) { // Get the tokens of SkyWork const inviteToken = await getLocalStorage("aiChatQueueWaitToken"); const token = await getLocalStorage("aiChatResearchToken"); - mainWindow.webContents.send("SKYWORK-TOKENS", { inviteToken, token }); + global.mainWindow.webContents.send("SKYWORK-TOKENS", { + inviteToken, + token, + }); } else if (url.startsWith("https://character.ai/")) { const token = await getLocalStorage("char_token"); - mainWindow.webContents.send("CHARACTER-AI-TOKENS", token); + global.mainWindow.webContents.send("CHARACTER-AI-TOKENS", token); } else if (url.startsWith("https://claude.ai/")) { const org = await getCookie("lastActiveOrg"); - mainWindow.webContents.send("CLAUDE-2-ORG", org); + global.mainWindow.webContents.send("CLAUDE-2-ORG", org); } else if (url.startsWith("https://poe.com/")) { const formkey = await newWin.webContents.executeJavaScript( "window.ereNdsRqhp2Rd3LEW();", ); - mainWindow.webContents.send("POE-FORMKEY", formkey); + global.mainWindow.webContents.send("POE-FORMKEY", formkey); } else if (url.startsWith("https://chatglm.cn/")) { const token = await getCookie("chatglm_token"); - mainWindow.webContents.send("CHATGLM-TOKENS", { token }); + global.mainWindow.webContents.send("CHATGLM-TOKENS", { token }); } else if (url.startsWith("https://kimi.moonshot.cn/")) { const access_token = await getLocalStorage("access_token"); const refresh_token = await getLocalStorage("refresh_token"); - mainWindow.webContents.send("KIMI-TOKENS", { + global.mainWindow.webContents.send("KIMI-TOKENS", { access_token, refresh_token, }); @@ -333,12 +336,13 @@ function createNewWindow(url, userAgent = "") { newWin.destroy(); // Destroy the window manually // Tell renderer process to check aviability - mainWindow.webContents.send("CHECK-AVAILABILITY", url); + global.mainWindow.webContents.send("CHECK-AVAILABILITY", url); }); + return newWin; } async function getCookies(filter) { - const cookies = await mainWindow.webContents.session.cookies.get({ + const cookies = await global.mainWindow.webContents.session.cookies.get({ ...filter, }); return cookies; @@ -348,6 +352,26 @@ ipcMain.handle("create-new-window", (event, url, userAgent) => { createNewWindow(url, userAgent); }); +/** @type {Object}*/ +const windows = {}; +ipcMain.handle("create-chat-window", async (event, options) => { + const { winName, url, userAgent } = options; + switch (winName) { + case "phind": + const win = createNewWindow(url, userAgent); + windows[winName] = win; + await sendPhind({ win, ...options }); + break; + default: + break; + } +}); + +ipcMain.handle("close-chat-window", (event, winName) => { + windows[winName]?.close(); + windows[winName] = undefined; +}); + ipcMain.handle("get-native-theme", () => { return Promise.resolve({ shouldUseDarkColors: nativeTheme.shouldUseDarkColors, @@ -389,7 +413,7 @@ ipcMain.handle("save-proxy-and-restart", async () => { // Proxy Setting End ipcMain.handle("set-is-show-menu-bar", (_, isShowMenuBar) => { - mainWindow.setMenuBarVisibility(isShowMenuBar); + global.mainWindow.setMenuBarVisibility(isShowMenuBar); }); ipcMain.handle("get-cookies", async (event, filter) => { @@ -397,7 +421,7 @@ ipcMain.handle("get-cookies", async (event, filter) => { }); nativeTheme.on("updated", () => { - mainWindow.webContents.send("on-updated-system-theme"); + global.mainWindow.webContents.send("on-updated-system-theme"); }); // Quit when all windows are closed. diff --git a/src/bots/PhindBot.js b/src/bots/PhindBot.js index e9614bba24..f1db84505b 100644 --- a/src/bots/PhindBot.js +++ b/src/bots/PhindBot.js @@ -1,5 +1,4 @@ import Bot from "@/bots/Bot"; -import store from "@/store"; import AsyncLock from "async-lock"; import axios from "axios"; import { SSE } from "sse.js"; @@ -22,7 +21,16 @@ export default class PhindBot extends Bot { * @returns {boolean} - true if the bot is available, false otherwise. */ async _checkAvailability() { - return true; + try { + const response = await axios.get( + "https://www.phind.com/api/auth/session", + ); + if (response?.data?.user?.userId) { + return true; + } + } catch (error) { + console.error("Error PhindBot check login:", error); + } } /** @@ -33,155 +41,96 @@ export default class PhindBot extends Bot { * @param {object} callbackParam - Just pass it to onUpdateResponse() as is */ async _sendPrompt(prompt, onUpdateResponse, callbackParam) { - try { - const context = await this.getChatContext(); - const rewrite = await axios.post( - "https://www.phind.com/api/infer/followup/rewrite", - { - questionToRewrite: prompt, - questionHistory: context.questionHistory, - answerHistory: context.answerHistory, - }, - ); - const search = await axios.post("https://www.phind.com/api/web/search", { - q: rewrite.data.query, - browserLanguage: "en-GB", - userSearchRules: {}, - }); - - const date = new Date(); - const formatDate = this.getFormattedDate(date); - const payload = JSON.stringify({ - questionHistory: context.questionHistory, - answerHistory: context.answerHistory, - question: prompt, - webResults: search.data, - options: { - date: formatDate, - language: "en-GB", - detailed: true, - anonUserId: await this.getUUID(), - answerModel: store.state.phind.model, - customLinks: [], - }, - context: "", - }); - - return new Promise((resolve, reject) => { - try { - const source = new SSE("https://www.phind.com/api/infer/answer", { - start: false, - payload, - }); - let text = ""; - let isSuccess = false; - source.addEventListener("message", (event) => { - if (event.data) { - if (event.data.startsWith("")) { - isSuccess = true; - } else { - text += event.data; - onUpdateResponse(callbackParam, { - content: text, - done: false, - }); - } - } - }); - - source.addEventListener("readystatechange", (event) => { - if (event.readyState === source.CLOSED) { - // after stream closed, done - if (isSuccess) { - // save answerHistory and questionHistory to context - this.setChatContext({ - answerHistory: [...context.answerHistory, text], - questionHistory: [...context.questionHistory, prompt], - }); - - // replace link with hostname - if (search.data && search.data.length) { - for (let i = 0; i < search.data.length; i++) { - const hostname = new URL(search.data[i].url).hostname; - text = text.replaceAll(`[Source${i}]`, `[${hostname}]`); - text = text.replaceAll( - `[^${i}^]`, - ` [${hostname}](${search.data[i].url})`, - ); - text = text.replaceAll( - `^${i}^`, - ` [${hostname}](${search.data[i].url})`, - ); - } + /** @type {{ message_history: Array }}*/ + const context = await this.getChatContext(); + ipcRenderer.invoke("create-chat-window", { + prompt, + winName: PhindBot._brandId, + url: `https://www.phind.com/agent${context.chatId ? `?cache=${context.chatId}` : ""}`, + }); + const onPhindRequest = (_, postData, text, resolve, reject) => { + try { + const source = new SSE("https://https.api.phind.com/agent/", { + start: false, + payload: postData, + }); + source.addEventListener("message", (event) => { + if (event.data) { + /** @type {{ "created": number, "model": string, "choices": [ { "index": number, "delta": { "role": string, "content": string } } ] }} */ + const response = JSON.parse(event.data); + if (response && response.choices && response.choices.length) { + for (const choice of response.choices) { + if (choice.delta && choice.delta.content) { + text += choice.delta.content; } } onUpdateResponse(callbackParam, { content: text, - done: true, }); - resolve(); } + } + }); + + source.addEventListener("readystatechange", (event) => { + if (event.readyState === source.CLOSED) { + context.message_history.push({ + role: "user", + content: prompt, + metadata: {}, + }); + context.message_history.push({ + role: "assistant", + content: text, + metadata: {}, + }); + this.setChatContext({ + ...context, + message_history: context.message_history, + }); + onUpdateResponse(callbackParam, { + content: text, + done: true, + }); + resolve(); + } + }); + source.addEventListener("error", (event) => { + console.error(event); + reject(this.getSSEDisplayError(event)); + }); + source.stream(); + } catch (err) { + reject(err); + } + }; + + let text = ""; + return new Promise((resolve, reject) => { + ipcRenderer.once("phind-request", (_, postData) => + onPhindRequest(_, postData, text, resolve, reject), + ); + }) + .then(async () => { + ipcRenderer.invoke("close-chat-window", PhindBot._brandId); + let response; + if (context.chatId) { + response = await axios.put("https://www.phind.com/api/db/chat", { + chatId: context.chatId, + messages: context.message_history?.slice(-2), }); - source.addEventListener("error", (event) => { - console.error(event); - reject(this.getSSEDisplayError(event)); + } else { + response = await axios.post("https://www.phind.com/api/db/chat", { + title: prompt, + messages: context.message_history?.slice(-2), }); - - // override default _onStreamProgress to fix missing new line in response due to trimming - source._onStreamProgress = function (e) { - if (!source.xhr) { - return; - } - - if (source.xhr.status !== 200) { - source._onStreamFailure(e); - return; - } - - if (source.readyState == source.CONNECTING) { - source.dispatchEvent(new CustomEvent("open")); - source._setReadyState(source.OPEN); - } - - var data = source.xhr.responseText.substring(source.progress); - - source.progress += data.length; - var parts = (source.chunk + data).split(/\r\n\r\n/); - var lastPart = parts.pop(); - for (let part of parts) { - // skip if data is empty - if (part === "data: ") { - continue; - } - - // newline - if (part === "data: \r\ndata: ") { - let event = new CustomEvent("message"); - event.data = "\n"; - source.dispatchEvent(event); - continue; - } - - const event = source._parseEventChunk(part); - source.dispatchEvent(event); - } - source.chunk = lastPart; - }; - source.stream(); - } catch (err) { - reject(err); + context.chatId = response.data; + this.setChatContext(context); } + }) + .catch((error) => { + console.error("Operation failed:", error); + ipcRenderer.invoke("close-chat-window", PhindBot._brandId); }); - } catch (error) { - if (error.request.status === 403) { - throw new Error( - `${error.request.status} ${error.request.responseText}`, - ); - } else { - console.error("Error PhindBot _sendPrompt:", error); - throw error; - } - } } /** @@ -191,21 +140,6 @@ export default class PhindBot extends Bot { * @returns {any} - Conversation structure. null if not supported. */ async createChatContext() { - return { answerHistory: [], questionHistory: [] }; - } - - getFormattedDate(date) { - let year = date.getFullYear(); - let month = (1 + date.getMonth()).toString().padStart(2, "0"); - let day = date.getDate().toString().padStart(2, "0"); - return month + "/" + day + "/" + year; - } - - async getUUID() { - const cookies = await ipcRenderer.invoke("get-cookies", { - domain: "www.phind.com", - }); - const uuidCookie = cookies.find((cookie) => cookie.name === "uuid"); - return uuidCookie ? uuidCookie.value : ""; + return { message_history: [], chatId: undefined }; } } diff --git a/src/components/BotSettings/PhindBotSettings.vue b/src/components/BotSettings/PhindBotSettings.vue index 9afd8223d9..8951ea4c93 100644 --- a/src/components/BotSettings/PhindBotSettings.vue +++ b/src/components/BotSettings/PhindBotSettings.vue @@ -1,32 +1,18 @@