diff --git a/manifest.config.ts b/manifest.config.ts index c99dee58..c6835817 100644 --- a/manifest.config.ts +++ b/manifest.config.ts @@ -6,7 +6,7 @@ export default defineManifest(async (env) => { name: '__MSG_appName__', description: '__MSG_appDesc__', default_locale: 'en', - version: '1.35.5', + version: '1.38.1', icons: { '16': 'src/assets/icon.png', '32': 'src/assets/icon.png', @@ -23,8 +23,12 @@ export default defineManifest(async (env) => { 'https://*.openai.com/', 'https://bard.google.com/', 'https://*.chathub.gg/', + 'https://*.duckduckgo.com/', + 'https://*.poe.com/', + 'https://*.anthropic.com/', + 'https://*.claude.ai/', ], - optional_host_permissions: ['https://*/*'], + optional_host_permissions: ['https://*/*', 'wss://*/*'], permissions: ['storage', 'unlimitedStorage', 'sidePanel', 'declarativeNetRequestWithHostAccess'], content_scripts: [ { @@ -58,6 +62,16 @@ export default defineManifest(async (env) => { enabled: true, path: 'src/rules/ddg.json', }, + { + id: 'ruleset_qianwen', + enabled: true, + path: 'src/rules/qianwen.json', + }, + { + id: 'ruleset_baichuan', + enabled: true, + path: 'src/rules/baichuan.json', + }, ], }, } diff --git a/package.json b/package.json index 06ac37b8..cecdc371 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@crxjs/vite-plugin": "^2.0.0-beta.18", "@headlessui/tailwindcss": "^0.2.0", - "@types/chrome": "^0.0.241", + "@types/humanize-duration": "^3.27.1", "@types/lodash-es": "^4.17.8", "@types/md5": "^2.3.2", "@types/react": "^18.2.18", @@ -20,11 +20,12 @@ "@types/react-scroll-to-bottom": "^4.2.1", "@types/turndown": "^5.0.1", "@types/uuid": "^9.0.2", - "@types/webextension-polyfill": "^0.10.1", + "@types/webextension-polyfill": "^0.10.2", "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", "@vitejs/plugin-react": "^4.0.4", "autoprefixer": "^10.4.14", + "chrome-types": "^0.1.231", "eslint": "^8.46.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-react": "^7.33.1", @@ -54,12 +55,14 @@ "cachified": "^3.5.4", "clsx": "^2.0.0", "cmdk": "^0.2.0", + "dayjs": "^1.11.9", "eventsource-parser": "^1.0.0", "framer-motion": "^10.16.0", "fuse.js": "^6.6.2", "github-markdown-css": "^5.2.0", "gpt3-tokenizer": "^1.1.5", "highlight.js": "^11.8.0", + "humanize-duration": "^3.29.0", "i18next": "^23.4.2", "i18next-browser-languagedetector": "^7.1.0", "immer": "^9.0.19", @@ -70,6 +73,7 @@ "lodash-es": "^4.17.21", "lucide-react": "^0.264.0", "md5": "^2.3.0", + "nanoid": "^4.0.2", "ofetch": "^1.1.1", "plausible-tracker": "^0.3.8", "react": "^18.2.0", diff --git a/src/app/bots/baichuan/api.ts b/src/app/bots/baichuan/api.ts new file mode 100644 index 00000000..a16d3652 --- /dev/null +++ b/src/app/bots/baichuan/api.ts @@ -0,0 +1,35 @@ +import { ofetch } from 'ofetch' +import { customAlphabet } from 'nanoid' +import { ChatError, ErrorCode } from '~utils/errors' + +interface UserInfo { + id: number +} + +export async function getUserInfo(): Promise { + const resp = await ofetch<{ data?: UserInfo; code: number; msg: string }>( + 'https://www.baichuan-ai.com/api/user/user-info', + { method: 'POST' }, + ) + if (resp.code === 401) { + throw new ChatError('请先登录百川账号', ErrorCode.BAICHUAN_WEB_UNAUTHORIZED) + } + if (resp.code !== 200) { + throw new Error(`Error: ${resp.code} ${resp.msg}`) + } + return resp.data! +} + +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789') + +function randomString(length: number) { + return nanoid(length) +} + +export function generateSessionId() { + return 'p' + randomString(10) +} + +export function generateMessageId() { + return 'U' + randomString(14) +} diff --git a/src/app/bots/baichuan/index.ts b/src/app/bots/baichuan/index.ts new file mode 100644 index 00000000..25e05f62 --- /dev/null +++ b/src/app/bots/baichuan/index.ts @@ -0,0 +1,125 @@ +import { AbstractBot, SendMessageParams } from '../abstract-bot' +import { requestHostPermission } from '~app/utils/permissions' +import { ChatError, ErrorCode } from '~utils/errors' +import { uuid } from '~utils' +import { generateMessageId, generateSessionId, getUserInfo } from './api' +import { streamAsyncIterable } from '~utils/stream-async-iterable' + +interface Message { + id: string + createdAt: number + data: string + from: 0 | 1 // human | bot +} + +interface ConversationContext { + conversationId: string + historyMessages: Message[] + userId: number + lastMessageId?: string +} + +export class BaichuanWebBot extends AbstractBot { + private conversationContext?: ConversationContext + + async doSendMessage(params: SendMessageParams) { + if (!(await requestHostPermission('https://*.baichuan-ai.com/'))) { + throw new ChatError('Missing baichuan-ai.com permission', ErrorCode.MISSING_HOST_PERMISSION) + } + + if (!this.conversationContext) { + const conversationId = generateSessionId() + const userInfo = await getUserInfo() + this.conversationContext = { conversationId, historyMessages: [], userId: userInfo.id } + } + + const { conversationId, lastMessageId, historyMessages, userId } = this.conversationContext + + const message: Message = { + id: generateMessageId(), + createdAt: Date.now(), + data: params.prompt, + from: 0, + } + + const resp = await fetch('https://www.baichuan-ai.com/api/chat/v1/chat', { + method: 'POST', + signal: params.signal, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + stream: true, + request_id: uuid(), + app_info: { id: 10001, name: 'baichuan_web' }, + user_info: { id: userId, status: 1 }, + prompt: { + id: message.id, + data: message.data, + from: message.from, + parent_id: lastMessageId || 0, + created_at: message.createdAt, + }, + session_info: { id: conversationId, name: '新的对话', created_at: Date.now() }, + parameters: { + repetition_penalty: -1, + temperature: -1, + top_k: -1, + top_p: -1, + max_new_tokens: -1, + do_sample: -1, + regenerate: 0, + }, + history: historyMessages, + }), + }) + + const decoder = new TextDecoder() + let result = '' + let answerMessageId: string | undefined + + for await (const uint8Array of streamAsyncIterable(resp.body!)) { + const str = decoder.decode(uint8Array) + console.debug('baichuan stream', str) + const lines = str.split('\n') + for (const line of lines) { + if (!line) { + continue + } + const data = JSON.parse(line) + if (!data.answer) { + continue + } + answerMessageId = data.answer.id + const text = data.answer.data + if (text) { + result += text + params.onEvent({ type: 'UPDATE_ANSWER', data: { text: result } }) + } + } + } + + this.conversationContext.historyMessages.push(message) + if (answerMessageId) { + this.conversationContext.lastMessageId = answerMessageId + if (result) { + this.conversationContext.historyMessages.push({ + id: answerMessageId, + data: result, + createdAt: Date.now(), + from: 1, + }) + } + } + + params.onEvent({ type: 'DONE' }) + } + + resetConversation() { + this.conversationContext = undefined + } + + get name() { + return '百川大模型' + } +} diff --git a/src/app/bots/bing/api.ts b/src/app/bots/bing/api.ts index 91fda290..7b92cf6a 100644 --- a/src/app/bots/bing/api.ts +++ b/src/app/bots/bing/api.ts @@ -1,5 +1,5 @@ import { random } from 'lodash-es' -import { FetchError, ofetch } from 'ofetch' +import { FetchError, FetchResponse, ofetch } from 'ofetch' import { uuid } from '~utils' import { ChatError, ErrorCode } from '~utils/errors' import { ConversationResponse } from './types' @@ -14,32 +14,41 @@ const API_ENDPOINT = 'https://www.bing.com/turing/conversation/create' export async function createConversation(): Promise { const headers = { 'x-ms-client-request-id': uuid(), - 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.0 OS/Win32', + 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.3 OS/macOS', } - let resp: ConversationResponse + let rawResponse: FetchResponse try { - resp = await ofetch(API_ENDPOINT, { headers, redirect: 'error' }) - if (!resp.result) { + rawResponse = await ofetch.raw(API_ENDPOINT, { headers, redirect: 'error' }) + if (!rawResponse._data?.result) { throw new Error('Invalid response') } } catch (err) { console.error('retry bing create', err) - resp = await ofetch(API_ENDPOINT, { + rawResponse = await ofetch.raw(API_ENDPOINT, { headers: { ...headers, 'x-forwarded-for': randomIP() }, redirect: 'error', }) - if (!resp) { + if (!rawResponse._data) { throw new FetchError(`Failed to fetch (${API_ENDPOINT})`) } } - if (resp.result.value !== 'Success') { - const message = `${resp.result.value}: ${resp.result.message}` - if (resp.result.value === 'UnauthorizedRequest') { + const data = rawResponse._data + + if (data.result.value !== 'Success') { + const message = `${data.result.value}: ${data.result.message}` + if (data.result.value === 'UnauthorizedRequest') { throw new ChatError(message, ErrorCode.BING_UNAUTHORIZED) } throw new Error(message) } - return resp + + const conversationSignature = rawResponse.headers.get('x-sydney-conversationsignature')! + const encryptedConversationSignature = rawResponse.headers.get('x-sydney-encryptedconversationsignature') || undefined + + data.conversationSignature = data.conversationSignature || conversationSignature + data.encryptedConversationSignature = encryptedConversationSignature + + return data } diff --git a/src/app/bots/bing/index.ts b/src/app/bots/bing/index.ts index 1ddba3d0..77183db6 100644 --- a/src/app/bots/bing/index.ts +++ b/src/app/bots/bing/index.ts @@ -1,6 +1,8 @@ import { ofetch } from 'ofetch' import WebSocketAsPromised from 'websocket-as-promised' +import { requestHostPermission } from '~app/utils/permissions' import { BingConversationStyle, getUserConfig } from '~services/user-config' +import { uuid } from '~utils' import { ChatError, ErrorCode } from '~utils/errors' import { AbstractBot, SendMessageParams } from '../abstract-bot' import { createConversation } from './api' @@ -13,23 +15,42 @@ const OPTIONS_SETS = [ 'disable_emoji_spoken_text', 'responsible_ai_policy_235', 'enablemm', - 'iycapbing', - 'iyxapbing', - 'objopinion', - 'rweasgv2', - 'dagslnv1', 'dv3sugg', - 'autosave', - 'iyoloxap', - 'iyoloneutral', - 'clgalileo', - 'gencontentv3', + 'iyxapbing', + 'iycapbing', + 'galileo', + 'saharagenconv5', + 'log2sph', + 'savememfilter', + 'uprofgen', + 'uprofupd', + 'uprofupdasy', + 'vidsumsnip', +] + +const SLICE_IDS = [ + 'tnaenableux', + 'adssqovr', + 'tnaenable', + 'arankc_1_9_3', + 'rankcf', + '0731ziv2s0', + '926buffall', + 'inosanewsmob', + 'wrapnoins', + 'prechr', + 'sydtransl', + '806log2sph', + '927uprofasy', + '919vidsnip', + '829suggtrims0', ] export class BingWebBot extends AbstractBot { private conversationContext?: ConversationInfo private buildChatRequest(conversation: ConversationInfo, message: string, imageUrl?: string) { + const requestId = uuid() const optionsSets = OPTIONS_SETS if (conversation.conversationStyle === BingConversationStyle.Precise) { optionsSets.push('h3precise') @@ -50,35 +71,22 @@ export class BingWebBot extends AbstractBot { 'GenerateContentQuery', 'SearchQuery', ], - sliceIds: [ - 'winmuid1tf', - 'anssupfor_c', - 'imgchatgptv2', - 'tts2cf', - 'contansperf', - 'mlchatpc8500w', - 'mlchatpc2', - 'ctrlworkpay', - 'winshortmsgtf', - 'cibctrl', - 'sydtransctrl', - 'sydconfigoptc', - '0705trt4', - '517opinion', - '628ajcopus0', - '330uaugs0', - '529rwea', - '0626snptrcs0', - '424dagslnv1', - ], + sliceIds: SLICE_IDS, + verbosity: 'verbose', + scenario: 'SERP', + plugins: [], isStartOfSession: conversation.invocationId === 0, message: { + timestamp: new Date().toISOString(), author: 'user', inputMethod: 'Keyboard', text: message, imageUrl, messageType: 'Chat', + requestId, + messageId: requestId, }, + requestId, conversationId: conversation.conversationId, conversationSignature: conversation.conversationSignature, participant: { id: conversation.clientId }, @@ -91,11 +99,15 @@ export class BingWebBot extends AbstractBot { } async doSendMessage(params: SendMessageParams) { + if (!(await requestHostPermission('wss://*.bing.com/'))) { + throw new ChatError('Missing bing.com permission', ErrorCode.MISSING_HOST_PERMISSION) + } if (!this.conversationContext) { const [conversation, { bingConversationStyle }] = await Promise.all([createConversation(), getUserConfig()]) this.conversationContext = { conversationId: conversation.conversationId, conversationSignature: conversation.conversationSignature, + encryptedConversationSignature: conversation.encryptedConversationSignature, clientId: conversation.clientId, invocationId: 0, conversationStyle: bingConversationStyle, @@ -109,7 +121,7 @@ export class BingWebBot extends AbstractBot { imageUrl = await this.uploadImage(params.image) } - const wsp = new WebSocketAsPromised('wss://sydney.bing.com/sydney/ChatHub', { + const wsp = new WebSocketAsPromised(this.buildWssUrl(conversation.encryptedConversationSignature), { packMessage: websocketUtils.packMessage, unpackMessage: websocketUtils.unpackMessage, }) @@ -234,4 +246,11 @@ export class BingWebBot extends AbstractBot { } return `https://www.bing.com/images/blob?bcid=${resp.blobId}` } + + private buildWssUrl(encryptedConversationSignature: string | undefined) { + if (!encryptedConversationSignature) { + return 'wss://sydney.bing.com/sydney/ChatHub' + } + return `wss://sydney.bing.com/sydney/ChatHub?sec_access_token=${encodeURIComponent(encryptedConversationSignature)}` + } } diff --git a/src/app/bots/bing/types.ts b/src/app/bots/bing/types.ts index 56f5a039..b8084730 100644 --- a/src/app/bots/bing/types.ts +++ b/src/app/bots/bing/types.ts @@ -4,6 +4,7 @@ export interface ConversationResponse { conversationId: string clientId: string conversationSignature: string + encryptedConversationSignature?: string result: { value: string message: null @@ -28,6 +29,7 @@ export interface ConversationInfo { conversationSignature: string invocationId: number conversationStyle: BingConversationStyle + encryptedConversationSignature?: string } export interface BingChatResponse { diff --git a/src/app/bots/chatgpt-webapp/requesters.ts b/src/app/bots/chatgpt-webapp/requesters.ts index e7eb7353..314116ad 100644 --- a/src/app/bots/chatgpt-webapp/requesters.ts +++ b/src/app/bots/chatgpt-webapp/requesters.ts @@ -1,4 +1,4 @@ -import Browser from 'webextension-polyfill' +import Browser, { Runtime } from 'webextension-polyfill' import { CHATGPT_HOME_URL } from '~app/consts' import { proxyFetch } from '~services/proxy-fetch' import { RequestInitSubset } from '~types/messaging' @@ -31,21 +31,29 @@ class ProxyFetchRequester implements Requester { } } - waitForProxyTabReady(onReady: (tab: Browser.Tabs.Tab) => void) { - Browser.runtime.onMessage.addListener(async function listener(message, sender) { - if (message.event === 'PROXY_TAB_READY') { - console.debug('new proxy tab ready') - Browser.runtime.onMessage.removeListener(listener) - onReady(sender.tab!) + waitForProxyTabReady(): Promise { + return new Promise((resolve, reject) => { + const listener = async function (message: any, sender: Runtime.MessageSender) { + if (message.event === 'PROXY_TAB_READY') { + console.debug('new proxy tab ready') + Browser.runtime.onMessage.removeListener(listener) + clearTimeout(timer) + resolve(sender.tab!) + } } + const timer = setTimeout(() => { + Browser.runtime.onMessage.removeListener(listener) + reject(new Error('Timeout waiting for ChatGPT tab')) + }, 10 * 1000) + + Browser.runtime.onMessage.addListener(listener) }) } async createProxyTab() { - return new Promise((resolve) => { - this.waitForProxyTabReady(resolve) - Browser.tabs.create({ url: CHATGPT_HOME_URL, pinned: true }) - }) + const readyPromise = this.waitForProxyTabReady() + Browser.tabs.create({ url: CHATGPT_HOME_URL, pinned: true }) + return readyPromise } async getProxyTab() { @@ -62,10 +70,9 @@ class ProxyFetchRequester implements Requester { await this.createProxyTab() return } - return new Promise((resolve) => { - this.waitForProxyTabReady(resolve) - Browser.tabs.reload(tab.id!) - }) + const readyPromise = this.waitForProxyTabReady() + Browser.tabs.reload(tab.id!) + return readyPromise } async fetch(url: string, options?: RequestInitSubset) { diff --git a/src/app/bots/claude-api/index.ts b/src/app/bots/claude-api/index.ts index 131a25b7..fc1cd82a 100644 --- a/src/app/bots/claude-api/index.ts +++ b/src/app/bots/claude-api/index.ts @@ -64,12 +64,8 @@ export class ClaudeApiBot extends AbstractBot { switch (this.config.claudeApiModel) { case ClaudeAPIModel['claude-instant-1']: return 'claude-instant-1' - case ClaudeAPIModel['claude-1']: - return 'claude-1' - case ClaudeAPIModel['claude-instant-1-100k']: - return 'claude-instant-1-100k' - case ClaudeAPIModel['claude-1-100k']: - return 'claude-1-100k' + default: + return 'claude-2' } } diff --git a/src/app/bots/gradio/index.ts b/src/app/bots/gradio/index.ts index 3fd0d371..89040a3c 100644 --- a/src/app/bots/gradio/index.ts +++ b/src/app/bots/gradio/index.ts @@ -9,8 +9,8 @@ function generateSessionHash() { } enum FnIndex { - Send = 7, - Receive = 8, + Send = 39, + Receive = 40, } interface ConversationContext { diff --git a/src/app/bots/index.ts b/src/app/bots/index.ts index 95a70fe8..5d67fb05 100644 --- a/src/app/bots/index.ts +++ b/src/app/bots/index.ts @@ -1,10 +1,11 @@ +import { BaichuanWebBot } from './baichuan' import { BardBot } from './bard' import { BingWebBot } from './bing' import { ChatGPTBot } from './chatgpt' import { ClaudeBot } from './claude' -import { GradioBot } from './gradio' import { LMSYSBot } from './lmsys' import { PiBot } from './pi' +import { QianwenWebBot } from './qianwen' import { XunfeiBot } from './xunfei' export type BotId = @@ -14,14 +15,12 @@ export type BotId = | 'claude' | 'xunfei' | 'vicuna' - | 'alpaca' | 'chatglm' | 'llama' - | 'oasst' - | 'rwkv' | 'pi' - | 'guanaco' | 'wizardlm' + | 'qianwen' + | 'baichuan' export function createBotInstance(botId: BotId) { switch (botId) { @@ -37,22 +36,18 @@ export function createBotInstance(botId: BotId) { return new XunfeiBot() case 'vicuna': return new LMSYSBot('vicuna-33b') - case 'alpaca': - return new LMSYSBot('alpaca-13b') case 'chatglm': return new LMSYSBot('chatglm2-6b') case 'llama': - return new GradioBot('wss://llama2.lepton.run/chat/queue/join', 'llama2', [0.5, 0.8, 512], 'html') - case 'oasst': - return new LMSYSBot('oasst-pythia-12b') - case 'rwkv': - return new LMSYSBot('RWKV-4-Raven-14B') - case 'guanaco': - return new LMSYSBot('guanaco-33b') + return new LMSYSBot('llama-2-70b-chat') case 'wizardlm': return new LMSYSBot('wizardlm-13b') case 'pi': return new PiBot() + case 'qianwen': + return new QianwenWebBot() + case 'baichuan': + return new BaichuanWebBot() } } diff --git a/src/app/bots/poe/api.ts b/src/app/bots/poe/api.ts index 9230ab70..06be2443 100644 --- a/src/app/bots/poe/api.ts +++ b/src/app/bots/poe/api.ts @@ -36,9 +36,7 @@ interface ChannelData { async function getFormkey() { const html: string = await ofetch('https://poe.com', { parseResponse: (txt) => txt }) - const r = html.match(/(.+)<\/head>/)! - const headHtml = r[1] - const formkey = await decodePoeFormkey(headHtml) + const formkey = await decodePoeFormkey(html) return formkey } diff --git a/src/app/bots/qianwen/api.ts b/src/app/bots/qianwen/api.ts new file mode 100644 index 00000000..f9a08686 --- /dev/null +++ b/src/app/bots/qianwen/api.ts @@ -0,0 +1,42 @@ +import { ofetch } from 'ofetch' +import { ChatError, ErrorCode } from '~utils/errors' + +interface CreationResponse { + data: { + sessionId: string + } + success: boolean + errorMsg: string | null + errorCode: string | null +} + +export async function createConversation(firstQuery: string, csrfToken: string) { + const resp = await ofetch('https://qianwen.aliyun.com/addSession', { + method: 'POST', + body: { firstQuery }, + headers: { + 'X-Xsrf-Token': csrfToken, + }, + }) + if (!resp.success) { + if (resp.errorCode === '4000') { + throw new ChatError('请先登录通义千问账号', ErrorCode.QIANWEN_WEB_UNAUTHORIZED) + } + throw new Error(`Error: ${resp.errorCode} ${resp.errorMsg}`) + } + return resp.data.sessionId +} + +function extractVariable(variableName: string, html: string) { + const regex = new RegExp(`${variableName}\\s?=\\s?"([^"]+)"`) + const match = regex.exec(html) + if (!match) { + throw new Error('Failed to get csrfToken') + } + return match[1] +} + +export async function getCsrfToken() { + const html = await ofetch('https://qianwen.aliyun.com', { parseResponse: (t) => t }) + return extractVariable('csrfToken', html) +} diff --git a/src/app/bots/qianwen/index.ts b/src/app/bots/qianwen/index.ts new file mode 100644 index 00000000..8c8cc0b7 --- /dev/null +++ b/src/app/bots/qianwen/index.ts @@ -0,0 +1,80 @@ +import { parseSSEResponse } from '~utils/sse' +import { AbstractBot, SendMessageParams } from '../abstract-bot' +import { requestHostPermission } from '~app/utils/permissions' +import { ChatError, ErrorCode } from '~utils/errors' +import { createConversation, getCsrfToken } from './api' +import { uuid } from '~utils' + +function generateMessageId() { + return uuid().replace(/-/g, '') +} + +interface ConversationContext { + conversationId: string + csrfToken: string + lastMessageId?: string +} + +export class QianwenWebBot extends AbstractBot { + private conversationContext?: ConversationContext + + async doSendMessage(params: SendMessageParams) { + if (!(await requestHostPermission('https://qianwen.aliyun.com/'))) { + throw new ChatError('Missing qianwen.aliyun.com permission', ErrorCode.MISSING_HOST_PERMISSION) + } + + if (!this.conversationContext) { + const csrfToken = await getCsrfToken() + const conversationId = await createConversation(params.prompt, csrfToken) + this.conversationContext = { conversationId, csrfToken } + } + + const resp = await fetch('https://qianwen.aliyun.com/conversation', { + method: 'POST', + signal: params.signal, + headers: { + 'Content-Type': 'application/json', + 'X-Xsrf-Token': this.conversationContext.csrfToken, + }, + body: JSON.stringify({ + action: 'next', + msgId: generateMessageId(), + parentMsgId: this.conversationContext.lastMessageId || '0', + contents: [{ contentType: 'text', content: params.prompt }], + timeout: 17, + sessionId: this.conversationContext.conversationId, + model: '', + userAction: 'chat', + openSearch: true, + }), + }) + + let done = false + + await parseSSEResponse(resp, (message) => { + console.debug('qianwen sse', message) + const data = JSON.parse(message) + const text = data.content[0] + if (text) { + params.onEvent({ type: 'UPDATE_ANSWER', data: { text } }) + } + if (data.stopReason === 'stop') { + this.conversationContext!.lastMessageId = data.msgId + done = true + params.onEvent({ type: 'DONE' }) + } + }) + + if (!done) { + params.onEvent({ type: 'DONE' }) + } + } + + resetConversation() { + this.conversationContext = undefined + } + + get name() { + return '通义千问' + } +} diff --git a/src/app/bots/xunfei/index.ts b/src/app/bots/xunfei/index.ts index e1410538..a5a335f4 100644 --- a/src/app/bots/xunfei/index.ts +++ b/src/app/bots/xunfei/index.ts @@ -36,7 +36,7 @@ export class XunfeiBot extends AbstractBot { form.append('fd', generateFD()) form.append('isBot', '0') - const resp = await fetch('https://xinghuo.xfyun.cn/iflygpt/u/chat_message/chat', { + const resp = await fetch('https://xinghuo.xfyun.cn/iflygpt-chat/u/chat_message/chat', { method: 'POST', signal: params.signal, body: form, @@ -73,4 +73,8 @@ export class XunfeiBot extends AbstractBot { resetConversation() { this.conversationContext = undefined } + + get name() { + return '讯飞星火' + } } diff --git a/src/app/components/Chat/ChatMessageInput.tsx b/src/app/components/Chat/ChatMessageInput.tsx index 94871463..6c59f9d6 100644 --- a/src/app/components/Chat/ChatMessageInput.tsx +++ b/src/app/components/Chat/ChatMessageInput.tsx @@ -137,7 +137,10 @@ const ChatMessageInput: FC = (props) => { }, []) const selectImage = useCallback(async () => { - const file = await fileOpen({ mimeTypes: ['image/jpg', 'image/png', 'image/gif'] }) + const file = await fileOpen({ + mimeTypes: ['image/jpg', 'image/jpeg', 'image/png', 'image/gif'], + extensions: ['.jpg', '.jpeg', '.png', '.gif'], + }) setImage(file) inputRef.current?.focus() }, []) diff --git a/src/app/components/Chat/ChatbotName.tsx b/src/app/components/Chat/ChatbotName.tsx new file mode 100644 index 00000000..bc2df843 --- /dev/null +++ b/src/app/components/Chat/ChatbotName.tsx @@ -0,0 +1,32 @@ +import { FC, memo } from 'react' +import dropdownIcon from '~/assets/icons/dropdown.svg' +import { BotId } from '~app/bots' +import SwitchBotDropdown from '../SwitchBotDropdown' +import Tooltip from '../Tooltip' + +interface Props { + botId: BotId + name: string + fullName?: string + onSwitchBot?: (botId: BotId) => void +} + +const ChatbotName: FC = (props) => { + const node = ( + + {props.name} + + ) + if (!props.onSwitchBot) { + return node + } + const triggerNode = ( +
+ {node} + +
+ ) + return +} + +export default memo(ChatbotName) diff --git a/src/app/components/Chat/ConversationPanel.tsx b/src/app/components/Chat/ConversationPanel.tsx index a97a2e07..4ef3cdf0 100644 --- a/src/app/components/Chat/ConversationPanel.tsx +++ b/src/app/components/Chat/ConversationPanel.tsx @@ -18,6 +18,7 @@ import Tooltip from '../Tooltip' import ChatMessageInput from './ChatMessageInput' import ChatMessageList from './ChatMessageList' import WebAccessCheckbox from './WebAccessCheckbox' +import ChatbotName from './ChatbotName' interface Props { botId: BotId @@ -93,15 +94,15 @@ const ConversationPanel: FC = (props) => {
- - {botInfo.name} - - {mode === 'compact' && props.onSwitchBot && ( - - )} +
diff --git a/src/app/components/Chat/ErrorAction.tsx b/src/app/components/Chat/ErrorAction.tsx index 5598147e..29a5792c 100644 --- a/src/app/components/Chat/ErrorAction.tsx +++ b/src/app/components/Chat/ErrorAction.tsx @@ -74,6 +74,20 @@ const ErrorAction: FC<{ error: ChatError }> = ({ error }) => { ) } + if (error.code === ErrorCode.QIANWEN_WEB_UNAUTHORIZED) { + return ( + + + + ) + } + if (error.code === ErrorCode.BAICHUAN_WEB_UNAUTHORIZED) { + return ( + + + + ) + } if (error.code === ErrorCode.GPT4_MODEL_WAITLIST) { return ( diff --git a/src/app/components/Chat/WebAccessCheckbox.tsx b/src/app/components/Chat/WebAccessCheckbox.tsx index 9a4c9ba6..c57b897a 100644 --- a/src/app/components/Chat/WebAccessCheckbox.tsx +++ b/src/app/components/Chat/WebAccessCheckbox.tsx @@ -1,12 +1,13 @@ import { Switch } from '@headlessui/react' +import { useSetAtom } from 'jotai' import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { BotId } from '~app/bots' import { usePremium } from '~app/hooks/use-premium' import { trackEvent } from '~app/plausible' +import { showPremiumModalAtom } from '~app/state' import { requestHostPermission } from '~app/utils/permissions' import { getUserConfig, updateUserConfig } from '~services/user-config' -import PremiumFeatureModal from '../Premium/Modal' import Toggle from '../Toggle' interface Props { @@ -16,7 +17,7 @@ interface Props { const WebAccessCheckbox: FC = (props) => { const { t } = useTranslation() const [checked, setChecked] = useState(null) - const [premiumModalOpen, setPremiumModalOpen] = useState(false) + const setPremiumModalOpen = useSetAtom(showPremiumModalAtom) const premiumState = usePremium() const configKey = useMemo(() => { @@ -44,7 +45,7 @@ const WebAccessCheckbox: FC = (props) => { async (newValue: boolean) => { trackEvent('toggle_web_access', { botId: props.botId }) if (!premiumState.activated && newValue) { - setPremiumModalOpen(true) + setPremiumModalOpen('web-access') return } if (!(await requestHostPermission('https://*.duckduckgo.com/'))) { @@ -55,7 +56,7 @@ const WebAccessCheckbox: FC = (props) => { updateUserConfig({ [configKey]: newValue }) } }, - [configKey, premiumState.activated, props.botId], + [configKey, premiumState.activated, props.botId, setPremiumModalOpen], ) if (checked === null) { @@ -72,7 +73,6 @@ const WebAccessCheckbox: FC = (props) => {
- ) } diff --git a/src/app/components/Dialog.tsx b/src/app/components/Dialog.tsx index e15581ad..6cec9b43 100644 --- a/src/app/components/Dialog.tsx +++ b/src/app/components/Dialog.tsx @@ -4,7 +4,7 @@ import closeIcon from '~/assets/icons/close.svg' import { cx } from '~/utils' interface Props { - title: string + title?: string open: boolean onClose: () => void className?: string @@ -42,16 +42,20 @@ const Dialog: FC> = (props) => { props.className, )} > - - - {props.title} - - + {props.title ? ( + + + {props.title} + + + ) : ( + + )} {props.children} diff --git a/src/app/components/GuideModal.tsx b/src/app/components/GuideModal.tsx index 06d58786..c00e0d17 100644 --- a/src/app/components/GuideModal.tsx +++ b/src/app/components/GuideModal.tsx @@ -1,26 +1,17 @@ -import { Link } from '@tanstack/react-router' import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import Browser from 'webextension-polyfill' -import { usePremium } from '~app/hooks/use-premium' +import { incrAppOpenTimes } from '~services/storage/open-times' import Button from './Button' import Dialog from './Dialog' -async function incrOpenTimes() { - const { openTimes = 0 } = await Browser.storage.sync.get('openTimes') - Browser.storage.sync.set({ openTimes: openTimes + 1 }) - return openTimes -} - const GuideModal: FC = () => { const { t } = useTranslation() const [open, setOpen] = useState(false) const [openTimes, setOpenTimes] = useState(0) - const premiumState = usePremium() useEffect(() => { - incrOpenTimes().then((t) => { - if (t === 15 || (t > 0 && t % 50 === 0)) { + incrAppOpenTimes().then((t) => { + if (t === 15) { setOpen(true) } setOpenTimes(t) @@ -44,26 +35,6 @@ const GuideModal: FC = () => { ) } - if (openTimes > 0 && openTimes % 50 === 0 && !premiumState.isLoading && !premiumState.activated) { - return ( - setOpen(false)} className="rounded-2xl w-[600px]"> -
-

- {t('You have opened ChatHub {{openTimes}} times, consider unlock all features?', { openTimes })} -

- setOpen(false)} - className="focus-visible:outline-none" - > -
-
- ) - } - return null } diff --git a/src/app/components/Layout.tsx b/src/app/components/Layout.tsx index c2f21334..2e6698d8 100644 --- a/src/app/components/Layout.tsx +++ b/src/app/components/Layout.tsx @@ -1,7 +1,9 @@ import { Outlet } from '@tanstack/react-router' import { useAtomValue } from 'jotai' import { followArcThemeAtom, themeColorAtom } from '~app/state' +import DiscountModal from './Premium/DiscountModal' import Sidebar from './Sidebar' +import PremiumModal from './Premium/Modal' function Layout() { const themeColor = useAtomValue(themeColorAtom) @@ -15,6 +17,8 @@ function Layout() {
+ + ) } diff --git a/src/app/components/Premium/DiscountModal.tsx b/src/app/components/Premium/DiscountModal.tsx new file mode 100644 index 00000000..f29acfb8 --- /dev/null +++ b/src/app/components/Premium/DiscountModal.tsx @@ -0,0 +1,58 @@ +import { useAtom, useSetAtom } from 'jotai' +import { FC, useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import * as api from '~/services/server-api' +import { trackEvent } from '~app/plausible' +import { showDiscountModalAtom, showPremiumModalAtom } from '~app/state' +import discountBg from '~assets/discount-bg.png' +import Button from '../Button' +import Dialog from '../Dialog' + +const DiscountModal: FC = () => { + const [open, setOpen] = useAtom(showDiscountModalAtom) + const [creating, setCreating] = useState(false) + const setShowPremiumModal = useSetAtom(showPremiumModalAtom) + const { t } = useTranslation() + + const createDiscount = useCallback(async () => { + trackEvent('create_discount') + setCreating(true) + await api.createDiscount() + setCreating(false) + setOpen(false) + setShowPremiumModal(true) + }, [setOpen, setShowPremiumModal]) + + useEffect(() => { + if (open) { + trackEvent('show_discount_modal') + } + }, [open]) + + return ( + setOpen(false)} className="min-w-[600px] shadow-inner"> +
+
+ +
+ 🎁 +

{t('Special Offer: 20% OFF')}

+

{t('TODAY ONLY')}

+
+
+
+
+ ) +} + +export default DiscountModal diff --git a/src/app/components/Premium/Modal.tsx b/src/app/components/Premium/Modal.tsx index 8e65acaf..bcb6f478 100644 --- a/src/app/components/Premium/Modal.tsx +++ b/src/app/components/Premium/Modal.tsx @@ -1,66 +1,60 @@ import { useNavigate } from '@tanstack/react-router' +import { useAtom } from 'jotai' import { FC, useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' +import { useDiscountCode } from '~app/hooks/use-purchase-info' import { trackEvent } from '~app/plausible' +import { showPremiumModalAtom } from '~app/state' +import { incrPremiumModalOpenTimes } from '~services/storage/open-times' import Button from '../Button' import Dialog from '../Dialog' -import FeatureList, { FeatureId } from './FeatureList' +import DiscountBadge from './DiscountBadge' +import FeatureList from './FeatureList' import PriceSection from './PriceSection' import Testimonials from './Testimonials' -import DiscountBadge from './DiscountBadge' -import Browser from 'webextension-polyfill' - -async function incrOpenTimes() { - const { premiumModalOpenTimes = 0 } = await Browser.storage.sync.get('premiumModalOpenTimes') - Browser.storage.sync.set({ premiumModalOpenTimes: premiumModalOpenTimes + 1 }) - return premiumModalOpenTimes + 1 -} -interface Props { - open: boolean - setOpen: (open: boolean) => void - feature?: FeatureId -} - -const PremiumModal: FC = (props) => { +const PremiumModal: FC = () => { const { t } = useTranslation() const navigate = useNavigate() + const [open, setOpen] = useAtom(showPremiumModalAtom) + const discountCode = useDiscountCode() + + const feature = typeof open === 'string' ? open : undefined useEffect(() => { - if (props.open) { - incrOpenTimes().then((openTimes) => { - trackEvent('show_premium_modal', { source: props.feature, openTimes }) + if (open) { + incrPremiumModalOpenTimes().then((openTimes) => { + trackEvent('show_premium_modal', { source: feature, openTimes }) }) } - }, [props.open, props.feature]) + }, [feature, open]) const onClickBuy = useCallback(() => { trackEvent('click_buy_premium', { source: 'premium_modal' }) navigate({ to: '/premium', search: { source: 'after_click_buy_premium' } }) }, [navigate]) + const close = useCallback(() => { + setOpen(false) + }, [setOpen]) + return ( - props.setOpen(false)} - className="min-w-[600px]" - > +
diff --git a/src/app/components/Premium/PriceSection.tsx b/src/app/components/Premium/PriceSection.tsx index 187dacb1..b5cad1ac 100644 --- a/src/app/components/Premium/PriceSection.tsx +++ b/src/app/components/Premium/PriceSection.tsx @@ -1,25 +1,74 @@ -import { FC } from 'react' +import dayjs, { Dayjs } from 'dayjs' +import humanizeDuration from 'humanize-duration' +import { FC, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import useImmutableSWR from 'swr/immutable' -import { fetchPremiumProduct } from '~services/server-api' +import { usePurchaseInfo } from '~app/hooks/use-purchase-info' +import { cx } from '~utils' -const PriceSection: FC = () => { +const DiscountCountDown: FC<{ price: number; endsAt: Dayjs }> = ({ price, endsAt }) => { + const [now, setNow] = useState(() => dayjs()) const { t } = useTranslation() - const priceQuery = useImmutableSWR('premium-price', async () => { - const product = await fetchPremiumProduct() - return product.price / 100 - }) + useEffect(() => { + const interval = setInterval(() => { + setNow(dayjs()) + }, 1000) + return () => clearInterval(interval) + }, []) + + if (endsAt.isBefore(now)) { + return null + } return ( -
- - {priceQuery.data ? `$${priceQuery.data}` : '$$$'} +
+ ${price / 100} + + {t('20% OFF: ends in')} {humanizeDuration(endsAt.diff(now), { units: ['d', 'h', 'm', 's'], round: true })} -
- $49 - / {t('Lifetime license')} +
+ ) +} + +const PriceSection: FC<{ align: 'center' | 'left' }> = (props) => { + const { t } = useTranslation() + const purchaseInfoQuery = usePurchaseInfo() + + const calculatedPrice = useMemo(() => { + if (!purchaseInfoQuery.data) { + return null + } + const { price, discount } = purchaseInfoQuery.data + if (discount) { + return discount.price / 100 + } + return price / 100 + }, [purchaseInfoQuery.data]) + + const discountEndsAt = useMemo(() => { + if (!purchaseInfoQuery.data) { + return null + } + const { discount } = purchaseInfoQuery.data + if (discount) { + return dayjs(discount.startTime).add(1, 'day') + } + }, [purchaseInfoQuery.data]) + + return ( +
+
+ + {calculatedPrice ? `$${calculatedPrice}` : '$$$'} + +
+ $49 + / {t('Lifetime license')} +
+ {discountEndsAt && purchaseInfoQuery.data && ( + + )}
) } diff --git a/src/app/components/Settings/EnabledBotsSettings.tsx b/src/app/components/Settings/EnabledBotsSettings.tsx index e2dedb8e..fa9a9bb5 100644 --- a/src/app/components/Settings/EnabledBotsSettings.tsx +++ b/src/app/components/Settings/EnabledBotsSettings.tsx @@ -28,7 +28,7 @@ const EnabledBotsSettings: FC = ({ userConfig, updateConfigValue }) => { ) return ( -
+
{Object.entries(CHATBOTS).map(([botId, bot]) => { const enabled = userConfig.enabledBots.includes(botId as BotId) return ( diff --git a/src/app/components/Sidebar/index.tsx b/src/app/components/Sidebar/index.tsx index 5c4eb02d..71d38fc4 100644 --- a/src/app/components/Sidebar/index.tsx +++ b/src/app/components/Sidebar/index.tsx @@ -1,8 +1,7 @@ import { Link } from '@tanstack/react-router' import { motion } from 'framer-motion' -import { cx } from '~/utils' -import { useAtom } from 'jotai' -import { useState } from 'react' +import { useAtom, useSetAtom } from 'jotai' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import allInOneIcon from '~/assets/all-in-one.svg' import collapseIcon from '~/assets/icons/collapse.svg' @@ -12,8 +11,12 @@ import settingIcon from '~/assets/icons/setting.svg' import themeIcon from '~/assets/icons/theme.svg' import logo from '~/assets/logo.svg' import minimalLogo from '~/assets/minimal-logo.svg' +import { cx } from '~/utils' import { useEnabledBots } from '~app/hooks/use-enabled-bots' -import { sidebarCollapsedAtom } from '~app/state' +import { showDiscountModalAtom, sidebarCollapsedAtom } from '~app/state' +import { getPremiumActivation } from '~services/premium' +import * as api from '~services/server-api' +import { getAppOpenTimes, getPremiumModalOpenTimes } from '~services/storage/open-times' import CommandBar from '../CommandBar' import GuideModal from '../GuideModal' import ThemeSettingModal from '../ThemeSettingModal' @@ -37,6 +40,20 @@ function Sidebar() { const [collapsed, setCollapsed] = useAtom(sidebarCollapsedAtom) const [themeSettingModalOpen, setThemeSettingModalOpen] = useState(false) const enabledBots = useEnabledBots() + const setShowDiscountModal = useSetAtom(showDiscountModalAtom) + + useEffect(() => { + Promise.all([getAppOpenTimes(), getPremiumModalOpenTimes()]).then(async ([appOpenTimes, premiumModalOpenTimes]) => { + if (getPremiumActivation()) { + return + } + const { show } = await api.checkDiscount({ appOpenTimes, premiumModalOpenTimes }) + if (show) { + setShowDiscountModal(true) + } + }) + }, []) + return ( - {themeSettingModalOpen && setThemeSettingModalOpen(false)} />} + setThemeSettingModalOpen(false)} /> ) } diff --git a/src/app/components/SwitchBotDropdown.tsx b/src/app/components/SwitchBotDropdown.tsx index 11a98c03..dd8f0882 100644 --- a/src/app/components/SwitchBotDropdown.tsx +++ b/src/app/components/SwitchBotDropdown.tsx @@ -1,29 +1,19 @@ import { Menu, Transition } from '@headlessui/react' -import { FC, Fragment, useCallback } from 'react' -import dropdownIcon from '~/assets/icons/dropdown.svg' +import { FC, Fragment, ReactNode } from 'react' import { BotId } from '~app/bots' import { useEnabledBots } from '~app/hooks/use-enabled-bots' interface Props { + triggerNode: ReactNode selectedBotId: BotId onChange: (botId: BotId) => void } const SwitchBotDropdown: FC = (props) => { const enabledBots = useEnabledBots() - - const onSelect = useCallback( - (botId: BotId) => { - props.onChange(botId) - }, - [props], - ) - return ( - - - + {props.triggerNode} = (props) => {
onSelect(botId)} + onClick={() => props.onChange(botId)} >
diff --git a/src/app/consts.ts b/src/app/consts.ts index 364a1571..7e7d3cb6 100644 --- a/src/app/consts.ts +++ b/src/app/consts.ts @@ -1,14 +1,12 @@ -import alpacaLogo from '~/assets/alpaca-logo.png' import claudeLogo from '~/assets/anthropic-logo.png' +import baichuanLogo from '~/assets/baichuan-logo.png' import bardLogo from '~/assets/bard-logo.svg' import bingLogo from '~/assets/bing-logo.svg' import chatglmLogo from '~/assets/chatglm-logo.svg' import chatgptLogo from '~/assets/chatgpt-logo.svg' -import guanacoLogo from '~/assets/guanaco-logo.png' import llamaLogo from '~/assets/llama-logo.png' -import oasstLogo from '~/assets/oasst-logo.svg' import piLogo from '~/assets/pi-logo.png' -import rwkvLogo from '~/assets/rwkv-logo.png' +import qianwenLogo from '~/assets/qianwen-logo.png' import vicunaLogo from '~/assets/vicuna-logo.jpg' import wizardlmLogo from '~/assets/wizardlm-logo.png' import xunfeiLogo from '~/assets/xunfei-logo.png' @@ -35,10 +33,6 @@ export const CHATBOTS: Record = { name: 'Llama 2', avatar: llamaLogo, }, - alpaca: { - name: 'Alpaca', - avatar: alpacaLogo, - }, vicuna: { name: 'Vicuna', avatar: vicunaLogo, @@ -47,6 +41,10 @@ export const CHATBOTS: Record = { name: 'Pi', avatar: piLogo, }, + wizardlm: { + name: 'WizardLM', + avatar: wizardlmLogo, + }, chatglm: { name: 'ChatGLM2', avatar: chatglmLogo, @@ -55,21 +53,13 @@ export const CHATBOTS: Record = { name: 'iFlytek Spark', avatar: xunfeiLogo, }, - oasst: { - name: 'OpenAssistant', - avatar: oasstLogo, + qianwen: { + name: 'Qianwen', + avatar: qianwenLogo, }, - rwkv: { - name: 'ChatRWKV', - avatar: rwkvLogo, - }, - guanaco: { - name: 'Guanaco', - avatar: guanacoLogo, - }, - wizardlm: { - name: 'WizardLM', - avatar: wizardlmLogo, + baichuan: { + name: 'Baichuan', + avatar: baichuanLogo, }, } diff --git a/src/app/hooks/use-premium.ts b/src/app/hooks/use-premium.ts index d8a3a6da..e342b528 100644 --- a/src/app/hooks/use-premium.ts +++ b/src/app/hooks/use-premium.ts @@ -1,25 +1,16 @@ -import { useAtom } from 'jotai' import { FetchError } from 'ofetch' import useSWR from 'swr' -import { licenseKeyAtom } from '~app/state' -import { clearLicenseInstances, getLicenseInstanceId, validateLicenseKey } from '~services/premium' +import { getPremiumActivation, validatePremium } from '~services/premium' export function usePremium() { - const [licenseKey, setLicenseKey] = useAtom(licenseKeyAtom) - - const activateQuery = useSWR<{ valid: true } | { valid: false; error?: string }>( - `license:${licenseKey}`, + const validationQuery = useSWR<{ valid: true } | { valid: false; error?: string }>( + 'premium-validation', async () => { - if (!licenseKey) { - return { valid: false } - } try { - return await validateLicenseKey(licenseKey) + return await validatePremium() } catch (err) { if (err instanceof FetchError) { if (err.status === 404) { - clearLicenseInstances() - setLicenseKey('') return { valid: false } } if (err.status === 400) { @@ -30,15 +21,15 @@ export function usePremium() { } }, { - fallbackData: getLicenseInstanceId(licenseKey) ? { valid: true } : undefined, + fallbackData: getPremiumActivation() ? { valid: true } : undefined, revalidateOnFocus: false, dedupingInterval: 10 * 60 * 1000, }, ) return { - activated: activateQuery.data?.valid, - isLoading: activateQuery.isLoading, - error: activateQuery.data?.valid === true ? undefined : activateQuery.data?.error, + activated: validationQuery.data?.valid, + isLoading: validationQuery.isLoading, + error: validationQuery.data?.valid === true ? undefined : validationQuery.data?.error, } } diff --git a/src/app/hooks/use-purchase-info.ts b/src/app/hooks/use-purchase-info.ts new file mode 100644 index 00000000..be465c48 --- /dev/null +++ b/src/app/hooks/use-purchase-info.ts @@ -0,0 +1,18 @@ +import dayjs from 'dayjs' +import useSWR from 'swr' +import { fetchPurchaseInfo } from '~services/server-api' + +export function usePurchaseInfo() { + return useSWR('premium-info', fetchPurchaseInfo) +} + +export function useDiscountCode() { + const { data } = usePurchaseInfo() + if (!data) { + return undefined + } + const { discount } = data + if (discount && dayjs(discount.startTime).add(1, 'day').isAfter()) { + return discount.code + } +} diff --git a/src/app/i18n/locales/french.json b/src/app/i18n/locales/french.json index 44686f00..73b6b914 100644 --- a/src/app/i18n/locales/french.json +++ b/src/app/i18n/locales/french.json @@ -35,7 +35,7 @@ "Four in one": "Quatre en un", "Activate up to 5 devices": "Activer jusqu'à 5 appareils", "Deactivate": "Désactiver", - "Get premium license": "Obtenir une licence premium", + "Buy premium license": "Obtenir une licence premium", "Theme Settings": "Paramètres du thème", "Theme Mode": "Mode du thème", "Theme Color": "Couleur du thème", diff --git a/src/app/i18n/locales/german.json b/src/app/i18n/locales/german.json index 2ee56c2e..0e55193a 100644 --- a/src/app/i18n/locales/german.json +++ b/src/app/i18n/locales/german.json @@ -35,7 +35,7 @@ "Four in one": "Vier in einem", "Activate up to 5 devices": "Aktivieren Sie bis zu 5 Geräte", "Deactivate": "Deaktivieren", - "Get premium license": "Premium-Lizenz erhalten", + "Buy premium license": "Premium-Lizenz erhalten", "Theme Settings": "Thema-Einstellungen", "Theme Mode": "Thema-Modus", "Theme Color": "Thema-Farbe", diff --git a/src/app/i18n/locales/indonesia.json b/src/app/i18n/locales/indonesia.json index 79b28873..383ac48b 100644 --- a/src/app/i18n/locales/indonesia.json +++ b/src/app/i18n/locales/indonesia.json @@ -35,7 +35,7 @@ "Four in one": "Empat dalam satu", "Activate up to 5 devices": "Aktifkan hingga 5 perangkat", "Deactivate": "Nonaktifkan", - "Get premium license": "Dapatkan lisensi premium", + "Buy premium license": "Dapatkan lisensi premium", "Theme Settings": "Pengaturan tema", "Theme Mode": "Mode tema", "Theme Color": "Warna tema", diff --git a/src/app/i18n/locales/japanese.json b/src/app/i18n/locales/japanese.json index 34ac7179..536d9e29 100644 --- a/src/app/i18n/locales/japanese.json +++ b/src/app/i18n/locales/japanese.json @@ -35,7 +35,7 @@ "Four in one": "4つ合わせて", "Activate up to 5 devices": "最大5台のデバイスをアクティブ化する", "Deactivate": "非アクティブ化", - "Get premium license": "プレミアムライセンスを入手する", + "Buy premium license": "プレミアムライセンスを入手する", "Theme Settings": "テーマ設定", "Theme Mode": "テーマモード", "Theme Color": "テーマカラー", @@ -87,5 +87,10 @@ "Display": "表示", "Display Settings": "表示設定", "Auto": "自動", - "Language": "言語" + "Language": "言語", + "Buy once, use forever": "一度購入し、永久に使用する", + "20% OFF: ends in": "20%オフ:終了日", + "Special Offer: 20% OFF": "特別オファー:20%オフ", + "TODAY ONLY": "今日限り", + "Get Discount": "割引を入手する" } diff --git a/src/app/i18n/locales/portuguese.json b/src/app/i18n/locales/portuguese.json index 865c27a5..5785cec9 100644 --- a/src/app/i18n/locales/portuguese.json +++ b/src/app/i18n/locales/portuguese.json @@ -35,7 +35,7 @@ "Four in one": "Quatro em um", "Activate up to 5 devices": "Ativar até 5 dispositivos", "Deactivate": "Desativar", - "Get premium license": "Obter licença premium", + "Buy premium license": "Obter licença premium", "Theme Settings": "Configurações do tema", "Theme Mode": "Modo do tema", "Theme Color": "Cor do tema", diff --git a/src/app/i18n/locales/simplified-chinese.json b/src/app/i18n/locales/simplified-chinese.json index 8037765d..6f4fed5e 100644 --- a/src/app/i18n/locales/simplified-chinese.json +++ b/src/app/i18n/locales/simplified-chinese.json @@ -35,7 +35,7 @@ "Four in one": "四合一", "Activate up to 5 devices": "最多可激活5台设备", "Deactivate": "反激活", - "Get premium license": "购买会员", + "Buy premium license": "购买会员", "Theme Settings": "主题设置", "Theme Mode": "主题模式", "Theme Color": "主题色", @@ -89,5 +89,9 @@ "Auto": "自动", "Language": "语言", "Limited-time offer": "限时特惠", - "Buy once, use forever": "一次性买断价" + "Buy once, use forever": "一次性买断价", + "20% OFF: ends in": "八折优惠,限时", + "Special Offer: 20% OFF": "特别优惠:立享八折", + "TODAY ONLY": "今日限定", + "Get Discount": "获取优惠" } diff --git a/src/app/i18n/locales/spanish.json b/src/app/i18n/locales/spanish.json index ea31bc32..2a6c310e 100644 --- a/src/app/i18n/locales/spanish.json +++ b/src/app/i18n/locales/spanish.json @@ -35,7 +35,7 @@ "Four in one": "Cuatro en uno", "Activate up to 5 devices": "Activar hasta 5 dispositivos", "Deactivate": "Desactivar", - "Get premium license": "Obtener licencia premium", + "Buy premium license": "Obtener licencia premium", "Theme Settings": "Configuración del tema", "Theme Mode": "Modo de tema", "Theme Color": "Color del tema", diff --git a/src/app/i18n/locales/thai.json b/src/app/i18n/locales/thai.json index c4588d44..9f05eae4 100644 --- a/src/app/i18n/locales/thai.json +++ b/src/app/i18n/locales/thai.json @@ -35,7 +35,7 @@ "Four in one": "สี่ในหนึ่ง", "Activate up to 5 devices": "เปิดใช้งานได้สูงสุด 5 เครื่อง", "Deactivate": "ปิดใช้งาน", - "Get premium license": "รับใบอนุญาตพรีเมียม", + "Buy premium license": "รับใบอนุญาตพรีเมียม", "Theme Settings": "การตั้งค่าธีม", "Theme Mode": "โหมดธีม", "Theme Color": "สีธีม", diff --git a/src/app/i18n/locales/traditional-chinese.json b/src/app/i18n/locales/traditional-chinese.json index 0b1b6ce2..0a87de62 100644 --- a/src/app/i18n/locales/traditional-chinese.json +++ b/src/app/i18n/locales/traditional-chinese.json @@ -35,7 +35,7 @@ "Four in one": "四合一", "Activate up to 5 devices": "最多可啟用5台設備", "Deactivate": "反啟用", - "Get premium license": "購買會員", + "Buy premium license": "購買會員", "Theme Settings": "主題設置", "Theme Mode": "主題模式", "Theme Color": "主題色", @@ -87,5 +87,11 @@ "Display": "顯示", "Display Settings": "顯示設置", "Auto": "自動", - "Language": "語言" + "Language": "語言", + "Limited-time offer": "限時優惠", + "Buy once, use forever": "買一次,永久使用", + "20% OFF: ends in": "八折優惠:即將結束", + "Special Offer: 20% OFF": "特別優惠:八折優惠", + "TODAY ONLY": "今日限定", + "Get Discount": "獲取優惠" } diff --git a/src/app/pages/MultiBotChatPanel.tsx b/src/app/pages/MultiBotChatPanel.tsx index 56ec79a7..d3e570fe 100644 --- a/src/app/pages/MultiBotChatPanel.tsx +++ b/src/app/pages/MultiBotChatPanel.tsx @@ -1,32 +1,36 @@ -import { cx } from '~/utils' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { atomWithStorage } from 'jotai/utils' -import { uniqBy } from 'lodash-es' -import { FC, Suspense, useCallback, useEffect, useMemo, useState } from 'react' +import { sample, uniqBy } from 'lodash-es' +import { FC, Suspense, useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { cx } from '~/utils' import Button from '~app/components/Button' import ChatMessageInput from '~app/components/Chat/ChatMessageInput' import LayoutSwitch from '~app/components/Chat/LayoutSwitch' -import PremiumFeatureModal from '~app/components/Premium/Modal' -import { Layout } from '~app/consts' +import { CHATBOTS, Layout } from '~app/consts' import { useChat } from '~app/hooks/use-chat' import { usePremium } from '~app/hooks/use-premium' import { trackEvent } from '~app/plausible' +import { showPremiumModalAtom } from '~app/state' import { BotId } from '../bots' import ConversationPanel from '../components/Chat/ConversationPanel' +const DEFAULT_BOTS: BotId[] = ['chatgpt', 'claude', 'bard', 'bing', 'llama', 'pi'] + const layoutAtom = atomWithStorage('multiPanelLayout', 2, undefined, { unstable_getOnInit: true }) -const twoPanelBotsAtom = atomWithStorage('multiPanelBots:2', ['chatgpt', 'claude']) -const threePanelBotsAtom = atomWithStorage('multiPanelBots:3', ['chatgpt', 'claude', 'bard']) -const fourPanelBotsAtom = atomWithStorage('multiPanelBots:4', ['chatgpt', 'claude', 'bard', 'bing']) -const sixPanelBotsAtom = atomWithStorage('multiPanelBots:6', [ - 'chatgpt', - 'claude', - 'bard', - 'bing', - 'pi', - 'llama', -]) +const twoPanelBotsAtom = atomWithStorage('multiPanelBots:2', DEFAULT_BOTS.slice(0, 2)) +const threePanelBotsAtom = atomWithStorage('multiPanelBots:3', DEFAULT_BOTS.slice(0, 3)) +const fourPanelBotsAtom = atomWithStorage('multiPanelBots:4', DEFAULT_BOTS.slice(0, 4)) +const sixPanelBotsAtom = atomWithStorage('multiPanelBots:6', DEFAULT_BOTS.slice(0, 6)) + +function replaceDeprecatedBots(bots: BotId[]): BotId[] { + return bots.map((bot) => { + if (CHATBOTS[bot]) { + return bot + } + return sample(DEFAULT_BOTS)! + }) +} const GeneralChatPanel: FC<{ chats: ReturnType[] @@ -37,15 +41,15 @@ const GeneralChatPanel: FC<{ const generating = useMemo(() => chats.some((c) => c.generating), [chats]) const [layout, setLayout] = useAtom(layoutAtom) - const [premiumModalOpen, setPremiumModalOpen] = useState(false) + const setPremiumModalOpen = useSetAtom(showPremiumModalAtom) const premiumState = usePremium() const disabled = useMemo(() => !premiumState.isLoading && !premiumState.activated, [premiumState]) useEffect(() => { if (disabled && (chats.length > 2 || supportImageInput)) { - setPremiumModalOpen(true) + setPremiumModalOpen('all-in-one-layout') } - }, [chats.length, disabled, supportImageInput]) + }, [chats.length, disabled, setPremiumModalOpen, supportImageInput]) const sendSingleMessage = useCallback( (input: string, botId: BotId) => { @@ -58,13 +62,13 @@ const GeneralChatPanel: FC<{ const sendAllMessage = useCallback( (input: string, image?: File) => { if (disabled && chats.length > 2) { - setPremiumModalOpen(true) + setPremiumModalOpen('all-in-one-layout') return } uniqBy(chats, (c) => c.botId).forEach((c) => c.sendMessage(input, image)) trackEvent('send_messages', { layout, disabled }) }, - [chats, disabled, layout], + [chats, disabled, layout, setPremiumModalOpen], ) const onSwitchBot = useCallback( @@ -126,13 +130,13 @@ const GeneralChatPanel: FC<{ supportImageInput={supportImageInput} />
-
) } const TwoBotChatPanel = () => { - const [multiPanelBotIds, setBots] = useAtom(twoPanelBotsAtom) + const [bots, setBots] = useAtom(twoPanelBotsAtom) + const multiPanelBotIds = useMemo(() => replaceDeprecatedBots(bots), [bots]) const chat1 = useChat(multiPanelBotIds[0]) const chat2 = useChat(multiPanelBotIds[1]) const chats = useMemo(() => [chat1, chat2], [chat1, chat2]) @@ -140,7 +144,8 @@ const TwoBotChatPanel = () => { } const ThreeBotChatPanel = () => { - const [multiPanelBotIds, setBots] = useAtom(threePanelBotsAtom) + const [bots, setBots] = useAtom(threePanelBotsAtom) + const multiPanelBotIds = useMemo(() => replaceDeprecatedBots(bots), [bots]) const chat1 = useChat(multiPanelBotIds[0]) const chat2 = useChat(multiPanelBotIds[1]) const chat3 = useChat(multiPanelBotIds[2]) @@ -149,7 +154,8 @@ const ThreeBotChatPanel = () => { } const FourBotChatPanel = () => { - const [multiPanelBotIds, setBots] = useAtom(fourPanelBotsAtom) + const [bots, setBots] = useAtom(fourPanelBotsAtom) + const multiPanelBotIds = useMemo(() => replaceDeprecatedBots(bots), [bots]) const chat1 = useChat(multiPanelBotIds[0]) const chat2 = useChat(multiPanelBotIds[1]) const chat3 = useChat(multiPanelBotIds[2]) @@ -159,7 +165,8 @@ const FourBotChatPanel = () => { } const SixBotChatPanel = () => { - const [multiPanelBotIds, setBots] = useAtom(sixPanelBotsAtom) + const [bots, setBots] = useAtom(sixPanelBotsAtom) + const multiPanelBotIds = useMemo(() => replaceDeprecatedBots(bots), [bots]) const chat1 = useChat(multiPanelBotIds[0]) const chat2 = useChat(multiPanelBotIds[1]) const chat3 = useChat(multiPanelBotIds[2]) diff --git a/src/app/pages/PremiumPage.tsx b/src/app/pages/PremiumPage.tsx index 9c51d2d4..dce127e4 100644 --- a/src/app/pages/PremiumPage.tsx +++ b/src/app/pages/PremiumPage.tsx @@ -1,7 +1,7 @@ import { useSearch } from '@tanstack/react-router' -import ConfettiExplosion from 'react-confetti-explosion' -import { useAtom } from 'jotai' +import { get as getPath } from 'lodash-es' import { useCallback, useState } from 'react' +import ConfettiExplosion from 'react-confetti-explosion' import { Toaster } from 'react-hot-toast' import { useTranslation } from 'react-i18next' import Button from '~app/components/Button' @@ -9,38 +9,49 @@ import DiscountBadge from '~app/components/Premium/DiscountBadge' import FeatureList from '~app/components/Premium/FeatureList' import PriceSection from '~app/components/Premium/PriceSection' import { usePremium } from '~app/hooks/use-premium' +import { useDiscountCode } from '~app/hooks/use-purchase-info' import { trackEvent } from '~app/plausible' import { premiumRoute } from '~app/router' -import { licenseKeyAtom } from '~app/state' -import { deactivateLicenseKey } from '~services/premium' +import { activatePremium, deactivatePremium } from '~services/premium' function PremiumPage() { const { t } = useTranslation() - const [licenseKey, setLicenseKey] = useAtom(licenseKeyAtom) const premiumState = usePremium() + const [activating, setActivating] = useState(false) const [deactivating, setDeactivating] = useState(false) + const [activationError, setActivationError] = useState('') const { source } = useSearch({ from: premiumRoute.id }) const [isExploding, setIsExploding] = useState(false) + const discountCode = useDiscountCode() - const activateLicense = useCallback(() => { + const activate = useCallback(async () => { const key = window.prompt('Enter your license key', '') - if (key) { - setLicenseKey(key) + if (!key) { + return } - }, [setLicenseKey]) - - const deactivateLicense = useCallback(async () => { - if (!licenseKey) { + setActivationError('') + setActivating(true) + trackEvent('activate_license') + try { + await activatePremium(key) + } catch (err) { + console.error('activation', err) + setActivationError(getPath(err, 'data.error') || 'Activation failed') + setActivating(false) return } + setTimeout(() => location.reload(), 500) + }, []) + + const deactivateLicense = useCallback(async () => { if (!window.confirm('Are you sure to deactivate this device?')) { return } setDeactivating(true) - await deactivateLicenseKey(licenseKey) - setLicenseKey('') + trackEvent('deactivate_license') + await deactivatePremium() setTimeout(() => location.reload(), 500) - }, [licenseKey, setLicenseKey]) + }, []) return (
@@ -48,7 +59,7 @@ function PremiumPage() { {!premiumState.activated && (
- +
)}
@@ -73,19 +84,19 @@ function PremiumPage() { ) : ( <> trackEvent('click_buy_premium', { source: 'premium_page' })} > -
- {!!premiumState.error && {premiumState.error}} + {!!(premiumState.error || activationError) && ( + {premiumState.error || activationError} + )} {isExploding && setIsExploding(false)} />}
diff --git a/src/app/pages/SidePanelPage.tsx b/src/app/pages/SidePanelPage.tsx index 4a626045..feeeb342 100644 --- a/src/app/pages/SidePanelPage.tsx +++ b/src/app/pages/SidePanelPage.tsx @@ -1,12 +1,12 @@ -import { cx } from '~/utils' import { useAtom } from 'jotai' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import clearIcon from '~/assets/icons/clear.svg' +import { cx } from '~/utils' import Button from '~app/components/Button' import ChatMessageInput from '~app/components/Chat/ChatMessageInput' import ChatMessageList from '~app/components/Chat/ChatMessageList' -import SwitchBotDropdown from '~app/components/SwitchBotDropdown' +import ChatbotName from '~app/components/Chat/ChatbotName' import { CHATBOTS } from '~app/consts' import { ConversationContext, ConversationContextValue } from '~app/context' import { useChat } from '~app/hooks/use-chat' @@ -43,8 +43,7 @@ function SidePanelPage() {
- {botInfo.name} - +
('sidePanelBot', 'chatgpt') +export const showDiscountModalAtom = atom(false) +export const showPremiumModalAtom = atom(false) diff --git a/src/assets/baichuan-logo.png b/src/assets/baichuan-logo.png new file mode 100644 index 00000000..7a18bbad Binary files /dev/null and b/src/assets/baichuan-logo.png differ diff --git a/src/assets/discount-bg.png b/src/assets/discount-bg.png new file mode 100644 index 00000000..a58b2a12 Binary files /dev/null and b/src/assets/discount-bg.png differ diff --git a/src/assets/qianwen-logo.png b/src/assets/qianwen-logo.png new file mode 100644 index 00000000..540b437a Binary files /dev/null and b/src/assets/qianwen-logo.png differ diff --git a/src/content-script/chatgpt-inpage-proxy.ts b/src/content-script/chatgpt-inpage-proxy.ts index 285a6912..e8fd2176 100644 --- a/src/content-script/chatgpt-inpage-proxy.ts +++ b/src/content-script/chatgpt-inpage-proxy.ts @@ -12,6 +12,7 @@ function injectTip() { div.style.padding = '10px' div.style.margin = '10px' div.style.border = '1px solid' + div.style.color = 'red' document.body.appendChild(div) } diff --git a/src/rules/baichuan.json b/src/rules/baichuan.json new file mode 100644 index 00000000..b807aa35 --- /dev/null +++ b/src/rules/baichuan.json @@ -0,0 +1,25 @@ +[ + { + "id": 1, + "priority": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "header": "origin", + "operation": "set", + "value": "https://www.baichuan-ai.com" + }, + { + "header": "referer", + "operation": "set", + "value": "https://www.baichuan-ai.com" + } + ] + }, + "condition": { + "requestDomains": ["www.baichuan-ai.com"], + "resourceTypes": ["xmlhttprequest"] + } + } +] diff --git a/src/rules/bing.json b/src/rules/bing.json index 72633a78..dd6f18f1 100644 --- a/src/rules/bing.json +++ b/src/rules/bing.json @@ -20,7 +20,7 @@ "condition": { "urlFilter": "bing", "isUrlFilterCaseSensitive": false, - "resourceTypes": ["xmlhttprequest"] + "resourceTypes": ["xmlhttprequest", "websocket"] } } ] diff --git a/src/rules/qianwen.json b/src/rules/qianwen.json new file mode 100644 index 00000000..74c6215c --- /dev/null +++ b/src/rules/qianwen.json @@ -0,0 +1,25 @@ +[ + { + "id": 1, + "priority": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "header": "origin", + "operation": "set", + "value": "https://qianwen.aliyun.com" + }, + { + "header": "referer", + "operation": "set", + "value": "https://qianwen.aliyun.com/chat" + } + ] + }, + "condition": { + "requestDomains": ["qianwen.aliyun.com"], + "resourceTypes": ["xmlhttprequest"] + } + } +] diff --git a/src/services/agent/index.ts b/src/services/agent/index.ts index 9b3c232f..652a0dc5 100644 --- a/src/services/agent/index.ts +++ b/src/services/agent/index.ts @@ -24,7 +24,7 @@ function buildPromptWithContext(input: string, context: string) { const FINAL_ANSWER_KEYWORD_REGEX = /"action":\s*"Final Answer"/ const WEB_SEARCH_KEYWORD_REGEX = /"action":\s*"web_search"/ -const ACTION_INPUT_REGEX = /"action_input":\s*"([^"]+)("\s*(```)?)?/ +const ACTION_INPUT_REGEX = /"action_input":\s*"((?:\\.|[^"])+)(?:"\s*(```)?)?/ async function* execute( input: string, diff --git a/src/services/premium.ts b/src/services/premium.ts index 4d2f56db..5a2adcb4 100644 --- a/src/services/premium.ts +++ b/src/services/premium.ts @@ -1,36 +1,58 @@ import { getBrowser, getOS } from '~app/utils/navigator' -import { activateLicense, deactivateLicense, validateLicense } from './lemonsqueezy' +import * as lemonsqueezy from './lemonsqueezy' + +interface PremiumActivation { + licenseKey: string + instanceId: string +} function getInstanceName() { return `${getOS()} / ${getBrowser()}` } -export async function validateLicenseKey(key: string) { - let instanceId = localStorage.getItem(`license_instance_id:${key}`) - if (!instanceId) { - instanceId = await activateLicense(key, getInstanceName()) - localStorage.setItem(`license_instance_id:${key}`, instanceId) - } - return validateLicense(key, instanceId) +export async function activatePremium(licenseKey: string): Promise { + const instanceId = await lemonsqueezy.activateLicense(licenseKey, getInstanceName()) + const data = { licenseKey, instanceId } + localStorage.setItem('premium', JSON.stringify(data)) + return data } -export async function deactivateLicenseKey(key: string) { - const instanceId = localStorage.getItem(`license_instance_id:${key}`) - if (!instanceId) { - return +export async function validatePremium() { + const activation = getPremiumActivation() + if (!activation) { + return { valid: false } } - await deactivateLicense(key, instanceId) - localStorage.removeItem(`license_instance_id:${key}`) + return lemonsqueezy.validateLicense(activation.licenseKey, activation.instanceId) } -export function getLicenseInstanceId(key: string) { - return localStorage.getItem(`license_instance_id:${key}`) +export async function deactivatePremium() { + const activation = getPremiumActivation() + if (!activation) { + return + } + await lemonsqueezy.deactivateLicense(activation.licenseKey, activation.instanceId) + localStorage.removeItem('premium') } -export function clearLicenseInstances() { - for (const k of Object.keys(localStorage)) { - if (k.startsWith('license_instance_id:')) { - localStorage.removeItem(k) - } +export function getPremiumActivation(): PremiumActivation | null { + const data = localStorage.getItem('premium') + if (data) { + return JSON.parse(data) + } + // Migrate old storage + const key = localStorage.getItem('licenseKey') + if (!key) { + return null + } + const licenseKey: string = JSON.parse(key) + const instanceId = localStorage.getItem(`license_instance_id:${licenseKey}`) + if (!instanceId) { + localStorage.removeItem('licenseKey') + return null } + const d = { licenseKey, instanceId } + localStorage.setItem('premium', JSON.stringify(d)) + localStorage.removeItem('licenseKey') + localStorage.removeItem(`license_instance_id:${licenseKey}`) + return d } diff --git a/src/services/server-api.ts b/src/services/server-api.ts index a28c4626..810ceb9f 100644 --- a/src/services/server-api.ts +++ b/src/services/server-api.ts @@ -1,9 +1,9 @@ import { ofetch } from 'ofetch' -export async function decodePoeFormkey(headHtml: string): Promise { +export async function decodePoeFormkey(html: string): Promise { const resp = await ofetch('https://chathub.gg/api/poe/decode-formkey', { method: 'POST', - body: { headHtml }, + body: { html }, }) return resp.formkey } @@ -33,3 +33,27 @@ interface Product { export async function fetchPremiumProduct() { return ofetch('https://chathub.gg/api/premium/product') } + +export async function createDiscount() { + return ofetch<{ code: string; startTime: number }>('https://chathub.gg/api/premium/discount/create', { + method: 'POST', + }) +} + +interface PurchaseInfo { + price: number + discount?: { + code: string + startTime: number + price: number + percent: number + } +} + +export async function fetchPurchaseInfo() { + return ofetch('https://chathub.gg/api/premium/info') +} + +export async function checkDiscount(params: { appOpenTimes: number; premiumModalOpenTimes: number }) { + return ofetch<{ show: boolean }>('https://chathub.gg/api/premium/discount/check', { params }) +} diff --git a/src/services/storage/open-times.ts b/src/services/storage/open-times.ts new file mode 100644 index 00000000..a4e4261a --- /dev/null +++ b/src/services/storage/open-times.ts @@ -0,0 +1,23 @@ +import Browser from 'webextension-polyfill' + +export async function getAppOpenTimes() { + const { openTimes = 0 } = await Browser.storage.sync.get('openTimes') + return openTimes +} + +export async function incrAppOpenTimes() { + const openTimes = await getAppOpenTimes() + Browser.storage.sync.set({ openTimes: openTimes + 1 }) + return openTimes + 1 +} + +export async function getPremiumModalOpenTimes() { + const { premiumModalOpenTimes = 0 } = await Browser.storage.sync.get('premiumModalOpenTimes') + return premiumModalOpenTimes +} + +export async function incrPremiumModalOpenTimes() { + const openTimes = await getPremiumModalOpenTimes() + Browser.storage.sync.set({ premiumModalOpenTimes: openTimes + 1 }) + return openTimes + 1 +} diff --git a/src/services/user-config.ts b/src/services/user-config.ts index 5b69fb09..5a70ea7f 100644 --- a/src/services/user-config.ts +++ b/src/services/user-config.ts @@ -43,9 +43,6 @@ export enum ClaudeMode { export enum ClaudeAPIModel { 'claude-2' = 'claude-2', 'claude-instant-1' = 'claude-instant-v1', - 'claude-1' = 'claude-v1', - 'claude-1-100k' = 'claude-v1-100k', - 'claude-instant-1-100k' = 'claude-instant-v1-100k', } export enum OpenRouterClaudeModel { @@ -95,6 +92,12 @@ export async function getUserConfig(): Promise { } else if (result.chatgptWebappModelName === 'gpt-4-mobile') { result.chatgptWebappModelName = ChatGPTWebModel['GPT-4'] } + if ( + result.claudeApiModel !== ClaudeAPIModel['claude-2'] || + result.claudeApiModel !== ClaudeAPIModel['claude-instant-1'] + ) { + result.claudeApiModel = ClaudeAPIModel['claude-2'] + } return defaults(result, userConfigWithDefaultValue) } diff --git a/src/utils/errors.ts b/src/utils/errors.ts index d38c7998..28b43ef2 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -19,6 +19,8 @@ export enum ErrorCode { CHATGPT_INSUFFICIENT_QUOTA = 'CHATGPT_INSUFFICIENT_QUOTA', CLAUDE_WEB_UNAUTHORIZED = 'CLAUDE_WEB_UNAUTHORIZED', CLAUDE_WEB_UNAVAILABLE = 'CLAUDE_WEB_UNAVAILABLE', + QIANWEN_WEB_UNAUTHORIZED = 'QIANWEN_WEB_UNAUTHORIZED', + BAICHUAN_WEB_UNAUTHORIZED = 'BAICHUAN_WEB_UNAUTHORIZED', } export class ChatError extends Error { diff --git a/tsconfig.json b/tsconfig.json index ae74146b..b93e3efa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,8 +18,8 @@ "baseUrl": ".", "paths": { "~*": ["./src/*"] - } + }, + "types": ["chrome-types"] }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] + "include": ["src", "vite.config.ts", "manifest.config.ts"] } diff --git a/tsconfig.node.json b/tsconfig.node.json deleted file mode 100644 index 943852b9..00000000 --- a/tsconfig.node.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Node", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts", "manifest.config.ts"] -} diff --git a/yarn.lock b/yarn.lock index 452d708f..090b9e15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1189,14 +1189,6 @@ resolved "https://registry.yarnpkg.com/@tanstack/store/-/store-0.0.1-beta.81.tgz#9fbc547bc4d1faaa2758b843d712fe02f76a7e5c" integrity sha512-OYFQ4RmDjSIzJnJrhsvpSpM4+lO/3NBaXGloC5IO3YNm0Ewgy/MSCePJgUamstXPmBAaLC2KEgi4O9FHyzA6lw== -"@types/chrome@^0.0.241": - version "0.0.241" - resolved "https://registry.npmmirror.com/@types/chrome/-/chrome-0.0.241.tgz#c6274ef2377ace4e67713591aacc13d8e6c1dca6" - integrity sha512-3WxC2D8zhyDnCU1GxyznoyUulLH6ReLWUWQm5LSM7S1rvV9w+k8TUNbWrFavk6zz2E1ws05lNawnSa7rK5kY8Q== - dependencies: - "@types/filesystem" "*" - "@types/har-format" "*" - "@types/debug@^4.0.0": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" @@ -1204,23 +1196,6 @@ dependencies: "@types/ms" "*" -"@types/filesystem@*": - version "0.0.32" - resolved "https://registry.npmmirror.com/@types/filesystem/-/filesystem-0.0.32.tgz#307df7cc084a2293c3c1a31151b178063e0a8edf" - integrity sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ== - dependencies: - "@types/filewriter" "*" - -"@types/filewriter@*": - version "0.0.29" - resolved "https://registry.npmmirror.com/@types/filewriter/-/filewriter-0.0.29.tgz#a48795ecadf957f6c0d10e0c34af86c098fa5bee" - integrity sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ== - -"@types/har-format@*": - version "1.2.11" - resolved "https://registry.npmmirror.com/@types/har-format/-/har-format-1.2.11.tgz#26aff34e9c782b2648cc45778abadcd930f7db43" - integrity sha512-T232/TneofqK30AD1LRrrf8KnjLvzrjWDp7eWST5KoiSzrBfRsLrWDPk4STQPW4NZG6v2MltnduBVmakbZOBIQ== - "@types/hast@^2.0.0": version "2.3.4" resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc" @@ -1228,6 +1203,11 @@ dependencies: "@types/unist" "*" +"@types/humanize-duration@^3.27.1": + version "3.27.1" + resolved "https://registry.npmmirror.com/@types/humanize-duration/-/humanize-duration-3.27.1.tgz#f14740d1f585a0a8e3f46359b62fda8b0eaa31e7" + integrity sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w== + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -1352,10 +1332,10 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b" integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ== -"@types/webextension-polyfill@^0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.10.1.tgz#63698f0ef78a069d2d307be3caaee5e70c12e09d" - integrity sha512-Sdg+E2F5JUbhkE1qX15QUxpyhfMFKRGJqND9nb1C0gNN4NR7kCV31/1GvNbg6Xe+m/JElJ9/lG5kepMzjGPuQw== +"@types/webextension-polyfill@^0.10.2": + version "0.10.2" + resolved "https://registry.npmmirror.com/@types/webextension-polyfill/-/webextension-polyfill-0.10.2.tgz#14175d96d76339fce4d2b9b7837846148296cb5a" + integrity sha512-L+T72DVTi/1azY6gw7PRmL49kvobLKQQWfOmHDBRSCkAvaCV4wk222J3ZODYA9Gf/UqUvtItu8FPJgWX5ktO9g== "@typescript-eslint/eslint-plugin@^5.60.1": version "5.60.1" @@ -1778,6 +1758,11 @@ chnl@^1.2.0: optionalDependencies: fsevents "~2.3.2" +chrome-types@^0.1.231: + version "0.1.231" + resolved "https://registry.npmmirror.com/chrome-types/-/chrome-types-0.1.231.tgz#be8730c7c43e1e2d2e27691ea4a664b440b35e5d" + integrity sha512-h3bavs+tROAeGE2pLJckJNQzFU5HN2yYEKEG+2byq1+MWrycBYxklCl6gRaI3Jjeyn1DzTTrFToOvdRCQfc/kg== + classnames@2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" @@ -1950,6 +1935,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== +dayjs@^1.11.9: + version "1.11.9" + resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" + integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== + debug@^2.0.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2842,6 +2832,11 @@ htmlparser2@^8.0.1: domutils "^3.0.1" entities "^4.3.0" +humanize-duration@^3.29.0: + version "3.29.0" + resolved "https://registry.npmmirror.com/humanize-duration/-/humanize-duration-3.29.0.tgz#beffaf7938388cd0f38c494f8970d6faebecf3c0" + integrity sha512-G5wZGwYTLaQAmYqhfK91aw3xt6wNbJW1RnWDh4qP1PvF4T/jnkjx2RVhG5kzB2PGsYGTn+oSDBQp+dMdILLxcg== + hyphenate-style-name@^1.0.3: version "1.0.4" resolved "https://registry.npmmirror.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" @@ -3948,6 +3943,11 @@ nanoid@^3.3.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^4.0.2: + version "4.0.2" + resolved "https://registry.npmmirror.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e" + integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -5362,7 +5362,7 @@ web-namespaces@^2.0.0: webextension-polyfill@^0.10.0: version "0.10.0" - resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8" + resolved "https://registry.npmmirror.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8" integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g== websocket-as-promised@^2.0.1: