diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9e8eff118..51bda84d5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -13,3 +13,4 @@ A clear and concise description of what the bug is. **Desktop (please complete the following information):** - OS: [e.g. Windows] - Browser [e.g. chrome, brave] + - ChatHub Version [you can find it in ChatHub Settings] diff --git a/.github/workflows/close-inactive-issues.yml b/.github/workflows/close-inactive-issues.yml new file mode 100644 index 000000000..d5cd3cf15 --- /dev/null +++ b/.github/workflows/close-inactive-issues.yml @@ -0,0 +1,22 @@ +name: Close inactive issues +on: + schedule: + - cron: "30 1 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v5 + with: + days-before-issue-stale: 30 + days-before-issue-close: 14 + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." + close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." + days-before-pr-stale: -1 + days-before-pr-close: -1 + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..c2632a15a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: Release Workflow + +permissions: + contents: write + +on: + push: + tags: + - 'v*.*.*' + +env: + VITE_PLAUSIBLE_API_HOST: ${{ vars.VITE_PLAUSIBLE_API_HOST }} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Setup yarn + run: corepack enable + + - name: Install dependencies + run: yarn install + + - name: Build + run: yarn build + + - name: Package + uses: vimtor/action-zip@v1.1 + with: + files: dist/ + dest: chathub.zip + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: chathub.zip diff --git a/.gitignore b/.gitignore index 50c8dda2a..704ad2b69 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ logs *.log npm-debug.log* -yarn-debug.log* -yarn-error.log* pnpm-debug.log* lerna-debug.log* @@ -24,3 +22,11 @@ dist-ssr *.sw? .env + +.yarn/* +!.yarn/cache +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..05e796717 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +geeguard.js diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 000000000..3186f3f07 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/README.md b/README.md index e1527c04e..5f093047e 100644 --- a/README.md +++ b/README.md @@ -6,48 +6,25 @@
-### ChatHub is an all-in-one chatbot client - -[![author][author-image]][author-url] -[![license][license-image]][license-url] -[![release][release-image]][release-url] -[![last commit][last-commit-image]][last-commit-url] - -English   |   [Indonesia](README_IN.md)   |   [简体中文](README_ZH-CN.md)   |   [繁體中文](README_ZH-TW.md)   |   [日本語](README_JA.md) - -## - ### Install Get ChatHub for Chromium -Get ChatHub for Microsoft Edge - -## - -[Screenshot](#-screenshot)   |   [Features](#-features)   |   [Supported Bots](#-supported-bots)   |   [Manual Installation](#-manual-installation)   |   [Build from Source](#-build-from-source)   |   [Changelog](#-changelog) - -[author-image]: https://img.shields.io/badge/author-wong2-blue.svg -[author-url]: https://github.com/wong2 -[license-image]: https://img.shields.io/github/license/chathub-dev/chathub?color=blue -[license-url]: https://github.com/chathub-dev/chathub/blob/main/LICENSE -[release-image]: https://img.shields.io/github/v/release/chathub-dev/chathub?color=blue -[release-url]: https://github.com/chathub-dev/chathub/releases/latest -[last-commit-image]: https://img.shields.io/github/last-commit/chathub-dev/chathub?label=last%20commit -[last-commit-url]: https://github.com/chathub-dev/chathub/commits
-## - ## 📷 Screenshot ![Screenshot](screenshots/extension.png?raw=true) -![Screenshot (Dark Mode)](screenshots/dark.png?raw=true) +## 🤝 Sponsors + + + + ## ✨ Features -- 🤖 Use different chatbots in one app, currently supporting ChatGPT, new Bing Chat, Google Bard, Claude, and 10+ open-source models including Alpaca, Vicuna, ChatGLM etc +- 🤖 Use different chatbots in one app, currently supporting ChatGPT, new Bing Chat, Google Bard, Claude, and open-source models including LLama2, Vicuna, ChatGLM etc - 💬 Chat with multiple chatbots at the same time, making it easy to compare their answers - 🚀 Support ChatGPT API and GPT-4 Browsing - 🔍 Shortcut to quickly activate the app anywhere in the browser @@ -57,148 +34,30 @@ English   |   [Indonesia](README_IN.md)   | & - 📥 Export and Import all your data - 🔗 Share conversation to markdown - 🌙 Dark mode +- 🌐 Web access ## 🤖 Supported Bots -* ChatGPT (via Webapp/API/Azure/Poe) -* Bing Chat -* Google Bard -* Claude (via Poe) -* iFlytek Spark -* ChatGLM -* Alpaca -* Vicuna -* Koala -* Dolly -* LLaMA -* StableLM -* OpenAssistant -* ChatRWKV -* ... - -## 🔧 Manual Installation - -- Download chathub.zip from [Releases](https://github.com/chathub-dev/chathub/releases) -- Unzip the file -- In Chrome/Edge go to the extensions page (chrome://extensions or edge://extensions) -- Enable Developer Mode -- Drag the unzipped folder anywhere on the page to import it (do not delete the folder afterward) +- ChatGPT (via Webapp/API/Azure/Poe) +- Bing Chat +- Google Bard +- Claude 2 (via Webapp/API/Poe) +- LLaMA 2 +- ChatGLM +- Pi by Inflection +- Vicuna +- WizardLM +- iFlytek Spark +- Tongyi Qianwen +- Baichuan +- ... ## 🔨 Build from Source - Clone the source code +- `corepack enable` - `yarn install` - `yarn build` -- Load `dist` folder to browser by following steps in _Manual Installation_ - -## 📜 Changelog - -### v1.22.0 - -- Support Claude API - -### v1.21.0 - -- Add more open-source models - -### v1.20.0 - -- Access from Chrome side panel - -### v1.19.0 - -- Quick access to prompts - -### v1.18.0 - -- Support Alpaca, Vicuna and ChatGLM - -### v1.17.0 - -- Support GPT-4 Browsing model - -### v1.16.5 - -- Add Azure OpenAI service support - -### v1.16.0 - -- Add custom theme setting - -### v1.15.0 - -- Add Xunfei Spark bot - -### v1.14.0 - -- Support more bots in all-in-one mode for premium users - -### v1.12.0 - -- Add premium license - -### v1.11.0 - -- Support Claude (via Poe) - -### v1.10.0 - -- Command + K - -### v1.9.4 - -- Dark mode - -### v1.9.3 - -- Support math formula with katex -- Save community prompt to local - -### v1.9.2 - -- Delete history messages - -### v1.9.0 - -- Share chat as markdown or via sharegpt.com - -### v1.8.0 - -- Import/Export all data -- Edit local prompts -- Switch chatbots for comparison - -### v1.7.0 - -- Add conversation history - -### v1.6.0 - -- Add support for Google Bard - -### v1.5.4 - -- Support GPT-4 model in ChatGPT api mode - -### v1.5.1 - -- Add i18n settings - -### v1.5.0 - -- Support GPT-4 model in ChatGPT Webapp mode - -### v1.4.0 - -- Add Prompt Library - -### v1.3.0 - -- Add copy code button -- Sync chat state between all-in-one and standalone mode -- Allows input while generating answer - -### v1.2.0 - -- Support copy message text -- Improve setting page form element style +- In Chrome/Edge go to the Extensions page (chrome://extensions or edge://extensions) +- Enable Developer Mode +- Drag the `dist` folder anywhere on the page to import it (do not delete the folder afterward) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 0fc3bfdd4..c7718fb5b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -3,6 +3,6 @@ "message": "ChatHub - All-in-one chatbot client" }, "appDesc": { - "message": "All your favourite chatbots in one place" + "message": "Use ChatGPT, Bing, Bard, Claude and more chatbots simultaneously" } } diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 64d273f0b..9def36532 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -3,6 +3,6 @@ "message": "ChatHub - Cliente de chatbot todo en uno" }, "appDesc": { - "message": "Mejora la interfaz de usuario de tus chatbots favoritos" + "message": "Utiliza ChatGPT, Bing, Bard, Claude y más chatbots simultáneamente" } -} \ No newline at end of file +} diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json new file mode 100644 index 000000000..dc0159cfd --- /dev/null +++ b/_locales/pt_BR/messages.json @@ -0,0 +1,8 @@ +{ + "appName": { + "message": "ChatHub - All-in-one chatbot client" + }, + "appDesc": { + "message": "Use o ChatGPT, Bing, Bard, Claude e mais chatbots simultaneamente" + } +} diff --git a/_locales/pt_PT/messages.json b/_locales/pt_PT/messages.json new file mode 100644 index 000000000..dc0159cfd --- /dev/null +++ b/_locales/pt_PT/messages.json @@ -0,0 +1,8 @@ +{ + "appName": { + "message": "ChatHub - All-in-one chatbot client" + }, + "appDesc": { + "message": "Use o ChatGPT, Bing, Bard, Claude e mais chatbots simultaneamente" + } +} diff --git a/manifest.config.ts b/manifest.config.ts index 90a70f090..1d162b91b 100644 --- a/manifest.config.ts +++ b/manifest.config.ts @@ -1,12 +1,12 @@ import { defineManifest } from '@crxjs/vite-plugin' -export default defineManifest(async (env) => { +export default defineManifest(async () => { return { manifest_version: 3, name: '__MSG_appName__', description: '__MSG_appDesc__', default_locale: 'en', - version: '1.26.1', + version: '1.45.7', icons: { '16': 'src/assets/icon.png', '32': 'src/assets/icon.png', @@ -23,9 +23,13 @@ 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://*/*'], - permissions: ['storage', 'unlimitedStorage', 'sidePanel'], + optional_host_permissions: ['https://*/*', 'wss://*/*'], + permissions: ['storage', 'unlimitedStorage', 'sidePanel', 'declarativeNetRequestWithHostAccess', 'scripting'], content_scripts: [ { matches: ['https://chat.openai.com/*'], @@ -46,5 +50,34 @@ export default defineManifest(async (env) => { side_panel: { default_path: 'sidepanel.html', }, + declarative_net_request: { + rule_resources: [ + { + id: 'ruleset_bing', + enabled: true, + path: 'src/rules/bing.json', + }, + { + id: 'ruleset_ddg', + 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', + }, + { + id: 'ruleset_pplx', + enabled: true, + path: 'src/rules/pplx.json', + }, + ], + }, } }) diff --git a/package.json b/package.json index dbd6c45f7..bda8ab1e2 100644 --- a/package.json +++ b/package.json @@ -8,93 +8,107 @@ "build": "tsc && vite build" }, "devDependencies": { - "@crxjs/vite-plugin": "^2.0.0-beta.18", - "@headlessui/tailwindcss": "^0.1.3", - "@types/lodash-es": "^4.17.7", - "@types/md5": "^2.3.2", - "@types/react": "^18.2.7", - "@types/react-color": "^3.0.6", - "@types/react-copy-to-clipboard": "^5.0.4", - "@types/react-dom": "^18.2.4", - "@types/react-scroll-to-bottom": "^4.2.0", - "@types/turndown": "^5.0.1", - "@types/uuid": "^9.0.1", - "@types/webextension-polyfill": "^0.10.0", - "@typescript-eslint/eslint-plugin": "^5.60.1", - "@typescript-eslint/parser": "^5.60.1", - "@vitejs/plugin-react": "^4.0.0", - "autoprefixer": "^10.4.14", - "eslint": "^8.41.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-react": "^7.32.2", + "@crxjs/vite-plugin": "2.0.0-beta.21", + "@headlessui/tailwindcss": "^0.2.0", + "@types/cookie": "^0.6.0", + "@types/humanize-duration": "^3.27.3", + "@types/lodash-es": "^4.17.12", + "@types/md5": "^2.3.5", + "@types/react": "18.2.18", + "@types/react-color": "^3.0.10", + "@types/react-copy-to-clipboard": "^5.0.7", + "@types/react-dom": "^18.2.18", + "@types/react-scroll-to-bottom": "^4.2.4", + "@types/turndown": "^5.0.4", + "@types/uuid": "^9.0.7", + "@types/webextension-polyfill": "^0.10.7", + "@typescript-eslint/eslint-plugin": "^6.15.0", + "@typescript-eslint/parser": "^6.15.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "chrome-types": "^0.1.246", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "postcss": "^8.4.24", + "postcss": "^8.4.32", "postcss-import": "^15.1.0", - "postcss-nesting": "^11.2.2", - "prettier": "^2.8.8", + "postcss-nesting": "^12.0.2", + "prettier": "^3.1.1", "process": "^0.11.10", - "sass": "^1.62.1", - "tailwind-scrollbar": "^3.0.4", - "tailwindcss": "^3.3.2", - "typescript": "^5.0.4", - "vite": "4.2", - "vite-tsconfig-paths": "^4.2.0" + "sass": "^1.69.5", + "tailwind-scrollbar": "^3.0.5", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.3", + "vite": "4.5.1", + "vite-tsconfig-paths": "^4.2.2" }, "dependencies": { - "@floating-ui/react": "^0.24.2", - "@headlessui/react": "^1.7.14", - "@heroicons/react": "^2.0.18", - "@radix-ui/react-dialog": "^1.0.4", - "@radix-ui/react-tooltip": "^1.0.6", - "@sentry/integrations": "^7.54.0", - "@sentry/react": "^7.54.0", - "@tanstack/react-router": "^0.0.1-beta.83", - "browser-fs-access": "^0.34.1", - "classnames": "^2.3.2", - "cmdk": "^0.2.0", - "eventsource-parser": "^1.0.0", - "fuse.js": "^6.6.2", - "github-markdown-css": "^5.2.0", + "@epic-web/cachified": "^4.0.0", + "@floating-ui/react": "^0.26.4", + "@google/generative-ai": "^0.1.3", + "@headlessui/react": "^1.7.17", + "@heroicons/react": "^2.1.1", + "@radix-ui/react-tooltip": "^1.0.7", + "@sentry/integrations": "^7.90.0", + "@sentry/react": "^7.90.0", + "@tanstack/react-router": "^1.43.6", + "async-cache-dedupe": "^2.0.0", + "browser-fs-access": "^0.35.0", + "browser-image-compression": "^2.0.2", + "clsx": "^2.0.0", + "compare-versions": "^6.1.0", + "cookie": "^0.6.0", + "dayjs": "^1.11.10", + "eventsource-parser": "^1.1.1", + "framer-motion": "^10.16.16", + "fuse.js": "^7.0.0", + "github-markdown-css": "^5.5.0", "gpt3-tokenizer": "^1.1.5", - "highlight.js": "^11.8.0", - "i18next": "^22.5.0", - "i18next-browser-languagedetector": "^7.0.2", - "immer": "^9.0.19", + "highlight.js": "^11.9.0", + "humanize-duration": "^3.31.0", + "i18next": "^23.7.11", + "i18next-browser-languagedetector": "^7.2.0", + "immer": "^10.0.3", "inter-ui": "^3.19.3", - "jotai": "^2.2.1", + "jotai": "^2.6.0", "jotai-immer": "^0.2.0", "js-base64": "^3.7.5", "lodash-es": "^4.17.21", - "lucide-react": "^0.252.0", "md5": "^2.3.0", - "ofetch": "^1.0.1", + "nanoid": "^5.0.4", + "ofetch": "^1.3.3", "plausible-tracker": "^0.3.8", "react": "^18.2.0", "react-color": "^2.19.3", + "react-confetti-explosion": "^2.1.2", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", - "react-i18next": "^12.3.1", - "react-icons": "^4.9.0", + "react-i18next": "^13.5.0", + "react-icons": "^4.12.0", "react-markdown": "^8.0.7", - "react-node-to-string": "^0.1.1", + "react-node-to-string": "^0.1.2", "react-scroll-to-bottom": "^4.2.0", "react-spinners": "^0.13.8", - "react-textarea-autosize": "^8.5.0", - "react-viewport-list": "^7.1.1", + "react-textarea-autosize": "^8.5.3", + "react-viewport-list": "^7.1.2", "rehype-highlight": "^6.0.0", - "rehype-stringify": "^9.0.3", + "rehype-stringify": "^9.0.4", "remark-breaks": "^3.0.3", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", "remark-parse": "^10.0.2", "remark-rehype": "^10.1.0", "remark-supersub": "^1.0.0", - "swr": "^2.1.5", + "slashes": "^3.0.12", + "swr": "^2.2.4", + "tailwind-merge": "^2.1.0", "turndown": "^7.1.2", "unified": "^10.1.2", - "uuid": "^9.0.0", + "uuid": "^9.0.1", "webextension-polyfill": "^0.10.0", "websocket-as-promised": "^2.0.1" - } + }, + "packageManager": "yarn@4.0.1" } diff --git a/public/js/v2/35536E1E-65B4-4D96-9D97-6ADB7EFF8147/api.js b/public/js/v2/35536E1E-65B4-4D96-9D97-6ADB7EFF8147/api.js new file mode 100644 index 000000000..dd8aac17f --- /dev/null +++ b/public/js/v2/35536E1E-65B4-4D96-9D97-6ADB7EFF8147/api.js @@ -0,0 +1 @@ +var arkoseLabsClientApi2c145230;!function(){var e={7983:function(e,t){"use strict";t.N=void 0;var n=/^([^\w]*)(javascript|data|vbscript)/im,r=/&#(\w+)(^\w|;)?/g,i=/&tab;/gi,o=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,a=/^.+(:|:)/gim,c=[".","/"];t.N=function(e){var t,s=(t=e||"",(t=t.replace(i," ")).replace(r,(function(e,t){return String.fromCharCode(t)}))).replace(o,"").trim();if(!s)return"about:blank";if(function(e){return c.indexOf(e[0])>-1}(s))return s;var u=s.match(a);if(!u)return s;var l=u[0];return n.test(l)?"about:blank":s}},3940:function(e,t){var n;!function(){"use strict";var r={}.hasOwnProperty;function i(){for(var e=[],t=0;t0?" ".concat(t[5]):""," {")),n+=e(t),r&&(n+="}"),t[2]&&(n+="}"),t[4]&&(n+="}"),n})).join("")},t.i=function(e,n,r,i,o){"string"==typeof e&&(e=[[null,e,void 0]]);var a={};if(r)for(var c=0;c0?" ".concat(l[5]):""," {").concat(l[1],"}")),l[5]=o),n&&(l[2]?(l[1]="@media ".concat(l[2]," {").concat(l[1],"}"),l[2]=n):l[2]=n),i&&(l[4]?(l[1]="@supports (".concat(l[4],") {").concat(l[1],"}"),l[4]=i):l[4]="".concat(i)),t.push(l))}},t}},3835:function(e){"use strict";e.exports=function(e){return e[1]}},913:function(e,t,n){var r,i,o;!function(a,c){"use strict";i=[n(4486)],void 0===(o="function"==typeof(r=function(e){var t=/(^|@)\S+:\d+/,n=/^\s*at .*(\S+:\d+|\(native\))/m,r=/^(eval@)?(\[native code])?$/;return{parse:function(e){if(void 0!==e.stacktrace||void 0!==e["opera#sourceloc"])return this.parseOpera(e);if(e.stack&&e.stack.match(n))return this.parseV8OrIE(e);if(e.stack)return this.parseFFOrSafari(e);throw new Error("Cannot parse given Error object")},extractLocation:function(e){if(-1===e.indexOf(":"))return[e];var t=/(.+?)(?::(\d+))?(?::(\d+))?$/.exec(e.replace(/[()]/g,""));return[t[1],t[2]||void 0,t[3]||void 0]},parseV8OrIE:function(t){return t.stack.split("\n").filter((function(e){return!!e.match(n)}),this).map((function(t){t.indexOf("(eval ")>-1&&(t=t.replace(/eval code/g,"eval").replace(/(\(eval at [^()]*)|(,.*$)/g,""));var n=t.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/^.*?\s+/,""),r=n.match(/ (\(.+\)$)/);n=r?n.replace(r[0],""):n;var i=this.extractLocation(r?r[1]:n),o=r&&n||void 0,a=["eval",""].indexOf(i[0])>-1?void 0:i[0];return new e({functionName:o,fileName:a,lineNumber:i[1],columnNumber:i[2],source:t})}),this)},parseFFOrSafari:function(t){return t.stack.split("\n").filter((function(e){return!e.match(r)}),this).map((function(t){if(t.indexOf(" > eval")>-1&&(t=t.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,":$1")),-1===t.indexOf("@")&&-1===t.indexOf(":"))return new e({functionName:t});var n=/((.*".+"[^@]*)?[^@]*)(?:@)/,r=t.match(n),i=r&&r[1]?r[1]:void 0,o=this.extractLocation(t.replace(n,""));return new e({functionName:i,fileName:o[0],lineNumber:o[1],columnNumber:o[2],source:t})}),this)},parseOpera:function(e){return!e.stacktrace||e.message.indexOf("\n")>-1&&e.message.split("\n").length>e.stacktrace.split("\n").length?this.parseOpera9(e):e.stack?this.parseOpera11(e):this.parseOpera10(e)},parseOpera9:function(t){for(var n=/Line (\d+).*script (?:in )?(\S+)/i,r=t.message.split("\n"),i=[],o=2,a=r.length;o/,"$2").replace(/\([^)]*\)/g,"")||void 0;o.match(/\(([^)]*)\)/)&&(n=o.replace(/^[^(]+\(([^)]*)\)$/,"$1"));var c=void 0===n||"[arguments not available]"===n?void 0:n.split(",");return new e({functionName:a,args:c,fileName:i[0],lineNumber:i[1],columnNumber:i[2],source:t})}),this)}}})?r.apply(t,i):r)||(e.exports=o)}()},2265:function(e){"use strict";var t=Object.prototype.hasOwnProperty,n="~";function r(){}function i(e,t,n){this.fn=e,this.context=t,this.once=n||!1}function o(e,t,r,o,a){if("function"!=typeof r)throw new TypeError("The listener must be a function");var c=new i(r,o||e,a),s=n?n+t:t;return e._events[s]?e._events[s].fn?e._events[s]=[e._events[s],c]:e._events[s].push(c):(e._events[s]=c,e._eventsCount++),e}function a(e,t){0==--e._eventsCount?e._events=new r:delete e._events[t]}function c(){this._events=new r,this._eventsCount=0}Object.create&&(r.prototype=Object.create(null),(new r).__proto__||(n=!1)),c.prototype.eventNames=function(){var e,r,i=[];if(0===this._eventsCount)return i;for(r in e=this._events)t.call(e,r)&&i.push(n?r.slice(1):r);return Object.getOwnPropertySymbols?i.concat(Object.getOwnPropertySymbols(e)):i},c.prototype.listeners=function(e){var t=n?n+e:e,r=this._events[t];if(!r)return[];if(r.fn)return[r.fn];for(var i=0,o=r.length,a=new Array(o);i-1},_e.prototype.set=function(e,t){var n=this.__data__,r=Ie(n,e);return r<0?n.push([e,t]):n[r][1]=t,this},Ae.prototype.clear=function(){this.__data__={hash:new ke,map:new(ve||_e),string:new ke}},Ae.prototype.delete=function(e){return Fe(this,e).delete(e)},Ae.prototype.get=function(e){return Fe(this,e).get(e)},Ae.prototype.has=function(e){return Fe(this,e).has(e)},Ae.prototype.set=function(e,t){return Fe(this,e).set(e,t),this},Te.prototype.clear=function(){this.__data__=new _e},Te.prototype.delete=function(e){return this.__data__.delete(e)},Te.prototype.get=function(e){return this.__data__.get(e)},Te.prototype.has=function(e){return this.__data__.has(e)},Te.prototype.set=function(e,t){var n=this.__data__;if(n instanceof _e){var r=n.__data__;if(!ve||r.length<199)return r.push([e,t]),this;n=this.__data__=new Ae(r)}return n.set(e,t),this};var Me=le?V(le,Object):function(){return[]},qe=function(e){return te.call(e)};function ze(e,t){return!!(t=null==t?i:t)&&("number"==typeof e||I.test(e))&&e>-1&&e%1==0&&e-1&&e%1==0&&e<=i}(e.length)&&!Xe(e)}var Be=fe||function(){return!1};function Xe(e){var t=Ge(e)?te.call(e):"";return t==s||t==u}function Ge(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function Je(e){return We(e)?Pe(e):function(e){if(!He(e))return de(e);var t=[];for(var n in Object(e))ee.call(e,n)&&"constructor"!=n&&t.push(n);return t}(e)}e.exports=function(e){return Re(e,!0,!0)}},4486:function(e,t){var n,r,i;!function(o,a){"use strict";r=[],void 0===(i="function"==typeof(n=function(){function e(e){return!isNaN(parseFloat(e))&&isFinite(e)}function t(e){return e.charAt(0).toUpperCase()+e.substring(1)}function n(e){return function(){return this[e]}}var r=["isConstructor","isEval","isNative","isToplevel"],i=["columnNumber","lineNumber"],o=["fileName","functionName","source"],a=["args"],c=["evalOrigin"],s=r.concat(i,o,a,c);function u(e){if(e)for(var n=0;n0?" ".concat(n.layer):""," {")),r+=n.css,i&&(r+="}"),n.media&&(r+="}"),n.supports&&(r+="}");var o=n.sourceMap;o&&"undefined"!=typeof btoa&&(r+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(o))))," */")),t.styleTagTransform(r,e,t.options)}(t,e,n)},remove:function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(t)}}}},4589:function(e){"use strict";e.exports=function(e,t){if(t.styleSheet)t.styleSheet.cssText=e;else{for(;t.firstChild;)t.removeChild(t.firstChild);t.appendChild(document.createTextNode(e))}}}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var o=t[r]={id:r,loaded:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.loaded=!0,o.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},n.nc=void 0;var r={};!function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(t){var n=function(t,n){if("object"!==e(t)||null===t)return t;var r=t[Symbol.toPrimitive];if(void 0!==r){var i=r.call(t,n||"default");if("object"!==e(i))return i;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===n?String:Number)(t)}(t,"string");return"symbol"===e(n)?n:String(n)}function i(e,n){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:"api",t=function(e){if(document.currentScript)return document.currentScript;var t="enforcement"===e?'script[id="enforcementScript"]':'script[src*="v2"][src*="api.js"][data-callback]',n=document.querySelectorAll(t);if(n&&1===n.length)return n[0];try{throw new Error}catch(e){try{var r=te().parse(e)[0].fileName;return document.querySelector('script[src="'.concat(r,'"]'))}catch(e){return null}}}(e);if(!t)return null;var n=t.src,r={};try{r=function(e){if(!e)throw new Error("Empty URL");var t=e.toLowerCase().split("/v2/").filter((function(e){return""!==e}));if(t.length<2)throw new Error("Invalid Client-API URL");var n=t[0],r=t[1].split("/").filter((function(e){return""!==e}));return{host:n,key:ne(r[0])?r[0].toUpperCase():null,extHost:n}}(n)}catch(e){}if(e===N){var i=window.location.hash;if(i.length>0){var o=("#"===i.charAt(0)?i.substring(1):i).split("&"),a=o[0];r.key=ne(a)?a:r.key,r.id=o[1]}}return r}(),ie=function(e,t){for(var n,r=0;r=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}var ve=function(){return window&&window.crypto&&"function"==typeof window.crypto.getRandomValues?([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(function(e){return(e^crypto.getRandomValues(new Uint8Array(1))[0]&15>>e/4).toString(16)})):"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(e){var t=16*Math.random()|0;return("x"==e?t:3&t|8).toString(16)}))},he=n(2265),ge=n.n(he),me=n(7983);function ye(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function be(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{};try{var n=function(e){return JSON.parse(e)}(e.data),r=n||{},i=r.data,o=r.key,a=r.message,c=r.type,s=Oe(i);if(a&&o===t.config.identifier)return t.emitter.emit(a,s),"broadcast"===c&&t.postMessageToParent({data:s,key:o,message:a}),void("emit"===c&&t.postMessageToChildren({data:s,key:o,message:a}));n&&"FunCaptcha-action"===n.msg&&t.postMessageToChildren({data:Se(Se({},n),n.payload||{})})}catch(n){if(e.data===q)return void t.emitter.emit(q,{});if(e.data===D)return void t.emitter.emit(D,{});if(e.data.msg===M)return void t.emitter.emit(M,{});"string"==typeof e.data&&-1!==e.data.indexOf("key_pressed_")&&t.config.iframePosition===N&&window.parent&&"function"==typeof window.parent.postMessage&&window.parent.postMessage(e.data,"*")}}}return o(e,[{key:"context",set:function(e){this.config.context=e}},{key:"identifier",set:function(e){this.config.identifier=e}},{key:"setup",value:function(e,t){var n,r,i;this.config.identifier!==this.identifier&&(n=window,r=this.config.identifier,(i=n[H])&&i[r]&&(i[r].listener&&window.removeEventListener("message",i[r].listener),i[r].error&&window.removeEventListener("error",i[r].error),delete i[r])),this.config.identifier=e,this.config.iframePosition=t,fe(window,this.config.identifier);var o=window[H][this.config.identifier].listener;o&&window.removeEventListener("message",o),de(window,this.config.identifier,"listener",this.messageListener),window.addEventListener("message",window[H][this.config.identifier].listener)}},{key:"postMessage",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0,n=t.data,r=t.key,i=t.message,o=t.type;if(se(e.postMessage)){var a=Se(Se({},n),{},{data:n,key:r,message:i,type:o});e.postMessage(function(e){return JSON.stringify(e)}(a),this.config.target)}}},{key:"postMessageToChildren",value:function(e){for(var t=e.data,n=e.key,r=e.message,i=document.querySelectorAll("iframe"),o=[],a=0;a1&&void 0!==arguments[1]&&arguments[1],n=Date.now();Te||(Te=n,Pe=n);var r=n-Te,i=n-Pe;Ie&&(t?console.debug("%c".concat(Ce).concat(e,": ").concat(r," since last event - ").concat(i," total time - ").concat(Date.now()),"color: ".concat(t,";")):console.debug("".concat(Ce).concat(e,": ").concat(r," since last event - ").concat(i," total time - ").concat(Date.now()))),Te=n},Me=n(3940),qe=n.n(Me),ze=We;!function(e,t){for(var n=119,r=134,i=122,o=118,a=133,c=131,s=137,u=107,l=113,f=We,d=e();;)try{if(379847===-parseInt(f(n))/1*(-parseInt(f(r))/2)+parseInt(f(i))/3+parseInt(f(o))/4+parseInt(f(a))/5+-parseInt(f(c))/6+parseInt(f(s))/7*(-parseInt(f(u))/8)+-parseInt(f(l))/9)break;d.push(d.shift())}catch(e){d.push(d.shift())}}(Je);var He,$e,Ue=(He=115,$e=!0,function(e,t){var n=$e?function(){if(t){var n=t[We(He)](e,arguments);return t=null,n}}:function(){};return $e=!1,n}),Ve=Ue(void 0,(function(){var e=117,t=121,n=132,r=109,i=We;return Ve[i(e)]()[i(t)](i(n)+i(r))[i(e)]().constructor(Ve)[i(t)]("(((.+)+)+)+$")}));function We(e,t){var n=Je();return We=function(e,t){return n[e-=102]},We(e,t)}Ve();var Be=[ze(135),ze(116)+ze(102)],Xe={};Xe[ze(130)]=!0;var Ge={};function Je(){var e=["operty","98752jXTTXx","hasOwnPr","+)+$","forEach","enabled","eOffset","3115449jUsVhz","call","apply","ECRespon","toString","1311240fFqRvl","63617knDNJe","prototyp","search","1622595aRpzHJ","theme","closeOnE","eButton","keys","observab","optional","hideClos","default","4164720ByzkrJ","(((.+)+)","1499220eaMZBt","18zEdzoa","lightbox","ility","182OLSPTB","sive","length","settings","landscap"];return(Je=function(){return e})()}Ge.default=!1;var Ze={};Ze[ze(124)+"sc"]=Xe,Ze[ze(129)+ze(125)]=Ge;var Ye={};Ye[ze(130)]=!0;var Qe={};Qe[ze(130)]=70;var et={};et[ze(111)]=Ye,et[ze(105)+ze(112)]=Qe;var tt={};tt[ze(130)]={};var nt={optional:!0},rt={};rt[ze(135)]=Ze,rt[ze(116)+ze(102)]=et,rt[ze(127)+ze(136)]=tt,rt.f=nt;var it=rt,ot=function(){var e=123,t=104,n=135,r=116,i=102,o=110,a=126,c=120,s=108,u=106,l=114,f=128,d=110,p=ze,v=arguments[p(103)]>0&&void 0!==arguments[0]?arguments[0]:{},h=v[p(e)],g=void 0===h?null:h,m=v[p(t)]||v,y={};y[p(n)]={},y[p(r)+p(i)]={};var b=y;["lightbox",p(r)+p(i)][p(o)]((function(e){var t=120,n=106,r=114,i=p,o=m[e]||{},a=it[e];Object.keys(a)[i(d)]((function(c){var s=i;Object[s(t)+"e"]["hasOwnPr"+s(n)][s(r)](o,c)?b[e][c]=o[c]:b[e][c]=a[c].default}))})),g&&(b.theme=g);it[p(n)],it[p(r)+p(i)];var w=pe(it,Be);return Object[p(a)](w).forEach((function(e){var t=p;Object[t(c)+"e"][t(s)+t(u)][t(l)](m,e)?b[e]=m[e]:!0!==it[e][t(f)]&&(b[e]=it[e].default)})),b},at=n(3379),ct=n.n(at),st=n(7795),ut=n.n(st),lt=n(569),ft=n.n(lt),dt=n(3565),pt=n.n(dt),vt=n(9216),ht=n.n(vt),gt=n(4589),mt=n.n(gt),yt=n(903),bt={};bt.styleTagTransform=mt(),bt.setAttributes=pt(),bt.insert=ft().bind(null,"head"),bt.domAPI=ut(),bt.insertStyleElement=ht();ct()(yt.Z,bt);var wt=yt.Z&&yt.Z.locals?yt.Z.locals:void 0;function Ot(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}var jt={show:!1,isActive:void 0,element:void 0,frame:void 0,mode:void 0,ECResponsive:!0,enforcementUrl:null},St=function(e,t){e.setAttribute("class",t)},xt=function(){return qe()(wt.container,function(e){for(var t=1;t=parseInt(i,10)&&(s=i),f<=parseInt(r,10)&&(s=r),a&&d>=parseInt(a,10)&&(u=a),o&&d<=parseInt(o,10)&&(u=o)}return s=le(s),{height:u=le(u),width:s}}({width:t,height:n,minWidth:r,maxWidth:o,minHeight:i,maxHeight:a,landscapeOffset:jt.ECResponsive.landscapeOffset||0});u=l.width,s=l.height}var f=!1;t&&t!==jt.frame.style.width&&(jt.frame.style.width=t,f=!0),n&&n!==jt.frame.style.height&&(jt.frame.style.height=n,f=!0),jt.mode===p&&(r&&r!==jt.frame.style["min-width"]&&(jt.frame.style["min-width"]=r,f=!0),i&&i!==jt.frame.style["min-height"]&&(jt.frame.style["min-height"]=i,f=!0),o&&o!==jt.frame.style["max-width"]&&(jt.frame.style["max-width"]=o,f=!0),a&&a!==jt.frame.style["max-height"]&&(jt.frame.style["max-height"]=a,f=!0)),f&&Ee.emit(A,{width:u,height:s}),document.activeElement!==jt.element&&!1===jt.mode&&jt.frame.focus()}}));var Et=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t={},n=["publicKey","data","isSDK","language","mode","onDataRequest","onCompleted","onHide","onReady","onReset","onResize","onShow","onShown","onSuppress","onError","onWarning","onFailed","onResize","settings","selector","accessibilitySettings","styleTheme","uaTheme","apiLoadTime","enableDirectionalInput","inlineRunOnTrigger"];return Object.keys(e).filter((function(e){return-1!==n.indexOf(e)})).forEach((function(n){t[n]=e[n]})),t};!function(e,t){for(var n=190,r=191,i=203,o=201,a=197,c=208,s=198,u=202,l=193,f=207,d=192,p=204,v=Tt,h=e();;)try{if(134883===parseInt(v(n))/1*(parseInt(v(r))/2)+-parseInt(v(i))/3*(parseInt(v(o))/4)+-parseInt(v(a))/5+-parseInt(v(c))/6+-parseInt(v(s))/7*(-parseInt(v(u))/8)+-parseInt(v(l))/9*(-parseInt(v(f))/10)+-parseInt(v(d))/11*(parseInt(v(p))/12))break;h.push(h.shift())}catch(e){h.push(h.shift())}}(At);var kt=function(){var e=195,t=!0;return function(n,r){var i=t?function(){if(r){var t=r[Tt(e)](n,arguments);return r=null,t}}:function(){};return t=!1,i}}(),_t=kt(void 0,(function(){var e=205,t=194,n=196,r=200,i=189,o=199,a=Tt;return _t[a(200)]()[a(e)](a(t)+a(n))[a(r)]()[a(i)+a(o)](_t).search(a(t)+a(n))}));function At(){var e=["331140bObYZE","3269dePuBo","tor","toString","124700NFjzSD","3256galLZq","3gvmdVa","594444sQfUHh","search","split","38730GmOFGi","1057680axFmxk","construc","3vjqrlw","142958HOfWqr","55qzyzgN","585eyUPFl","(((.+)+)","apply","+)+$"];return(At=function(){return e})()}function Tt(e,t){var n=At();return Tt=function(e,t){return n[e-=189]},Tt(e,t)}_t();!function(e,t){for(var n=459,r=464,i=458,o=461,a=475,c=471,s=454,u=477,l=455,f=476,d=463,p=Rt,v=e();;)try{if(217126===parseInt(p(n))/1+-parseInt(p(r))/2+parseInt(p(i))/3*(parseInt(p(o))/4)+parseInt(p(a))/5*(parseInt(p(c))/6)+-parseInt(p(s))/7*(parseInt(p(u))/8)+-parseInt(p(l))/9+parseInt(p(f))/10*(parseInt(p(d))/11))break;v.push(v.shift())}catch(e){v.push(v.shift())}}(It);var Pt=function(){var e=479,t=!0;return function(n,r){var i=t?function(){if(r){var t=r[Rt(e)](n,arguments);return r=null,t}}:function(){};return t=!1,i}}(),Ct=Pt(void 0,(function(){var e=469,t=466,n=457,r=462,i=460,o=473,a=466,c=457,s=Rt;return Ct[s(462)]()[s(e)](s(t)+s(n))[s(r)]()[s(i)+s(o)](Ct)[s(e)](s(a)+s(c))}));function It(){var e=["724465bXqSUS","322020jhTTBg","118424nMYndJ","nOnTrigg","apply","77wyYzHF","1267956rJKgkn","location","+)+$","9iclmJq","198561xLEuCT","construc","339236wQBSpJ","toString","66YyrQIj","540500wywSxE","language","(((.+)+)","href","__nightm","search","isSDK","6TpKyDX","inlineRu","tor","are"];return(It=function(){return e})()}function Rt(e,t){var n=It();return Rt=function(e,t){return n[e-=454]},Rt(e,t)}Ct();var Lt,Nt,Dt=function(){var e=456,t=467,n=467,r=Rt;return window[r(e)][r(t)]?function(e){return e||"string"==typeof e?e[Tt(206)]("?")[0]:null}(window[r(e)][r(n)]):null},Ft=function(e){return"boolean"==typeof e?e:null},Kt=function(){var e=474,t=Rt;return!!window[t(468)+t(e)]};function Mt(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function qt(e){for(var t=1;t3&&void 0!==arguments[3]?arguments[3]:5e3,i=t,o=n,a=ve(),s=function(){var e={},t=window.navigator;if(e.platform=t.platform,e.language=t.language,t.connection)try{e.connection={effectiveType:t.connection.effectiveType,rtt:t.connection.rtt,downlink:t.connection.downlink}}catch(e){}return e}(),u={},l={},f=e,d=null,p={},v=null,h=null,g={timerCheckInterval:r},m=!1,y=!1,b=!1,w=!1,O=!1,j=function(){var e;if(w){for(var t=arguments.length,n=new Array(t),r=0;r0&&void 0!==arguments[0]?arguments[0]:{},t=e.timerId,n=e.type;if(!0===g.enabled){var r=t?c({},t,u[t]):u,d=Object.keys(r).reduce((function(e,t){r[t].logged=!0;var n=r[t],i=(n.logged,pe(n,ke));return Ae(Ae({},e),{},c({},t,i))}),{}),m={id:a,publicKey:f,capiVersion:o,mode:h,suppressed:O,device:s,error:p,windowError:l,sessionId:v,timers:d,sampled:n===Re};j("Logging Metrics:",m);try{var y=new XMLHttpRequest;y.open("POST",i),y.send(JSON.stringify(m))}catch(e){}}},x=function(e){return g&&Object.prototype.hasOwnProperty.call(g,"".concat(e,"Threshold"))?g["".concat(e,"Threshold")]:Ne[e]},E=function e(){if(b)return!1;var t=!1;return m&&(Object.keys(u).forEach((function(e){var n=x(e),r=u[e],i=r.diff,o=r.logged,a=r.end;if(0!==n&&!0!==o&&(i&&i>n&&(t=!0,u[e].logged=!0),!i&&!a)){var c=u[e].start,s=Date.now(),l=s-c;l>n&&(u[e].diff=l,u[e].end=s,u[e].logged=!0,t=!0)}})),t&&S()),d=setTimeout(e,g.timerCheckInterval),!0},k=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return Ae(Ae({},{start:null,end:null,diff:null,threshold:null,logged:!1,metrics:{}}),e)},_=function(){return{id:a,publicKey:f,sessionId:v,mode:h,settings:g,device:s,error:p,windowError:l,timers:u,debugEnabled:w}},A=function(){clearTimeout(d)};d=setTimeout(E,g.timerCheckInterval);try{"true"===window.localStorage.getItem("capiDebug")&&(w=!0,window.capiObserver={getValues:_})}catch(e){}return{getValues:_,timerStart:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Date.now(),n=u[e]||{};if(!n.start){var r=x(e);j("".concat(e," started:"),t),u[e]=k(Ae(Ae({},n),{},{start:t,threshold:r}))}},timerEnd:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Date.now(),n=u[e];n&&!n.end&&(n.end=t,n.diff=n.end-n.start,j("".concat(e," ended:"),t,n.diff),b&&S({timerId:e,type:Re}))},timerCheck:E,subTimerStart:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:Date.now(),r=arguments.length>3?arguments[3]:void 0,i=u[e];i||(i=k()),i.end||(i.metrics[t]=Ae({start:n,end:null,diff:null},r&&{info:r}),u[e]=i,j("".concat(e,".").concat(t," started:"),n))},subTimerEnd:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:Date.now(),r=u[e];if(r&&!r.end){var i=r.metrics[t];i&&(i.end=n,i.diff=i.end-i.start,j("".concat(e,".").concat(t," ended:"),n,i.diff))}},cancelIntervalTimer:A,setup:function(e,t){m=!0,g=Ae(Ae({},g),function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return Object.keys(De).reduce((function(t,n){var r=e[n],i=De[n];if("boolean"===i.type)return Ae(Ae({},t),{},c({},n,"boolean"==typeof r?r:i.default));var o="float"===i.type?parseFloat(r,0):parseInt(r,10);return Ae(Ae({},t),{},c({},n,isNaN(o)?i.default:o))}),{})}(e)),h=t,Object.keys(u).forEach((function(e){var t=x(e);u[e].threshold=t}));var n,r=g.samplePercentage;n=r,(b=Math.random()<=n/100)&&A(),j("Session sampled:",b)},setSession:function(e){v=e},logError:function(e){y||(p=e,y=!0,S({type:Le}))},logWindowError:function(e,t,n,r){g&&!0!==g.windowErrorEnabled||(l[e]={message:t,filename:n,stack:r})},debugLog:j,setSuppressed:function(){O=!0},setPublicKey:function(e){f=e,y=!1,p={},["onShown","onComplete"].forEach((function(e){if(u[e]){var t=u[e].threshold||null;u[e]=k({threshold:t})}}))},observabilityTimer:Fe,apiLoadTimerSetup:function(e,t){u[e]=Ae(Ae({},t),{},{logged:!1}),b&&S({timerId:e,type:Re})}}}(zt,"".concat($t).concat("/metrics/ui"),d,5e3);Ut.subTimerStart(U,B);var Vt=function(e){return"arkose-".concat(e,"-wrapper")},Wt={},Bt="onCompleted",Xt="onHide",Gt="onReady",Jt="onReset",Zt="onShow",Yt="onShown",Qt="onSuppress",en="onFailed",tn="onError",nn="onWarning",rn="onResize",on="onDataRequest",an=(c(Lt={},g,Bt),c(Lt,m,Xt),c(Lt,y,Gt),c(Lt,b,Gt),c(Lt,w,Jt),c(Lt,O,Zt),c(Lt,S,Yt),c(Lt,j,Qt),c(Lt,h,en),c(Lt,x,tn),c(Lt,E,nn),c(Lt,k,rn),c(Lt,_,on),Lt);Ke("Set all hooks");var cn=o((function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.completed,r=t.token,i=t.suppressed,o=t.error,c=t.warning,s=t.width,u=t.height,l=t.requested;a(this,e),this.completed=!!n,this.token=r||null,this.suppressed=!!i,this.error=o||null,this.warning=c||null,this.width=s||0,this.height=u||0,this.requested=l||null}));Ke("Instantiated Ark Hook Class");var sn=function(e){var t=document.createElement("div");return t.setAttribute("aria-hidden",!0),t.setAttribute("class",Vt(e||zt)),t},un=function(){var e,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return qt(qt({element:sn(),inactiveElement:null,bodyElement:document.querySelector("body"),savedActiveElement:null,modifiedSiblings:[],challengeLoadedEvents:[],container:null,elements:function(){return document.querySelectorAll(Wt.config.selector)},initialSetupCompleted:!1,enforcementLoaded:!1,enforcementReady:!1,getPublicKeyTimeout:null,isActive:!1,isHidden:!1,isReady:!1,isConfigured:!1,suppressed:!1,isResettingChallenge:!1,lastResetTimestamp:0,isCompleteReset:!1,fpData:null,onReadyEventCheck:[],width:0,height:0,token:null,externalRequested:!1},t),{},{config:qt(qt({},zt?{publicKey:zt}:{}),{},{selector:(e=zt,"[data-".concat(f,'-public-key="').concat(e,'"]')),styleTheme:t.config&&t.config.styleTheme||z,siteData:{location:{...window.location,origin:"https://chat.openai.com"}},apiLoadTime:null,settings:{},accessibilitySettings:{lockFocusToModal:!0}},t.config),events:qt({},t.events)})},ln=function(e){var t=Wt.events[an[e]];if(se(t)){for(var n=arguments.length,r=new Array(n>1?n-1:0),i=1;i0&&void 0!==arguments[0]&&arguments[0],t=Wt,n=t.element,r=t.bodyElement,i=t.container,o=t.events,a=t.lastResetTimestamp,c=t.config;if(c.publicKey){var s=Date.now();if(!(s-a<100)){Wt.lastResetTimestamp=s,Wt.isActive=!1,Wt.completed=!1,Wt.token=null,Wt.isReady=!1,Wt.onReadyEventCheck=[],fn(),r&&o&&(r.removeEventListener("click",o.bodyClicked),window.removeEventListener("keyup",o.escapePressed),Wt.events.bodyClicked=null,Wt.events.escapePressed=null);var u=n;Wt.inactiveElement=u,Wt.element=void 0,Wt.element=sn(c.publicKey),i&&u&&i.contains(u)&&(Ee.emit("enforcement detach"),setTimeout((function(){try{i.removeChild(u)}catch(e){}}),5e3)),Wt=un(l()(Wt)),e||ln(w,new cn(Wt)),yn()}}},pn=function(e){Wt.element.setAttribute("aria-hidden",e)},vn=function(){Ke("Showing enforcement"),Wt.enforcementReady&&!Wt.isActive&&(Ee.emit("trigger show"),Wt.isHidden&&(Wt.isHidden=!1,Wt.isReady&&Ee.emit(P,{token:Wt.token})))},hn=function(){var e=(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}).manual;Wt.isActive=!1,e&&(Wt.isHidden=!0),ln(m,new cn(Wt)),Wt.savedActiveElement&&(Wt.savedActiveElement.focus(),Wt.savedActiveElement=null),ue(Wt,"config.mode")!==p&&function(){for(var e=Wt.modifiedSiblings,t=0;t=0&&n.indexOf(Wt.config.publicKey)>=0){var i=r.stack;Ut.logWindowError("integration",t,n,i)}})),window.addEventListener("error",window[H][e].error)}(e),Ke("Set up window error"),Wt=un({id:e})},wn=function(){var e,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};Wt.initialSetupCompleted=!0;var n=function(e){return e===p?p:"lightbox"}(t.mode||ue(Wt,"config.mode")),r=t.styleTheme||z,i=Wt.isConfigured&&r!==Wt.config.styleTheme;Wt.isConfigured=!0;var o=zt||Wt.config.publicKey||null,a=!1;t.publicKey&&o!==t.publicKey&&(!function(e){Ke("Seting up key"),de(window,Wt.id,"publicKey",e),Ut.setPublicKey(e),Wt.element&&Wt.element.getAttribute&&(Wt.element.getAttribute("class").match(e)||Wt.element.setAttribute("class",Vt(e))),Ke("Set up key")}(t.publicKey),o=t.publicKey,Wt.config.publicKey&&Wt.config.publicKey!==t.publicKey&&(a=!0)),Wt.config=qt(qt(qt(qt({},Wt.config),t),{mode:n}),{},{styleTheme:r,publicKey:o,language:""!==t.language?t.language||Wt.config.language:void 0}),Wt.events=qt(qt({},Wt.events),{},(c(e={},Bt,t[Bt]||Wt.events[Bt]),c(e,en,t[en]||Wt.events[en]),c(e,Xt,t[Xt]||Wt.events[Xt]),c(e,Gt,t[Gt]||Wt.events[Gt]),c(e,Jt,t[Jt]||Wt.events[Jt]),c(e,Zt,t[Zt]||Wt.events[Zt]),c(e,Yt,t[Yt]||Wt.events[Yt]),c(e,Qt,t[Qt]||Wt.events[Qt]),c(e,tn,t[tn]||Wt.events[tn]),c(e,nn,t[nn]||Wt.events[nn]),c(e,rn,t[rn]||Wt.events[rn]),c(e,on,t[on]||Wt.events[on]),e)),Wt.config.pageLevel=function(e){var t,n=465,r=470,i=472,o=478,a=Rt;return{chref:Dt(),clang:null!==(t=e[a(n)])&&void 0!==t?t:null,surl:null,sdk:Ft(e[a(r)])||!1,nm:Kt(),triggeredInline:e[a(i)+a(o)+"er"]||!1}}(Wt.config),Ke("Configured initial state"),Ee.emit(C,Wt.config),Ke("Emitt Config event"),i||a?(Ke("Resetting enforcement"),dn(!0)):(Ke("Call setup mode"),yn()),"lightbox"===n&&(Wt.element.setAttribute("aria-modal",!0),Wt.element.setAttribute("role","dialog"))},On=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.event,n=e.observability;if(Wt.onReadyEventCheck.push(t),n){var r=n.timerId,i=n.subTimerId,o=n.time;Ut.subTimerEnd(r,i,o)}Q[t]&&Ut.subTimerEnd(U,Q[t]);var a=[T,K,R];Ut.subTimerStart(U,G);var c=function(e,t){var n,r,i=[],o=e.length,a=t.length;for(n=0;n0&&void 0!==arguments[0]?arguments[0]:{};Ut.timerStart(U),[J,Y,Z].forEach((function(e){Ut.subTimerStart(U,e)})),wn(Et(e))},getConfig:function(){return l()(Wt.config)},dataResponse:function(e){if(Wt.requested){var t={message:I,data:e,key:Wt.config.publicKey,type:"emit"};Ee.emit(I,t),Wt.requested=null}},reset:function(){dn()},run:vn,version:d},xn=ie.getAttribute("data-callback");Ke("Set up Every function"),Ee.on("show enforcement",(function(){Wt.isReady||(Ut.timerStart(V),Ut.timerStart(W)),Wt.isActive=!0,Wt.savedActiveElement=document.activeElement,ln(O,new cn(Wt)),ue(Wt,"config.mode")!==p&&function(){var e=Wt.bodyElement.children;Wt.modifiedSiblings=[];for(var t=0;t0&&void 0!==arguments[0]?arguments[0]:{};Wt.completed=!0,Wt.token=e.token,Ut.timerEnd(W),ln(g,new cn(Wt)),ue(Wt,"config.mode")!==p&&(Wt.isCompleteReset=!0,dn())})),Ee.on("hide enforcement",hn),Ee.on(A,(function(e){var t=e.width,n=e.height;Wt.width=t,Wt.height=n,ln(k,new cn(Wt))})),Ee.on(T,(function(){Ke("Got enforcement loaded","darkblue"),Wt.enforcementLoaded=!0,On({event:T}),Wt.initialSetupCompleted&&Ee.emit(C,Wt.config)})),Ee.on("challenge suppressed",(function(e){var t=e.token;Wt.isActive=!1,Wt.suppressed=!0,jn({token:t}),Ut.setSuppressed(),Ut.timerEnd(V),ln(j,new cn(Wt))})),Ee.on("data initial",On),Ee.on("settings fp collected",On),Ee.on("challenge token",jn),Ee.on("challenge window error",(function(e){var t=e.message,n=e.source,r=e.stack;Ut.logWindowError("challenge",t,n,r)})),Ee.on(R,(function(e){var t=e.event,n=void 0===t?{}:t,r=e.settings,i=void 0===r?{}:r,o=e.observability;Wt.config.settings=i;var a=function(e){return ue(e,"observability",{})}(Wt.config.settings);Ut.setup(a,Wt.config.mode);var c=ue(Wt,"config.apiLoadTime");c&&Ut.apiLoadTimerSetup($,c),On({event:n,observability:o}),fn()})),Ee.on("challenge fail number limit reached",(function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};Wt.isActive=!1,Wt.isHidden=!0,Wt.token=e.token,ln(h,new cn(Wt),e)})),Ee.on("error",(function(e){var t=qt({source:null},e.error);Wt.error=t,Ut.logError(t),ln(x,new cn(Wt)),hn()})),Ee.on("warning",(function(e){var t=qt({source:null},e.warning);Wt.warning=t,Ut.logError(t),ln(E,new cn(Wt))})),Ee.on("data_request",(function(e){e.sdk&&(Wt.requested=e,ln(_,new cn(Wt)))})),Ee.on(K,On),Ee.on(F,(function(e){var t=e.action,n=e.timerId,r=e.subTimerId,i=e.time,o=e.info,a="".concat(r?"subTimer":"timer").concat("end"===t?"End":"Start"),c=r?[n,r,i,o]:[n,i];Ut[a].apply(Ut,c)})),Ee.on("force reset",(function(){dn()})),Ee.on("redraw challenge",(function(){Wt.element&&(Wt.element.querySelector("iframe").style.display="inline")})),Ke("Set up Every emitter"),xn?(Ke("Attempting callback"),function e(){if(!se(window[xn]))return setTimeout(e,1e3);var t=document.querySelectorAll(".".concat(Vt(zt)));return t&&t.length&&Array.prototype.slice.call(t).forEach((function(e){try{e.parentNode.removeChild(e)}catch(e){}})),Ke("Cleaned up iframes"),bn(),window[xn](Sn)}()):(Ke("Start setup function"),bn())}(),arkoseLabsClientApi2c145230=r}(); \ No newline at end of file diff --git a/screenshots/extension.png b/screenshots/extension.png index 502457cd7..8fa250bac 100644 Binary files a/screenshots/extension.png and b/screenshots/extension.png differ diff --git a/screenshots/stream-logo.jpg b/screenshots/stream-logo.jpg new file mode 100644 index 000000000..8542aa6f3 Binary files /dev/null and b/screenshots/stream-logo.jpg differ diff --git a/src/app/bots/abstract-bot.ts b/src/app/bots/abstract-bot.ts index 9cf8c0fad..6842feb89 100644 --- a/src/app/bots/abstract-bot.ts +++ b/src/app/bots/abstract-bot.ts @@ -1,12 +1,15 @@ import { Sentry } from '~services/sentry' import { ChatError, ErrorCode } from '~utils/errors' +import { streamAsyncIterable } from '~utils/stream-async-iterable' + +export type AnwserPayload = { + text: string +} export type Event = | { type: 'UPDATE_ANSWER' - data: { - text: string - } + data: AnwserPayload } | { type: 'DONE' @@ -16,31 +19,71 @@ export type Event = error: ChatError } -export interface SendMessageParams { +export interface MessageParams { prompt: string - onEvent: (event: Event) => void + rawUserInput?: string + image?: File signal?: AbortSignal } +export interface SendMessageParams extends MessageParams { + onEvent: (event: Event) => void +} + export abstract class AbstractBot { - async sendMessage(params: SendMessageParams) { - try { - await this.doSendMessage(params) - } catch (err) { + public async sendMessage(params: MessageParams) { + return this.doSendMessageGenerator(params) + } + + protected async *doSendMessageGenerator(params: MessageParams) { + const wrapError = (err: unknown) => { + Sentry.captureException(err) if (err instanceof ChatError) { - params.onEvent({ type: 'ERROR', error: err }) - } else if (!params.signal?.aborted) { + return err + } + if (!params.signal?.aborted) { // ignore user abort exception - params.onEvent({ type: 'ERROR', error: new ChatError((err as Error).message, ErrorCode.UNKOWN_ERROR) }) + return new ChatError((err as Error).message, ErrorCode.UNKOWN_ERROR) } - Sentry.captureException(err) } + const stream = new ReadableStream({ + start: (controller) => { + this.doSendMessage({ + prompt: params.prompt, + rawUserInput: params.rawUserInput, + image: params.image, + signal: params.signal, + onEvent(event) { + if (event.type === 'UPDATE_ANSWER') { + controller.enqueue(event.data) + } else if (event.type === 'DONE') { + controller.close() + } else if (event.type === 'ERROR') { + const error = wrapError(event.error) + if (error) { + controller.error(error) + } + } + }, + }).catch((err) => { + const error = wrapError(err) + if (error) { + controller.error(error) + } + }) + }, + }) + yield* streamAsyncIterable(stream) } get name(): string | undefined { return undefined } + get supportsImageInput() { + return false + } + abstract doSendMessage(params: SendMessageParams): Promise abstract resetConversation(): void } @@ -59,18 +102,26 @@ class DummyBot extends AbstractBot { export abstract class AsyncAbstractBot extends AbstractBot { #bot: AbstractBot + #initializeError?: Error constructor() { super() this.#bot = new DummyBot() - this.initializeBot().then((bot) => { - this.#bot = bot - }) + this.initializeBot() + .then((bot) => { + this.#bot = bot + }) + .catch((err) => { + this.#initializeError = err + }) } abstract initializeBot(): Promise doSendMessage(params: SendMessageParams) { + if (this.#bot instanceof DummyBot && this.#initializeError) { + throw this.#initializeError + } return this.#bot.doSendMessage(params) } @@ -81,4 +132,8 @@ export abstract class AsyncAbstractBot extends AbstractBot { get name() { return this.#bot.name } + + get supportsImageInput() { + return this.#bot.supportsImageInput + } } diff --git a/src/app/bots/baichuan/api.ts b/src/app/bots/baichuan/api.ts new file mode 100644 index 000000000..a16d36520 --- /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 000000000..d506145a6 --- /dev/null +++ b/src/app/bots/baichuan/index.ts @@ -0,0 +1,131 @@ +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({ + assistant: {}, + assistant_info: {}, + retry: 3, + type: "input", + 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, + attachments: [] + }, + 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, + wse:true + }, + 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/bard/api.ts b/src/app/bots/bard/api.ts index 46986eae8..27aa64f24 100644 --- a/src/app/bots/bard/api.ts +++ b/src/app/bots/bard/api.ts @@ -8,9 +8,15 @@ function extractFromHTML(variableName: string, html: string) { } export async function fetchRequestParams() { - const html = await ofetch('https://bard.google.com/faq') + const html = await ofetch('https://bard.google.com/', { responseType: 'text' }) + const atValue = extractFromHTML('SNlM0e', html) const blValue = extractFromHTML('cfb2h', html) + + if (!atValue) { + throw new ChatError('There is no logged-in Google account in this browser', ErrorCode.BARD_UNAUTHORIZED) + } + return { atValue, blValue } } @@ -18,7 +24,7 @@ export function parseBardResponse(resp: string) { const data = JSON.parse(resp.split('\n')[3]) const payload = JSON.parse(data[0][2]) if (!payload) { - throw new ChatError('Failed to access Bard', ErrorCode.BARD_EMPTY_RESPONSE) + throw new ChatError('Failed to load bard response', ErrorCode.BARD_EMPTY_RESPONSE) } console.debug('bard response payload', payload) diff --git a/src/app/bots/bard/index.ts b/src/app/bots/bard/index.ts index 4bca018a3..21bf0cffc 100644 --- a/src/app/bots/bard/index.ts +++ b/src/app/bots/bard/index.ts @@ -22,6 +22,21 @@ export class BardBot extends AbstractBot { } } const { requestParams, contextIds } = this.conversationContext + + let imageUrl: string | undefined + if (params.image) { + imageUrl = await this.uploadImage(params.image) + } + + const payload = [ + null, + JSON.stringify([ + [params.prompt, 0, null, imageUrl ? [[[imageUrl, 1], params.image!.name]] : []], + null, + contextIds, + ]), + ] + const resp = await ofetch( 'https://bard.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate', { @@ -34,7 +49,7 @@ export class BardBot extends AbstractBot { }, body: new URLSearchParams({ at: requestParams.atValue!, - 'f.req': JSON.stringify([null, `[[${JSON.stringify(params.prompt)}],null,${JSON.stringify(contextIds)}]`]), + 'f.req': JSON.stringify(payload), }), parseResponse: (txt) => txt, }, @@ -51,4 +66,41 @@ export class BardBot extends AbstractBot { resetConversation() { this.conversationContext = undefined } + + get supportsImageInput() { + return true + } + + private async uploadImage(image: File) { + const headers = { + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'push-id': 'feeds/mcudyrk2a4khkz', + 'x-goog-upload-header-content-length': image.size.toString(), + 'x-goog-upload-protocol': 'resumable', + 'x-tenant-id': 'bard-storage', + } + const resp = await ofetch.raw('https://content-push.googleapis.com/upload/', { + method: 'POST', + headers: { + ...headers, + 'x-goog-upload-command': 'start', + }, + body: new URLSearchParams({ [`File name: ${image.name}`]: '' }), + }) + const uploadUrl = resp.headers.get('x-goog-upload-url') + console.debug('Bard upload url', uploadUrl) + if (!uploadUrl) { + throw new Error('Failed to upload image') + } + const uploadResult = await ofetch(uploadUrl, { + method: 'POST', + headers: { + ...headers, + 'x-goog-upload-command': 'upload, finalize', + 'x-goog-upload-offset': '0', + }, + body: image, + }) + return uploadResult as string + } } diff --git a/src/app/bots/bing/api.ts b/src/app/bots/bing/api.ts index 4d11a2421..7b92cf6ac 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,35 +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) } - if (resp.result.value === 'Forbidden') { - throw new ChatError(message, ErrorCode.BING_FORBIDDEN) - } 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 09ed98985..63f093a4e 100644 --- a/src/app/bots/bing/index.ts +++ b/src/app/bots/bing/index.ts @@ -1,10 +1,13 @@ +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' import { ChatResponseMessage, ConversationInfo, InvocationEventType } from './types' -import { convertMessageToMarkdown, websocketUtils } from './utils' +import { convertMessageToMarkdown, file2base64, websocketUtils } from './utils' const OPTIONS_SETS = [ 'nlu_direct_response_filter', @@ -12,29 +15,48 @@ const OPTIONS_SETS = [ 'disable_emoji_spoken_text', 'responsible_ai_policy_235', 'enablemm', - 'iycapbing', - 'iyxapbing', - 'objopinion', - 'rweasgv2', - 'dagslnv1', 'dv3sugg', 'autosave', - 'iyoloxap', - 'iyoloneutral', + 'glfluxv15', 'clgalileo', - 'gencontentv3', + 'clgalileonsr', + 'mtreasoncls3', + 'sahararespv2', + 'gptvprvc', + 'fluxprod', + 'revimglnk', + 'revimgsrc1', +] + +const SLICE_IDS = [ + '0712newass0', + '0212boptpsc', + 'plgbd2c', + '1113gldcl1s1', + '1201reason', + '124multi2ts0', + 'cacdupereccf', + 'cacmuidarb', + 'cacfrwebt2cf', + 'sswebtop2cf', ] export class BingWebBot extends AbstractBot { private conversationContext?: ConversationInfo - private buildChatRequest(conversation: ConversationInfo, message: string) { - const optionsSets = OPTIONS_SETS + private buildChatRequest(conversation: ConversationInfo, message: string, imageUrl?: string) { + const requestId = uuid() + + const optionsSets = [...OPTIONS_SETS] + let tone = 'Balanced' if (conversation.conversationStyle === BingConversationStyle.Precise) { optionsSets.push('h3precise') + tone = 'Precise' } else if (conversation.conversationStyle === BingConversationStyle.Creative) { optionsSets.push('h3imaginative') + tone = 'Creative' } + return { arguments: [ { @@ -49,37 +71,27 @@ 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: [], + conversationHistoryOptionsSets: ['autosave', 'savemem', 'uprofupd', 'uprofgen'], 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 }, + tone, }, ], invocationId: conversation.invocationId.toString(), @@ -89,11 +101,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, @@ -102,7 +118,12 @@ export class BingWebBot extends AbstractBot { const conversation = this.conversationContext! - const wsp = new WebSocketAsPromised('wss://sydney.bing.com/sydney/ChatHub', { + let imageUrl: string | undefined + if (params.image) { + imageUrl = await this.uploadImage(params.image) + } + + const wsp = new WebSocketAsPromised(this.buildWssUrl(conversation.encryptedConversationSignature), { packMessage: websocketUtils.packMessage, unpackMessage: websocketUtils.unpackMessage, }) @@ -114,7 +135,7 @@ export class BingWebBot extends AbstractBot { console.debug('bing ws event', event) if (JSON.stringify(event) === '{}') { wsp.sendPacked({ type: 6 }) - wsp.sendPacked(this.buildChatRequest(conversation, params.prompt)) + wsp.sendPacked(this.buildChatRequest(conversation, params.prompt, imageUrl)) conversation.invocationId += 1 } else if (event.type === 6) { wsp.sendPacked({ type: 6 }) @@ -132,11 +153,23 @@ export class BingWebBot extends AbstractBot { } else if (event.type === 2) { const messages = event.item.messages as ChatResponseMessage[] | undefined if (!messages) { + if (event.item.result.value === 'UnauthorizedRequest') { + this.conversationContext = undefined + params.onEvent({ + type: 'ERROR', + error: new ChatError('UnauthorizedRequest', ErrorCode.BING_UNAUTHORIZED), + }) + return + } + const captcha = event.item.result.value === 'CaptchaChallenge' + if (captcha) { + this.conversationContext = undefined + } params.onEvent({ type: 'ERROR', error: new ChatError( event.item.result.error || 'Unknown error', - event.item.result.value === 'CaptchaChallenge' ? ErrorCode.BING_CAPTCHA : ErrorCode.UNKOWN_ERROR, + captcha ? ErrorCode.BING_CAPTCHA : ErrorCode.UNKOWN_ERROR, ), }) return @@ -176,11 +209,54 @@ export class BingWebBot extends AbstractBot { wsp.close() }) - await wsp.open() + try { + await wsp.open() + } catch (err) { + wsp.removeAllListeners() + throw new ChatError((err as Error).message, ErrorCode.NETWORK_ERROR) + } + wsp.sendPacked({ protocol: 'json', version: 1 }) } resetConversation() { this.conversationContext = undefined } + + get supportsImageInput() { + return true + } + + private async uploadImage(image: File) { + const formData = new FormData() + formData.append( + 'knowledgeRequest', + JSON.stringify({ + imageInfo: {}, + knowledgeRequest: { + invokedSkills: ['ImageById'], + subscriptionId: 'Bing.Chat.Multimodal', + invokedSkillsRequestData: { enableFaceBlur: false }, + convoData: { convoid: '', convotone: 'Balanced' }, + }, + }), + ) + formData.append('imageBase64', await file2base64(image)) + const resp = await ofetch<{ blobId: string }>('https://www.bing.com/images/kblob', { + method: 'POST', + body: formData, + }) + if (!resp.blobId) { + console.debug('kblob response: ', resp) + throw new Error('Failed to upload image') + } + 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 56f5a039b..b80847304 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/bing/utils.ts b/src/app/bots/bing/utils.ts index d06549eb6..310240036 100644 --- a/src/app/bots/bing/utils.ts +++ b/src/app/bots/bing/utils.ts @@ -4,6 +4,9 @@ export function convertMessageToMarkdown(message: ChatResponseMessage): string { if (message.messageType === 'InternalSearchQuery') { return message.text } + if (message.messageType === 'InternalLoaderMessage') { + return `_${message.text}_` + } for (const card of message.adaptiveCards) { for (const block of card.body) { if (block.type === 'TextBlock') { @@ -17,7 +20,7 @@ export function convertMessageToMarkdown(message: ChatResponseMessage): string { const RecordSeparator = String.fromCharCode(30) export const websocketUtils = { - packMessage(data: any) { + packMessage(data: unknown) { return `${JSON.stringify(data)}${RecordSeparator}` }, unpackMessage(data: string | ArrayBuffer | Blob) { @@ -28,3 +31,19 @@ export const websocketUtils = { .map((s) => JSON.parse(s)) }, } + +export async function file2base64(file: File, keepHeader = false): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + if (keepHeader) { + resolve(reader.result as string) + } else { + const base64String = (reader.result as string).replace('data:', '').replace(/^.+,/, '') + resolve(base64String) + } + } + reader.onerror = reject + reader.readAsDataURL(file) + }) +} diff --git a/src/app/bots/chatgpt-api/consts.ts b/src/app/bots/chatgpt-api/consts.ts deleted file mode 100644 index b57fa429f..000000000 --- a/src/app/bots/chatgpt-api/consts.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ChatMessage { - role: 'system' | 'assistant' | 'user' - content: string -} diff --git a/src/app/bots/chatgpt-api/index.ts b/src/app/bots/chatgpt-api/index.ts index a493d25d8..201fbba7d 100644 --- a/src/app/bots/chatgpt-api/index.ts +++ b/src/app/bots/chatgpt-api/index.ts @@ -1,26 +1,41 @@ +import { isArray } from 'lodash-es' import { DEFAULT_CHATGPT_SYSTEM_MESSAGE } from '~app/consts' import { UserConfig } from '~services/user-config' import { ChatError, ErrorCode } from '~utils/errors' import { parseSSEResponse } from '~utils/sse' import { AbstractBot, SendMessageParams } from '../abstract-bot' -import { ChatMessage } from './consts' -import { updateTokenUsage } from './usage' +import { file2base64 } from '../bing/utils' +import { ChatMessage } from './types' interface ConversationContext { messages: ChatMessage[] } -const CONTEXT_SIZE = 10 +const CONTEXT_SIZE = 9 export abstract class AbstractChatGPTApiBot extends AbstractBot { private conversationContext?: ConversationContext - buildMessages(): ChatMessage[] { + private buildUserMessage(prompt: string, imageUrl?: string): ChatMessage { + if (!imageUrl) { + return { role: 'user', content: prompt } + } + return { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl, detail: 'low' } }, + ], + } + } + + private buildMessages(prompt: string, imageUrl?: string): ChatMessage[] { const currentDate = new Date().toISOString().split('T')[0] const systemMessage = this.getSystemMessage().replace('{current_date}', currentDate) return [ { role: 'system', content: systemMessage }, ...this.conversationContext!.messages.slice(-(CONTEXT_SIZE + 1)), + this.buildUserMessage(prompt, imageUrl), ] } @@ -32,9 +47,16 @@ export abstract class AbstractChatGPTApiBot extends AbstractBot { if (!this.conversationContext) { this.conversationContext = { messages: [] } } - this.conversationContext.messages.push({ role: 'user', content: params.prompt }) - const resp = await this.fetchCompletionApi(params.signal) + let imageUrl: string | undefined + if (params.image) { + imageUrl = await file2base64(params.image, true) + } + + const resp = await this.fetchCompletionApi(this.buildMessages(params.prompt, imageUrl), params.signal) + + // add user message to context only after fetch success + this.conversationContext.messages.push(this.buildUserMessage(params.rawUserInput || params.prompt, imageUrl)) let done = false const result: ChatMessage = { role: 'assistant', content: '' } @@ -44,7 +66,6 @@ export abstract class AbstractChatGPTApiBot extends AbstractBot { params.onEvent({ type: 'DONE' }) const messages = this.conversationContext!.messages messages.push(result) - updateTokenUsage(messages).catch(console.error) } await parseSSEResponse(resp, (message) => { @@ -81,7 +102,7 @@ export abstract class AbstractChatGPTApiBot extends AbstractBot { this.conversationContext = undefined } - abstract fetchCompletionApi(signal?: AbortSignal): Promise + abstract fetchCompletionApi(messages: ChatMessage[], signal?: AbortSignal): Promise } export class ChatGPTApiBot extends AbstractChatGPTApiBot { @@ -98,8 +119,12 @@ export class ChatGPTApiBot extends AbstractChatGPTApiBot { return this.config.chatgptApiSystemMessage || DEFAULT_CHATGPT_SYSTEM_MESSAGE } - async fetchCompletionApi(signal?: AbortSignal) { - const { openaiApiKey, openaiApiHost, chatgptApiModel } = this.config + async fetchCompletionApi(messages: ChatMessage[], signal?: AbortSignal) { + const { openaiApiKey, openaiApiHost } = this.config + const hasImageInput = messages.some( + (message) => isArray(message.content) && message.content.some((part) => part.type === 'image_url'), + ) + const model = hasImageInput ? 'gpt-4-vision-preview' : this.getModelName() const resp = await fetch(`${openaiApiHost}/v1/chat/completions`, { method: 'POST', signal, @@ -108,14 +133,12 @@ export class ChatGPTApiBot extends AbstractChatGPTApiBot { Authorization: `Bearer ${openaiApiKey}`, }, body: JSON.stringify({ - model: this.getModelName(), - messages: this.buildMessages(), + model, + messages, + max_tokens: hasImageInput ? 500 : undefined, stream: true, }), }) - if (!resp.ok && resp.status === 404 && chatgptApiModel.includes('gpt-4')) { - throw new ChatError(`You don't have API access to ${chatgptApiModel} model`, ErrorCode.GPT4_MODEL_WAITLIST) - } if (!resp.ok) { const error = await resp.text() if (error.includes('insufficient_quota')) { @@ -127,14 +150,11 @@ export class ChatGPTApiBot extends AbstractChatGPTApiBot { private getModelName() { const { chatgptApiModel } = this.config - if (chatgptApiModel === 'gpt-3.5-turbo') { - return 'gpt-3.5-turbo-0613' - } - if (chatgptApiModel === 'gpt-4') { - return 'gpt-4-0613' + if (chatgptApiModel === 'gpt-4-turbo') { + return 'gpt-4-1106-preview' } - if (chatgptApiModel === 'gpt-4-32k') { - return 'gpt-4-32k-0613' + if (chatgptApiModel === 'gpt-3.5-turbo') { + return 'gpt-3.5-turbo-1106' } return chatgptApiModel } @@ -142,4 +162,8 @@ export class ChatGPTApiBot extends AbstractChatGPTApiBot { get name() { return `ChatGPT (API/${this.config.chatgptApiModel})` } + + get supportsImageInput() { + return true + } } diff --git a/src/app/bots/chatgpt-api/types.ts b/src/app/bots/chatgpt-api/types.ts new file mode 100644 index 000000000..bfc3d5584 --- /dev/null +++ b/src/app/bots/chatgpt-api/types.ts @@ -0,0 +1,13 @@ +export type ContentPart = + | { type: 'text'; text: string } + | { type: 'image_url'; image_url: { url: string; detail?: 'low' | 'high' } } + +export type ChatMessage = + | { + role: 'system' | 'assistant' + content: string + } + | { + role: 'user' + content: string | ContentPart[] + } diff --git a/src/app/bots/chatgpt-api/usage.ts b/src/app/bots/chatgpt-api/usage.ts deleted file mode 100644 index b1ca75c5d..000000000 --- a/src/app/bots/chatgpt-api/usage.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { incrTokenUsage } from '~services/storage/token-usage' -import { ChatMessage } from './consts' - -import GPT3Tokenizer from 'gpt3-tokenizer' - -const tokenizer = new GPT3Tokenizer({ type: 'gpt3' }) - -function countTokens(str: string) { - const encoded = tokenizer.encode(str) - return encoded.bpe.length -} - -// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb -function countMessagesTokens(messages: ChatMessage[]) { - let n = 0 - for (const m of messages) { - n += countTokens(m.content) - n += countTokens(m.role) - } - return n + 2 -} - -export async function updateTokenUsage(messages: ChatMessage[]) { - const tokens = countMessagesTokens(messages) - await incrTokenUsage(tokens) -} diff --git a/src/app/bots/chatgpt-azure/index.ts b/src/app/bots/chatgpt-azure/index.ts index 8261dc5d8..193c6a67a 100644 --- a/src/app/bots/chatgpt-azure/index.ts +++ b/src/app/bots/chatgpt-azure/index.ts @@ -1,5 +1,6 @@ import { UserConfig } from '~services/user-config' import { AbstractChatGPTApiBot } from '../chatgpt-api' +import { ChatMessage } from '../chatgpt-api/types' export class ChatGPTAzureApiBot extends AbstractChatGPTApiBot { constructor( @@ -11,8 +12,8 @@ export class ChatGPTAzureApiBot extends AbstractChatGPTApiBot { super() } - async fetchCompletionApi(signal?: AbortSignal) { - const endpoint = `https://${this.config.azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${this.config.azureOpenAIApiDeploymentName}/chat/completions?api-version=2023-03-15-preview` + async fetchCompletionApi(messages: ChatMessage[], signal?: AbortSignal) { + const endpoint = `https://${this.config.azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${this.config.azureOpenAIApiDeploymentName}/chat/completions?api-version=2025-01-01-preview` return fetch(endpoint, { method: 'POST', signal, @@ -21,7 +22,7 @@ export class ChatGPTAzureApiBot extends AbstractChatGPTApiBot { 'api-key': this.config.azureOpenAIApiKey, }, body: JSON.stringify({ - messages: this.buildMessages(), + messages, stream: true, }), }) diff --git a/src/app/bots/chatgpt-webapp/arkose/generator.js b/src/app/bots/chatgpt-webapp/arkose/generator.js new file mode 100644 index 000000000..db31bc0ba --- /dev/null +++ b/src/app/bots/chatgpt-webapp/arkose/generator.js @@ -0,0 +1,59 @@ +import Browser from 'webextension-polyfill' + +class ArkoseTokenGenerator { + constructor() { + this.enforcement = undefined + this.pendingPromises = [] + window.useArkoseSetupEnforcement = this.useArkoseSetupEnforcement.bind(this) + this.injectScript() + } + + useArkoseSetupEnforcement(enforcement) { + this.enforcement = enforcement + enforcement.setConfig({ + onCompleted: (r) => { + console.debug('enforcement.onCompleted', r) + this.pendingPromises.forEach((promise) => { + promise.resolve(r.token) + }) + this.pendingPromises = [] + }, + onReady: () => { + console.debug('enforcement.onReady') + }, + onError: (r) => { + console.debug('enforcement.onError', r) + this.pendingPromises.forEach((promise) => { + promise.reject(new Error('Error generating arkose token')) + }) + }, + onFailed: (r) => { + console.debug('enforcement.onFailed', r) + this.pendingPromises.forEach((promise) => { + promise.reject(new Error('Failed to generate arkose token')) + }) + }, + }) + } + + injectScript() { + const script = document.createElement('script') + script.src = Browser.runtime.getURL('/js/v2/35536E1E-65B4-4D96-9D97-6ADB7EFF8147/api.js') + script.async = true + script.defer = true + script.setAttribute('data-callback', 'useArkoseSetupEnforcement') + document.body.appendChild(script) + } + + async generate() { + if (!this.enforcement) { + return + } + return new Promise((resolve, reject) => { + this.pendingPromises = [{ resolve, reject }] // store only one promise for now. + this.enforcement.run() + }) + } +} + +export const arkoseTokenGenerator = new ArkoseTokenGenerator() diff --git a/src/app/bots/chatgpt-webapp/arkose/index.ts b/src/app/bots/chatgpt-webapp/arkose/index.ts new file mode 100644 index 000000000..f84fa9c2a --- /dev/null +++ b/src/app/bots/chatgpt-webapp/arkose/index.ts @@ -0,0 +1,10 @@ +import { arkoseTokenGenerator } from './generator' +import { fetchArkoseToken } from './server' + +export async function getArkoseToken() { + const token = await arkoseTokenGenerator.generate() + if (token) { + return token + } + return fetchArkoseToken() +} diff --git a/src/app/bots/chatgpt-webapp/arkose.ts b/src/app/bots/chatgpt-webapp/arkose/server.ts similarity index 100% rename from src/app/bots/chatgpt-webapp/arkose.ts rename to src/app/bots/chatgpt-webapp/arkose/server.ts diff --git a/src/app/bots/chatgpt-webapp/client.ts b/src/app/bots/chatgpt-webapp/client.ts index 1d6e77696..18012445f 100644 --- a/src/app/bots/chatgpt-webapp/client.ts +++ b/src/app/bots/chatgpt-webapp/client.ts @@ -1,3 +1,4 @@ +import { ofetch } from 'ofetch' import { RequestInitSubset } from '~types/messaging' import { ChatError, ErrorCode } from '~utils/errors' import { Requester, globalFetchRequester, proxyFetchRequester } from './requesters' @@ -30,12 +31,12 @@ class ChatGPTClient { } const data = await resp.json().catch(() => ({})) if (!data.accessToken) { - throw new ChatError('UNAUTHORIZED', ErrorCode.CHATGPT_UNAUTHORIZED) + throw new ChatError('There is no logged-in ChatGPT account in this browser.', ErrorCode.CHATGPT_UNAUTHORIZED) } return data.accessToken } - private async requestBackendAPIWithToken(token: string, method: string, path: string, data?: unknown) { + private async requestBackendAPIWithToken(token: string, method: 'GET' | 'POST', path: string, data?: unknown) { return this.fetch(`https://chat.openai.com/backend-api${path}`, { method, headers: { @@ -51,6 +52,47 @@ class ChatGPTClient { return resp.models } + async generateChatTitle(token: string, conversationId: string, messageId: string) { + await this.requestBackendAPIWithToken(token, 'POST', `/conversation/gen_title/${conversationId}`, { + message_id: messageId, + }) + } + + async createFileUpload(token: string, file: File): Promise<{ fileId: string; uploadUrl: string }> { + const resp = await this.requestBackendAPIWithToken(token, 'POST', '/files', { + file_name: file.name, + file_size: file.size, + use_case: 'multimodal', + }) + const data = await resp.json() + if (data.status !== 'success') { + throw new Error('Failed to init ChatGPT file upload') + } + return { + fileId: data.file_id, + uploadUrl: data.upload_url, + } + } + + async completeFileUpload(token: string, fileId: string) { + await this.requestBackendAPIWithToken(token, 'POST', `/files/${fileId}/uploaded`, {}) + } + + async uploadFile(token: string, file: File) { + const { fileId, uploadUrl } = await this.createFileUpload(token, file) + await ofetch(uploadUrl, { + method: 'PUT', + body: file, + headers: { + 'x-ms-blob-type': 'BlockBlob', + 'x-ms-version': '2020-04-08', + 'Content-Type': file.type, + }, + }) + await this.completeFileUpload(token, fileId) + return fileId + } + // Switch to proxy mode, or refresh the proxy tab async fixAuthState() { if (this.requester === proxyFetchRequester) { diff --git a/src/app/bots/chatgpt-webapp/index.ts b/src/app/bots/chatgpt-webapp/index.ts index d5e5a7ba0..7560c1f3e 100644 --- a/src/app/bots/chatgpt-webapp/index.ts +++ b/src/app/bots/chatgpt-webapp/index.ts @@ -1,16 +1,35 @@ +import { get as getPath } from 'lodash-es' import { v4 as uuidv4 } from 'uuid' +import { getImageSize } from '~app/utils/image-size' import { ChatGPTWebModel } from '~services/user-config' import { ChatError, ErrorCode } from '~utils/errors' import { parseSSEResponse } from '~utils/sse' import { AbstractBot, SendMessageParams } from '../abstract-bot' -import { fetchArkoseToken } from './arkose' +import { getArkoseToken } from './arkose' import { chatGPTClient } from './client' -import { ResponseContent } from './types' +import { ImageContent, ResponseContent, ResponsePayload } from './types' function removeCitations(text: string) { return text.replaceAll(/\u3010\d+\u2020source\u3011/g, '') } +function parseResponseContent(content: ResponseContent): { text?: string; image?: ImageContent } { + if (content.content_type === 'text') { + return { text: removeCitations(content.parts[0]) } + } + if (content.content_type === 'code') { + return { text: '_' + content.text + '_' } + } + if (content.content_type === 'multimodal_text') { + for (const part of content.parts) { + if (part.content_type === 'image_asset_pointer') { + return { image: part } + } + } + } + return {} +} + interface ConversationContext { conversationId: string lastMessageId: string @@ -31,16 +50,40 @@ export class ChatGPTWebBot extends AbstractBot { return 'text-davinci-002-render-sha' } + private async uploadImage(image: File): Promise { + const fileId = await chatGPTClient.uploadFile(this.accessToken!, image) + const size = await getImageSize(image) + return { + asset_pointer: `file-service://${fileId}`, + width: size.width, + height: size.height, + size_bytes: image.size, + } + } + + private buildMessage(prompt: string, image?: ImageContent) { + return { + id: uuidv4(), + author: { role: 'user' }, + content: image + ? { content_type: 'multimodal_text', parts: [image, prompt] } + : { content_type: 'text', parts: [prompt] }, + } + } + async doSendMessage(params: SendMessageParams) { if (!this.accessToken) { this.accessToken = await chatGPTClient.getAccessToken() } + const modelName = await this.getModelName() console.debug('Using model:', modelName) - let arkoseToken: string | undefined - if (modelName.startsWith('gpt-4')) { - arkoseToken = await fetchArkoseToken() + const arkoseToken = await getArkoseToken() + + let image: ImageContent | undefined = undefined + if (params.image) { + image = await this.uploadImage(params.image) } const resp = await chatGPTClient.fetch('https://chat.openai.com/backend-api/conversation', { @@ -52,58 +95,54 @@ export class ChatGPTWebBot extends AbstractBot { }, body: JSON.stringify({ action: 'next', - messages: [ - { - id: uuidv4(), - author: { role: 'user' }, - content: { - content_type: 'text', - parts: [params.prompt], - }, - }, - ], + messages: [this.buildMessage(params.prompt, image)], model: modelName, conversation_id: this.conversationContext?.conversationId || undefined, parent_message_id: this.conversationContext?.lastMessageId || uuidv4(), arkose_token: arkoseToken, + conversation_mode: { kind: 'primary_assistant' }, }), }) + const isFirstMessage = !this.conversationContext + await parseSSEResponse(resp, (message) => { console.debug('chatgpt sse message', message) if (message === '[DONE]') { params.onEvent({ type: 'DONE' }) return } - let data + let parsed: ResponsePayload | { message: null; error: string } try { - data = JSON.parse(message) + parsed = JSON.parse(message) } catch (err) { console.error(err) return } - const content = data.message?.content as ResponseContent | undefined - if (!content) { + if (!parsed.message && parsed.error) { + params.onEvent({ + type: 'ERROR', + error: new ChatError(parsed.error, ErrorCode.UNKOWN_ERROR), + }) + return + } + + const payload = parsed as ResponsePayload + + const role = getPath(payload, 'message.author.role') + if (role !== 'assistant' && role !== 'tool') { return } - let text: string - if (content.content_type === 'text') { - text = content.parts[0] - text = removeCitations(text) - } else if (content.content_type === 'code') { - text = '_' + content.text + '_' - } else { + + const content = payload.message?.content as ResponseContent | undefined + if (!content) { return } + + const { text } = parseResponseContent(content) if (text) { - this.conversationContext = { - conversationId: data.conversation_id, - lastMessageId: data.message.id, - } - params.onEvent({ - type: 'UPDATE_ANSWER', - data: { text }, - }) + this.conversationContext = { conversationId: payload.conversation_id, lastMessageId: payload.message.id } + params.onEvent({ type: 'UPDATE_ANSWER', data: { text } }) } }).catch((err: Error) => { if (err.message.includes('token_expired')) { @@ -111,6 +150,12 @@ export class ChatGPTWebBot extends AbstractBot { } throw err }) + + // auto generate title on first response + if (isFirstMessage && this.conversationContext) { + const c = this.conversationContext + chatGPTClient.generateChatTitle(this.accessToken, c.conversationId, c.lastMessageId) + } } resetConversation() { @@ -120,4 +165,8 @@ export class ChatGPTWebBot extends AbstractBot { get name() { return `ChatGPT (webapp/${this.model})` } + + get supportsImageInput() { + return true + } } diff --git a/src/app/bots/chatgpt-webapp/requesters.ts b/src/app/bots/chatgpt-webapp/requesters.ts index e7eb73536..05a1a0e9b 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,30 @@ 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!) + return true + } } + 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 +71,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/chatgpt-webapp/types.ts b/src/app/bots/chatgpt-webapp/types.ts index 05f23dead..edb652f4a 100644 --- a/src/app/bots/chatgpt-webapp/types.ts +++ b/src/app/bots/chatgpt-webapp/types.ts @@ -1,3 +1,14 @@ +export type ResponsePayload = { + conversation_id: string + message: { + id: string + author: { role: 'assistant' | 'tool' | 'user' } + content: ResponseContent + recipient: 'all' | string + } + error: null +} + export type ResponseContent = | { content_type: 'text' @@ -11,6 +22,10 @@ export type ResponseContent = content_type: 'tether_browsing_display' result: string } + | { + content_type: 'multimodal_text' + parts: ({ content_type: 'image_asset_pointer' } & ImageContent)[] + } export type ResponseCitation = { start_ix: number @@ -21,3 +36,10 @@ export type ResponseCitation = { text: string } } + +export interface ImageContent { + asset_pointer: string // file-service://file-5JUtfsLd8O0GEZzjtFmWvZr8 + size_bytes: number + width: number + height: number +} diff --git a/src/app/bots/chatgpt/index.ts b/src/app/bots/chatgpt/index.ts index bc9c3a7dd..bbfe5cf5b 100644 --- a/src/app/bots/chatgpt/index.ts +++ b/src/app/bots/chatgpt/index.ts @@ -1,10 +1,12 @@ import { ChatGPTMode, getUserConfig } from '~/services/user-config' +import * as agent from '~services/agent' import { ChatError, ErrorCode } from '~utils/errors' -import { AsyncAbstractBot } from '../abstract-bot' +import { AsyncAbstractBot, MessageParams } from '../abstract-bot' import { ChatGPTApiBot } from '../chatgpt-api' import { ChatGPTAzureApiBot } from '../chatgpt-azure' import { ChatGPTWebBot } from '../chatgpt-webapp' import { PoeWebBot } from '../poe' +import { OpenRouterBot } from '../openrouter' export class ChatGPTBot extends AsyncAbstractBot { async initializeBot() { @@ -34,6 +36,21 @@ export class ChatGPTBot extends AsyncAbstractBot { if (chatgptMode === ChatGPTMode.Poe) { return new PoeWebBot(config.chatgptPoeModelName) } + if (chatgptMode === ChatGPTMode.OpenRouter) { + if (!config.openrouterApiKey) { + throw new ChatError('OpenRouter API key not set', ErrorCode.API_KEY_NOT_SET) + } + const model = `openai/${config.openrouterOpenAIModel}` + return new OpenRouterBot({ apiKey: config.openrouterApiKey, model }) + } return new ChatGPTWebBot(config.chatgptWebappModelName) } + + async sendMessage(params: MessageParams) { + const { chatgptWebAccess } = await getUserConfig() + if (chatgptWebAccess) { + return agent.execute(params.prompt, (prompt) => this.doSendMessageGenerator({ ...params, prompt }), params.signal) + } + return this.doSendMessageGenerator(params) + } } diff --git a/src/app/bots/claude-api/index.ts b/src/app/bots/claude-api/index.ts index 131a25b7c..d556afad5 100644 --- a/src/app/bots/claude-api/index.ts +++ b/src/app/bots/claude-api/index.ts @@ -63,13 +63,9 @@ export class ClaudeApiBot extends AbstractBot { private getModelName() { 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' + return 'claude-instant-1.2' + default: + return 'claude-2.1' } } diff --git a/src/app/bots/claude-web/api.ts b/src/app/bots/claude-web/api.ts new file mode 100644 index 000000000..6f12f5fc2 --- /dev/null +++ b/src/app/bots/claude-web/api.ts @@ -0,0 +1,46 @@ +import { FetchError, ofetch } from 'ofetch' +import { uuid } from '~utils' +import { ChatError, ErrorCode } from '~utils/errors' + +export async function fetchOrganizationId(): Promise { + let resp: Response + try { + resp = await fetch('https://claude.ai/api/organizations', { redirect: 'error', cache: 'no-cache' }) + } catch (err) { + console.error(err) + throw new ChatError('Claude webapp not avaiable in your country', ErrorCode.CLAUDE_WEB_UNAVAILABLE) + } + if (resp.status === 403) { + throw new ChatError('There is no logged-in Claude account in this browser.', ErrorCode.CLAUDE_WEB_UNAUTHORIZED) + } + const orgs = await resp.json() + return orgs[0].uuid +} + +export async function createConversation(organizationId: string): Promise { + const id = uuid() + try { + await ofetch(`https://claude.ai/api/organizations/${organizationId}/chat_conversations`, { + method: 'POST', + body: { name: '', uuid: id }, + }) + } catch (err) { + if (err instanceof FetchError && err.status === 403) { + throw new ChatError('There is no logged-in Claude account in this browser.', ErrorCode.CLAUDE_WEB_UNAUTHORIZED) + } + throw err + } + return id +} + +export async function generateChatTitle(organizationId: string, conversationId: string, content: string) { + await ofetch('https://claude.ai/api/generate_chat_title', { + method: 'POST', + body: { + organization_uuid: organizationId, + conversation_uuid: conversationId, + recent_titles: [], + message_content: content, + }, + }) +} diff --git a/src/app/bots/claude-web/index.ts b/src/app/bots/claude-web/index.ts new file mode 100644 index 000000000..185250fcd --- /dev/null +++ b/src/app/bots/claude-web/index.ts @@ -0,0 +1,88 @@ +import { parseSSEResponse } from '~utils/sse' +import { AbstractBot, SendMessageParams } from '../abstract-bot' +import { createConversation, fetchOrganizationId, generateChatTitle } from './api' +import { requestHostPermission } from '~app/utils/permissions' +import { ChatError, ErrorCode } from '~utils/errors' + +interface ConversationContext { + conversationId: string +} + +export class ClaudeWebBot extends AbstractBot { + private organizationId?: string + private conversationContext?: ConversationContext + private model: string + + constructor() { + super() + this.model = 'claude-2.1' + } + + async doSendMessage(params: SendMessageParams): Promise { + if (!(await requestHostPermission('https://*.claude.ai/'))) { + throw new ChatError('Missing claude.ai permission', ErrorCode.MISSING_HOST_PERMISSION) + } + + if (!this.organizationId) { + this.organizationId = await fetchOrganizationId() + } + + if (!this.conversationContext) { + const conversationId = await createConversation(this.organizationId) + this.conversationContext = { conversationId } + generateChatTitle(this.organizationId, conversationId, params.prompt).catch(console.error) + } + + const resp = await fetch('https://claude.ai/api/append_message', { + method: 'POST', + signal: params.signal, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + organization_uuid: this.organizationId, + conversation_uuid: this.conversationContext.conversationId, + text: params.prompt, + completion: { + prompt: params.prompt, + model: this.model, + }, + attachments: [], + }), + }) + + // different models are available for different accounts + if (!resp.ok && resp.status === 403 && this.model === 'claude-2.1') { + if ((await resp.text()).includes('model_not_allowed')) { + this.model = 'claude-2.0' + return this.doSendMessage(params) + } + } + + let result = '' + + await parseSSEResponse(resp, (message) => { + console.debug('claude sse message', message) + const payload = JSON.parse(message) + if (payload.completion) { + result += payload.completion + params.onEvent({ + type: 'UPDATE_ANSWER', + data: { text: result.trimStart() }, + }) + } else if (payload.error) { + throw new Error(JSON.stringify(payload.error)) + } + }) + + params.onEvent({ type: 'DONE' }) + } + + resetConversation() { + this.conversationContext = undefined + } + + get name() { + return 'Claude (webapp/claude-2)' + } +} diff --git a/src/app/bots/claude/index.ts b/src/app/bots/claude/index.ts index d5406d506..8872863fe 100644 --- a/src/app/bots/claude/index.ts +++ b/src/app/bots/claude/index.ts @@ -1,7 +1,11 @@ import { ClaudeMode, getUserConfig } from '~/services/user-config' -import { AsyncAbstractBot } from '../abstract-bot' +import * as agent from '~services/agent' +import { AsyncAbstractBot, MessageParams } from '../abstract-bot' import { ClaudeApiBot } from '../claude-api' +import { ClaudeWebBot } from '../claude-web' import { PoeWebBot } from '../poe' +import { ChatError, ErrorCode } from '~utils/errors' +import { OpenRouterBot } from '../openrouter' export class ClaudeBot extends AsyncAbstractBot { async initializeBot() { @@ -15,6 +19,24 @@ export class ClaudeBot extends AsyncAbstractBot { claudeApiModel: config.claudeApiModel, }) } + if (claudeMode === ClaudeMode.Webapp) { + return new ClaudeWebBot() + } + if (claudeMode === ClaudeMode.OpenRouter) { + if (!config.openrouterApiKey) { + throw new ChatError('OpenRouter API key not set', ErrorCode.API_KEY_NOT_SET) + } + const model = `anthropic/${config.openrouterClaudeModel}` + return new OpenRouterBot({ apiKey: config.openrouterApiKey, model }) + } return new PoeWebBot(config.poeModel) } + + async sendMessage(params: MessageParams) { + const { claudeWebAccess } = await getUserConfig() + if (claudeWebAccess) { + return agent.execute(params.prompt, (prompt) => this.doSendMessageGenerator({ ...params, prompt }), params.signal) + } + return this.doSendMessageGenerator(params) + } } diff --git a/src/app/bots/gemini-api/index.ts b/src/app/bots/gemini-api/index.ts new file mode 100644 index 000000000..679a5a942 --- /dev/null +++ b/src/app/bots/gemini-api/index.ts @@ -0,0 +1,58 @@ +import { GoogleGenerativeAI, ChatSession } from '@google/generative-ai' +import { AbstractBot, AsyncAbstractBot, SendMessageParams } from '../abstract-bot' +import { getUserConfig } from '~services/user-config' + +interface ConversationContext { + chatSession: ChatSession +} + +export class GeminiApiBot extends AbstractBot { + private conversationContext?: ConversationContext + sdk: GoogleGenerativeAI + + constructor(public apiKey: string) { + super() + this.sdk = new GoogleGenerativeAI(apiKey) + } + + async doSendMessage(params: SendMessageParams) { + if (!this.conversationContext) { + const model = this.sdk.getGenerativeModel({ model: 'gemini-pro' }) + const chatSession = model.startChat() + this.conversationContext = { chatSession } + } + + const result = await this.conversationContext.chatSession.sendMessageStream(params.prompt) + + let text = '' + for await (const chunk of result.stream) { + const chunkText = chunk.text() + console.debug('gemini stream', chunkText) + text += chunkText + params.onEvent({ type: 'UPDATE_ANSWER', data: { text } }) + } + + if (!text) { + params.onEvent({ type: 'UPDATE_ANSWER', data: { text: 'Empty response' } }) + } + params.onEvent({ type: 'DONE' }) + } + + resetConversation() { + this.conversationContext = undefined + } + + get name() { + return 'Gemini Pro' + } +} + +export class GeminiBot extends AsyncAbstractBot { + async initializeBot() { + const { geminiApiKey } = await getUserConfig() + if (!geminiApiKey) { + throw new Error('Gemini API key missing') + } + return new GeminiApiBot(geminiApiKey) + } +} diff --git a/src/app/bots/gradio/index.ts b/src/app/bots/gradio/index.ts new file mode 100644 index 000000000..a9a24b2f4 --- /dev/null +++ b/src/app/bots/gradio/index.ts @@ -0,0 +1,126 @@ +import WebSocketAsPromised from 'websocket-as-promised' +import { ChatError, ErrorCode } from '~utils/errors' +import { AbstractBot, SendMessageParams } from '../abstract-bot' +import { html2md } from '~app/utils/markdown' + +function generateSessionHash() { + // https://stackoverflow.com/a/12502559/325241 + return Math.random().toString(36).substring(2) +} + +enum FnIndex { + Send = 39, + Receive = 40, +} + +interface ConversationContext { + sessionHash: string +} + +export class GradioBot extends AbstractBot { + private conversationContext?: ConversationContext + + constructor( + public wsUrl: string, + public model: string, + public params: number[], + public mode?: 'text' | 'html', + ) { + super() + } + + async doSendMessage(params: SendMessageParams) { + if (!this.conversationContext) { + const sessionHash = await this.createSession(params.signal) + this.conversationContext = { sessionHash } + } + + const sendWsp = await this.connectWebsocket( + FnIndex.Send, + this.conversationContext.sessionHash, + [null, this.model, params.prompt], + params.onEvent, + ) + const receiveWsp = await this.connectWebsocket( + FnIndex.Receive, + this.conversationContext.sessionHash, + [null, ...this.params], + params.onEvent, + ) + + params.signal?.addEventListener('abort', () => { + ;[sendWsp, receiveWsp].forEach((wsp) => { + wsp.removeAllListeners() + wsp.close() + }) + }) + } + + async connectWebsocket(fnIndex: number, sessionHash: string, data: unknown[], onEvent: SendMessageParams['onEvent']) { + const wsp = new WebSocketAsPromised(this.wsUrl, { + packMessage: (data) => JSON.stringify(data), + unpackMessage: (data) => JSON.parse(data as string), + }) + + wsp.onUnpackedMessage.addListener(async (event) => { + if (event.msg === 'send_hash') { + wsp.sendPacked({ fn_index: fnIndex, session_hash: sessionHash }) + } else if (event.msg === 'send_data') { + wsp.sendPacked({ + fn_index: fnIndex, + data, + event_data: null, + session_hash: sessionHash, + }) + } else if (event.msg === 'process_generating') { + if (event.success && event.output.data) { + if (fnIndex === FnIndex.Receive) { + const outputData = event.output.data + if (outputData[1].length > 0) { + const text = outputData[1][outputData[1].length - 1][1] + onEvent({ + type: 'UPDATE_ANSWER', + data: { + text: this.mode === 'html' ? html2md(text) : text, + }, + }) + } + } + } else { + onEvent({ type: 'ERROR', error: new ChatError(event.output.error, ErrorCode.UNKOWN_ERROR) }) + } + } else if (event.msg === 'queue_full') { + onEvent({ type: 'ERROR', error: new ChatError('queue_full', ErrorCode.UNKOWN_ERROR) }) + } else if (event.msg === 'process_completed' && fnIndex === FnIndex.Receive && !event.output.data[1].length) { + onEvent({ + type: 'ERROR', + error: new ChatError('Session has been inactive for too long', ErrorCode.LMSYS_SESSION_EXPIRED), + }) + } + }) + + if (fnIndex === FnIndex.Receive) { + wsp.onClose.addListener(() => { + wsp.removeAllListeners() + onEvent({ type: 'DONE' }) + }) + } + + try { + await wsp.open() + } catch (err) { + console.error('WS open error', err) + throw new ChatError('Failed to establish websocket connection.', ErrorCode.NETWORK_ERROR) + } + + return wsp + } + + resetConversation() { + this.conversationContext = undefined + } + + public async createSession(_signal?: AbortSignal) { + return generateSessionHash() + } +} diff --git a/src/app/bots/grok/index.ts b/src/app/bots/grok/index.ts new file mode 100644 index 000000000..683aec6a0 --- /dev/null +++ b/src/app/bots/grok/index.ts @@ -0,0 +1,152 @@ +import { FetchError, ofetch } from 'ofetch' +import Browser from 'webextension-polyfill' +import { requestHostPermission } from '~app/utils/permissions' +import { ChatError, ErrorCode } from '~utils/errors' +import { streamAsyncIterable } from '~utils/stream-async-iterable' +import { AbstractBot, SendMessageParams } from '../abstract-bot' + +const AUTHORIZATION_VALUE = + 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs=1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' + +interface StreamMessage { + result: { + sender: string + message: string + query: string + } +} + +interface ChatMessage { + sender: 1 | 2 + message: string +} + +interface ConversationContext { + conversationId: string + messages: ChatMessage[] +} + +export class GrokWebBot extends AbstractBot { + private csrfToken?: string + private conversationContext?: ConversationContext + + constructor() { + super() + } + + async doSendMessage(params: SendMessageParams) { + if (!(await requestHostPermission('https://*.twitter.com/'))) { + throw new ChatError('Missing twitter.com permission', ErrorCode.MISSING_HOST_PERMISSION) + } + + if (!this.csrfToken) { + this.csrfToken = await this.readCsrfToken() + } + + if (!this.conversationContext) { + const conversationId = await this.getConversationId() + this.conversationContext = { conversationId, messages: [] } + } + + this.conversationContext.messages.push({ sender: 1, message: params.prompt }) + + const resp = await fetch('https://api.twitter.com/2/grok/add_response.json', { + method: 'POST', + headers: { + Authorization: AUTHORIZATION_VALUE, + 'x-csrf-token': this.csrfToken!, + }, + body: JSON.stringify({ + conversationId: this.conversationContext.conversationId, + responses: this.conversationContext.messages, + systemPromptName: 'fun', + }), + signal: params.signal, + }) + + if (!resp.ok) { + throw new Error(resp.status.toString() + ' ' + (await resp.text())) + } + + const decoder = new TextDecoder() + let result = '' + + for await (const uint8Array of streamAsyncIterable(resp.body!)) { + const str = decoder.decode(uint8Array) + console.debug('grok stream', str) + const lines = str.split('\n') + for (const line of lines) { + if (!line) { + continue + } + const payload: StreamMessage = JSON.parse(line) + if (!payload.result) { + continue + } + if (!result && !payload.result.message && payload.result.query) { + params.onEvent({ type: 'UPDATE_ANSWER', data: { text: '_' + payload.result.query + '_' } }) + } else { + const text = payload.result.message + if (text.startsWith('[link]')) { + // [link](#tweet=1711679181984346515)\n\n==\n\n[link](#tweet=1663711402643845122) + // skip special Twitter card message for now + } else { + result += text + params.onEvent({ type: 'UPDATE_ANSWER', data: { text: result } }) + } + } + } + } + + this.conversationContext.messages.push({ sender: 2, message: result }) + params.onEvent({ type: 'DONE' }) + } + + private async getConversationId(): Promise { + try { + const resp = await ofetch('https://twitter.com/i/api/2/grok/conversation_id.json', { + headers: { + Authorization: AUTHORIZATION_VALUE, + 'x-csrf-token': this.csrfToken!, + }, + }) + return resp.conversationId + } catch (err) { + if (err instanceof FetchError) { + if (err.status === 401) { + throw new ChatError('Grok is only available to Twitter Premium+ subscribers', ErrorCode.GROK_UNAVAILABLE) + } + if (err.status === 451) { + throw new ChatError('Grok is not available in your country', ErrorCode.GROK_UNAVAILABLE) + } + // csrf & cookie mismatch + if (err.status === 403) { + this.csrfToken = await this.readCsrfToken({ refresh: true }) + return this.getConversationId() + } + } + throw err + } + } + + private async readCsrfToken({ refresh }: { refresh?: boolean } = {}): Promise { + const token = await Browser.runtime.sendMessage({ + type: 'read-twitter-csrf-token', + data: { refresh }, + target: 'background', + }) + console.debug('twitter csrf token', token) + if (!token) { + throw new ChatError('There is no logged-in Twitter account in this browser.', ErrorCode.TWITTER_UNAUTHORIZED) + } + return token + } + + resetConversation() { + this.conversationContext = undefined + } + + get name() { + return 'Grok' + } +} diff --git a/src/app/bots/index.ts b/src/app/bots/index.ts index d9673acad..f9983fb3a 100644 --- a/src/app/bots/index.ts +++ b/src/app/bots/index.ts @@ -1,8 +1,14 @@ +import { BaichuanWebBot } from './baichuan' import { BardBot } from './bard' import { BingWebBot } from './bing' import { ChatGPTBot } from './chatgpt' import { ClaudeBot } from './claude' +import { GeminiBot } from './gemini-api' +import { GrokWebBot } from './grok' import { LMSYSBot } from './lmsys' +import { PerplexityBot } from './perplexity' +import { PiBot } from './pi' +import { QianwenWebBot } from './qianwen' import { XunfeiBot } from './xunfei' export type BotId = @@ -10,16 +16,20 @@ export type BotId = | 'bing' | 'bard' | 'claude' + | 'perplexity' | 'xunfei' | 'vicuna' - | 'alpaca' + | 'falcon' + | 'mistral' | 'chatglm' - | 'koala' - | 'dolly' | 'llama' - | 'stablelm' - | 'oasst' - | 'rwkv' + | 'pi' + | 'wizardlm' + | 'qianwen' + | 'baichuan' + | 'yi' + | 'grok' + | 'gemini' export function createBotInstance(botId: BotId) { switch (botId) { @@ -34,23 +44,31 @@ export function createBotInstance(botId: BotId) { case 'xunfei': return new XunfeiBot() case 'vicuna': - return new LMSYSBot('vicuna-13b') - case 'alpaca': - return new LMSYSBot('alpaca-13b') + return new LMSYSBot('vicuna-33b') case 'chatglm': - return new LMSYSBot('chatglm-6b') - case 'koala': - return new LMSYSBot('koala-13b') - case 'dolly': - return new LMSYSBot('dolly-v2-12b') + return new LMSYSBot('chatglm2-6b') case 'llama': - return new LMSYSBot('llama-13b') - case 'stablelm': - return new LMSYSBot('stablelm-tuned-alpha-7b') - case 'oasst': - return new LMSYSBot('oasst-pythia-12b') - case 'rwkv': - return new LMSYSBot('RWKV-4-Raven-14B') + return new LMSYSBot('llama-2-70b-chat') + case 'wizardlm': + return new LMSYSBot('wizardlm-13b') + case 'falcon': + return new LMSYSBot('falcon-180b-chat') + case 'mistral': + return new LMSYSBot('mixtral-8x7b-instruct-v0.1') + case 'yi': + return new LMSYSBot('yi-34b-chat') + case 'pi': + return new PiBot() + case 'qianwen': + return new QianwenWebBot() + case 'baichuan': + return new BaichuanWebBot() + case 'perplexity': + return new PerplexityBot() + case 'grok': + return new GrokWebBot() + case 'gemini': + return new GeminiBot() } } diff --git a/src/app/bots/lmsys/index.ts b/src/app/bots/lmsys/index.ts index c5052581f..a7e3268c0 100644 --- a/src/app/bots/lmsys/index.ts +++ b/src/app/bots/lmsys/index.ts @@ -1,110 +1,46 @@ import WebSocketAsPromised from 'websocket-as-promised' -import { html2md } from '~app/utils/markdown' +import { GradioBot } from '../gradio' import { ChatError, ErrorCode } from '~utils/errors' -import { AbstractBot, SendMessageParams } from '../abstract-bot' -import { generateSessionHash } from './utils' - -enum FnIndex { - Send = 7, - Receive = 8, -} - -interface ConversationContext { - sessionHash: string -} - -export class LMSYSBot extends AbstractBot { - public model: string - private conversationContext?: ConversationContext +export class LMSYSBot extends GradioBot { constructor(model: string) { - super() - this.model = model - } - - async doSendMessage(params: SendMessageParams) { - if (!this.conversationContext) { - this.conversationContext = { sessionHash: generateSessionHash() } - } - - const sendWsp = await this.connectWebsocket( - FnIndex.Send, - this.conversationContext.sessionHash, - [null, this.model, params.prompt], - params.onEvent, - ) - const receiveWsp = await this.connectWebsocket( - FnIndex.Receive, - this.conversationContext.sessionHash, - [null, 0.7, 1, 512], - params.onEvent, - ) - - params.signal?.addEventListener('abort', () => { - ;[sendWsp, receiveWsp].forEach((wsp) => { - wsp.removeAllListeners() - wsp.close() - }) - }) + super('wss://chat.lmsys.org/queue/join', model, [0.7, 1, 512], 'text') } - async connectWebsocket(fnIndex: number, sessionHash: string, data: unknown[], onEvent: SendMessageParams['onEvent']) { - const wsp = new WebSocketAsPromised('wss://chat.lmsys.org/queue/join', { + private async initializeSession( + fnIndex: number, + sessionHash: string, + data: unknown[], + signal?: AbortSignal, + ): Promise { + const wsp = new WebSocketAsPromised(this.wsUrl, { packMessage: (data) => JSON.stringify(data), unpackMessage: (data) => JSON.parse(data as string), }) - - wsp.onUnpackedMessage.addListener(async (event) => { - if (event.msg === 'send_hash') { - wsp.sendPacked({ fn_index: fnIndex, session_hash: sessionHash }) - } else if (event.msg === 'send_data') { - wsp.sendPacked({ - fn_index: fnIndex, - data, - event_data: null, - session_hash: sessionHash, - }) - } else if (event.msg === 'process_generating') { - if (event.success && event.output.data) { - if (fnIndex === FnIndex.Receive) { - const outputData = event.output.data - if (outputData[1].length > 0) { - const html = outputData[1][outputData[1].length - 1][1] - const text = html2md(html) - onEvent({ type: 'UPDATE_ANSWER', data: { text } }) - } - } - } else { - onEvent({ type: 'ERROR', error: new ChatError(event.output.error, ErrorCode.UNKOWN_ERROR) }) + signal?.addEventListener('abort', () => wsp.close()) + return new Promise((resolve, reject) => { + wsp.onUnpackedMessage.addListener((event) => { + if (event.msg === 'send_hash') { + wsp.sendPacked({ fn_index: fnIndex, session_hash: sessionHash }) + } else if (event.msg === 'send_data') { + wsp.sendPacked({ fn_index: fnIndex, data, event_data: null, session_hash: sessionHash }) + } else if (event.msg === 'process_completed') { + resolve() } - } else if (event.msg === 'queue_full') { - onEvent({ type: 'ERROR', error: new ChatError('queue_full', ErrorCode.UNKOWN_ERROR) }) - } else if (event.msg === 'process_completed' && fnIndex === FnIndex.Receive && !event.output.data[1].length) { - onEvent({ - type: 'ERROR', - error: new ChatError('Session has been inactive for too long', ErrorCode.LMSYS_SESSION_EXPIRED), - }) - } - }) - - if (fnIndex === FnIndex.Receive) { - wsp.onClose.addListener(() => { - wsp.removeAllListeners() - onEvent({ type: 'DONE' }) }) - } - - try { - await wsp.open() - } catch (err) { - console.error('lmsys ws open error', err) - throw new ChatError('Failed to establish websocket connection.', ErrorCode.NETWORK_ERROR) - } - - return wsp + wsp.open().catch((err) => { + console.error('lmsys ws open error', err) + reject(new ChatError('Failed to establish websocket connection.', ErrorCode.LMSYS_WS_ERROR)) + }) + }) } - resetConversation() { - this.conversationContext = undefined + async createSession(signal?: AbortSignal) { + const sessionHash = await super.createSession(signal) + await Promise.all([ + this.initializeSession(36, sessionHash, [], signal), + this.initializeSession(43, sessionHash, [{}], signal), + ]) + return sessionHash } } diff --git a/src/app/bots/lmsys/utils.ts b/src/app/bots/lmsys/utils.ts deleted file mode 100644 index 8aa2e98b0..000000000 --- a/src/app/bots/lmsys/utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function generateSessionHash() { - // https://stackoverflow.com/a/12502559/325241 - return Math.random().toString(36).substring(2) -} diff --git a/src/app/bots/openrouter/index.ts b/src/app/bots/openrouter/index.ts new file mode 100644 index 000000000..54c792106 --- /dev/null +++ b/src/app/bots/openrouter/index.ts @@ -0,0 +1,109 @@ +import { requestHostPermission } from '~app/utils/permissions' +import { ChatError, ErrorCode } from '~utils/errors' +import { parseSSEResponse } from '~utils/sse' +import { AbstractBot, SendMessageParams } from '../abstract-bot' + +interface ChatMessage { + role: 'system' | 'assistant' | 'user' + content: string +} + +interface ConversationContext { + messages: ChatMessage[] +} + +const CONTEXT_SIZE = 9 + +export class OpenRouterBot extends AbstractBot { + private conversationContext?: ConversationContext + + constructor(private config: { apiKey: string; model: string }) { + super() + } + + buildMessages(prompt: string): ChatMessage[] { + return [...this.conversationContext!.messages.slice(-(CONTEXT_SIZE + 1)), { role: 'user', content: prompt }] + } + + async doSendMessage(params: SendMessageParams) { + if (!(await requestHostPermission('https://*.openrouter.ai/'))) { + throw new ChatError('Missing openrouter.ai permission', ErrorCode.MISSING_HOST_PERMISSION) + } + + if (!this.conversationContext) { + this.conversationContext = { messages: [] } + } + + const resp = await this.fetchCompletionApi(this.buildMessages(params.prompt), params.signal) + + this.conversationContext.messages.push({ + role: 'user', + content: params.rawUserInput || params.prompt, + }) + + let done = false + const result: ChatMessage = { role: 'assistant', content: '' } + + const finish = () => { + done = true + params.onEvent({ type: 'DONE' }) + const messages = this.conversationContext!.messages + messages.push(result) + } + + await parseSSEResponse(resp, (message) => { + console.debug('openrouter sse message', message) + if (message === '[DONE]') { + finish() + return + } + let data + try { + data = JSON.parse(message) + } catch (err) { + console.error(err) + return + } + if (data?.choices?.length) { + const delta = data.choices[0].delta + if (delta?.content) { + result.content += delta.content + params.onEvent({ + type: 'UPDATE_ANSWER', + data: { text: result.content }, + }) + } + } + }) + + if (!done) { + finish() + } + } + + async fetchCompletionApi(messages: ChatMessage[], signal?: AbortSignal): Promise { + return fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + signal, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.config.apiKey}`, + 'HTTP-Referer': 'https://chathub.gg', + 'X-Title': 'ChatHub', + }, + body: JSON.stringify({ + model: this.config.model, + messages, + stream: true, + }), + }) + } + + resetConversation() { + this.conversationContext = undefined + } + + get name() { + return `OpenRouter/${this.config.model}` + } +} diff --git a/src/app/bots/perplexity-api/api.ts b/src/app/bots/perplexity-api/api.ts new file mode 100644 index 000000000..60957cab8 --- /dev/null +++ b/src/app/bots/perplexity-api/api.ts @@ -0,0 +1,24 @@ +import { ofetch } from 'ofetch' + +async function getSessionId() { + const resp: string = await ofetch('https://labs-api.perplexity.ai/socket.io/?transport=polling&EIO=4') + const data = JSON.parse(resp.slice(1)) + const sessionId: string = data.sid + return sessionId +} + +async function initSession(sessionId: string) { + const resp = await ofetch(`https://labs-api.perplexity.ai/socket.io/?EIO=4&transport=polling&sid=${sessionId}`, { + method: 'POST', + body: '40{"jwt":"anonymous-ask-user"}', + }) + if (resp !== 'OK') { + throw new Error('Failed to init perplexity session') + } +} + +export async function createSession(): Promise { + const sessionId = await getSessionId() + await initSession(sessionId) + return sessionId +} diff --git a/src/app/bots/perplexity-api/index.ts b/src/app/bots/perplexity-api/index.ts new file mode 100644 index 000000000..3bb32b942 --- /dev/null +++ b/src/app/bots/perplexity-api/index.ts @@ -0,0 +1,81 @@ +import { parseSSEResponse } from '~utils/sse' +import { AbstractBot, SendMessageParams } from '../abstract-bot' + +interface ChatMessage { + role: 'user' | 'assistant' + content: string +} + +interface ConversationContext { + messages: ChatMessage[] +} + +export class PerplexityApiBot extends AbstractBot { + private conversationContext?: ConversationContext + + constructor( + public apiKey: string, + public model: string, + ) { + super() + } + + async doSendMessage(params: SendMessageParams) { + if (!this.conversationContext) { + this.conversationContext = { messages: [] } + } + + const message: ChatMessage = { role: 'user', content: params.prompt } + const resp = await this.fetchCompletionApi([...this.conversationContext.messages, message], params.signal) + + // add user message to context only after fetch success + this.conversationContext.messages.push(message) + + let answer: string = '' + + await parseSSEResponse(resp, (message) => { + console.debug('pplx sse message', message) + let data + try { + data = JSON.parse(message) + } catch (err) { + console.error(err) + return + } + if (data?.choices?.length) { + const message = data.choices[0].message + if (message.role === 'assistant' && message.content) { + answer = message.content + params.onEvent({ type: 'UPDATE_ANSWER', data: { text: answer } }) + } + } + }) + + this.conversationContext.messages.push({ role: 'assistant', content: answer }) + params.onEvent({ type: 'DONE' }) + } + + private async fetchCompletionApi(messages: ChatMessage[], signal?: AbortSignal) { + return fetch('https://api.perplexity.ai/chat/completions', { + method: 'POST', + signal, + body: JSON.stringify({ + model: this.model, + messages, + stream: true, + }), + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + }, + }) + } + + resetConversation() { + this.conversationContext = undefined + } + + get name() { + return 'Perplexity (API)' + } +} diff --git a/src/app/bots/perplexity-web/api.ts b/src/app/bots/perplexity-web/api.ts new file mode 100644 index 000000000..69b129e86 --- /dev/null +++ b/src/app/bots/perplexity-web/api.ts @@ -0,0 +1,33 @@ +import { FetchError, ofetch } from 'ofetch' +import { ChatError, ErrorCode } from '~utils/errors' + +async function getSessionId() { + let resp: string + try { + resp = await ofetch('https://labs-api.perplexity.ai/socket.io/?transport=polling&EIO=4') + } catch (err) { + if (err instanceof FetchError && err.status === 403) { + throw new ChatError('Please pass Perplexity security check', ErrorCode.PPLX_FORBIDDEN_ERROR) + } + throw err + } + const data = JSON.parse(resp.slice(1)) + const sessionId: string = data.sid + return sessionId +} + +async function initSession(sessionId: string) { + const resp = await ofetch(`https://labs-api.perplexity.ai/socket.io/?EIO=4&transport=polling&sid=${sessionId}`, { + method: 'POST', + body: '40{"jwt":"anonymous-ask-user"}', + }) + if (resp !== 'OK') { + throw new Error('Failed to init perplexity session') + } +} + +export async function createSession(): Promise { + const sessionId = await getSessionId() + await initSession(sessionId) + return sessionId +} diff --git a/src/app/bots/perplexity-web/index.ts b/src/app/bots/perplexity-web/index.ts new file mode 100644 index 000000000..4720c1ea0 --- /dev/null +++ b/src/app/bots/perplexity-web/index.ts @@ -0,0 +1,98 @@ +import WebSocketAsPromised from 'websocket-as-promised' +import { requestHostPermissions } from '~app/utils/permissions' +import { ChatError, ErrorCode } from '~utils/errors' +import { AbstractBot, SendMessageParams } from '../abstract-bot' +import { createSession } from './api' + +interface ConversationContext { + wsp: WebSocketAsPromised +} + +export class PerplexityLabsBot extends AbstractBot { + private conversationContext?: ConversationContext + + constructor(public model: string) { + super() + } + + private buildMessage(prompt: string) { + const params = [ + 'perplexity_playground', + { + version: '2.1', + source: 'default', + model: this.model, + messages: [{ role: 'user', content: prompt, priority: 0 }], + }, + ] + return `42${JSON.stringify(params)}` + } + + private async setupWebsocket(sessionId: string): Promise { + const wsp = new WebSocketAsPromised( + `wss://labs-api.perplexity.ai/socket.io/?EIO=4&transport=websocket&sid=${sessionId}`, + ) + return new Promise((resolve, reject) => { + wsp.onOpen.addListener(() => { + wsp.send('2probe') + wsp.send('5') + }) + wsp.onMessage.addListener((data: string) => { + if (data === '2') { + wsp.send('3') + } else if (data === '6') { + resolve(wsp) + } + }) + wsp.open().catch(reject) + }) + } + + async doSendMessage(params: SendMessageParams) { + if (!(await requestHostPermissions(['https://*.perplexity.ai/', 'wss://*.perplexity.ai/']))) { + throw new ChatError('Missing perplexity.ai permission', ErrorCode.MISSING_HOST_PERMISSION) + } + + if (!this.conversationContext) { + const sessionId = await createSession() + const wsp = await this.setupWebsocket(sessionId) + this.conversationContext = { wsp } + } + + const { wsp } = this.conversationContext + + const listener = (data: string) => { + console.debug('pplx ws data', data) + if (!data.startsWith('42')) { + return + } + const payload = JSON.parse(data.slice(2)) + if (payload[0] !== 'pplx-70b-online_query_progress') { + return + } + const chunk = payload[1] + if (chunk.output) { + params.onEvent({ type: 'UPDATE_ANSWER', data: { text: chunk.output } }) + } + if (chunk.status === 'completed') { + wsp.onMessage.removeListener(listener) + params.onEvent({ type: 'DONE' }) + } + if (chunk.status === 'failed') { + wsp.onMessage.removeListener(listener) + params.onEvent({ type: 'ERROR', error: new ChatError('failed', ErrorCode.UNKOWN_ERROR) }) + } + } + + wsp.onMessage.addListener(listener) + wsp.send(this.buildMessage(params.prompt)) + } + + resetConversation() { + this.conversationContext = undefined + } + + get name() { + return 'Perplexity (webapp)' + } +} diff --git a/src/app/bots/perplexity/index.ts b/src/app/bots/perplexity/index.ts new file mode 100644 index 000000000..64a535af0 --- /dev/null +++ b/src/app/bots/perplexity/index.ts @@ -0,0 +1,17 @@ +import { PerplexityMode, getUserConfig } from '~/services/user-config' +import { AsyncAbstractBot } from '../abstract-bot' +import { PerplexityApiBot } from '../perplexity-api' +import { PerplexityLabsBot } from '../perplexity-web' + +export class PerplexityBot extends AsyncAbstractBot { + async initializeBot() { + const { perplexityMode, ...config } = await getUserConfig() + if (perplexityMode === PerplexityMode.API) { + if (!config.perplexityApiKey) { + throw new Error('Perplexity API key missing') + } + return new PerplexityApiBot(config.perplexityApiKey, 'pplx-70b-online') + } + return new PerplexityLabsBot('pplx-70b-online') + } +} diff --git a/src/app/bots/pi/index.ts b/src/app/bots/pi/index.ts new file mode 100644 index 000000000..b6b04b097 --- /dev/null +++ b/src/app/bots/pi/index.ts @@ -0,0 +1,46 @@ +import { requestHostPermission } from '~app/utils/permissions' +import { ChatError, ErrorCode } from '~utils/errors' +import { parseSSEResponse } from '~utils/sse' +import { AbstractBot, SendMessageParams } from '../abstract-bot' + +interface ConversationContext { + initialized: boolean +} + +export class PiBot extends AbstractBot { + private conversationContext?: ConversationContext + + async doSendMessage(params: SendMessageParams) { + if (!(await requestHostPermission('https://*.pi.ai/'))) { + throw new ChatError('Missing pi.ai permission', ErrorCode.MISSING_HOST_PERMISSION) + } + + if (!this.conversationContext?.initialized) { + await fetch('https://pi.ai/api/chat/start', { method: 'POST' }) + this.conversationContext = { initialized: true } + } + + const resp = await fetch('https://pi.ai/api/chat', { + method: 'POST', + signal: params.signal, + body: JSON.stringify({ text: params.prompt }), + headers: { + 'Content-Type': 'application/json', + }, + }) + + await parseSSEResponse(resp, (message) => { + console.debug('pi sse', message) + const data = JSON.parse(message) + if (data.text) { + params.onEvent({ type: 'UPDATE_ANSWER', data: { text: data.text } }) + } + }) + + params.onEvent({ type: 'DONE' }) + } + + resetConversation() { + this.conversationContext = undefined + } +} diff --git a/src/app/bots/poe/api.ts b/src/app/bots/poe/api.ts index 617e0ee06..06be24434 100644 --- a/src/app/bots/poe/api.ts +++ b/src/app/bots/poe/api.ts @@ -1,13 +1,14 @@ -import { ofetch } from 'ofetch' import md5 from 'md5' -import ChatViewQuery from './graphql/ChatViewQuery.graphql?raw' +import { ofetch } from 'ofetch' +import i18n from '~app/i18n' +import { decodePoeFormkey } from '~services/server-api' +import { ChatError, ErrorCode } from '~utils/errors' import AddMessageBreakMutation from './graphql/AddMessageBreakMutation.graphql?raw' +import ChatViewQuery from './graphql/ChatViewQuery.graphql?raw' +import MessageAddedSubscription from './graphql/MessageAddedSubscription.graphql?raw' import SendMessageMutation from './graphql/SendMessageMutation.graphql?raw' import SubscriptionsMutation from './graphql/SubscriptionsMutation.graphql?raw' -import MessageAddedSubscription from './graphql/MessageAddedSubscription.graphql?raw' import ViewerStateUpdatedSubscription from './graphql/ViewerStateUpdatedSubscription.graphql?raw' -import { ChatError, ErrorCode } from '~utils/errors' -import i18n from '~app/i18n' export const GRAPHQL_QUERIES = { AddMessageBreakMutation, @@ -35,15 +36,8 @@ interface ChannelData { async function getFormkey() { const html: string = await ofetch('https://poe.com', { parseResponse: (txt) => txt }) - const r = html.match(/