├── scripts ├── .gitignore ├── proxychains.template.conf ├── init-proxy.sh └── delete-deployment-preview.sh ├── app ├── mcp │ ├── mcp_config.default.json │ ├── utils.ts │ ├── client.ts │ └── logger.ts ├── components │ ├── voice-print │ │ ├── index.ts │ │ └── voice-print.module.scss │ ├── realtime-chat │ │ ├── index.ts │ │ └── realtime-chat.module.scss │ ├── sd │ │ ├── index.tsx │ │ ├── sd-panel.module.scss │ │ └── sd.module.scss │ ├── model-config.module.scss │ ├── input-range.module.scss │ ├── artifacts.module.scss │ ├── plugin.module.scss │ ├── input-range.tsx │ ├── settings.module.scss │ ├── message-selector.module.scss │ ├── button.module.scss │ ├── button.tsx │ ├── auth.module.scss │ └── error.tsx ├── icons │ ├── bot.png │ ├── chatgpt.png │ ├── play.svg │ ├── pause.svg │ ├── zoom.svg │ ├── discovery.svg │ ├── arrow.svg │ ├── speak.svg │ ├── speak-stop.svg │ ├── hd.svg │ ├── llm-icons │ │ ├── grok.svg │ │ ├── gemini.svg │ │ ├── mistral.svg │ │ ├── doubao.svg │ │ ├── hunyuan.svg │ │ ├── openai.svg │ │ ├── claude.svg │ │ ├── wenxin.svg │ │ ├── qwen.svg │ │ └── deepseek.svg │ ├── left.svg │ ├── down.svg │ ├── voice.svg │ ├── share.svg │ ├── fire.svg │ ├── size.svg │ ├── bottom.svg │ ├── power.svg │ ├── history.svg │ ├── shortcutkey.svg │ ├── palette.svg │ ├── voice-white.svg │ ├── dark.svg │ ├── close.svg │ ├── send-white.svg │ ├── headphone.svg │ ├── rename.svg │ ├── return.svg │ ├── voice-off.svg │ ├── copy.svg │ ├── settings.svg │ ├── menu.svg │ ├── reload.svg │ ├── add.svg │ ├── three-dots.svg │ ├── sd.svg │ ├── mcp.svg │ ├── export.svg │ ├── eye.svg │ ├── clear.svg │ ├── prompt.svg │ ├── auto.svg │ ├── loading.svg │ ├── chat.svg │ ├── drag.svg │ ├── edit.svg │ ├── download.svg │ ├── logo.svg │ ├── brain.svg │ ├── eye-off.svg │ ├── github.svg │ ├── tool.svg │ ├── max.svg │ ├── min.svg │ ├── mask.svg │ ├── image.svg │ └── confirm.svg ├── store │ └── index.ts ├── masks │ ├── typing.ts │ ├── build.ts │ └── index.ts ├── utils │ ├── clone.ts │ ├── merge.ts │ ├── object.ts │ ├── token.ts │ ├── baidu.ts │ ├── hooks.ts │ ├── auth-settings-events.ts │ ├── cloud │ │ └── index.ts │ ├── format.ts │ ├── cloudflare.ts │ ├── indexedDB-storage.ts │ ├── audio.ts │ └── store.ts ├── styles │ ├── animation.scss │ ├── window.scss │ └── highlight.scss ├── page.tsx ├── typing.ts ├── config │ ├── client.ts │ └── build.ts ├── polyfill.ts ├── api │ ├── azure.ts │ ├── config │ │ └── route.ts │ ├── upstash │ │ └── [action] │ │ │ └── [...key] │ │ │ └── route.ts │ ├── artifacts │ │ └── route.ts │ └── openai.ts ├── client │ └── controller.ts ├── global.d.ts ├── layout.tsx └── command.ts ├── src-tauri ├── build.rs ├── .gitignore ├── icons │ ├── 32x32.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ └── Square310x310Logo.png ├── src │ └── main.rs └── Cargo.toml ├── vercel.json ├── public ├── macos.png ├── favicon.ico ├── robots.txt ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── site.webmanifest ├── plugins.json ├── serviceWorkerRegister.js ├── audio-processor.js └── serviceWorker.js ├── .husky └── pre-commit ├── .eslintignore ├── docs ├── images │ ├── cover.png │ ├── more.png │ ├── head-cover.png │ ├── settings.png │ ├── upstash-1.png │ ├── upstash-2.png │ ├── upstash-3.png │ ├── upstash-4.png │ ├── upstash-5.png │ ├── upstash-6.png │ ├── upstash-7.png │ ├── enable-actions.jpg │ ├── bt │ │ ├── bt-install-1.jpeg │ │ ├── bt-install-2.jpeg │ │ ├── bt-install-3.jpeg │ │ ├── bt-install-4.jpeg │ │ ├── bt-install-5.jpeg │ │ └── bt-install-6.jpeg │ ├── enable-actions-sync.jpg │ └── vercel │ │ ├── vercel-create-1.jpg │ │ ├── vercel-create-2.jpg │ │ ├── vercel-create-3.jpg │ │ ├── vercel-env-edit.jpg │ │ └── vercel-redeploy.jpg ├── translation.md ├── synchronise-chat-logs-cn.md ├── synchronise-chat-logs-ko.md ├── synchronise-chat-logs-ja.md ├── bt-cn.md ├── synchronise-chat-logs-en.md ├── synchronise-chat-logs-es.md ├── vercel-cn.md ├── vercel-ja.md ├── cloudflare-pages-ko.md ├── vercel-ko.md ├── cloudflare-pages-cn.md ├── cloudflare-pages-ja.md ├── cloudflare-pages-en.md ├── cloudflare-pages-es.md └── vercel-es.md ├── .lintstagedrc.json ├── .eslintrc.json ├── test ├── sum-module.test.ts ├── model-provider.test.ts └── vision-model-checker.test.ts ├── .prettierrc.js ├── .babelrc ├── .github ├── workflows │ ├── issue-translator.yml │ ├── test.yml │ ├── remove_deploy_preview.yml │ ├── docker.yml │ └── sync.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── 2_feature_request_cn.yml │ ├── 2_feature_request.yml │ ├── 1_bug_report_cn.yml │ └── 1_bug_report.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitpod.yml ├── jest.setup.ts ├── .gitignore ├── tsconfig.json ├── jest.config.ts ├── LICENSE ├── docker-compose.yml ├── .dockerignore ├── Dockerfile └── .env.template /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | proxychains.conf 2 | -------------------------------------------------------------------------------- /app/mcp/mcp_config.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": {} 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /app/components/voice-print/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./voice-print"; 2 | -------------------------------------------------------------------------------- /app/components/realtime-chat/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./realtime-chat"; 2 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/components/sd/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./sd"; 2 | export * from "./sd-panel"; 3 | -------------------------------------------------------------------------------- /public/macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/public/macos.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /app/icons/bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/app/icons/bot.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/public/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/serviceWorker.js 2 | app/mcp/mcp_config.json 3 | app/mcp/mcp_config.default.json -------------------------------------------------------------------------------- /app/icons/chatgpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/app/icons/chatgpt.png -------------------------------------------------------------------------------- /docs/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/cover.png -------------------------------------------------------------------------------- /docs/images/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/more.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | User-agent: vitals.vercel-insights.com 4 | Allow: / -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /docs/images/head-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/head-cover.png -------------------------------------------------------------------------------- /docs/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/settings.png -------------------------------------------------------------------------------- /docs/images/upstash-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/upstash-1.png -------------------------------------------------------------------------------- /docs/images/upstash-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/upstash-2.png -------------------------------------------------------------------------------- /docs/images/upstash-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/upstash-3.png -------------------------------------------------------------------------------- /docs/images/upstash-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/upstash-4.png -------------------------------------------------------------------------------- /docs/images/upstash-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/upstash-5.png -------------------------------------------------------------------------------- /docs/images/upstash-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/upstash-6.png -------------------------------------------------------------------------------- /docs/images/upstash-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/upstash-7.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/public/favicon-32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /docs/images/enable-actions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/enable-actions.jpg -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /docs/images/bt/bt-install-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/bt/bt-install-1.jpeg -------------------------------------------------------------------------------- /docs/images/bt/bt-install-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/bt/bt-install-2.jpeg -------------------------------------------------------------------------------- /docs/images/bt/bt-install-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/bt/bt-install-3.jpeg -------------------------------------------------------------------------------- /docs/images/bt/bt-install-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/bt/bt-install-4.jpeg -------------------------------------------------------------------------------- /docs/images/bt/bt-install-5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/bt/bt-install-5.jpeg -------------------------------------------------------------------------------- /docs/images/bt/bt-install-6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/bt/bt-install-6.jpeg -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/images/enable-actions-sync.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/enable-actions-sync.jpg -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/vercel/vercel-create-1.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/vercel/vercel-create-2.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/vercel/vercel-create-3.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-env-edit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/vercel/vercel-env-edit.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-redeploy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/docs/images/vercel/vercel-redeploy.jpg -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-chatgpt-next-web/main/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [ 3 | "eslint --fix", 4 | "prettier --write" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /app/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./chat"; 2 | export * from "./update"; 3 | export * from "./access"; 4 | export * from "./config"; 5 | export * from "./plugin"; 6 | -------------------------------------------------------------------------------- /app/components/model-config.module.scss: -------------------------------------------------------------------------------- 1 | .select-compress-model { 2 | width: 60%; 3 | select { 4 | max-width: 100%; 5 | white-space: normal; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "plugins": ["prettier", "unused-imports"], 4 | "rules": { 5 | "unused-imports/no-unused-imports": "warn" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/sum-module.test.ts: -------------------------------------------------------------------------------- 1 | function sum(a: number, b: number) { 2 | return a + b; 3 | } 4 | 5 | describe("sum module", () => { 6 | test("adds 1 + 2 to equal 3", () => { 7 | expect(sum(1, 2)).toBe(3); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: false, 7 | trailingComma: 'all', 8 | bracketSpacing: true, 9 | arrowParens: 'always', 10 | }; 11 | -------------------------------------------------------------------------------- /app/masks/typing.ts: -------------------------------------------------------------------------------- 1 | import { ModelConfig } from "../store"; 2 | import { type Mask } from "../store/mask"; 3 | 4 | export type BuiltinMask = Omit & { 5 | builtin: Boolean; 6 | modelConfig: Partial; 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/proxychains.template.conf: -------------------------------------------------------------------------------- 1 | strict_chain 2 | proxy_dns 3 | 4 | remote_dns_subnet 224 5 | 6 | tcp_read_time_out 15000 7 | tcp_connect_time_out 8000 8 | 9 | localnet 127.0.0.0/255.0.0.0 10 | 11 | [ProxyList] 12 | socks4 127.0.0.1 9050 13 | -------------------------------------------------------------------------------- /app/components/voice-print/voice-print.module.scss: -------------------------------------------------------------------------------- 1 | .voice-print { 2 | width: 100%; 3 | height: 60px; 4 | margin: 20px 0; 5 | 6 | canvas { 7 | width: 100%; 8 | height: 100%; 9 | filter: brightness(1.2); // 增加整体亮度 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "next/babel", 5 | { 6 | "preset-env": { 7 | "targets": { 8 | "browsers": ["> 0.25%, not dead"] 9 | } 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /app/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /scripts/init-proxy.sh: -------------------------------------------------------------------------------- 1 | dir="$(dirname "$0")" 2 | config=$dir/proxychains.conf 3 | host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //') 4 | echo "proxying to $host_ip" 5 | cp $dir/proxychains.template.conf $config 6 | sed -i "\$s/.*/http $host_ip 7890/" $config 7 | -------------------------------------------------------------------------------- /app/icons/zoom.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/utils/clone.ts: -------------------------------------------------------------------------------- 1 | export function deepClone(obj: T) { 2 | return JSON.parse(JSON.stringify(obj)); 3 | } 4 | 5 | export function ensure( 6 | obj: T, 7 | keys: Array<[keyof T][number]>, 8 | ) { 9 | return keys.every( 10 | (k) => obj[k] !== undefined && obj[k] !== null && obj[k] !== "", 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/components/input-range.module.scss: -------------------------------------------------------------------------------- 1 | .input-range { 2 | border: var(--border-in-light); 3 | border-radius: 10px; 4 | padding: 5px 10px 5px 10px; 5 | font-size: 12px; 6 | display: flex; 7 | justify-content: space-between; 8 | max-width: 40%; 9 | 10 | input[type="range"] { 11 | max-width: calc(100% - 34px); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/icons/discovery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/mcp/utils.ts: -------------------------------------------------------------------------------- 1 | export function isMcpJson(content: string) { 2 | return content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/); 3 | } 4 | 5 | export function extractMcpJson(content: string) { 6 | const match = content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/); 7 | if (match && match.length === 3) { 8 | return { clientId: match[1], mcp: JSON.parse(match[2]) }; 9 | } 10 | return null; 11 | } 12 | -------------------------------------------------------------------------------- /app/icons/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/styles/animation.scss: -------------------------------------------------------------------------------- 1 | @keyframes slide-in { 2 | from { 3 | opacity: 0; 4 | transform: translateY(20px); 5 | } 6 | 7 | to { 8 | opacity: 1; 9 | transform: translateY(0px); 10 | } 11 | } 12 | 13 | @keyframes slide-in-from-top { 14 | from { 15 | opacity: 0; 16 | transform: translateY(-20px); 17 | } 18 | 19 | to { 20 | opacity: 1; 21 | transform: translateY(0px); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/issue-translator.yml: -------------------------------------------------------------------------------- 1 | name: Issue Translator 2 | on: 3 | issue_comment: 4 | types: [created] 5 | issues: 6 | types: [opened] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: usthe/issues-translate-action@v2.7 13 | with: 14 | IS_MODIFY_TITLE: false 15 | CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 16 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from "@vercel/analytics/react"; 2 | import { Home } from "./components/home"; 3 | import { getServerSideConfig } from "./config/server"; 4 | 5 | const serverConfig = getServerSideConfig(); 6 | 7 | export default async function App() { 8 | return ( 9 | <> 10 | 11 | {serverConfig?.isVercel && ( 12 | <> 13 | 14 | 15 | )} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/utils/merge.ts: -------------------------------------------------------------------------------- 1 | export function merge(target: any, source: any) { 2 | Object.keys(source).forEach(function (key) { 3 | if ( 4 | source.hasOwnProperty(key) && // Check if the property is not inherited 5 | source[key] && 6 | typeof source[key] === "object" || key === "__proto__" || key === "constructor" 7 | ) { 8 | merge((target[key] = target[key] || {}), source[key]); 9 | return; 10 | } 11 | target[key] = source[key]; 12 | }); 13 | } -------------------------------------------------------------------------------- /app/utils/object.ts: -------------------------------------------------------------------------------- 1 | export function omit( 2 | obj: T, 3 | ...keys: U 4 | ): Omit { 5 | const ret: any = { ...obj }; 6 | keys.forEach((key) => delete ret[key]); 7 | return ret; 8 | } 9 | 10 | export function pick( 11 | obj: T, 12 | ...keys: U 13 | ): Pick { 14 | const ret: any = {}; 15 | keys.forEach((key) => (ret[key] = obj[key])); 16 | return ret; 17 | } 18 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 6 | 7 | tasks: 8 | - init: yarn install && yarn run dev 9 | command: yarn run dev 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/icons/speak.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | mod stream; 5 | 6 | fn main() { 7 | tauri::Builder::default() 8 | .invoke_handler(tauri::generate_handler![stream::stream_fetch]) 9 | .plugin(tauri_plugin_window_state::Builder::default().build()) 10 | .run(tauri::generate_context!()) 11 | .expect("error while running tauri application"); 12 | } 13 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NextChat", 3 | "short_name": "NextChat", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/", 17 | "theme_color": "#ffffff", 18 | "background_color": "#ffffff", 19 | "display": "standalone" 20 | } -------------------------------------------------------------------------------- /app/icons/speak-stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/hd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/icons/llm-icons/grok.svg: -------------------------------------------------------------------------------- 1 | 3 | Grok 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/icons/left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/translation.md: -------------------------------------------------------------------------------- 1 | # How to add a new translation? 2 | 3 | Assume that we are adding a new translation for `new`. 4 | 5 | 1. copy `app/locales/en.ts` to `app/locales/new.ts`; 6 | 2. edit `new.ts`, change `const en: LocaleType = ` to `const new: PartialLocaleType`, and `export default new;`; 7 | 3. edit `app/locales/index.ts`: 8 | 4. `import new from './new.ts'`; 9 | 5. add `new` to `ALL_LANGS`; 10 | 6. add `new: "new lang"` to `ALL_LANG_OPTIONS`; 11 | 7. translate the strings in `new.ts`; 12 | 8. submit a pull request, and the author will merge it. 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /app/utils/token.ts: -------------------------------------------------------------------------------- 1 | export function estimateTokenLength(input: string): number { 2 | let tokenLength = 0; 3 | 4 | for (let i = 0; i < input.length; i++) { 5 | const charCode = input.charCodeAt(i); 6 | 7 | if (charCode < 128) { 8 | // ASCII character 9 | if (charCode <= 122 && charCode >= 65) { 10 | // a-Z 11 | tokenLength += 0.25; 12 | } else { 13 | tokenLength += 0.5; 14 | } 15 | } else { 16 | // Unicode character 17 | tokenLength += 1.5; 18 | } 19 | } 20 | 21 | return tokenLength; 22 | } 23 | -------------------------------------------------------------------------------- /app/icons/voice.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_feature_request_cn.yml: -------------------------------------------------------------------------------- 1 | name: '🌠 功能需求' 2 | description: '提出需求或建议' 3 | title: '[Feature Request] ' 4 | labels: ['enhancement'] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: '🥰 需求描述' 9 | description: 请添加一个清晰且简洁的问题描述,阐述您希望通过这个功能需求解决的问题。 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: '🧐 解决方案' 15 | description: 请清晰且简洁地描述您想要的解决方案。 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: '📝 补充信息' 21 | description: 在这里添加关于问题的任何其他背景信息。 -------------------------------------------------------------------------------- /app/utils/baidu.ts: -------------------------------------------------------------------------------- 1 | import { BAIDU_OATUH_URL } from "../constant"; 2 | /** 3 | * 使用 AK,SK 生成鉴权签名(Access Token) 4 | * @return 鉴权签名信息 5 | */ 6 | export async function getAccessToken( 7 | clientId: string, 8 | clientSecret: string, 9 | ): Promise<{ 10 | access_token: string; 11 | expires_in: number; 12 | error?: number; 13 | }> { 14 | const res = await fetch( 15 | `${BAIDU_OATUH_URL}?grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}`, 16 | { 17 | method: "POST", 18 | mode: "cors", 19 | }, 20 | ); 21 | const resJson = await res.json(); 22 | return resJson; 23 | } 24 | -------------------------------------------------------------------------------- /app/icons/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/fire.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/masks/build.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { CN_MASKS } from "./cn"; 4 | import { TW_MASKS } from "./tw"; 5 | import { EN_MASKS } from "./en"; 6 | 7 | import { type BuiltinMask } from "./typing"; 8 | 9 | const BUILTIN_MASKS: Record = { 10 | cn: CN_MASKS, 11 | tw: TW_MASKS, 12 | en: EN_MASKS, 13 | }; 14 | 15 | const dirname = path.dirname(__filename); 16 | 17 | fs.writeFile( 18 | dirname + "/../../public/masks.json", 19 | JSON.stringify(BUILTIN_MASKS, null, 4), 20 | function (error) { 21 | if (error) { 22 | console.error("[Build] failed to build masks", error); 23 | } 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /app/icons/size.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/typing.ts: -------------------------------------------------------------------------------- 1 | export type Updater = (updater: (value: T) => void) => void; 2 | 3 | export const ROLES = ["system", "user", "assistant"] as const; 4 | export type MessageRole = (typeof ROLES)[number]; 5 | 6 | export interface RequestMessage { 7 | role: MessageRole; 8 | content: string; 9 | } 10 | 11 | export type DalleSize = "1024x1024" | "1792x1024" | "1024x1792"; 12 | export type DalleQuality = "standard" | "hd"; 13 | export type DalleStyle = "vivid" | "natural"; 14 | 15 | export type ModelSize = 16 | | "1024x1024" 17 | | "1792x1024" 18 | | "1024x1792" 19 | | "768x1344" 20 | | "864x1152" 21 | | "1344x768" 22 | | "1152x864" 23 | | "1440x720" 24 | | "720x1440"; 25 | -------------------------------------------------------------------------------- /app/icons/bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/power.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /public/plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "dalle3", 4 | "name": "Dalle3", 5 | "schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/dalle/openapi.json" 6 | }, 7 | { 8 | "id": "arxivsearch", 9 | "name": "ArxivSearch", 10 | "schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/arxivsearch/openapi.json" 11 | }, 12 | { 13 | "id": "duckduckgolite", 14 | "name": "DuckDuckGoLiteSearch", 15 | "schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/duckduckgolite/openapi.json" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /app/icons/history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | // Learn more: https://github.com/testing-library/jest-dom 2 | import "@testing-library/jest-dom"; 3 | import { jest } from "@jest/globals"; 4 | 5 | global.fetch = jest.fn(() => 6 | Promise.resolve({ 7 | ok: true, 8 | status: 200, 9 | json: () => Promise.resolve([]), 10 | headers: new Headers(), 11 | redirected: false, 12 | statusText: "OK", 13 | type: "basic", 14 | url: "", 15 | body: null, 16 | bodyUsed: false, 17 | arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), 18 | blob: () => Promise.resolve(new Blob()), 19 | formData: () => Promise.resolve(new FormData()), 20 | text: () => Promise.resolve(""), 21 | } as Response), 22 | ); 23 | -------------------------------------------------------------------------------- /app/components/artifacts.module.scss: -------------------------------------------------------------------------------- 1 | .artifacts { 2 | display: flex; 3 | width: 100%; 4 | height: 100%; 5 | flex-direction: column; 6 | &-header { 7 | display: flex; 8 | align-items: center; 9 | height: 36px; 10 | padding: 20px; 11 | background: var(--second); 12 | } 13 | &-title { 14 | flex: 1; 15 | text-align: center; 16 | font-weight: bold; 17 | font-size: 24px; 18 | } 19 | &-content { 20 | flex-grow: 1; 21 | padding: 0 20px 20px 20px; 22 | background-color: var(--second); 23 | } 24 | } 25 | 26 | .artifacts-iframe { 27 | width: 100%; 28 | border: var(--border-in-light); 29 | border-radius: 6px; 30 | background-color: var(--gray); 31 | } 32 | -------------------------------------------------------------------------------- /app/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useAccessStore, useAppConfig } from "../store"; 3 | import { collectModelsWithDefaultModel } from "./model"; 4 | 5 | export function useAllModels() { 6 | const accessStore = useAccessStore(); 7 | const configStore = useAppConfig(); 8 | const models = useMemo(() => { 9 | return collectModelsWithDefaultModel( 10 | configStore.models, 11 | [configStore.customModels, accessStore.customModels].join(","), 12 | accessStore.defaultModel, 13 | ); 14 | }, [ 15 | accessStore.customModels, 16 | accessStore.defaultModel, 17 | configStore.customModels, 18 | configStore.models, 19 | ]); 20 | 21 | return models; 22 | } 23 | -------------------------------------------------------------------------------- /app/utils/auth-settings-events.ts: -------------------------------------------------------------------------------- 1 | import { sendGAEvent } from "@next/third-parties/google"; 2 | 3 | export function trackConversationGuideToCPaymentClick() { 4 | sendGAEvent("event", "ConversationGuideToCPaymentClick", { value: 1 }); 5 | } 6 | 7 | export function trackAuthorizationPageButtonToCPaymentClick() { 8 | sendGAEvent("event", "AuthorizationPageButtonToCPaymentClick", { value: 1 }); 9 | } 10 | 11 | export function trackAuthorizationPageBannerToCPaymentClick() { 12 | sendGAEvent("event", "AuthorizationPageBannerToCPaymentClick", { 13 | value: 1, 14 | }); 15 | } 16 | 17 | export function trackSettingsPageGuideToCPaymentClick() { 18 | sendGAEvent("event", "SettingsPageGuideToCPaymentClick", { value: 1 }); 19 | } 20 | -------------------------------------------------------------------------------- /app/icons/shortcutkey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-cn.md: -------------------------------------------------------------------------------- 1 | # 同步聊天记录 2 | ## 准备工作 3 | - GitHub账号 4 | - 拥有自己搭建过的ChatGPT-Next-Web的服务器 5 | - [UpStash](https://upstash.com) 6 | 7 | ## 开始教程 8 | 1. 注册UpStash账号 9 | 2. 创建数据库 10 | 11 | ![注册登录](./images/upstash-1.png) 12 | 13 | ![创建数据库](./images/upstash-2.png) 14 | 15 | ![选择服务器](./images/upstash-3.png) 16 | 17 | 3. 找到REST API,分别复制UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN(⚠切记⚠:不要泄露Token!) 18 | 19 | ![复制](./images/upstash-4.png) 20 | 21 | 4. UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN复制到你的同步配置,点击**检查可用性** 22 | 23 | ![同步1](./images/upstash-5.png) 24 | 25 | 如果没什么问题,那就成功了 26 | 27 | ![同步可用性完成的样子](./images/upstash-6.png) 28 | 29 | 5. Success! 30 | 31 | ![好耶~!](./images/upstash-7.png) 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | dev 38 | 39 | .vscode 40 | .idea 41 | 42 | # docker-compose env files 43 | .env 44 | 45 | *.key 46 | *.key.pub 47 | 48 | masks.json 49 | 50 | # mcp config 51 | app/mcp/mcp_config.json 52 | -------------------------------------------------------------------------------- /app/config/client.ts: -------------------------------------------------------------------------------- 1 | import { BuildConfig, getBuildConfig } from "./build"; 2 | 3 | export function getClientConfig() { 4 | if (typeof document !== "undefined") { 5 | // client side 6 | return JSON.parse(queryMeta("config") || "{}") as BuildConfig; 7 | } 8 | 9 | if (typeof process !== "undefined") { 10 | // server side 11 | return getBuildConfig(); 12 | } 13 | } 14 | 15 | function queryMeta(key: string, defaultValue?: string): string { 16 | let ret: string; 17 | if (document) { 18 | const meta = document.head.querySelector( 19 | `meta[name='${key}']`, 20 | ) as HTMLMetaElement; 21 | ret = meta?.content ?? ""; 22 | } else { 23 | ret = defaultValue ?? ""; 24 | } 25 | 26 | return ret; 27 | } 28 | -------------------------------------------------------------------------------- /app/polyfill.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Array { 3 | at(index: number): T | undefined; 4 | } 5 | } 6 | 7 | if (!Array.prototype.at) { 8 | Array.prototype.at = function (index: number) { 9 | // Get the length of the array 10 | const length = this.length; 11 | 12 | // Convert negative index to a positive index 13 | if (index < 0) { 14 | index = length + index; 15 | } 16 | 17 | // Return undefined if the index is out of range 18 | if (index < 0 || index >= length) { 19 | return undefined; 20 | } 21 | 22 | // Use Array.prototype.slice method to get value at the specified index 23 | return Array.prototype.slice.call(this, index, index + 1)[0]; 24 | }; 25 | } 26 | 27 | export {}; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /app/components/plugin.module.scss: -------------------------------------------------------------------------------- 1 | .plugin-title { 2 | font-weight: bolder; 3 | font-size: 16px; 4 | margin: 10px 0; 5 | } 6 | .plugin-content { 7 | font-size: 14px; 8 | font-family: inherit; 9 | pre code { 10 | max-height: 240px; 11 | overflow-y: auto; 12 | white-space: pre-wrap; 13 | min-width: 280px; 14 | } 15 | } 16 | 17 | .plugin-schema { 18 | display: flex; 19 | justify-content: flex-end; 20 | flex-direction: row; 21 | 22 | input { 23 | margin-right: 20px; 24 | 25 | @media screen and (max-width: 600px) { 26 | margin-right: 0px; 27 | } 28 | } 29 | 30 | @media screen and (max-width: 600px) { 31 | flex-direction: column; 32 | gap: 5px; 33 | 34 | button { 35 | padding: 10px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/icons/palette.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/icons/voice-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: '🌠 Feature Request' 2 | description: 'Suggest an idea' 3 | title: '[Feature Request] ' 4 | labels: ['enhancement'] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: '🥰 Feature Description' 9 | description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: '🧐 Proposed Solution' 15 | description: Describe the solution you'd like in a clear and concise manner. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: '📝 Additional Information' 21 | description: Add any other context about the problem here. -------------------------------------------------------------------------------- /app/styles/window.scss: -------------------------------------------------------------------------------- 1 | .window-header { 2 | padding: 14px 20px; 3 | border-bottom: rgba(0, 0, 0, 0.1) 1px solid; 4 | position: relative; 5 | 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | } 10 | 11 | .window-header-title { 12 | max-width: calc(100% - 100px); 13 | overflow: hidden; 14 | 15 | .window-header-main-title { 16 | font-size: 20px; 17 | font-weight: bolder; 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | white-space: nowrap; 21 | display: block; 22 | max-width: 50vw; 23 | } 24 | 25 | .window-header-sub-title { 26 | font-size: 14px; 27 | } 28 | } 29 | 30 | .window-actions { 31 | display: inline-flex; 32 | } 33 | 34 | .window-action-button:not(:first-child) { 35 | margin-left: 10px; 36 | } 37 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-ko.md: -------------------------------------------------------------------------------- 1 | # UpStash를 사용하여 채팅 기록 동기화 2 | ## 사전 준비물 3 | - GitHub 계정 4 | - 자체 ChatGPT-Next-Web 서버 설정 5 | - [UpStash](https://upstash.com) 6 | 7 | ## 시작하기 8 | 1. UpStash 계정 등록 9 | 2. 데이터베이스 생성 10 | 11 | ![등록 및 로그인](./images/upstash-1.png) 12 | 13 | ![데이터베이스 생성](./images/upstash-2.png) 14 | 15 | ![서버 선택](./images/upstash-3.png) 16 | 17 | 3. REST API를 찾아 UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 복사합니다 (⚠주의⚠: 토큰을 공유하지 마십시오!) 18 | 19 | ![복사](./images/upstash-4.png) 20 | 21 | 4. UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 동기화 구성에 복사한 다음 **가용성 확인**을 클릭합니다. 22 | 23 | ![동기화 1](./images/upstash-5.png) 24 | 25 | 모든 것이 정상인 경우,이 단계를 성공적으로 완료했습니다. 26 | 27 | ![동기화 가용성 확인 완료](./images/upstash-6.png) 28 | 29 | 5. 성공! 30 | 31 | ![잘 했어요~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /app/icons/dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-ja.md: -------------------------------------------------------------------------------- 1 | # UpStashを使用してチャットログを同期する 2 | ## 事前準備 3 | - GitHubアカウント 4 | - 自分自身でChatGPT-Next-Webのサーバーをセットアップしていること 5 | - [UpStash](https://upstash.com) 6 | 7 | ## 始める 8 | 1. UpStashアカウントを登録します。 9 | 2. データベースを作成します。 10 | 11 | ![登録とログイン](./images/upstash-1.png) 12 | 13 | ![データベースの作成](./images/upstash-2.png) 14 | 15 | ![サーバーの選択](./images/upstash-3.png) 16 | 17 | 3. REST APIを見つけ、UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENをコピーします(⚠重要⚠:トークンを共有しないでください!) 18 | 19 | ![コピー](./images/upstash-4.png) 20 | 21 | 4. UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENを同期設定にコピーし、次に「可用性を確認」をクリックします。 22 | 23 | ![同期1](./images/upstash-5.png) 24 | 25 | すべてが正常であれば、このステップは成功です。 26 | 27 | ![同期可用性チェックが完了しました](./images/upstash-6.png) 28 | 29 | 5. 成功! 30 | 31 | ![お疲れ様でした~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /app/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/send-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/headphone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 11 | -------------------------------------------------------------------------------- /app/icons/rename.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/return.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/voice-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 11 | 13 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest"; 2 | import nextJest from "next/jest.js"; 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: "./", 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | const config: Config = { 11 | coverageProvider: "v8", 12 | testEnvironment: "jsdom", 13 | testMatch: ["**/*.test.js", "**/*.test.ts", "**/*.test.jsx", "**/*.test.tsx"], 14 | setupFilesAfterEnv: ["/jest.setup.ts"], 15 | moduleNameMapper: { 16 | "^@/(.*)$": "/$1", 17 | }, 18 | extensionsToTreatAsEsm: [".ts", ".tsx"], 19 | injectGlobals: true, 20 | }; 21 | 22 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 23 | export default createJestConfig(config); 24 | -------------------------------------------------------------------------------- /app/icons/llm-icons/gemini.svg: -------------------------------------------------------------------------------- 1 | 2 | Gemini 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /app/icons/llm-icons/mistral.svg: -------------------------------------------------------------------------------- 1 | 2 | Mistral 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/utils/cloud/index.ts: -------------------------------------------------------------------------------- 1 | import { createWebDavClient } from "./webdav"; 2 | import { createUpstashClient } from "./upstash"; 3 | 4 | export enum ProviderType { 5 | WebDAV = "webdav", 6 | UpStash = "upstash", 7 | } 8 | 9 | export const SyncClients = { 10 | [ProviderType.UpStash]: createUpstashClient, 11 | [ProviderType.WebDAV]: createWebDavClient, 12 | } as const; 13 | 14 | type SyncClientConfig = { 15 | [K in keyof typeof SyncClients]: (typeof SyncClients)[K] extends ( 16 | _: infer C, 17 | ) => any 18 | ? C 19 | : never; 20 | }; 21 | 22 | export type SyncClient = { 23 | get: (key: string) => Promise; 24 | set: (key: string, value: string) => Promise; 25 | check: () => Promise; 26 | }; 27 | 28 | export function createSyncClient( 29 | provider: T, 30 | config: SyncClientConfig[T], 31 | ): SyncClient { 32 | return SyncClients[provider](config as any) as any; 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "!*" 9 | pull_request: 10 | types: 11 | - review_requested 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 18 25 | cache: "yarn" 26 | 27 | - name: Cache node_modules 28 | uses: actions/cache@v4 29 | with: 30 | path: node_modules 31 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} 32 | restore-keys: | 33 | ${{ runner.os }}-node_modules- 34 | 35 | - name: Install dependencies 36 | run: yarn install 37 | 38 | - name: Run Jest tests 39 | run: yarn test:ci 40 | -------------------------------------------------------------------------------- /app/icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/input-range.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styles from "./input-range.module.scss"; 3 | import clsx from "clsx"; 4 | 5 | interface InputRangeProps { 6 | onChange: React.ChangeEventHandler; 7 | title?: string; 8 | value: number | string; 9 | className?: string; 10 | min: string; 11 | max: string; 12 | step: string; 13 | aria: string; 14 | } 15 | 16 | export function InputRange({ 17 | onChange, 18 | title, 19 | value, 20 | className, 21 | min, 22 | max, 23 | step, 24 | aria, 25 | }: InputRangeProps) { 26 | return ( 27 |
28 | {title || value} 29 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/api/azure.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider } from "@/app/constant"; 2 | import { prettyObject } from "@/app/utils/format"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | import { auth } from "./auth"; 5 | import { requestOpenai } from "./common"; 6 | 7 | export async function handle( 8 | req: NextRequest, 9 | { params }: { params: { path: string[] } }, 10 | ) { 11 | console.log("[Azure Route] params ", params); 12 | 13 | if (req.method === "OPTIONS") { 14 | return NextResponse.json({ body: "OK" }, { status: 200 }); 15 | } 16 | 17 | const subpath = params.path.join("/"); 18 | 19 | const authResult = auth(req, ModelProvider.GPT); 20 | if (authResult.error) { 21 | return NextResponse.json(authResult, { 22 | status: 401, 23 | }); 24 | } 25 | 26 | try { 27 | return await requestOpenai(req); 28 | } catch (e) { 29 | console.error("[Azure] ", e); 30 | return NextResponse.json(prettyObject(e)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/icons/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/utils/format.ts: -------------------------------------------------------------------------------- 1 | export function prettyObject(msg: any) { 2 | const obj = msg; 3 | if (typeof msg !== "string") { 4 | msg = JSON.stringify(msg, null, " "); 5 | } 6 | if (msg === "{}") { 7 | return obj.toString(); 8 | } 9 | if (msg.startsWith("```json")) { 10 | return msg; 11 | } 12 | return ["```json", msg, "```"].join("\n"); 13 | } 14 | 15 | export function* chunks(s: string, maxBytes = 1000 * 1000) { 16 | const decoder = new TextDecoder("utf-8"); 17 | let buf = new TextEncoder().encode(s); 18 | while (buf.length) { 19 | let i = buf.lastIndexOf(32, maxBytes + 1); 20 | // If no space found, try forward search 21 | if (i < 0) i = buf.indexOf(32, maxBytes); 22 | // If there's no space at all, take all 23 | if (i < 0) i = buf.length; 24 | // This is a safe cut-off point; never half-way a multi-byte 25 | yield decoder.decode(buf.slice(0, i)); 26 | buf = buf.slice(i + 1); // Skip space (if any) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/api/config/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import { getServerSideConfig } from "../../config/server"; 4 | 5 | const serverConfig = getServerSideConfig(); 6 | 7 | // Danger! Do not hard code any secret value here! 8 | // 警告!不要在这里写入任何敏感信息! 9 | const DANGER_CONFIG = { 10 | needCode: serverConfig.needCode, 11 | hideUserApiKey: serverConfig.hideUserApiKey, 12 | disableGPT4: serverConfig.disableGPT4, 13 | hideBalanceQuery: serverConfig.hideBalanceQuery, 14 | disableFastLink: serverConfig.disableFastLink, 15 | customModels: serverConfig.customModels, 16 | defaultModel: serverConfig.defaultModel, 17 | visionModels: serverConfig.visionModels, 18 | }; 19 | 20 | declare global { 21 | type DangerConfig = typeof DANGER_CONFIG; 22 | } 23 | 24 | async function handle() { 25 | return NextResponse.json(DANGER_CONFIG); 26 | } 27 | 28 | export const GET = handle; 29 | export const POST = handle; 30 | 31 | export const runtime = "edge"; 32 | -------------------------------------------------------------------------------- /app/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/bt-cn.md: -------------------------------------------------------------------------------- 1 | # 宝塔面板 的部署说明 2 | 3 | ## 拥有自己的宝塔 4 | 当你需要通过 宝塔面板 部署本项目之前,需要在服务器上先安装好 宝塔面板工具。 接下来的 部署流程 都建立在已有宝塔面板的前提下。宝塔安装请参考 ([宝塔官网](https://www.bt.cn/new/download.html)) 5 | 6 | > 注意:本项目需要宝塔面板版本 9.2.0 及以上 7 | 8 | ## 一键安装 9 | ![bt-install-1](./images/bt/bt-install-1.jpeg) 10 | 1. 在 宝塔面板 -> Docker -> 应用商店 页面,搜索 ChatGPT-Next-Web 找到本项目的docker应用; 11 | 2. 点击 安装 开始部署本项目 12 | 13 | ![bt-install-2](./images/bt/bt-install-2.jpeg) 14 | 1. 在项目配置页,根据要求开始配置环境变量; 15 | 2. 如勾选 允许外部访问 配置,请注意为配置的 web端口 开放安全组端口访问权限; 16 | 3. 请确保你添加了正确的 Open Api Key,否则无法使用;当配置 OpenAI官方 提供的key(国内无法访问),请配置代理地址; 17 | 4. 建议配置 访问权限密码,否则部署后所有人均可使用已配置的 Open Api Key(当允许外部访问时); 18 | 5. 点击 确认 开始自动部署。 19 | 20 | ## 如何访问 21 | ![bt-install-3](./images/bt/bt-install-3.jpeg) 22 | 通过根据 服务器IP地址 和配置的 web端口 http://$(host):$(port),在浏览器中打开 ChatGPT-Next-Web。 23 | 24 | ![bt-install-4](./images/bt/bt-install-4.jpeg) 25 | 若配置了 访问权限密码,访问大模型前需要登录,请点击 登录,获取访问权限。 26 | 27 | ![bt-install-5](./images/bt/bt-install-5.jpeg) 28 | 29 | ![bt-install-6](./images/bt/bt-install-6.jpeg) 30 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-en.md: -------------------------------------------------------------------------------- 1 | # Synchronize Chat Logs with UpStash 2 | ## Prerequisites 3 | - GitHub account 4 | - Your own ChatGPT-Next-Web server set up 5 | - [UpStash](https://upstash.com) 6 | 7 | ## Getting Started 8 | 1. Register for an UpStash account. 9 | 2. Create a database. 10 | 11 | ![Register and Login](./images/upstash-1.png) 12 | 13 | ![Create Database](./images/upstash-2.png) 14 | 15 | ![Select Server](./images/upstash-3.png) 16 | 17 | 3. Find the REST API and copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (⚠Important⚠: Do not share your token!) 18 | 19 | ![Copy](./images/upstash-4.png) 20 | 21 | 4. Copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN into your synchronization configuration, then click **Check Availability**. 22 | 23 | ![Synchronize 1](./images/upstash-5.png) 24 | 25 | If everything is in order, you've successfully completed this step. 26 | 27 | ![Sync Availability Check Completed](./images/upstash-6.png) 28 | 29 | 5. Success! 30 | 31 | ![Great job~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /app/components/sd/sd-panel.module.scss: -------------------------------------------------------------------------------- 1 | .ctrl-param-item { 2 | display: flex; 3 | justify-content: space-between; 4 | min-height: 40px; 5 | padding: 10px 0; 6 | animation: slide-in ease 0.6s; 7 | flex-direction: column; 8 | 9 | .ctrl-param-item-header { 10 | display: flex; 11 | align-items: center; 12 | 13 | .ctrl-param-item-title { 14 | font-size: 14px; 15 | font-weight: bolder; 16 | margin-bottom: 5px; 17 | } 18 | } 19 | 20 | .ctrl-param-item-sub-title { 21 | font-size: 12px; 22 | font-weight: normal; 23 | margin-top: 3px; 24 | } 25 | textarea { 26 | appearance: none; 27 | border-radius: 10px; 28 | border: var(--border-in-light); 29 | min-height: 36px; 30 | box-sizing: border-box; 31 | background: var(--white); 32 | color: var(--black); 33 | padding: 0 10px; 34 | max-width: 50%; 35 | font-family: inherit; 36 | } 37 | } 38 | 39 | .ai-models { 40 | button { 41 | margin-bottom: 10px; 42 | padding: 10px; 43 | width: 100%; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/icons/reload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/controller.ts: -------------------------------------------------------------------------------- 1 | // To store message streaming controller 2 | export const ChatControllerPool = { 3 | controllers: {} as Record, 4 | 5 | addController( 6 | sessionId: string, 7 | messageId: string, 8 | controller: AbortController, 9 | ) { 10 | const key = this.key(sessionId, messageId); 11 | this.controllers[key] = controller; 12 | return key; 13 | }, 14 | 15 | stop(sessionId: string, messageId: string) { 16 | const key = this.key(sessionId, messageId); 17 | const controller = this.controllers[key]; 18 | controller?.abort(); 19 | }, 20 | 21 | stopAll() { 22 | Object.values(this.controllers).forEach((v) => v.abort()); 23 | }, 24 | 25 | hasPending() { 26 | return Object.values(this.controllers).length > 0; 27 | }, 28 | 29 | remove(sessionId: string, messageId: string) { 30 | const key = this.key(sessionId, messageId); 31 | delete this.controllers[key]; 32 | }, 33 | 34 | key(sessionId: string, messageIndex: string) { 35 | return `${sessionId},${messageIndex}`; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /app/icons/three-dots.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/sd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | -------------------------------------------------------------------------------- /app/icons/mcp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-es.md: -------------------------------------------------------------------------------- 1 | # Sincronizzare i Log delle Chat con UpStash 2 | ## Prerequisiti 3 | - Account GitHub 4 | - Server ChatGPT-Next-Web di propria configurazione 5 | - [UpStash](https://upstash.com) 6 | 7 | ## Per iniziare 8 | 1. Registrarsi per un account UpStash. 9 | 2. Creare un database. 10 | 11 | ![Registrarsi ed Accedere](./images/upstash-1.png) 12 | 13 | ![Creare un Database](./images/upstash-2.png) 14 | 15 | ![Selezionare il Server](./images/upstash-3.png) 16 | 17 | 3. Trovare l'API REST e copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN (⚠Importante⚠: Non condividere il token!) 18 | 19 | ![Copia](./images/upstash-4.png) 20 | 21 | 4. Copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN nella configurazione di sincronizzazione, quindi fare clic su **Verifica la Disponibilità**. 22 | 23 | ![Sincronizzazione 1](./images/upstash-5.png) 24 | 25 | Se tutto è in ordine, hai completato con successo questa fase. 26 | 27 | ![Verifica la Disponibilità della Sincronizzazione Completata](./images/upstash-6.png) 28 | 29 | 5. Successo! 30 | 31 | ![Ottimo lavoro~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 NextChat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/icons/export.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/clear.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/remove_deploy_preview.yml: -------------------------------------------------------------------------------- 1 | name: Removedeploypreview 2 | 3 | permissions: 4 | contents: read 5 | statuses: write 6 | pull-requests: write 7 | 8 | env: 9 | VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} 10 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 11 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} 12 | 13 | on: 14 | pull_request_target: 15 | types: 16 | - closed 17 | 18 | jobs: 19 | delete-deployments: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Extract branch name 25 | shell: bash 26 | run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT 27 | id: extract_branch 28 | 29 | - name: Hash branch name 30 | uses: pplanel/hash-calculator-action@v1.3.1 31 | id: hash_branch 32 | with: 33 | input: ${{ steps.extract_branch.outputs.branch }} 34 | method: MD5 35 | 36 | - name: Call the delete-deployment-preview.sh script 37 | env: 38 | META_TAG: ${{ steps.hash_branch.outputs.digest }} 39 | run: | 40 | bash ./scripts/delete-deployment-preview.sh 41 | -------------------------------------------------------------------------------- /app/icons/prompt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/model-provider.test.ts: -------------------------------------------------------------------------------- 1 | import { getModelProvider } from "../app/utils/model"; 2 | 3 | describe("getModelProvider", () => { 4 | test("should return model and provider when input contains '@'", () => { 5 | const input = "model@provider"; 6 | const [model, provider] = getModelProvider(input); 7 | expect(model).toBe("model"); 8 | expect(provider).toBe("provider"); 9 | }); 10 | 11 | test("should return model and undefined provider when input does not contain '@'", () => { 12 | const input = "model"; 13 | const [model, provider] = getModelProvider(input); 14 | expect(model).toBe("model"); 15 | expect(provider).toBeUndefined(); 16 | }); 17 | 18 | test("should handle multiple '@' characters correctly", () => { 19 | const input = "model@provider@extra"; 20 | const [model, provider] = getModelProvider(input); 21 | expect(model).toBe("model@provider"); 22 | expect(provider).toBe("extra"); 23 | }); 24 | 25 | test("should return empty strings when input is empty", () => { 26 | const input = ""; 27 | const [model, provider] = getModelProvider(input); 28 | expect(model).toBe(""); 29 | expect(provider).toBeUndefined(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /app/masks/index.ts: -------------------------------------------------------------------------------- 1 | import { Mask } from "../store/mask"; 2 | 3 | import { type BuiltinMask } from "./typing"; 4 | export { type BuiltinMask } from "./typing"; 5 | 6 | export const BUILTIN_MASK_ID = 100000; 7 | 8 | export const BUILTIN_MASK_STORE = { 9 | buildinId: BUILTIN_MASK_ID, 10 | masks: {} as Record, 11 | get(id?: string) { 12 | if (!id) return undefined; 13 | return this.masks[id] as Mask | undefined; 14 | }, 15 | add(m: BuiltinMask) { 16 | const mask = { ...m, id: this.buildinId++, builtin: true }; 17 | this.masks[mask.id] = mask; 18 | return mask; 19 | }, 20 | }; 21 | 22 | export const BUILTIN_MASKS: BuiltinMask[] = []; 23 | 24 | if (typeof window != "undefined") { 25 | // run in browser skip in next server 26 | fetch("/masks.json") 27 | .then((res) => res.json()) 28 | .catch((error) => { 29 | console.error("[Fetch] failed to fetch masks", error); 30 | return { cn: [], tw: [], en: [] }; 31 | }) 32 | .then((masks) => { 33 | const { cn = [], tw = [], en = [] } = masks; 34 | return [...cn, ...tw, ...en].map((m) => { 35 | BUILTIN_MASKS.push(BUILTIN_MASK_STORE.add(m)); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /app/components/sd/sd.module.scss: -------------------------------------------------------------------------------- 1 | .sd-img-list{ 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: space-between; 5 | .sd-img-item{ 6 | width: 48%; 7 | .sd-img-item-info{ 8 | flex:1; 9 | width: 100%; 10 | overflow: hidden; 11 | user-select: text; 12 | p{ 13 | margin: 6px; 14 | font-size: 12px; 15 | } 16 | .line-1{ 17 | overflow: hidden; 18 | white-space: nowrap; 19 | text-overflow: ellipsis; 20 | } 21 | } 22 | .pre-img{ 23 | display: flex; 24 | width: 130px; 25 | justify-content: center; 26 | align-items: center; 27 | background-color: var(--second); 28 | border-radius: 10px; 29 | } 30 | .img{ 31 | width: 130px; 32 | height: 130px; 33 | border-radius: 10px; 34 | overflow: hidden; 35 | cursor: pointer; 36 | transition: all .3s; 37 | &:hover{ 38 | opacity: .7; 39 | } 40 | } 41 | &:not(:last-child){ 42 | margin-bottom: 20px; 43 | } 44 | } 45 | } 46 | 47 | @media only screen and (max-width: 600px) { 48 | .sd-img-list{ 49 | .sd-img-item{ 50 | width: 100%; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /public/serviceWorkerRegister.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | window.addEventListener('DOMContentLoaded', function () { 3 | navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) { 4 | console.log('ServiceWorker registration successful with scope: ', registration.scope); 5 | const sw = registration.installing || registration.waiting 6 | if (sw) { 7 | sw.onstatechange = function() { 8 | if (sw.state === 'installed') { 9 | // SW installed. Reload for SW intercept serving SW-enabled page. 10 | console.log('ServiceWorker installed reload page'); 11 | window.location.reload(); 12 | } 13 | } 14 | } 15 | registration.update().then(res => { 16 | console.log('ServiceWorker registration update: ', res); 17 | }); 18 | window._SW_ENABLED = true 19 | }, function (err) { 20 | console.error('ServiceWorker registration failed: ', err); 21 | }); 22 | navigator.serviceWorker.addEventListener('controllerchange', function() { 23 | console.log('ServiceWorker controllerchange '); 24 | window.location.reload(true); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /app/icons/auto.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/loading.svg: -------------------------------------------------------------------------------- 1 | Layer 1 -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### 💻 变更类型 | Change Type 2 | 3 | 4 | 5 | - [ ] feat 6 | - [ ] fix 7 | - [ ] refactor 8 | - [ ] perf 9 | - [ ] style 10 | - [ ] test 11 | - [ ] docs 12 | - [ ] ci 13 | - [ ] chore 14 | - [ ] build 15 | 16 | #### 🔀 变更说明 | Description of Change 17 | 18 | 22 | 23 | #### 📝 补充信息 | Additional Information 24 | 25 | 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | chatgpt-next-web: 4 | profiles: [ "no-proxy" ] 5 | container_name: chatgpt-next-web 6 | image: yidadaa/chatgpt-next-web 7 | ports: 8 | - 3000:3000 9 | environment: 10 | - OPENAI_API_KEY=$OPENAI_API_KEY 11 | - GOOGLE_API_KEY=$GOOGLE_API_KEY 12 | - CODE=$CODE 13 | - BASE_URL=$BASE_URL 14 | - OPENAI_ORG_ID=$OPENAI_ORG_ID 15 | - HIDE_USER_API_KEY=$HIDE_USER_API_KEY 16 | - DISABLE_GPT4=$DISABLE_GPT4 17 | - ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY 18 | - DISABLE_FAST_LINK=$DISABLE_FAST_LINK 19 | - OPENAI_SB=$OPENAI_SB 20 | 21 | chatgpt-next-web-proxy: 22 | profiles: [ "proxy" ] 23 | container_name: chatgpt-next-web-proxy 24 | image: yidadaa/chatgpt-next-web 25 | ports: 26 | - 3000:3000 27 | environment: 28 | - OPENAI_API_KEY=$OPENAI_API_KEY 29 | - GOOGLE_API_KEY=$GOOGLE_API_KEY 30 | - CODE=$CODE 31 | - PROXY_URL=$PROXY_URL 32 | - BASE_URL=$BASE_URL 33 | - OPENAI_ORG_ID=$OPENAI_ORG_ID 34 | - HIDE_USER_API_KEY=$HIDE_USER_API_KEY 35 | - DISABLE_GPT4=$DISABLE_GPT4 36 | - ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY 37 | - DISABLE_FAST_LINK=$DISABLE_FAST_LINK 38 | - OPENAI_SB=$OPENAI_SB 39 | -------------------------------------------------------------------------------- /app/icons/chat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/utils/cloudflare.ts: -------------------------------------------------------------------------------- 1 | export function cloudflareAIGatewayUrl(fetchUrl: string) { 2 | // rebuild fetchUrl, if using cloudflare ai gateway 3 | // document: https://developers.cloudflare.com/ai-gateway/providers/openai/ 4 | 5 | const paths = fetchUrl.split("/"); 6 | if ("gateway.ai.cloudflare.com" == paths[2]) { 7 | // is cloudflare.com ai gateway 8 | // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/azure-openai/{resource_name}/{deployment_name}/chat/completions?api-version=2023-05-15' 9 | if ("azure-openai" == paths[6]) { 10 | // is azure gateway 11 | return paths.slice(0, 8).concat(paths.slice(-3)).join("/"); // rebuild ai gateway azure_url 12 | } 13 | // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai/chat/completions 14 | if ("openai" == paths[6]) { 15 | // is openai gateway 16 | return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway openai_url 17 | } 18 | // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic/v1/messages \ 19 | if ("anthropic" == paths[6]) { 20 | // is anthropic gateway 21 | return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway anthropic_url 22 | } 23 | // TODO: Amazon Bedrock, Groq, HuggingFace... 24 | } 25 | return fetchUrl; 26 | } 27 | -------------------------------------------------------------------------------- /app/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.jpg"; 2 | declare module "*.png"; 3 | declare module "*.woff2"; 4 | declare module "*.woff"; 5 | declare module "*.ttf"; 6 | declare module "*.scss" { 7 | const content: Record; 8 | export default content; 9 | } 10 | 11 | declare module "*.svg"; 12 | 13 | declare interface Window { 14 | __TAURI__?: { 15 | writeText(text: string): Promise; 16 | invoke(command: string, payload?: Record): Promise; 17 | dialog: { 18 | save(options?: Record): Promise; 19 | }; 20 | fs: { 21 | writeBinaryFile(path: string, data: Uint8Array): Promise; 22 | writeTextFile(path: string, data: string): Promise; 23 | }; 24 | notification: { 25 | requestPermission(): Promise; 26 | isPermissionGranted(): Promise; 27 | sendNotification(options: string | Options): void; 28 | }; 29 | updater: { 30 | checkUpdate(): Promise; 31 | installUpdate(): Promise; 32 | onUpdaterEvent( 33 | handler: (status: UpdateStatusResult) => void, 34 | ): Promise; 35 | }; 36 | http: { 37 | fetch( 38 | url: string, 39 | options?: Record, 40 | ): Promise>; 41 | }; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /app/utils/indexedDB-storage.ts: -------------------------------------------------------------------------------- 1 | import { StateStorage } from "zustand/middleware"; 2 | import { get, set, del, clear } from "idb-keyval"; 3 | import { safeLocalStorage } from "@/app/utils"; 4 | 5 | const localStorage = safeLocalStorage(); 6 | 7 | class IndexedDBStorage implements StateStorage { 8 | public async getItem(name: string): Promise { 9 | try { 10 | const value = (await get(name)) || localStorage.getItem(name); 11 | return value; 12 | } catch (error) { 13 | return localStorage.getItem(name); 14 | } 15 | } 16 | 17 | public async setItem(name: string, value: string): Promise { 18 | try { 19 | const _value = JSON.parse(value); 20 | if (!_value?.state?._hasHydrated) { 21 | console.warn("skip setItem", name); 22 | return; 23 | } 24 | await set(name, value); 25 | } catch (error) { 26 | localStorage.setItem(name, value); 27 | } 28 | } 29 | 30 | public async removeItem(name: string): Promise { 31 | try { 32 | await del(name); 33 | } catch (error) { 34 | localStorage.removeItem(name); 35 | } 36 | } 37 | 38 | public async clear(): Promise { 39 | try { 40 | await clear(); 41 | } catch (error) { 42 | localStorage.clear(); 43 | } 44 | } 45 | } 46 | 47 | export const indexedDBStorage = new IndexedDBStorage(); 48 | -------------------------------------------------------------------------------- /docs/vercel-cn.md: -------------------------------------------------------------------------------- 1 | # Vercel 的使用说明 2 | 3 | ## 如何新建项目 4 | 当你从 Github fork 本项目之后,需要重新在 Vercel 创建一个全新的 Vercel 项目来重新部署,你需要按照下列步骤进行。 5 | 6 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 7 | 1. 进入 Vercel 控制台首页; 8 | 2. 点击 Add New; 9 | 3. 选择 Project。 10 | 11 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 12 | 1. 在 Import Git Repository 处,搜索 chatgpt-next-web; 13 | 2. 选中新 fork 的项目,点击 Import。 14 | 15 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 16 | 1. 在项目配置页,点开 Environmane Variables 开始配置环境变量; 17 | 2. 依次新增名为 OPENAI_API_KEY 和 CODE ([访问密码](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/357296986609c14de10bf210871d30e2f67a8784/docs/faq-cn.md#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F-code-%E6%98%AF%E4%BB%80%E4%B9%88%E5%BF%85%E9%A1%BB%E8%AE%BE%E7%BD%AE%E5%90%97)) 的环境变量; 18 | 3. 填入环境变量对应的值; 19 | 4. 点击 Add 确认增加环境变量; 20 | 5. 请确保你添加了 OPENAI_API_KEY,否则无法使用; 21 | 6. 点击 Deploy,创建完成,耐心等待 5 分钟左右部署完成。 22 | 23 | ## 如何增加自定义域名 24 | [TODO] 25 | 26 | ## 如何更改环境变量 27 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 28 | 1. 进去 Vercel 项目内部控制台,点击顶部的 Settings 按钮; 29 | 2. 点击左侧的 Environment Variables; 30 | 3. 点击已有条目的右侧按钮; 31 | 4. 选择 Edit 进行编辑,然后保存即可。 32 | 33 | ⚠️️ 注意:每次修改完环境变量,你都需要[重新部署项目](#如何重新部署)来让改动生效! 34 | 35 | ## 如何重新部署 36 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 37 | 1. 进入 Vercel 项目内部控制台,点击顶部的 Deployments 按钮; 38 | 2. 选择列表最顶部一条的右侧按钮; 39 | 3. 点击 Redeploy 即可重新部署。 40 | -------------------------------------------------------------------------------- /docs/vercel-ja.md: -------------------------------------------------------------------------------- 1 | # Vercel 使用説明書 2 | 3 | ## 新規プロジェクトの作成方法 4 | 5 | このプロジェクトを GitHub からフォークし、Vercel で新しい Vercel プロジェクトを作成して再デプロイする必要がある場合は、以下の手順に従ってください。 6 | 7 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 8 | 9 | 1. Vercel コンソールのホームページにアクセスします; 10 | 2. 新規追加をクリックする; 11 | 3. プロジェクトを選択します。 12 | 13 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 14 | 15 | 1. Git リポジトリのインポートで、chatgpt-next-web を検索します; 16 | 2 .新しいフォークプロジェクトを選択し、インポートをクリックします。 17 | 18 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 19 | 20 | 1. Project Settings ページで、Environment Variables をクリックして環境変数を設定する; 21 | 2. OPENAI_API_KEY と CODE という名前の環境変数を追加します; 22 | 3. 環境変数に対応する値を入力します; 23 | 4. Add をクリックして、環境変数の追加を確認する; 24 | 5. OPENAI_API_KEY を必ず追加してください; 25 | 6. Deploy をクリックして作成し、デプロイが完了するまで約 5 分間辛抱強く待つ。 26 | 27 | ## カスタムドメイン名の追加方法 28 | 29 | \[TODO] 30 | 31 | ## 環境変数の変更方法 32 | 33 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 34 | 35 | 1. 内部 Vercel プロジェクトコンソールに移動し、上部の設定ボタンをクリックします; 36 | 2. 左側の Environment Variables をクリックします; 37 | 3. 既存のエントリーの右側のボタンをクリックします; 38 | 4. 編集を選択して編集し、保存する。 39 | 40 | ⚠️️ 注意: [プロジェクトの再デプロイ](#再実装の方法)環境変数を変更するたびに、変更を有効にするために必要です! 41 | 42 | ## 再実装の方法 43 | 44 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 45 | 46 | 1. Vercelプロジェクトの内部コンソールに移動し、一番上のDeploymentsボタンをクリックします; 47 | 2. リストの一番上の項目の右のボタンを選択します; 48 | 3. 再デプロイをクリックして再デプロイします。 49 | -------------------------------------------------------------------------------- /public/audio-processor.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | class AudioRecorderProcessor extends AudioWorkletProcessor { 3 | constructor() { 4 | super(); 5 | this.isRecording = false; 6 | this.bufferSize = 2400; // 100ms at 24kHz 7 | this.currentBuffer = []; 8 | 9 | this.port.onmessage = (event) => { 10 | if (event.data.command === "START_RECORDING") { 11 | this.isRecording = true; 12 | } else if (event.data.command === "STOP_RECORDING") { 13 | this.isRecording = false; 14 | 15 | if (this.currentBuffer.length > 0) { 16 | this.sendBuffer(); 17 | } 18 | } 19 | }; 20 | } 21 | 22 | sendBuffer() { 23 | if (this.currentBuffer.length > 0) { 24 | const audioData = new Float32Array(this.currentBuffer); 25 | this.port.postMessage({ 26 | eventType: "audio", 27 | audioData: audioData, 28 | }); 29 | this.currentBuffer = []; 30 | } 31 | } 32 | 33 | process(inputs) { 34 | const input = inputs[0]; 35 | if (input.length > 0 && this.isRecording) { 36 | const audioData = input[0]; 37 | 38 | this.currentBuffer.push(...audioData); 39 | 40 | if (this.currentBuffer.length >= this.bufferSize) { 41 | this.sendBuffer(); 42 | } 43 | } 44 | return true; 45 | } 46 | } 47 | 48 | registerProcessor("audio-recorder-processor", AudioRecorderProcessor); 49 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-ko.md: -------------------------------------------------------------------------------- 1 | ## Cloudflare 페이지 배포 가이드 2 | 3 | ## 새 프로젝트를 만드는 방법 4 | 이 프로젝트를 Github에서 포크한 다음 dash.cloudflare.com에 로그인하고 페이지로 이동합니다. 5 | 6 | 1. "프로젝트 만들기"를 클릭합니다. 7 | 2. "Git에 연결"을 선택합니다. 8 | 3. Cloudflare 페이지를 GitHub 계정과 연결합니다. 9 | 4. 포크한 프로젝트를 선택합니다. 10 | 5. "설정 시작"을 클릭합니다. 11 | 6. "프로젝트 이름" 및 "프로덕션 브랜치"의 기본값을 사용하거나 필요에 따라 변경합니다. 12 | 7. "빌드 설정"에서 "프레임워크 프리셋" 옵션을 선택하고 "Next.js"를 선택합니다. 13 | 8. node:buffer 버그로 인해 지금은 기본 "빌드 명령어"를 사용하지 마세요. 다음 명령을 사용하세요: 14 | ``` 15 | npx @cloudflare/next-on-pages --experimental-minify 16 | ``` 17 | 9. "빌드 출력 디렉토리"의 경우 기본값을 사용하고 수정하지 마십시오. 18 | 10. "루트 디렉토리"는 수정하지 마십시오. 19 | 11. "환경 변수"의 경우 ">"를 클릭한 다음 "변수 추가"를 클릭합니다. 다음에 따라 정보를 입력합니다: 20 | 21 | - node_version=20.1`. 22 | - next_telemetry_disable=1`. 23 | - `OPENAI_API_KEY=자신의 API 키` 24 | - ``yarn_version=1.22.19`` 25 | - ``php_version=7.4``. 26 | 27 | 실제 필요에 따라 다음 옵션을 선택적으로 입력합니다: 28 | 29 | - `CODE= 선택적으로 액세스 비밀번호를 입력하며 쉼표를 사용하여 여러 비밀번호를 구분할 수 있습니다`. 30 | - `OPENAI_ORG_ID= 선택 사항, OpenAI에서 조직 ID 지정` 31 | - `HIDE_USER_API_KEY=1 선택 사항, 사용자가 API 키를 입력하지 못하도록 합니다. 32 | - `DISABLE_GPT4=1 옵션, 사용자가 GPT-4를 사용하지 못하도록 설정` 12. 33 | 34 | 12. "저장 후 배포"를 클릭합니다. 35 | 13. 호환성 플래그를 입력해야 하므로 "배포 취소"를 클릭합니다. 36 | 14. "빌드 설정", "기능"으로 이동하여 "호환성 플래그"를 찾습니다. 37 | 15. "프로덕션 호환성 플래그 구성" 및 "프리뷰 호환성 플래그 구성"에서 "nodejs_compat"를 입력합니다. 38 | 16. "배포"로 이동하여 "배포 다시 시도"를 클릭합니다. 39 | 17. 즐기세요! -------------------------------------------------------------------------------- /app/icons/llm-icons/doubao.svg: -------------------------------------------------------------------------------- 1 | 2 | Doubao 3 | 4 | 5 | 7 | 9 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /app/config/build.ts: -------------------------------------------------------------------------------- 1 | import tauriConfig from "../../src-tauri/tauri.conf.json"; 2 | import { DEFAULT_INPUT_TEMPLATE } from "../constant"; 3 | 4 | export const getBuildConfig = () => { 5 | if (typeof process === "undefined") { 6 | throw Error( 7 | "[Server Config] you are importing a nodejs-only module outside of nodejs", 8 | ); 9 | } 10 | 11 | const buildMode = process.env.BUILD_MODE ?? "standalone"; 12 | const isApp = !!process.env.BUILD_APP; 13 | const version = "v" + tauriConfig.package.version; 14 | 15 | const commitInfo = (() => { 16 | try { 17 | const childProcess = require("child_process"); 18 | const commitDate: string = childProcess 19 | .execSync('git log -1 --format="%at000" --date=unix') 20 | .toString() 21 | .trim(); 22 | const commitHash: string = childProcess 23 | .execSync('git log --pretty=format:"%H" -n 1') 24 | .toString() 25 | .trim(); 26 | 27 | return { commitDate, commitHash }; 28 | } catch (e) { 29 | console.error("[Build Config] No git or not from git repo."); 30 | return { 31 | commitDate: "unknown", 32 | commitHash: "unknown", 33 | }; 34 | } 35 | })(); 36 | 37 | return { 38 | version, 39 | ...commitInfo, 40 | buildMode, 41 | isApp, 42 | template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE, 43 | }; 44 | }; 45 | 46 | export type BuildConfig = ReturnType; 47 | -------------------------------------------------------------------------------- /app/icons/llm-icons/hunyuan.svg: -------------------------------------------------------------------------------- 1 | 2 | Hunyuan 3 | 4 | 5 | 6 | 7 | 9 | 11 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/utils/audio.ts: -------------------------------------------------------------------------------- 1 | type TTSPlayer = { 2 | init: () => void; 3 | play: (audioBuffer: ArrayBuffer, onended: () => void | null) => Promise; 4 | stop: () => void; 5 | }; 6 | 7 | export function createTTSPlayer(): TTSPlayer { 8 | let audioContext: AudioContext | null = null; 9 | let audioBufferSourceNode: AudioBufferSourceNode | null = null; 10 | 11 | const init = () => { 12 | audioContext = new (window.AudioContext || window.webkitAudioContext)(); 13 | audioContext.suspend(); 14 | }; 15 | 16 | const play = async (audioBuffer: ArrayBuffer, onended: () => void | null) => { 17 | if (audioBufferSourceNode) { 18 | audioBufferSourceNode.stop(); 19 | audioBufferSourceNode.disconnect(); 20 | } 21 | 22 | const buffer = await audioContext!.decodeAudioData(audioBuffer); 23 | audioBufferSourceNode = audioContext!.createBufferSource(); 24 | audioBufferSourceNode.buffer = buffer; 25 | audioBufferSourceNode.connect(audioContext!.destination); 26 | audioContext!.resume().then(() => { 27 | audioBufferSourceNode!.start(); 28 | }); 29 | audioBufferSourceNode.onended = onended; 30 | }; 31 | 32 | const stop = () => { 33 | if (audioBufferSourceNode) { 34 | audioBufferSourceNode.stop(); 35 | audioBufferSourceNode.disconnect(); 36 | audioBufferSourceNode = null; 37 | } 38 | if (audioContext) { 39 | audioContext.close(); 40 | audioContext = null; 41 | } 42 | }; 43 | 44 | return { init, play, stop }; 45 | } 46 | -------------------------------------------------------------------------------- /docs/vercel-ko.md: -------------------------------------------------------------------------------- 1 | # Vercel 사용 방법 2 | 3 | ## 새 프로젝트 생성 방법 4 | 이 프로젝트를 Github에서 포크한 후, 다시 배포하려면 Vercel에서 새로운 Vercel 프로젝트를 생성해야 하며, 다음 단계를 따라야 합니다. 5 | 6 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 7 | 1. Vercel 콘솔 홈 페이지로 이동합니다; 8 | 2. 새로 추가를 클릭합니다; 9 | 3. 프로젝트를 선택합니다. 10 | 11 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 12 | 1. Git 리포지토리 가져오기에서 chatgpt-next-web을 검색합니다. 13 | 2. 새로 포크된 프로젝트를 선택하고 가져오기를 클릭합니다. 14 | 15 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 16 | 1. 프로젝트 구성 페이지에서 환경 변수 설정을 클릭하여 환경 변수 설정을 시작합니다; 17 | 2. OPENAI_API_KEY, CODE ([Access Code](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/357296986609c14de10bf210871d30e2f67a8784/docs/faq-cn.md#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F-code-%E6%98%AF%E4%BB%80%E4%B9%88%E5%BF%85%E9%A1%BB%E8%AE%BE%E7%BD%AE%E5%90%97)). 환경 변수를 설정합니다; 18 | 3. 환경 변수의 값을 입력합니다; 19 | 4. 추가를 클릭하여 환경 변수 추가를 확인합니다; 20 | 5. OPENAI_API_KEY를 추가해야 하며, 그렇지 않으면 작동하지 않습니다; 21 | 6. 배포를 클릭하여 도메인 이름 생성을 완료하고 배포가 완료될 때까지 약 5분간 기다립니다. 22 | 23 | ## 사용자 정의 도메인 네임 추가 방법 24 | [TODO] 25 | 26 | ## 환경 변수 변경 방법 27 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 28 | 1. 버셀 프로젝트의 내부 콘솔로 이동하여 상단의 설정 버튼을 클릭합니다; 29 | 2. 왼쪽의 환경 변수를 클릭합니다; 30 | 3. 기존 항목 오른쪽에 있는 버튼을 클릭합니다; 31 | 4. 편집을 선택하여 수정하고 저장합니다. 32 | 33 | ⚠️️ 참고: 환경 변수를 변경할 때마다 [프로젝트를 재배포](#如何重新部署)해야 변경 사항을 적용할 수 있습니다! 34 | 35 | ## 재배포 방법 36 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 37 | 1. 버셀 내부 프로젝트 콘솔로 이동하여 상단의 배포 버튼을 클릭합니다; 38 | 2. 목록에서 맨 위 항목 오른쪽에 있는 버튼을 선택합니다; 39 | 3. 재배포를 클릭하여 재배포합니다. -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | push_to_registry: 10 | name: Push Docker image to Docker Hub 11 | runs-on: ubuntu-latest 12 | steps: 13 | - 14 | name: Check out the repo 15 | uses: actions/checkout@v3 16 | - 17 | name: Log in to Docker Hub 18 | uses: docker/login-action@v2 19 | with: 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | 23 | - 24 | name: Extract metadata (tags, labels) for Docker 25 | id: meta 26 | uses: docker/metadata-action@v4 27 | with: 28 | images: yidadaa/chatgpt-next-web 29 | tags: | 30 | type=raw,value=latest 31 | type=ref,event=tag 32 | 33 | - 34 | name: Set up QEMU 35 | uses: docker/setup-qemu-action@v2 36 | 37 | - 38 | name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v2 40 | 41 | - 42 | name: Build and push Docker image 43 | uses: docker/build-push-action@v4 44 | with: 45 | context: . 46 | platforms: linux/amd64,linux/arm64 47 | push: true 48 | tags: ${{ steps.meta.outputs.tags }} 49 | labels: ${{ steps.meta.outputs.labels }} 50 | cache-from: type=gha 51 | cache-to: type=gha,mode=max 52 | 53 | -------------------------------------------------------------------------------- /app/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-cn.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages 部署指南 2 | 3 | ## 如何新建项目 4 | 5 | 在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。 6 | 7 | 1. 点击 "Create a project"。 8 | 2. 选择 "Connect to Git"。 9 | 3. 关联 Cloudflare Pages 和你的 GitHub 账号。 10 | 4. 选中你 fork 的此项目。 11 | 5. 点击 "Begin setup"。 12 | 6. 对于 "Project name" 和 "Production branch",可以使用默认值,也可以根据需要进行更改。 13 | 7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。 14 | 8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令: 15 | ``` 16 | npx @cloudflare/next-on-pages --experimental-minify 17 | ``` 18 | 9. 对于 "Build output directory",使用默认值并且不要修改。 19 | 10. 不要修改 "Root Directory"。 20 | 11. 对于 "Environment variables",点击 ">" 然后点击 "Add variable"。按照以下信息填写: 21 | 22 | - `NODE_VERSION=20.1` 23 | - `NEXT_TELEMETRY_DISABLE=1` 24 | - `OPENAI_API_KEY=你自己的API Key` 25 | - `YARN_VERSION=1.22.19` 26 | - `PHP_VERSION=7.4` 27 | 28 | 根据实际需要,可以选择填写以下选项: 29 | 30 | - `CODE= 可选填,访问密码,可以使用逗号隔开多个密码` 31 | - `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID` 32 | - `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key` 33 | - `DISABLE_GPT4=1 可选,不让用户使用 GPT-4` 34 | - `ENABLE_BALANCE_QUERY=1 可选,启用余额查询功能` 35 | - `DISABLE_FAST_LINK=1 可选,禁用从链接解析预制设置` 36 | 37 | 12. 点击 "Save and Deploy"。 38 | 13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。 39 | 14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。 40 | 15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。 41 | 16. 前往 "Deployments",点击 "Retry deployment"。 42 | 17. Enjoy. 43 | -------------------------------------------------------------------------------- /scripts/delete-deployment-preview.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Set the pipefail option. 3 | set -o pipefail 4 | 5 | # Get the Vercel API endpoints. 6 | GET_DEPLOYMENTS_ENDPOINT="https://api.vercel.com/v6/deployments" 7 | DELETE_DEPLOYMENTS_ENDPOINT="https://api.vercel.com/v13/deployments" 8 | 9 | # Create a list of deployments. 10 | deployments=$(curl -s -X GET "$GET_DEPLOYMENTS_ENDPOINT/?projectId=$VERCEL_PROJECT_ID&teamId=$VERCEL_ORG_ID" -H "Authorization: Bearer $VERCEL_TOKEN ") 11 | #deployments=$(curl -s -X GET "$GET_DEPLOYMENTS_ENDPOINT/?projectId=$VERCEL_PROJECT_ID" -H "Authorization: Bearer $VERCEL_TOKEN ") 12 | 13 | # Filter the deployments list by meta.base_hash === meta tag. 14 | filtered_deployments=$(echo -E $deployments | jq --arg META_TAG "$META_TAG" '[.deployments[] | select(.meta.base_hash | type == "string" and contains($META_TAG)) | .uid] | join(",")') 15 | filtered_deployments="${filtered_deployments//\"/}" # Remove double quotes 16 | 17 | # Clears the values from filtered_deployments 18 | IFS=',' read -ra values <<<"$filtered_deployments" 19 | 20 | echo "META_TAG ${META_TAG}" 21 | echo "Filtered deployments ${filtered_deployments}" 22 | 23 | # Iterate over the filtered deployments list. 24 | for uid in "${values[@]}"; do 25 | echo "Deleting ${uid}" 26 | 27 | delete_url="${DELETE_DEPLOYMENTS_ENDPOINT}/${uid}?teamId=${VERCEL_ORG_ID}" 28 | echo $delete_url 29 | 30 | # Make DELETE a request to the /v13/deployments/{id} endpoint. 31 | curl -X DELETE $delete_url -H "Authorization: Bearer $VERCEL_TOKEN" 32 | 33 | echo "Deleted!" 34 | done 35 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | *.lcov 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Node.js dependencies 28 | /node_modules 29 | /jspm_packages 30 | 31 | # TypeScript v1 declaration files 32 | typings 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Output of 'npm pack' 44 | *.tgz 45 | 46 | # Yarn Integrity file 47 | .yarn-integrity 48 | 49 | # dotenv environment variable files 50 | .env 51 | .env.test 52 | 53 | # local env files 54 | .env*.local 55 | 56 | # Next.js build output 57 | .next 58 | out 59 | 60 | # Nuxt.js build output 61 | .nuxt 62 | dist 63 | 64 | # Gatsby files 65 | .cache/ 66 | 67 | 68 | # Vuepress build output 69 | .vuepress/dist 70 | 71 | # Serverless directories 72 | .serverless/ 73 | 74 | # FuseBox cache 75 | .fusebox/ 76 | 77 | # DynamoDB Local files 78 | .dynamodb/ 79 | 80 | # Temporary folders 81 | tmp 82 | temp 83 | 84 | # IDE and editor directories 85 | .idea 86 | .vscode 87 | *.swp 88 | *.swo 89 | *~ 90 | 91 | # OS generated files 92 | .DS_Store 93 | Thumbs.db 94 | 95 | # secret key 96 | *.key 97 | *.key.pub 98 | -------------------------------------------------------------------------------- /app/mcp/client.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 3 | import { MCPClientLogger } from "./logger"; 4 | import { ListToolsResponse, McpRequestMessage, ServerConfig } from "./types"; 5 | import { z } from "zod"; 6 | 7 | const logger = new MCPClientLogger(); 8 | 9 | export async function createClient( 10 | id: string, 11 | config: ServerConfig, 12 | ): Promise { 13 | logger.info(`Creating client for ${id}...`); 14 | 15 | const transport = new StdioClientTransport({ 16 | command: config.command, 17 | args: config.args, 18 | env: { 19 | ...Object.fromEntries( 20 | Object.entries(process.env) 21 | .filter(([_, v]) => v !== undefined) 22 | .map(([k, v]) => [k, v as string]), 23 | ), 24 | ...(config.env || {}), 25 | }, 26 | }); 27 | 28 | const client = new Client( 29 | { 30 | name: `nextchat-mcp-client-${id}`, 31 | version: "1.0.0", 32 | }, 33 | { 34 | capabilities: {}, 35 | }, 36 | ); 37 | await client.connect(transport); 38 | return client; 39 | } 40 | 41 | export async function removeClient(client: Client) { 42 | logger.info(`Removing client...`); 43 | await client.close(); 44 | } 45 | 46 | export async function listTools(client: Client): Promise { 47 | return client.listTools(); 48 | } 49 | 50 | export async function executeRequest( 51 | client: Client, 52 | request: McpRequestMessage, 53 | ) { 54 | return client.request(request, z.any()); 55 | } 56 | -------------------------------------------------------------------------------- /app/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/components/realtime-chat/realtime-chat.module.scss: -------------------------------------------------------------------------------- 1 | .realtime-chat { 2 | width: 100%; 3 | justify-content: center; 4 | align-items: center; 5 | position: relative; 6 | display: flex; 7 | flex-direction: column; 8 | height: 100%; 9 | padding: 20px; 10 | box-sizing: border-box; 11 | .circle-mic { 12 | width: 150px; 13 | height: 150px; 14 | border-radius: 50%; 15 | background: linear-gradient(to bottom right, #a0d8ef, #f0f8ff); 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | .icon-center { 21 | font-size: 24px; 22 | } 23 | 24 | .bottom-icons { 25 | display: flex; 26 | justify-content: space-between; 27 | align-items: center; 28 | width: 100%; 29 | position: absolute; 30 | bottom: 20px; 31 | box-sizing: border-box; 32 | padding: 0 20px; 33 | } 34 | 35 | .icon-left, 36 | .icon-right { 37 | width: 46px; 38 | height: 46px; 39 | font-size: 36px; 40 | background: var(--second); 41 | border-radius: 50%; 42 | padding: 2px; 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | cursor: pointer; 47 | &:hover { 48 | opacity: 0.8; 49 | } 50 | } 51 | 52 | &.mobile { 53 | display: none; 54 | } 55 | } 56 | 57 | .pulse { 58 | animation: pulse 1.5s infinite; 59 | } 60 | 61 | @keyframes pulse { 62 | 0% { 63 | transform: scale(1); 64 | opacity: 0.7; 65 | } 66 | 50% { 67 | transform: scale(1.1); 68 | opacity: 1; 69 | } 70 | 100% { 71 | transform: scale(1); 72 | opacity: 0.7; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/icons/brain.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nextchat" 3 | version = "0.1.0" 4 | description = "A cross platform app for LLM ChatBot." 5 | authors = ["Yidadaa"] 6 | license = "mit" 7 | repository = "" 8 | default-run = "nextchat" 9 | edition = "2021" 10 | rust-version = "1.60" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.5.1", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.5.4", features = [ "http-all", 21 | "notification-all", 22 | "fs-all", 23 | "clipboard-all", 24 | "dialog-all", 25 | "shell-open", 26 | "updater", 27 | "window-close", 28 | "window-hide", 29 | "window-maximize", 30 | "window-minimize", 31 | "window-set-icon", 32 | "window-set-ignore-cursor-events", 33 | "window-set-resizable", 34 | "window-show", 35 | "window-start-dragging", 36 | "window-unmaximize", 37 | "window-unminimize", 38 | ] } 39 | tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } 40 | percent-encoding = "2.3.1" 41 | reqwest = "0.11.18" 42 | futures-util = "0.3.30" 43 | bytes = "1.7.2" 44 | 45 | [features] 46 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. 47 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. 48 | # DO NOT REMOVE!! 49 | custom-protocol = ["tauri/custom-protocol"] 50 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-ja.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages 導入ガイド 2 | 3 | ## 新規プロジェクトの作成方法 4 | GitHub でこのプロジェクトをフォークし、dash.cloudflare.com にログインして Pages にアクセスします。 5 | 6 | 1. "Create a project" をクリックする。 7 | 2. "Connect to Git" を選択する。 8 | 3. Cloudflare Pages を GitHub アカウントに接続します。 9 | 4. フォークしたプロジェクトを選択します。 10 | 5. "Begin setup" をクリックする。 11 | 6. "Project name" と "Production branch" はデフォルト値を使用するか、必要に応じて変更してください。 12 | 7. "Build Settings" で、"Framework presets" オプションを選択し、"Next.js" を選択します。 13 | 8. node:buffer のバグのため、デフォルトの "Build command" は使用しないでください。代わりに、以下のコマンドを使用してください: 14 | ``` 15 | npx @cloudflare/next-on-pages --experimental-minify 16 | ``` 17 | 9. "Build output directory" はデフォルト値を使用し、変更しない。 18 | 10. "Root Directory" を変更しない。 19 | 11. "Environment variables" は、">" をクリックし、"Add variable" をクリックします。そして以下の情報を入力します: 20 | - `NODE_VERSION=20.1` 21 | - `NEXT_TELEMETRY_DISABLE=1` 22 | - `OPENAI_API_KEY=your_own_API_key` 23 | - `YARN_VERSION=1.22.19` 24 | - `PHP_VERSION=7.4` 25 | 26 | 必要に応じて、以下の項目を入力してください: 27 | 28 | - `CODE= Optional, access passwords, multiple passwords can be separated by commas` 29 | - `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI` 30 | - `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key` 31 | - `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4` 32 | 33 | 12. "Save and Deploy" をクリックする。 34 | 13. 互換性フラグを記入する必要があるため、"Cancel deployment" をクリックする。 35 | 14. "Build settings" の "Functions" から "Compatibility flags" を見つける。 36 | 15. "Configure Production compatibility flag" と "Configure Preview compatibility flag" の両方に "nodejs_compat "を記入する。 37 | 16. "Deployments" に移動し、"Retry deployment" をクリックします。 38 | 17. お楽しみください。 39 | -------------------------------------------------------------------------------- /app/components/settings.module.scss: -------------------------------------------------------------------------------- 1 | .settings { 2 | padding: 20px; 3 | overflow: auto; 4 | } 5 | 6 | .avatar { 7 | cursor: pointer; 8 | position: relative; 9 | z-index: 1; 10 | } 11 | 12 | .edit-prompt-modal { 13 | display: flex; 14 | flex-direction: column; 15 | 16 | .edit-prompt-title { 17 | max-width: unset; 18 | margin-bottom: 20px; 19 | text-align: left; 20 | } 21 | .edit-prompt-content { 22 | max-width: unset; 23 | } 24 | } 25 | 26 | .user-prompt-modal { 27 | min-height: 40vh; 28 | 29 | .user-prompt-search { 30 | width: 100%; 31 | max-width: 100%; 32 | margin-bottom: 10px; 33 | background-color: var(--gray); 34 | } 35 | 36 | .user-prompt-list { 37 | border: var(--border-in-light); 38 | border-radius: 10px; 39 | 40 | .user-prompt-item { 41 | display: flex; 42 | justify-content: space-between; 43 | padding: 10px; 44 | 45 | &:not(:last-child) { 46 | border-bottom: var(--border-in-light); 47 | } 48 | 49 | .user-prompt-header { 50 | max-width: calc(100% - 100px); 51 | 52 | .user-prompt-title { 53 | font-size: 14px; 54 | line-height: 2; 55 | font-weight: bold; 56 | } 57 | .user-prompt-content { 58 | font-size: 12px; 59 | } 60 | } 61 | 62 | .user-prompt-buttons { 63 | display: flex; 64 | align-items: center; 65 | column-gap: 2px; 66 | 67 | .user-prompt-button { 68 | //height: 100%; 69 | padding: 7px; 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | .subtitle-button { 77 | button { 78 | overflow:visible ; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/components/message-selector.module.scss: -------------------------------------------------------------------------------- 1 | .message-selector { 2 | .message-filter { 3 | display: flex; 4 | 5 | .search-bar { 6 | max-width: unset; 7 | flex-grow: 1; 8 | margin-right: 10px; 9 | } 10 | 11 | .actions { 12 | display: flex; 13 | 14 | button:not(:last-child) { 15 | margin-right: 10px; 16 | } 17 | } 18 | 19 | @media screen and (max-width: 600px) { 20 | flex-direction: column; 21 | 22 | .search-bar { 23 | margin-right: 0; 24 | } 25 | 26 | .actions { 27 | margin-top: 20px; 28 | 29 | button { 30 | flex-grow: 1; 31 | } 32 | } 33 | } 34 | } 35 | 36 | .messages { 37 | margin-top: 20px; 38 | border-radius: 10px; 39 | border: var(--border-in-light); 40 | overflow: hidden; 41 | 42 | .message { 43 | display: flex; 44 | align-items: center; 45 | padding: 8px 10px; 46 | cursor: pointer; 47 | 48 | &-selected { 49 | background-color: var(--second); 50 | } 51 | 52 | &:not(:last-child) { 53 | border-bottom: var(--border-in-light); 54 | } 55 | 56 | .avatar { 57 | margin-right: 10px; 58 | } 59 | 60 | .body { 61 | flex: 1; 62 | max-width: calc(100% - 80px); 63 | 64 | .date { 65 | font-size: 12px; 66 | line-height: 1.2; 67 | opacity: 0.5; 68 | } 69 | 70 | .content { 71 | font-size: 12px; 72 | } 73 | } 74 | 75 | .checkbox { 76 | display: flex; 77 | justify-content: flex-end; 78 | flex: 1; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/mcp/logger.ts: -------------------------------------------------------------------------------- 1 | // ANSI color codes for terminal output 2 | const colors = { 3 | reset: "\x1b[0m", 4 | bright: "\x1b[1m", 5 | dim: "\x1b[2m", 6 | green: "\x1b[32m", 7 | yellow: "\x1b[33m", 8 | red: "\x1b[31m", 9 | blue: "\x1b[34m", 10 | }; 11 | 12 | export class MCPClientLogger { 13 | private readonly prefix: string; 14 | private readonly debugMode: boolean; 15 | 16 | constructor( 17 | prefix: string = "NextChat MCP Client", 18 | debugMode: boolean = false, 19 | ) { 20 | this.prefix = prefix; 21 | this.debugMode = debugMode; 22 | } 23 | 24 | info(message: any) { 25 | this.print(colors.blue, message); 26 | } 27 | 28 | success(message: any) { 29 | this.print(colors.green, message); 30 | } 31 | 32 | error(message: any) { 33 | this.print(colors.red, message); 34 | } 35 | 36 | warn(message: any) { 37 | this.print(colors.yellow, message); 38 | } 39 | 40 | debug(message: any) { 41 | if (this.debugMode) { 42 | this.print(colors.dim, message); 43 | } 44 | } 45 | 46 | /** 47 | * Format message to string, if message is object, convert to JSON string 48 | */ 49 | private formatMessage(message: any): string { 50 | return typeof message === "object" 51 | ? JSON.stringify(message, null, 2) 52 | : message; 53 | } 54 | 55 | /** 56 | * Print formatted message to console 57 | */ 58 | private print(color: string, message: any) { 59 | const formattedMessage = this.formatMessage(message); 60 | const logMessage = `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`; 61 | 62 | // 只使用 console.log,这样日志会显示在 Tauri 的终端中 63 | console.log(logMessage); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/components/button.module.scss: -------------------------------------------------------------------------------- 1 | .icon-button { 2 | background-color: var(--white); 3 | border-radius: 10px; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | padding: 10px; 8 | cursor: pointer; 9 | transition: all 0.3s ease; 10 | overflow: hidden; 11 | user-select: none; 12 | outline: none; 13 | border: none; 14 | color: var(--black); 15 | 16 | &[disabled] { 17 | cursor: not-allowed; 18 | opacity: 0.5; 19 | } 20 | 21 | &.primary { 22 | background-color: var(--primary); 23 | color: white; 24 | 25 | path { 26 | fill: white !important; 27 | } 28 | } 29 | 30 | &.danger { 31 | color: rgba($color: red, $alpha: 0.8); 32 | border-color: rgba($color: red, $alpha: 0.5); 33 | background-color: rgba($color: red, $alpha: 0.05); 34 | 35 | &:hover { 36 | border-color: red; 37 | background-color: rgba($color: red, $alpha: 0.1); 38 | } 39 | 40 | path { 41 | fill: red !important; 42 | } 43 | } 44 | 45 | &:hover, 46 | &:focus { 47 | border-color: var(--primary); 48 | } 49 | } 50 | 51 | .shadow { 52 | box-shadow: var(--card-shadow); 53 | } 54 | 55 | .border { 56 | border: var(--border-in-light); 57 | } 58 | 59 | .icon-button-icon { 60 | width: 16px; 61 | height: 16px; 62 | display: flex; 63 | justify-content: center; 64 | align-items: center; 65 | } 66 | 67 | @media only screen and (max-width: 600px) { 68 | .icon-button { 69 | padding: 16px; 70 | } 71 | } 72 | 73 | .icon-button-text { 74 | font-size: 12px; 75 | overflow: hidden; 76 | text-overflow: ellipsis; 77 | white-space: nowrap; 78 | 79 | &:not(:first-child) { 80 | margin-left: 5px; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/icons/eye-off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Upstream Sync 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | schedule: 8 | - cron: "0 0 * * *" # every day 9 | workflow_dispatch: 10 | 11 | jobs: 12 | sync_latest_from_upstream: 13 | name: Sync latest commits from upstream repo 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event.repository.fork }} 16 | 17 | steps: 18 | # Step 1: run a standard checkout action 19 | - name: Checkout target repo 20 | uses: actions/checkout@v3 21 | 22 | # Step 2: run the sync action 23 | - name: Sync upstream changes 24 | id: sync 25 | uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 26 | with: 27 | upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web 28 | upstream_sync_branch: main 29 | target_sync_branch: main 30 | target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set 31 | 32 | # Set test_mode true to run tests instead of the true action!! 33 | test_mode: false 34 | 35 | - name: Sync check 36 | if: failure() 37 | run: | 38 | echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" 39 | echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates" 40 | exit 1 41 | -------------------------------------------------------------------------------- /app/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import styles from "./button.module.scss"; 4 | import { CSSProperties } from "react"; 5 | import clsx from "clsx"; 6 | 7 | export type ButtonType = "primary" | "danger" | null; 8 | 9 | export function IconButton(props: { 10 | onClick?: () => void; 11 | icon?: JSX.Element; 12 | type?: ButtonType; 13 | text?: string; 14 | bordered?: boolean; 15 | shadow?: boolean; 16 | className?: string; 17 | title?: string; 18 | disabled?: boolean; 19 | tabIndex?: number; 20 | autoFocus?: boolean; 21 | style?: CSSProperties; 22 | aria?: string; 23 | }) { 24 | return ( 25 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /app/icons/llm-icons/openai.svg: -------------------------------------------------------------------------------- 1 | 3 | OpenAI 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/icons/llm-icons/claude.svg: -------------------------------------------------------------------------------- 1 | 2 | Claude 3 | 4 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /app/icons/tool.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/llm-icons/wenxin.svg: -------------------------------------------------------------------------------- 1 | 2 | Wenxin 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/icons/max.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS base 2 | 3 | FROM base AS deps 4 | 5 | RUN apk add --no-cache libc6-compat 6 | 7 | WORKDIR /app 8 | 9 | COPY package.json yarn.lock ./ 10 | 11 | RUN yarn config set registry 'https://registry.npmmirror.com/' 12 | RUN yarn install 13 | 14 | FROM base AS builder 15 | 16 | RUN apk update && apk add --no-cache git 17 | 18 | ENV OPENAI_API_KEY="" 19 | ENV GOOGLE_API_KEY="" 20 | ENV CODE="" 21 | 22 | WORKDIR /app 23 | COPY --from=deps /app/node_modules ./node_modules 24 | COPY . . 25 | 26 | RUN yarn build 27 | 28 | FROM base AS runner 29 | WORKDIR /app 30 | 31 | RUN apk add proxychains-ng 32 | 33 | ENV PROXY_URL="" 34 | ENV OPENAI_API_KEY="" 35 | ENV GOOGLE_API_KEY="" 36 | ENV CODE="" 37 | ENV ENABLE_MCP="" 38 | 39 | COPY --from=builder /app/public ./public 40 | COPY --from=builder /app/.next/standalone ./ 41 | COPY --from=builder /app/.next/static ./.next/static 42 | COPY --from=builder /app/.next/server ./.next/server 43 | 44 | RUN mkdir -p /app/app/mcp && chmod 777 /app/app/mcp 45 | COPY --from=builder /app/app/mcp/mcp_config.default.json /app/app/mcp/mcp_config.json 46 | 47 | EXPOSE 3000 48 | 49 | CMD if [ -n "$PROXY_URL" ]; then \ 50 | export HOSTNAME="0.0.0.0"; \ 51 | protocol=$(echo $PROXY_URL | cut -d: -f1); \ 52 | host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ 53 | port=$(echo $PROXY_URL | cut -d: -f3); \ 54 | conf=/etc/proxychains.conf; \ 55 | echo "strict_chain" > $conf; \ 56 | echo "proxy_dns" >> $conf; \ 57 | echo "remote_dns_subnet 224" >> $conf; \ 58 | echo "tcp_read_time_out 15000" >> $conf; \ 59 | echo "tcp_connect_time_out 8000" >> $conf; \ 60 | echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \ 61 | echo "localnet ::1/128" >> $conf; \ 62 | echo "[ProxyList]" >> $conf; \ 63 | echo "$protocol $host $port" >> $conf; \ 64 | cat /etc/proxychains.conf; \ 65 | proxychains -f $conf node server.js; \ 66 | else \ 67 | node server.js; \ 68 | fi 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_bug_report_cn.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 反馈缺陷' 2 | description: '反馈一个问题/缺陷' 3 | title: '[Bug] ' 4 | labels: ['bug'] 5 | body: 6 | - type: dropdown 7 | attributes: 8 | label: '📦 部署方式' 9 | multiple: true 10 | options: 11 | - '官方安装包' 12 | - 'Vercel' 13 | - 'Zeabur' 14 | - 'Sealos' 15 | - 'Netlify' 16 | - 'Docker' 17 | - 'Other' 18 | validations: 19 | required: true 20 | - type: input 21 | attributes: 22 | label: '📌 软件版本' 23 | validations: 24 | required: true 25 | 26 | - type: dropdown 27 | attributes: 28 | label: '💻 系统环境' 29 | multiple: true 30 | options: 31 | - 'Windows' 32 | - 'macOS' 33 | - 'Ubuntu' 34 | - 'Other Linux' 35 | - 'iOS' 36 | - 'iPad OS' 37 | - 'Android' 38 | - 'Other' 39 | validations: 40 | required: true 41 | - type: input 42 | attributes: 43 | label: '📌 系统版本' 44 | validations: 45 | required: true 46 | - type: dropdown 47 | attributes: 48 | label: '🌐 浏览器' 49 | multiple: true 50 | options: 51 | - 'Chrome' 52 | - 'Edge' 53 | - 'Safari' 54 | - 'Firefox' 55 | - 'Other' 56 | validations: 57 | required: true 58 | - type: input 59 | attributes: 60 | label: '📌 浏览器版本' 61 | validations: 62 | required: true 63 | - type: textarea 64 | attributes: 65 | label: '🐛 问题描述' 66 | description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。 67 | validations: 68 | required: true 69 | - type: textarea 70 | attributes: 71 | label: '📷 复现步骤' 72 | description: 请提供一个清晰且简洁的描述,说明如何复现问题。 73 | - type: textarea 74 | attributes: 75 | label: '🚦 期望结果' 76 | description: 请提供一个清晰且简洁的描述,说明您期望发生什么。 77 | - type: textarea 78 | attributes: 79 | label: '📝 补充信息' 80 | description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。 -------------------------------------------------------------------------------- /app/icons/min.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/mask.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/vision-model-checker.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from "@jest/globals"; 2 | import { isVisionModel } from "../app/utils"; 3 | 4 | describe("isVisionModel", () => { 5 | const originalEnv = process.env; 6 | 7 | beforeEach(() => { 8 | jest.resetModules(); 9 | process.env = { ...originalEnv }; 10 | }); 11 | 12 | afterEach(() => { 13 | process.env = originalEnv; 14 | }); 15 | 16 | test("should identify vision models using regex patterns", () => { 17 | const visionModels = [ 18 | "gpt-4-vision", 19 | "claude-3-opus", 20 | "gemini-1.5-pro", 21 | "gemini-2.0", 22 | "gemini-exp-vision", 23 | "learnlm-vision", 24 | "qwen-vl-max", 25 | "qwen2-vl-max", 26 | "gpt-4-turbo", 27 | "dall-e-3", 28 | ]; 29 | 30 | visionModels.forEach((model) => { 31 | expect(isVisionModel(model)).toBe(true); 32 | }); 33 | }); 34 | 35 | test("should exclude specific models", () => { 36 | expect(isVisionModel("claude-3-5-haiku-20241022")).toBe(false); 37 | }); 38 | 39 | test("should not identify non-vision models", () => { 40 | const nonVisionModels = [ 41 | "gpt-3.5-turbo", 42 | "gpt-4-turbo-preview", 43 | "claude-2", 44 | "regular-model", 45 | ]; 46 | 47 | nonVisionModels.forEach((model) => { 48 | expect(isVisionModel(model)).toBe(false); 49 | }); 50 | }); 51 | 52 | test("should identify models from VISION_MODELS env var", () => { 53 | process.env.VISION_MODELS = "custom-vision-model,another-vision-model"; 54 | 55 | expect(isVisionModel("custom-vision-model")).toBe(true); 56 | expect(isVisionModel("another-vision-model")).toBe(true); 57 | expect(isVisionModel("unrelated-model")).toBe(false); 58 | }); 59 | 60 | test("should handle empty or missing VISION_MODELS", () => { 61 | process.env.VISION_MODELS = ""; 62 | expect(isVisionModel("unrelated-model")).toBe(false); 63 | 64 | delete process.env.VISION_MODELS; 65 | expect(isVisionModel("unrelated-model")).toBe(false); 66 | expect(isVisionModel("gpt-4-vision")).toBe(true); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-en.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages Deployment Guide 2 | 3 | ## How to create a new project 4 | 5 | Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. 6 | 7 | 1. Click "Create a project". 8 | 2. Choose "Connect to Git". 9 | 3. Connect Cloudflare Pages to your GitHub account. 10 | 4. Select the forked project. 11 | 5. Click "Begin setup". 12 | 6. For "Project name" and "Production branch", use the default values or change them as needed. 13 | 7. In "Build Settings", choose the "Framework presets" option and select "Next.js". 14 | 8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command: 15 | ``` 16 | npx @cloudflare/next-on-pages --experimental-minify 17 | ``` 18 | 9. For "Build output directory", use the default value and do not modify it. 19 | 10. Do not modify "Root Directory". 20 | 11. For "Environment variables", click ">" and then "Add variable". Fill in the following information: 21 | 22 | - `NODE_VERSION=20.1` 23 | - `NEXT_TELEMETRY_DISABLE=1` 24 | - `OPENAI_API_KEY=your_own_API_key` 25 | - `YARN_VERSION=1.22.19` 26 | - `PHP_VERSION=7.4` 27 | 28 | Optionally fill in the following based on your needs: 29 | 30 | - `CODE= Optional, access passwords, multiple passwords can be separated by commas` 31 | - `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI` 32 | - `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key` 33 | - `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4` 34 | - `ENABLE_BALANCE_QUERY=1 Optional, allow users to query balance` 35 | - `DISABLE_FAST_LINK=1 Optional, disable parse settings from url` 36 | - `OPENAI_SB=1 Optional,use the third-party OpenAI-SB API` 37 | 38 | 12. Click "Save and Deploy". 39 | 13. Click "Cancel deployment" because you need to fill in Compatibility flags. 40 | 14. Go to "Build settings", "Functions", and find "Compatibility flags". 41 | 15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag". 42 | 16. Go to "Deployments" and click "Retry deployment". 43 | 17. Enjoy. 44 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-es.md: -------------------------------------------------------------------------------- 1 | # Guía de implementación de Cloudflare Pages 2 | 3 | ## Cómo crear un nuevo proyecto 4 | 5 | Bifurca el proyecto en Github, luego inicia sesión en dash.cloudflare.com y ve a Pages. 6 | 7 | 1. Haga clic en "Crear un proyecto". 8 | 2. Selecciona Conectar a Git. 9 | 3. Vincula páginas de Cloudflare a tu cuenta de GitHub. 10 | 4. Seleccione este proyecto que bifurcó. 11 | 5. Haga clic en "Comenzar configuración". 12 | 6. Para "Nombre del proyecto" y "Rama de producción", puede utilizar los valores predeterminados o cambiarlos según sea necesario. 13 | 7. En Configuración de compilación, seleccione la opción Ajustes preestablecidos de Framework y seleccione Siguiente.js. 14 | 8. Debido a los errores de node:buffer, no use el "comando Construir" predeterminado por ahora. Utilice el siguiente comando: 15 | ``` 16 | npx @cloudflare/next-on-pages --experimental-minify 17 | ``` 18 | 9. Para "Generar directorio de salida", utilice los valores predeterminados y no los modifique. 19 | 10. No modifique el "Directorio raíz". 20 | 11. Para "Variables de entorno", haga clic en ">" y luego haga clic en "Agregar variable". Rellene la siguiente información: 21 | 22 | * `NODE_VERSION=20.1` 23 | * `NEXT_TELEMETRY_DISABLE=1` 24 | * `OPENAI_API_KEY=你自己的API Key` 25 | * `YARN_VERSION=1.22.19` 26 | * `PHP_VERSION=7.4` 27 | 28 | Dependiendo de sus necesidades reales, puede completar opcionalmente las siguientes opciones: 29 | 30 | * `CODE= 可选填,访问密码,可以使用逗号隔开多个密码` 31 | * `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID` 32 | * `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key` 33 | * `DISABLE_GPT4=1 可选,不让用户使用 GPT-4` 34 | 12. Haga clic en "Guardar e implementar". 35 | 13. Haga clic en "Cancelar implementación" porque necesita rellenar los indicadores de compatibilidad. 36 | 14. Vaya a "Configuración de compilación", "Funciones" y busque "Indicadores de compatibilidad". 37 | 15. Rellene "nodejs_compat" en "Configurar indicador de compatibilidad de producción" y "Configurar indicador de compatibilidad de vista previa". 38 | 16. Vaya a "Implementaciones" y haga clic en "Reintentar implementación". 39 | 17. Disfrutar. 40 | -------------------------------------------------------------------------------- /app/icons/image.svg: -------------------------------------------------------------------------------- 1 | Layer 1 -------------------------------------------------------------------------------- /app/api/upstash/[action]/[...key]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | async function handle( 4 | req: NextRequest, 5 | { params }: { params: { action: string; key: string[] } }, 6 | ) { 7 | const requestUrl = new URL(req.url); 8 | const endpoint = requestUrl.searchParams.get("endpoint"); 9 | 10 | if (req.method === "OPTIONS") { 11 | return NextResponse.json({ body: "OK" }, { status: 200 }); 12 | } 13 | const [...key] = params.key; 14 | // only allow to request to *.upstash.io 15 | if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) { 16 | return NextResponse.json( 17 | { 18 | error: true, 19 | msg: "you are not allowed to request " + params.key.join("/"), 20 | }, 21 | { 22 | status: 403, 23 | }, 24 | ); 25 | } 26 | 27 | // only allow upstash get and set method 28 | if (params.action !== "get" && params.action !== "set") { 29 | console.log("[Upstash Route] forbidden action ", params.action); 30 | return NextResponse.json( 31 | { 32 | error: true, 33 | msg: "you are not allowed to request " + params.action, 34 | }, 35 | { 36 | status: 403, 37 | }, 38 | ); 39 | } 40 | 41 | const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`; 42 | 43 | const method = req.method; 44 | const shouldNotHaveBody = ["get", "head"].includes( 45 | method?.toLowerCase() ?? "", 46 | ); 47 | 48 | const fetchOptions: RequestInit = { 49 | headers: { 50 | authorization: req.headers.get("authorization") ?? "", 51 | }, 52 | body: shouldNotHaveBody ? null : req.body, 53 | method, 54 | // @ts-ignore 55 | duplex: "half", 56 | }; 57 | 58 | console.log("[Upstash Proxy]", targetUrl, fetchOptions); 59 | const fetchResult = await fetch(targetUrl, fetchOptions); 60 | 61 | console.log("[Any Proxy]", targetUrl, { 62 | status: fetchResult.status, 63 | statusText: fetchResult.statusText, 64 | }); 65 | 66 | return fetchResult; 67 | } 68 | 69 | export const POST = handle; 70 | export const GET = handle; 71 | export const OPTIONS = handle; 72 | 73 | export const runtime = "edge"; 74 | -------------------------------------------------------------------------------- /app/icons/confirm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-page-custom-font */ 2 | import "./styles/globals.scss"; 3 | import "./styles/markdown.scss"; 4 | import "./styles/highlight.scss"; 5 | import { getClientConfig } from "./config/client"; 6 | import type { Metadata, Viewport } from "next"; 7 | import { SpeedInsights } from "@vercel/speed-insights/next"; 8 | import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google"; 9 | import { getServerSideConfig } from "./config/server"; 10 | 11 | export const metadata: Metadata = { 12 | title: "NextChat", 13 | description: "Your personal ChatGPT Chat Bot.", 14 | appleWebApp: { 15 | title: "NextChat", 16 | statusBarStyle: "default", 17 | }, 18 | }; 19 | 20 | export const viewport: Viewport = { 21 | width: "device-width", 22 | initialScale: 1, 23 | maximumScale: 1, 24 | themeColor: [ 25 | { media: "(prefers-color-scheme: light)", color: "#fafafa" }, 26 | { media: "(prefers-color-scheme: dark)", color: "#151515" }, 27 | ], 28 | }; 29 | 30 | export default function RootLayout({ 31 | children, 32 | }: { 33 | children: React.ReactNode; 34 | }) { 35 | const serverConfig = getServerSideConfig(); 36 | 37 | return ( 38 | 39 | 40 | 41 | 45 | 50 | 51 | 52 | 53 | {children} 54 | {serverConfig?.isVercel && ( 55 | <> 56 | 57 | 58 | )} 59 | {serverConfig?.gtmId && ( 60 | <> 61 | 62 | 63 | )} 64 | {serverConfig?.gaId && ( 65 | <> 66 | 67 | 68 | )} 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/icons/llm-icons/qwen.svg: -------------------------------------------------------------------------------- 1 | 2 | Qwen 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /app/icons/llm-icons/deepseek.svg: -------------------------------------------------------------------------------- 1 | 2 | DeepSeek 3 | 4 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /docs/vercel-es.md: -------------------------------------------------------------------------------- 1 | # Instrucciones de uso de Verbel 2 | 3 | ## Cómo crear un nuevo proyecto 4 | 5 | Cuando bifurca este proyecto desde Github y necesita crear un nuevo proyecto de Vercel en Vercel para volver a implementarlo, debe seguir los pasos a continuación. 6 | 7 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 8 | 9 | 1. Vaya a la página de inicio de la consola de Vercel; 10 | 2. Haga clic en Agregar nuevo; 11 | 3. Seleccione Proyecto. 12 | 13 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 14 | 15 | 1. En Import Git Repository, busque chatgpt-next-web; 16 | 2. Seleccione el proyecto de la nueva bifurcación y haga clic en Importar. 17 | 18 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 19 | 20 | 1. En la página de configuración del proyecto, haga clic en Variables de entorno para configurar las variables de entorno; 21 | 2. Agregar variables de entorno denominadas OPENAI_API_KEY y CODE; 22 | 3. Rellenar los valores correspondientes a las variables de entorno; 23 | 4. Haga clic en Agregar para confirmar la adición de variables de entorno; 24 | 5. Asegúrese de agregar OPENAI_API_KEY, de lo contrario no funcionará; 25 | 6. Haga clic en Implementar, créelo y espere pacientemente unos 5 minutos a que se complete la implementación. 26 | 27 | ## Cómo agregar un nombre de dominio personalizado 28 | 29 | \[TODO] 30 | 31 | ## Cómo cambiar las variables de entorno 32 | 33 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 34 | 35 | 1. Vaya a la consola interna del proyecto Vercel y haga clic en el botón Configuración en la parte superior; 36 | 2. Haga clic en Variables de entorno a la izquierda; 37 | 3. Haga clic en el botón a la derecha de una entrada existente; 38 | 4. Seleccione Editar para editarlo y, a continuación, guárdelo. 39 | 40 | ⚠️️ Nota: Lo necesita cada vez que modifique las variables de entorno[Volver a implementar el proyecto](#如何重新部署)para que los cambios surtan efecto! 41 | 42 | ## Cómo volver a implementar 43 | 44 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 45 | 46 | 1. Vaya a la consola interna del proyecto Vercel y haga clic en el botón Implementaciones en la parte superior; 47 | 2. Seleccione el botón derecho del artículo superior de la lista; 48 | 3. Haga clic en Volver a implementar para volver a implementar. 49 | -------------------------------------------------------------------------------- /public/serviceWorker.js: -------------------------------------------------------------------------------- 1 | const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache"; 2 | const CHATGPT_NEXT_WEB_FILE_CACHE = "chatgpt-next-web-file"; 3 | let a="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";let nanoid=(e=21)=>{let t="",r=crypto.getRandomValues(new Uint8Array(e));for(let n=0;n { 50 | const url = new URL(e.request.url); 51 | if (/^\/api\/cache/.test(url.pathname)) { 52 | if ('GET' == e.request.method) { 53 | e.respondWith(caches.match(e.request)) 54 | } 55 | if ('POST' == e.request.method) { 56 | e.respondWith(upload(e.request, url)) 57 | } 58 | if ('DELETE' == e.request.method) { 59 | e.respondWith(remove(e.request, url)) 60 | } 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /app/command.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useSearchParams } from "react-router-dom"; 3 | import Locale from "./locales"; 4 | 5 | type Command = (param: string) => void; 6 | interface Commands { 7 | fill?: Command; 8 | submit?: Command; 9 | mask?: Command; 10 | code?: Command; 11 | settings?: Command; 12 | } 13 | 14 | export function useCommand(commands: Commands = {}) { 15 | const [searchParams, setSearchParams] = useSearchParams(); 16 | 17 | useEffect(() => { 18 | let shouldUpdate = false; 19 | searchParams.forEach((param, name) => { 20 | const commandName = name as keyof Commands; 21 | if (typeof commands[commandName] === "function") { 22 | commands[commandName]!(param); 23 | searchParams.delete(name); 24 | shouldUpdate = true; 25 | } 26 | }); 27 | 28 | if (shouldUpdate) { 29 | setSearchParams(searchParams); 30 | } 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, [searchParams, commands]); 33 | } 34 | 35 | interface ChatCommands { 36 | new?: Command; 37 | newm?: Command; 38 | next?: Command; 39 | prev?: Command; 40 | clear?: Command; 41 | fork?: Command; 42 | del?: Command; 43 | } 44 | 45 | // Compatible with Chinese colon character ":" 46 | export const ChatCommandPrefix = /^[::]/; 47 | 48 | export function useChatCommand(commands: ChatCommands = {}) { 49 | function extract(userInput: string) { 50 | const match = userInput.match(ChatCommandPrefix); 51 | if (match) { 52 | return userInput.slice(1) as keyof ChatCommands; 53 | } 54 | return userInput as keyof ChatCommands; 55 | } 56 | 57 | function search(userInput: string) { 58 | const input = extract(userInput); 59 | const desc = Locale.Chat.Commands; 60 | return Object.keys(commands) 61 | .filter((c) => c.startsWith(input)) 62 | .map((c) => ({ 63 | title: desc[c as keyof ChatCommands], 64 | content: ":" + c, 65 | })); 66 | } 67 | 68 | function match(userInput: string) { 69 | const command = extract(userInput); 70 | const matched = typeof commands[command] === "function"; 71 | 72 | return { 73 | matched, 74 | invoke: () => matched && commands[command]!(userInput), 75 | }; 76 | } 77 | 78 | return { match, search }; 79 | } 80 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 Bug Report' 2 | description: 'Report an bug' 3 | title: '[Bug] ' 4 | labels: ['bug'] 5 | body: 6 | - type: dropdown 7 | attributes: 8 | label: '📦 Deployment Method' 9 | multiple: true 10 | options: 11 | - 'Official installation package' 12 | - 'Vercel' 13 | - 'Zeabur' 14 | - 'Sealos' 15 | - 'Netlify' 16 | - 'Docker' 17 | - 'Other' 18 | validations: 19 | required: true 20 | - type: input 21 | attributes: 22 | label: '📌 Version' 23 | validations: 24 | required: true 25 | 26 | - type: dropdown 27 | attributes: 28 | label: '💻 Operating System' 29 | multiple: true 30 | options: 31 | - 'Windows' 32 | - 'macOS' 33 | - 'Ubuntu' 34 | - 'Other Linux' 35 | - 'iOS' 36 | - 'iPad OS' 37 | - 'Android' 38 | - 'Other' 39 | validations: 40 | required: true 41 | - type: input 42 | attributes: 43 | label: '📌 System Version' 44 | validations: 45 | required: true 46 | - type: dropdown 47 | attributes: 48 | label: '🌐 Browser' 49 | multiple: true 50 | options: 51 | - 'Chrome' 52 | - 'Edge' 53 | - 'Safari' 54 | - 'Firefox' 55 | - 'Other' 56 | validations: 57 | required: true 58 | - type: input 59 | attributes: 60 | label: '📌 Browser Version' 61 | validations: 62 | required: true 63 | - type: textarea 64 | attributes: 65 | label: '🐛 Bug Description' 66 | description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail. 67 | validations: 68 | required: true 69 | - type: textarea 70 | attributes: 71 | label: '📷 Recurrence Steps' 72 | description: A clear and concise description of how to recurrence. 73 | - type: textarea 74 | attributes: 75 | label: '🚦 Expected Behavior' 76 | description: A clear and concise description of what you expected to happen. 77 | - type: textarea 78 | attributes: 79 | label: '📝 Additional Information' 80 | description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here. -------------------------------------------------------------------------------- /app/components/auth.module.scss: -------------------------------------------------------------------------------- 1 | .auth-page { 2 | display: flex; 3 | justify-content: flex-start; 4 | align-items: center; 5 | height: 100%; 6 | width: 100%; 7 | flex-direction: column; 8 | .top-banner { 9 | position: relative; 10 | width: 100%; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | padding: 12px 64px; 15 | box-sizing: border-box; 16 | background: var(--second); 17 | .top-banner-inner { 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | font-size: 14px; 22 | line-height: 150%; 23 | span { 24 | gap: 8px; 25 | a { 26 | display: inline-flex; 27 | align-items: center; 28 | text-decoration: none; 29 | margin-left: 8px; 30 | color: var(--primary); 31 | } 32 | } 33 | } 34 | .top-banner-close { 35 | cursor: pointer; 36 | position: absolute; 37 | top: 50%; 38 | right: 48px; 39 | transform: translateY(-50%); 40 | } 41 | } 42 | 43 | @media (max-width: 600px) { 44 | .top-banner { 45 | padding: 12px 24px 12px 12px; 46 | .top-banner-close { 47 | right: 10px; 48 | } 49 | .top-banner-inner { 50 | .top-banner-logo { 51 | margin-right: 8px; 52 | } 53 | } 54 | } 55 | } 56 | 57 | .auth-header { 58 | display: flex; 59 | justify-content: space-between; 60 | width: 100%; 61 | padding: 10px; 62 | box-sizing: border-box; 63 | animation: slide-in-from-top ease 0.3s; 64 | } 65 | 66 | .auth-logo { 67 | margin-top: 10vh; 68 | transform: scale(1.4); 69 | } 70 | 71 | .auth-title { 72 | font-size: 24px; 73 | font-weight: bold; 74 | line-height: 2; 75 | margin-bottom: 1vh; 76 | } 77 | 78 | .auth-tips { 79 | font-size: 14px; 80 | } 81 | 82 | .auth-input { 83 | margin: 3vh 0; 84 | } 85 | 86 | .auth-input-second { 87 | margin: 0 0 3vh 0; 88 | } 89 | 90 | .auth-actions { 91 | display: flex; 92 | justify-content: center; 93 | flex-direction: column; 94 | 95 | button:not(:last-child) { 96 | margin-bottom: 10px; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/styles/highlight.scss: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | pre { 3 | padding: 0; 4 | } 5 | 6 | pre, 7 | code { 8 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; 9 | } 10 | 11 | pre code { 12 | display: block; 13 | overflow-x: auto; 14 | padding: 1em; 15 | } 16 | 17 | code { 18 | padding: 3px 5px; 19 | } 20 | 21 | .hljs, 22 | pre { 23 | background: #1a1b26; 24 | color: #cbd2ea; 25 | } 26 | 27 | /*! 28 | Theme: Tokyo-night-Dark 29 | origin: https://github.com/enkia/tokyo-night-vscode-theme 30 | Description: Original highlight.js style 31 | Author: (c) Henri Vandersleyen 32 | License: see project LICENSE 33 | Touched: 2022 34 | */ 35 | .hljs-comment, 36 | .hljs-meta { 37 | color: #565f89; 38 | } 39 | 40 | .hljs-deletion, 41 | .hljs-doctag, 42 | .hljs-regexp, 43 | .hljs-selector-attr, 44 | .hljs-selector-class, 45 | .hljs-selector-id, 46 | .hljs-selector-pseudo, 47 | .hljs-tag, 48 | .hljs-template-tag, 49 | .hljs-variable.language_ { 50 | color: #f7768e; 51 | } 52 | 53 | .hljs-link, 54 | .hljs-literal, 55 | .hljs-number, 56 | .hljs-params, 57 | .hljs-template-variable, 58 | .hljs-type, 59 | .hljs-variable { 60 | color: #ff9e64; 61 | } 62 | 63 | .hljs-attribute, 64 | .hljs-built_in { 65 | color: #e0af68; 66 | } 67 | 68 | .hljs-keyword, 69 | .hljs-property, 70 | .hljs-subst, 71 | .hljs-title, 72 | .hljs-title.class_, 73 | .hljs-title.class_.inherited__, 74 | .hljs-title.function_ { 75 | color: #7dcfff; 76 | } 77 | 78 | .hljs-selector-tag { 79 | color: #73daca; 80 | } 81 | 82 | .hljs-addition, 83 | .hljs-bullet, 84 | .hljs-quote, 85 | .hljs-string, 86 | .hljs-symbol { 87 | color: #9ece6a; 88 | } 89 | 90 | .hljs-code, 91 | .hljs-formula, 92 | .hljs-section { 93 | color: #7aa2f7; 94 | } 95 | 96 | .hljs-attr, 97 | .hljs-char.escape_, 98 | .hljs-keyword, 99 | .hljs-name, 100 | .hljs-operator { 101 | color: #bb9af7; 102 | } 103 | 104 | .hljs-punctuation { 105 | color: #c0caf5; 106 | } 107 | 108 | .hljs-emphasis { 109 | font-style: italic; 110 | } 111 | 112 | .hljs-strong { 113 | font-weight: 700; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/components/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { IconButton } from "./button"; 5 | import GithubIcon from "../icons/github.svg"; 6 | import ResetIcon from "../icons/reload.svg"; 7 | import { ISSUE_URL } from "../constant"; 8 | import Locale from "../locales"; 9 | import { showConfirm } from "./ui-lib"; 10 | import { useSyncStore } from "../store/sync"; 11 | import { useChatStore } from "../store/chat"; 12 | 13 | interface IErrorBoundaryState { 14 | hasError: boolean; 15 | error: Error | null; 16 | info: React.ErrorInfo | null; 17 | } 18 | 19 | export class ErrorBoundary extends React.Component { 20 | constructor(props: any) { 21 | super(props); 22 | this.state = { hasError: false, error: null, info: null }; 23 | } 24 | 25 | componentDidCatch(error: Error, info: React.ErrorInfo) { 26 | // Update state with error details 27 | this.setState({ hasError: true, error, info }); 28 | } 29 | 30 | clearAndSaveData() { 31 | try { 32 | useSyncStore.getState().export(); 33 | } finally { 34 | useChatStore.getState().clearAllData(); 35 | } 36 | } 37 | 38 | render() { 39 | if (this.state.hasError) { 40 | // Render error message 41 | return ( 42 |
43 |

Oops, something went wrong!

44 |
45 |             {this.state.error?.toString()}
46 |             {this.state.info?.componentStack}
47 |           
48 | 49 |
50 | 51 | } 54 | bordered 55 | /> 56 | 57 | } 59 | text="Clear All Data" 60 | onClick={async () => { 61 | if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) { 62 | this.clearAndSaveData(); 63 | } 64 | }} 65 | bordered 66 | /> 67 |
68 |
69 | ); 70 | } 71 | // if no error occurred, render children 72 | return this.props.children; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/utils/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { combine, persist, createJSONStorage } from "zustand/middleware"; 3 | import { Updater } from "../typing"; 4 | import { deepClone } from "./clone"; 5 | import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; 6 | 7 | type SecondParam = T extends ( 8 | _f: infer _F, 9 | _s: infer S, 10 | ...args: infer _U 11 | ) => any 12 | ? S 13 | : never; 14 | 15 | type MakeUpdater = { 16 | lastUpdateTime: number; 17 | _hasHydrated: boolean; 18 | 19 | markUpdate: () => void; 20 | update: Updater; 21 | setHasHydrated: (state: boolean) => void; 22 | }; 23 | 24 | type SetStoreState = ( 25 | partial: T | Partial | ((state: T) => T | Partial), 26 | replace?: boolean | undefined, 27 | ) => void; 28 | 29 | export function createPersistStore( 30 | state: T, 31 | methods: ( 32 | set: SetStoreState>, 33 | get: () => T & MakeUpdater, 34 | ) => M, 35 | persistOptions: SecondParam>>, 36 | ) { 37 | persistOptions.storage = createJSONStorage(() => indexedDBStorage); 38 | const oldOonRehydrateStorage = persistOptions?.onRehydrateStorage; 39 | persistOptions.onRehydrateStorage = (state) => { 40 | oldOonRehydrateStorage?.(state); 41 | return () => state.setHasHydrated(true); 42 | }; 43 | 44 | return create( 45 | persist( 46 | combine( 47 | { 48 | ...state, 49 | lastUpdateTime: 0, 50 | _hasHydrated: false, 51 | }, 52 | (set, get) => { 53 | return { 54 | ...methods(set, get as any), 55 | 56 | markUpdate() { 57 | set({ lastUpdateTime: Date.now() } as Partial< 58 | T & M & MakeUpdater 59 | >); 60 | }, 61 | update(updater) { 62 | const state = deepClone(get()); 63 | updater(state); 64 | set({ 65 | ...state, 66 | lastUpdateTime: Date.now(), 67 | }); 68 | }, 69 | setHasHydrated: (state: boolean) => { 70 | set({ _hasHydrated: state } as Partial>); 71 | }, 72 | } as M & MakeUpdater; 73 | }, 74 | ), 75 | persistOptions as any, 76 | ), 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /app/api/artifacts/route.ts: -------------------------------------------------------------------------------- 1 | import md5 from "spark-md5"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import { getServerSideConfig } from "@/app/config/server"; 4 | 5 | async function handle(req: NextRequest, res: NextResponse) { 6 | const serverConfig = getServerSideConfig(); 7 | const storeUrl = () => 8 | `https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`; 9 | const storeHeaders = () => ({ 10 | Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`, 11 | }); 12 | if (req.method === "POST") { 13 | const clonedBody = await req.text(); 14 | const hashedCode = md5.hash(clonedBody).trim(); 15 | const body: { 16 | key: string; 17 | value: string; 18 | expiration_ttl?: number; 19 | } = { 20 | key: hashedCode, 21 | value: clonedBody, 22 | }; 23 | try { 24 | const ttl = parseInt(serverConfig.cloudflareKVTTL as string); 25 | if (ttl > 60) { 26 | body["expiration_ttl"] = ttl; 27 | } 28 | } catch (e) { 29 | console.error(e); 30 | } 31 | const res = await fetch(`${storeUrl()}/bulk`, { 32 | headers: { 33 | ...storeHeaders(), 34 | "Content-Type": "application/json", 35 | }, 36 | method: "PUT", 37 | body: JSON.stringify([body]), 38 | }); 39 | const result = await res.json(); 40 | console.log("save data", result); 41 | if (result?.success) { 42 | return NextResponse.json( 43 | { code: 0, id: hashedCode, result }, 44 | { status: res.status }, 45 | ); 46 | } 47 | return NextResponse.json( 48 | { error: true, msg: "Save data error" }, 49 | { status: 400 }, 50 | ); 51 | } 52 | if (req.method === "GET") { 53 | const id = req?.nextUrl?.searchParams?.get("id"); 54 | const res = await fetch(`${storeUrl()}/values/${id}`, { 55 | headers: storeHeaders(), 56 | method: "GET", 57 | }); 58 | return new Response(res.body, { 59 | status: res.status, 60 | statusText: res.statusText, 61 | headers: res.headers, 62 | }); 63 | } 64 | return NextResponse.json( 65 | { error: true, msg: "Invalid request" }, 66 | { status: 400 }, 67 | ); 68 | } 69 | 70 | export const POST = handle; 71 | export const GET = handle; 72 | 73 | export const runtime = "edge"; 74 | -------------------------------------------------------------------------------- /app/api/openai.ts: -------------------------------------------------------------------------------- 1 | import { type OpenAIListModelResponse } from "@/app/client/platforms/openai"; 2 | import { getServerSideConfig } from "@/app/config/server"; 3 | import { ModelProvider, OpenaiPath } from "@/app/constant"; 4 | import { prettyObject } from "@/app/utils/format"; 5 | import { NextRequest, NextResponse } from "next/server"; 6 | import { auth } from "./auth"; 7 | import { requestOpenai } from "./common"; 8 | 9 | const ALLOWED_PATH = new Set(Object.values(OpenaiPath)); 10 | 11 | function getModels(remoteModelRes: OpenAIListModelResponse) { 12 | const config = getServerSideConfig(); 13 | 14 | if (config.disableGPT4) { 15 | remoteModelRes.data = remoteModelRes.data.filter( 16 | (m) => 17 | !( 18 | m.id.startsWith("gpt-4") || 19 | m.id.startsWith("chatgpt-4o") || 20 | m.id.startsWith("o1") || 21 | m.id.startsWith("o3") 22 | ) || m.id.startsWith("gpt-4o-mini"), 23 | ); 24 | } 25 | 26 | return remoteModelRes; 27 | } 28 | 29 | export async function handle( 30 | req: NextRequest, 31 | { params }: { params: { path: string[] } }, 32 | ) { 33 | console.log("[OpenAI Route] params ", params); 34 | 35 | if (req.method === "OPTIONS") { 36 | return NextResponse.json({ body: "OK" }, { status: 200 }); 37 | } 38 | 39 | const subpath = params.path.join("/"); 40 | 41 | if (!ALLOWED_PATH.has(subpath)) { 42 | console.log("[OpenAI Route] forbidden path ", subpath); 43 | return NextResponse.json( 44 | { 45 | error: true, 46 | msg: "you are not allowed to request " + subpath, 47 | }, 48 | { 49 | status: 403, 50 | }, 51 | ); 52 | } 53 | 54 | const authResult = auth(req, ModelProvider.GPT); 55 | if (authResult.error) { 56 | return NextResponse.json(authResult, { 57 | status: 401, 58 | }); 59 | } 60 | 61 | try { 62 | const response = await requestOpenai(req); 63 | 64 | // list models 65 | if (subpath === OpenaiPath.ListModelPath && response.status === 200) { 66 | const resJson = (await response.json()) as OpenAIListModelResponse; 67 | const availableModels = getModels(resJson); 68 | return NextResponse.json(availableModels, { 69 | status: response.status, 70 | }); 71 | } 72 | 73 | return response; 74 | } catch (e) { 75 | console.error("[OpenAI] ", e); 76 | return NextResponse.json(prettyObject(e)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # Your openai api key. (required) 2 | OPENAI_API_KEY=sk-xxxx 3 | 4 | # DeepSeek Api Key. (Optional) 5 | DEEPSEEK_API_KEY= 6 | 7 | # Access password, separated by comma. (optional) 8 | CODE=your-password 9 | 10 | # You can start service behind a proxy. (optional) 11 | PROXY_URL=http://localhost:7890 12 | 13 | # Enable MCP functionality (optional) 14 | # Default: Empty (disabled) 15 | # Set to "true" to enable MCP functionality 16 | ENABLE_MCP= 17 | 18 | # (optional) 19 | # Default: Empty 20 | # Google Gemini Pro API key, set if you want to use Google Gemini Pro API. 21 | GOOGLE_API_KEY= 22 | 23 | # (optional) 24 | # Default: https://generativelanguage.googleapis.com/ 25 | # Google Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url. 26 | GOOGLE_URL= 27 | 28 | # Override openai api request base url. (optional) 29 | # Default: https://api.openai.com 30 | # Examples: http://your-openai-proxy.com 31 | BASE_URL= 32 | 33 | # Specify OpenAI organization ID.(optional) 34 | # Default: Empty 35 | OPENAI_ORG_ID= 36 | 37 | # (optional) 38 | # Default: Empty 39 | # If you do not want users to use GPT-4, set this value to 1. 40 | DISABLE_GPT4= 41 | 42 | # (optional) 43 | # Default: Empty 44 | # If you do not want users to input their own API key, set this value to 1. 45 | HIDE_USER_API_KEY= 46 | 47 | # (optional) 48 | # Default: Empty 49 | # If you do want users to query balance, set this value to 1. 50 | ENABLE_BALANCE_QUERY= 51 | 52 | # (optional) 53 | # Default: Empty 54 | # If you want to disable parse settings from url, set this value to 1. 55 | DISABLE_FAST_LINK= 56 | 57 | # (optional) 58 | # Default: Empty 59 | # To control custom models, use + to add a custom model, use - to hide a model, use name=displayName to customize model name, separated by comma. 60 | CUSTOM_MODELS= 61 | 62 | # (optional) 63 | # Default: Empty 64 | # Change default model 65 | DEFAULT_MODEL= 66 | 67 | # anthropic claude Api Key.(optional) 68 | ANTHROPIC_API_KEY= 69 | 70 | ### anthropic claude Api version. (optional) 71 | ANTHROPIC_API_VERSION= 72 | 73 | ### anthropic claude Api url (optional) 74 | ANTHROPIC_URL= 75 | 76 | ### (optional) 77 | WHITE_WEBDAV_ENDPOINTS= 78 | 79 | ### siliconflow Api key (optional) 80 | SILICONFLOW_API_KEY= 81 | 82 | ### siliconflow Api url (optional) 83 | SILICONFLOW_URL= 84 | 85 | ### 302.AI Api key (optional) 86 | AI302_API_KEY= 87 | 88 | ### 302.AI Api url (optional) 89 | AI302_URL= 90 | --------------------------------------------------------------------------------