├── .npmrc ├── config ├── index.ts └── proxy.ts ├── service ├── .npmrc ├── .vscode │ ├── extensions.json │ └── settings.json ├── .eslintrc.json ├── src │ ├── chatgpt │ │ ├── types.ts │ │ └── index.ts │ ├── utils │ │ ├── index.ts │ │ └── is.ts │ ├── middleware │ │ ├── auth.ts │ │ └── limiter.ts │ ├── types.ts │ └── index.ts ├── tsup.config.ts ├── .gitignore ├── tsconfig.json ├── .env.example └── package.json ├── src ├── utils │ ├── storage │ │ ├── index.ts │ │ └── local.ts │ ├── functions │ │ ├── index.ts │ │ └── debounce.ts │ ├── crypto │ │ └── index.ts │ ├── request │ │ ├── axios.ts │ │ └── index.ts │ ├── format │ │ └── index.ts │ └── is │ │ └── index.ts ├── assets │ ├── avatar.jpg │ ├── ball.jpeg │ └── recommend.json ├── styles │ ├── lib │ │ ├── tailwind.css │ │ └── highlight.less │ └── global.less ├── views │ ├── chat │ │ ├── layout │ │ │ ├── index.ts │ │ │ ├── sider │ │ │ │ ├── Footer.vue │ │ │ │ ├── index.vue │ │ │ │ └── List.vue │ │ │ ├── Layout.vue │ │ │ └── Permission.vue │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── Message │ │ │ │ ├── style.less │ │ │ │ ├── Avatar.vue │ │ │ │ ├── Text.vue │ │ │ │ └── index.vue │ │ │ └── Header │ │ │ │ └── index.vue │ │ └── hooks │ │ │ ├── useUsingContext.ts │ │ │ ├── useChat.ts │ │ │ ├── useCopyCode.ts │ │ │ └── useScroll.ts │ └── exception │ │ ├── 404 │ │ └── index.vue │ │ └── 500 │ │ └── index.vue ├── components │ ├── custom │ │ ├── index.ts │ │ └── GithubSite.vue │ └── common │ │ ├── index.ts │ │ ├── HoverButton │ │ ├── Button.vue │ │ └── index.vue │ │ ├── SvgIcon │ │ └── index.vue │ │ ├── NaiveProvider │ │ └── index.vue │ │ ├── UserAvatar │ │ └── index.vue │ │ ├── Setting │ │ ├── Advanced.vue │ │ ├── index.vue │ │ ├── About.vue │ │ └── General.vue │ │ └── PromptStore │ │ └── index.vue ├── plugins │ ├── index.ts │ ├── assets.ts │ └── scrollbarStyle.ts ├── store │ ├── modules │ │ ├── index.ts │ │ ├── auth │ │ │ ├── helper.ts │ │ │ └── index.ts │ │ ├── prompt │ │ │ ├── helper.ts │ │ │ └── index.ts │ │ ├── chat │ │ │ ├── helper.ts │ │ │ └── index.ts │ │ ├── user │ │ │ ├── index.ts │ │ │ └── helper.ts │ │ ├── settings │ │ │ ├── index.ts │ │ │ └── helper.ts │ │ └── app │ │ │ ├── helper.ts │ │ │ └── index.ts │ └── index.ts ├── typings │ ├── env.d.ts │ ├── global.d.ts │ └── chat.d.ts ├── hooks │ ├── useBasicLayout.ts │ ├── useLanguage.ts │ ├── useIconRender.ts │ └── useTheme.ts ├── main.ts ├── App.vue ├── router │ ├── permission.ts │ └── index.ts ├── locales │ ├── index.ts │ ├── zh-CN.ts │ ├── zh-TW.ts │ └── en-US.ts └── api │ └── index.ts ├── docs ├── c1.png ├── c2.png ├── alipay.png ├── docker.png ├── wechat.png ├── c1-2.8.0.png ├── c1-2.9.0.png ├── c2-2.8.0.png └── c2-2.9.0.png ├── .commitlintrc.json ├── .eslintrc.cjs ├── public ├── favicon.ico ├── pwa-192x192.png ├── pwa-512x512.png └── favicon.svg ├── .husky ├── pre-commit └── commit-msg ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .dockerignore ├── postcss.config.js ├── start.cmd ├── start.sh ├── .editorconfig ├── docker-compose ├── readme.md ├── nginx │ └── nginx.conf └── docker-compose.yml ├── .env ├── .gitattributes ├── .gitignore ├── tailwind.config.js ├── tsconfig.json ├── .github └── workflows │ ├── ci.yml │ └── build_docker.yml ├── Dockerfile ├── license ├── CONTRIBUTING.md ├── vite.config.ts ├── index.html ├── package.json ├── CONTRIBUTING.en.md ├── README.md ├── README.en.md └── CHANGELOG.md /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './proxy' 2 | -------------------------------------------------------------------------------- /service/.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './local' 2 | -------------------------------------------------------------------------------- /docs/c1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/docs/c1.png -------------------------------------------------------------------------------- /docs/c2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/docs/c2.png -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@antfu'], 4 | } 5 | -------------------------------------------------------------------------------- /docs/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/docs/alipay.png -------------------------------------------------------------------------------- /docs/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/docs/docker.png -------------------------------------------------------------------------------- /docs/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/docs/wechat.png -------------------------------------------------------------------------------- /docs/c1-2.8.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/docs/c1-2.8.0.png -------------------------------------------------------------------------------- /docs/c1-2.9.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/docs/c1-2.9.0.png -------------------------------------------------------------------------------- /docs/c2-2.8.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/docs/c2-2.8.0.png -------------------------------------------------------------------------------- /docs/c2-2.9.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/docs/c2-2.9.0.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /service/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/src/assets/avatar.jpg -------------------------------------------------------------------------------- /src/assets/ball.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/src/assets/ball.jpeg -------------------------------------------------------------------------------- /src/styles/lib/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/public/pwa-192x192.png -------------------------------------------------------------------------------- /public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuguokai/chatgpt-web/HEAD/public/pwa-512x512.png -------------------------------------------------------------------------------- /src/views/chat/layout/index.ts: -------------------------------------------------------------------------------- 1 | import ChatLayout from './Layout.vue' 2 | 3 | export { ChatLayout } 4 | -------------------------------------------------------------------------------- /src/components/custom/index.ts: -------------------------------------------------------------------------------- 1 | import GithubSite from './GithubSite.vue' 2 | 3 | export { GithubSite } 4 | -------------------------------------------------------------------------------- /src/views/chat/components/index.ts: -------------------------------------------------------------------------------- 1 | import Message from './Message/index.vue' 2 | 3 | export { Message } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | */node_modules 3 | node_modules 4 | Dockerfile 5 | .* 6 | */.* 7 | !.env 8 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /service/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["build"], 4 | "extends": ["@antfu"] 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import setupAssets from './assets' 2 | import setupScrollbarStyle from './scrollbarStyle' 3 | 4 | export { setupAssets, setupScrollbarStyle } 5 | -------------------------------------------------------------------------------- /src/store/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app' 2 | export * from './chat' 3 | export * from './user' 4 | export * from './prompt' 5 | export * from './settings' 6 | export * from './auth' 7 | -------------------------------------------------------------------------------- /src/styles/global.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app { 4 | height: 100%; 5 | } 6 | 7 | body { 8 | padding-bottom: constant(safe-area-inset-bottom); 9 | padding-bottom: env(safe-area-inset-bottom); 10 | } 11 | -------------------------------------------------------------------------------- /start.cmd: -------------------------------------------------------------------------------- 1 | cd ./service 2 | start pnpm start > service.log & 3 | echo "Start service complete!" 4 | 5 | 6 | cd .. 7 | echo "" > front.log 8 | start pnpm dev > front.log & 9 | echo "Start front complete!" 10 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | 2 | cd ./service 3 | nohup pnpm start > service.log & 4 | echo "Start service complete!" 5 | 6 | 7 | cd .. 8 | echo "" > front.log 9 | nohup pnpm dev > front.log & 10 | echo "Start front complete!" 11 | tail -f front.log 12 | -------------------------------------------------------------------------------- /src/utils/functions/index.ts: -------------------------------------------------------------------------------- 1 | export function getCurrentDate() { 2 | const date = new Date() 3 | const day = date.getDate() 4 | const month = date.getMonth() + 1 5 | const year = date.getFullYear() 6 | return `${year}-${month}-${day}` 7 | } 8 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createPinia } from 'pinia' 3 | 4 | export const store = createPinia() 5 | 6 | export function setupStore(app: App) { 7 | app.use(store) 8 | } 9 | 10 | export * from './modules' 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = tab 8 | indent_size = 2 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /src/components/custom/GithubSite.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/typings/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_GLOB_API_URL: string; 5 | readonly VITE_APP_API_BASE_URL: string; 6 | readonly VITE_GLOB_OPEN_LONG_REPLY: string; 7 | readonly VITE_GLOB_APP_PWA: string; 8 | } 9 | -------------------------------------------------------------------------------- /docker-compose/readme.md: -------------------------------------------------------------------------------- 1 | ### docker-compose 部署教程 2 | - 将打包好的前端文件放到 `nginx/html` 目录下 3 | - ```shell 4 | # 启动 5 | docker-compose up -d 6 | ``` 7 | - ```shell 8 | # 查看运行状态 9 | docker ps 10 | ``` 11 | - ```shell 12 | # 结束运行 13 | docker-compose down 14 | ``` 15 | -------------------------------------------------------------------------------- /service/src/chatgpt/types.ts: -------------------------------------------------------------------------------- 1 | import type { ChatMessage } from 'chatgpt' 2 | 3 | export interface RequestOptions { 4 | message: string 5 | lastContext?: { conversationId?: string; parentMessageId?: string } 6 | process?: (chat: ChatMessage) => void 7 | systemMessage?: string 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/useBasicLayout.ts: -------------------------------------------------------------------------------- 1 | import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' 2 | 3 | export function useBasicLayout() { 4 | const breakpoints = useBreakpoints(breakpointsTailwind) 5 | const isMobile = breakpoints.smaller('sm') 6 | 7 | return { isMobile } 8 | } 9 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Glob API URL 2 | VITE_GLOB_API_URL=/api 3 | 4 | VITE_APP_API_BASE_URL=http://127.0.0.1:3002/ 5 | 6 | # Whether long replies are supported, which may result in higher API fees 7 | VITE_GLOB_OPEN_LONG_REPLY=false 8 | 9 | # When you want to use PWA 10 | VITE_GLOB_APP_PWA=false 11 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $loadingBar?: import('naive-ui').LoadingBarProviderInst; 3 | $dialog?: import('naive-ui').DialogProviderInst; 4 | $message?: import('naive-ui').MessageProviderInst; 5 | $notification?: import('naive-ui').NotificationProviderInst; 6 | } 7 | -------------------------------------------------------------------------------- /service/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | outDir: 'build', 6 | target: 'es2020', 7 | format: ['esm'], 8 | splitting: false, 9 | sourcemap: true, 10 | minify: false, 11 | shims: true, 12 | dts: false, 13 | }) 14 | -------------------------------------------------------------------------------- /src/store/modules/auth/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'SECRET_TOKEN' 4 | 5 | export function getToken() { 6 | return ss.get(LOCAL_NAME) 7 | } 8 | 9 | export function setToken(token: string) { 10 | return ss.set(LOCAL_NAME, token) 11 | } 12 | 13 | export function removeToken() { 14 | return ss.remove(LOCAL_NAME) 15 | } 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | "*.vue" eol=lf 2 | "*.js" eol=lf 3 | "*.ts" eol=lf 4 | "*.jsx" eol=lf 5 | "*.tsx" eol=lf 6 | "*.cjs" eol=lf 7 | "*.cts" eol=lf 8 | "*.mjs" eol=lf 9 | "*.mts" eol=lf 10 | "*.json" eol=lf 11 | "*.html" eol=lf 12 | "*.css" eol=lf 13 | "*.less" eol=lf 14 | "*.scss" eol=lf 15 | "*.sass" eol=lf 16 | "*.styl" eol=lf 17 | "*.md" eol=lf 18 | -------------------------------------------------------------------------------- /src/components/common/index.ts: -------------------------------------------------------------------------------- 1 | import HoverButton from './HoverButton/index.vue' 2 | import NaiveProvider from './NaiveProvider/index.vue' 3 | import SvgIcon from './SvgIcon/index.vue' 4 | import UserAvatar from './UserAvatar/index.vue' 5 | import Setting from './Setting/index.vue' 6 | import PromptStore from './PromptStore/index.vue' 7 | 8 | export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore } 9 | -------------------------------------------------------------------------------- /config/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyOptions } from 'vite' 2 | 3 | export function createViteProxy(isOpenProxy: boolean, viteEnv: ImportMetaEnv) { 4 | if (!isOpenProxy) 5 | return 6 | 7 | const proxy: Record = { 8 | '/api': { 9 | target: viteEnv.VITE_APP_API_BASE_URL, 10 | changeOrigin: true, 11 | rewrite: path => path.replace('/api/', '/'), 12 | }, 13 | } 14 | 15 | return proxy 16 | } 17 | -------------------------------------------------------------------------------- /service/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | }, 7 | "eslint.validate": [ 8 | "javascript", 9 | "typescript", 10 | "json", 11 | "jsonc", 12 | "json5", 13 | "yaml" 14 | ], 15 | "cSpell.words": [ 16 | "antfu", 17 | "chatgpt", 18 | "esno", 19 | "GPTAPI", 20 | "OPENAI" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /service/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | build 32 | -------------------------------------------------------------------------------- /src/components/common/HoverButton/Button.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /src/utils/crypto/index.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js' 2 | 3 | const CryptoSecret = '__CRYPTO_SECRET__' 4 | 5 | export function enCrypto(data: any) { 6 | const str = JSON.stringify(data) 7 | return CryptoJS.AES.encrypt(str, CryptoSecret).toString() 8 | } 9 | 10 | export function deCrypto(data: string) { 11 | const bytes = CryptoJS.AES.decrypt(data, CryptoSecret) 12 | const str = bytes.toString(CryptoJS.enc.Utf8) 13 | 14 | if (str) 15 | return JSON.parse(str) 16 | 17 | return null 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | # Environment variables files 32 | /service/.env 33 | -------------------------------------------------------------------------------- /src/utils/functions/debounce.ts: -------------------------------------------------------------------------------- 1 | type CallbackFunc = (...args: T) => void 2 | 3 | export function debounce( 4 | func: CallbackFunc, 5 | wait: number, 6 | ): (...args: T) => void { 7 | let timeoutId: ReturnType | undefined 8 | 9 | return (...args: T) => { 10 | const later = () => { 11 | clearTimeout(timeoutId) 12 | func(...args) 13 | } 14 | 15 | clearTimeout(timeoutId) 16 | timeoutId = setTimeout(later, wait) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/store/modules/prompt/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'promptStore' 4 | 5 | export type PromptList = [] 6 | 7 | export interface PromptStore { 8 | promptList: PromptList 9 | } 10 | 11 | export function getLocalPromptList(): PromptStore { 12 | const promptStore: PromptStore | undefined = ss.get(LOCAL_NAME) 13 | return promptStore ?? { promptList: [] } 14 | } 15 | 16 | export function setLocalPromptList(promptStore: PromptStore): void { 17 | ss.set(LOCAL_NAME, promptStore) 18 | } 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import { setupI18n } from './locales' 4 | import { setupAssets, setupScrollbarStyle } from './plugins' 5 | import { setupStore } from './store' 6 | import { setupRouter } from './router' 7 | 8 | async function bootstrap() { 9 | const app = createApp(App) 10 | setupAssets() 11 | 12 | setupScrollbarStyle() 13 | 14 | setupStore(app) 15 | 16 | setupI18n(app) 17 | 18 | await setupRouter(app) 19 | 20 | app.mount('#app') 21 | } 22 | 23 | bootstrap() 24 | -------------------------------------------------------------------------------- /src/plugins/assets.ts: -------------------------------------------------------------------------------- 1 | import 'katex/dist/katex.min.css' 2 | import '@/styles/lib/tailwind.css' 3 | import '@/styles/lib/highlight.less' 4 | import '@/styles/lib/github-markdown.less' 5 | import '@/styles/global.less' 6 | 7 | /** Tailwind's Preflight Style Override */ 8 | function naiveStyleOverride() { 9 | const meta = document.createElement('meta') 10 | meta.name = 'naive-ui-style' 11 | document.head.appendChild(meta) 12 | } 13 | 14 | function setupAssets() { 15 | naiveStyleOverride() 16 | } 17 | 18 | export default setupAssets 19 | -------------------------------------------------------------------------------- /src/components/common/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /src/store/modules/prompt/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { PromptStore } from './helper' 3 | import { getLocalPromptList, setLocalPromptList } from './helper' 4 | 5 | export const usePromptStore = defineStore('prompt-store', { 6 | state: (): PromptStore => getLocalPromptList(), 7 | 8 | actions: { 9 | updatePromptList(promptList: []) { 10 | this.$patch({ promptList }) 11 | setLocalPromptList({ promptList }) 12 | }, 13 | getPromptList() { 14 | return this.$state 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'class', 4 | content: [ 5 | './index.html', 6 | './src/**/*.{vue,js,ts,jsx,tsx}', 7 | ], 8 | theme: { 9 | extend: { 10 | animation: { 11 | blink: 'blink 1.2s infinite steps(1, start)', 12 | }, 13 | keyframes: { 14 | blink: { 15 | '0%, 100%': { 'background-color': 'currentColor' }, 16 | '50%': { 'background-color': 'transparent' }, 17 | }, 18 | }, 19 | }, 20 | }, 21 | plugins: [], 22 | } 23 | -------------------------------------------------------------------------------- /docker-compose/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | charset utf-8; 5 | error_page 500 502 503 504 /50x.html; 6 | location / { 7 | root /usr/share/nginx/html; 8 | try_files $uri /index.html; 9 | } 10 | 11 | location /api { 12 | proxy_set_header X-Real-IP $remote_addr; #转发用户IP 13 | proxy_pass http://app:3002; 14 | } 15 | 16 | proxy_set_header Host $host; 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header REMOTE-HOST $remote_addr; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | } 21 | -------------------------------------------------------------------------------- /service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "esnext" 6 | ], 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "strict": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "baseUrl": ".", 17 | "outDir": "build", 18 | "noEmit": true 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "build" 23 | ], 24 | "include": [ 25 | "**/*.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /src/assets/recommend.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "awesome-chatgpt-prompts-zh", 4 | "desc": "ChatGPT 中文调教指南", 5 | "downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json", 6 | "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh" 7 | }, 8 | { 9 | "key": "awesome-chatgpt-prompts-zh-TW", 10 | "desc": "ChatGPT 中文調教指南 (透過 OpenAI / OpenCC 協助,從簡體中文轉換為繁體中文的版本)", 11 | "downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh-TW.json", 12 | "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /src/store/modules/chat/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'chatStorage' 4 | 5 | export function defaultState(): Chat.ChatState { 6 | const uuid = 1002 7 | return { 8 | active: uuid, 9 | usingContext: true, 10 | history: [{ uuid, title: 'New Chat', isEdit: false }], 11 | chat: [{ uuid, data: [] }], 12 | } 13 | } 14 | 15 | export function getLocalState(): Chat.ChatState { 16 | const localState = ss.get(LOCAL_NAME) 17 | return { ...defaultState(), ...localState } 18 | } 19 | 20 | export function setLocalState(state: Chat.ChatState) { 21 | ss.set(LOCAL_NAME, state) 22 | } 23 | -------------------------------------------------------------------------------- /service/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | interface SendResponseOptions { 2 | type: 'Success' | 'Fail' 3 | message?: string 4 | data?: T 5 | } 6 | 7 | export function sendResponse(options: SendResponseOptions) { 8 | if (options.type === 'Success') { 9 | return Promise.resolve({ 10 | message: options.message ?? null, 11 | data: options.data ?? null, 12 | status: options.type, 13 | }) 14 | } 15 | 16 | // eslint-disable-next-line prefer-promise-reject-errors 17 | return Promise.reject({ 18 | message: options.message ?? 'Failed', 19 | data: options.data ?? null, 20 | status: options.type, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "ESNext", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "jsx": "preserve", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "skipLibCheck": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | }, 20 | "types": ["vite/client", "node", "naive-ui/volar"] 21 | }, 22 | "exclude": ["node_modules", "dist", "service"] 23 | } 24 | -------------------------------------------------------------------------------- /src/store/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { UserInfo, UserState } from './helper' 3 | import { defaultSetting, getLocalState, setLocalState } from './helper' 4 | 5 | export const useUserStore = defineStore('user-store', { 6 | state: (): UserState => getLocalState(), 7 | actions: { 8 | updateUserInfo(userInfo: Partial) { 9 | this.userInfo = { ...this.userInfo, ...userInfo } 10 | this.recordState() 11 | }, 12 | 13 | resetUserInfo() { 14 | this.userInfo = { ...defaultSetting().userInfo } 15 | this.recordState() 16 | }, 17 | 18 | recordState() { 19 | setLocalState(this.$state) 20 | }, 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useUsingContext.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useMessage } from 'naive-ui' 3 | import { t } from '@/locales' 4 | import { useChatStore } from '@/store' 5 | 6 | export function useUsingContext() { 7 | const ms = useMessage() 8 | const chatStore = useChatStore() 9 | const usingContext = computed(() => chatStore.usingContext) 10 | 11 | function toggleUsingContext() { 12 | chatStore.setUsingContext(!usingContext.value) 13 | if (usingContext.value) 14 | ms.success(t('chat.turnOnContext')) 15 | else 16 | ms.warning(t('chat.turnOffContext')) 17 | } 18 | 19 | return { 20 | usingContext, 21 | toggleUsingContext, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/store/modules/settings/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { SettingsState } from './helper' 3 | import { defaultSetting, getLocalState, removeLocalState, setLocalState } from './helper' 4 | 5 | export const useSettingStore = defineStore('setting-store', { 6 | state: (): SettingsState => getLocalState(), 7 | actions: { 8 | updateSetting(settings: Partial) { 9 | this.$state = { ...this.$state, ...settings } 10 | this.recordState() 11 | }, 12 | 13 | resetSetting() { 14 | this.$state = defaultSetting() 15 | removeLocalState() 16 | }, 17 | 18 | recordState() { 19 | setLocalState(this.$state) 20 | }, 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /service/src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { isNotEmptyString } from '../utils/is' 2 | 3 | const auth = async (req, res, next) => { 4 | const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY 5 | if (isNotEmptyString(AUTH_SECRET_KEY)) { 6 | try { 7 | const Authorization = req.header('Authorization') 8 | if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim()) 9 | throw new Error('Error: 无访问权限 | No access rights') 10 | next() 11 | } 12 | catch (error) { 13 | res.send({ status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null }) 14 | } 15 | } 16 | else { 17 | next() 18 | } 19 | } 20 | 21 | export { auth } 22 | -------------------------------------------------------------------------------- /src/hooks/useLanguage.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { enUS, zhCN, zhTW } from 'naive-ui' 3 | import { useAppStore } from '@/store' 4 | import { setLocale } from '@/locales' 5 | 6 | export function useLanguage() { 7 | const appStore = useAppStore() 8 | 9 | const language = computed(() => { 10 | switch (appStore.language) { 11 | case 'en-US': 12 | setLocale('en-US') 13 | return enUS 14 | case 'zh-CN': 15 | setLocale('zh-CN') 16 | return zhCN 17 | case 'zh-TW': 18 | setLocale('zh-TW') 19 | return zhTW 20 | default: 21 | setLocale('zh-CN') 22 | return enUS 23 | } 24 | }) 25 | 26 | return { language } 27 | } 28 | -------------------------------------------------------------------------------- /service/.env.example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key - https://platform.openai.com/overview 2 | OPENAI_API_KEY= 3 | 4 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 5 | OPENAI_ACCESS_TOKEN= 6 | 7 | # OpenAI API Base URL - https://api.openai.com 8 | OPENAI_API_BASE_URL= 9 | 10 | # OpenAI API Model - https://platform.openai.com/docs/models 11 | OPENAI_API_MODEL= 12 | 13 | # Reverse Proxy 14 | API_REVERSE_PROXY= 15 | 16 | # timeout 17 | TIMEOUT_MS=100000 18 | 19 | # Rate Limit 20 | MAX_REQUEST_PER_HOUR= 21 | 22 | # Secret key 23 | AUTH_SECRET_KEY= 24 | 25 | # Socks Proxy Host 26 | SOCKS_PROXY_HOST= 27 | 28 | # Socks Proxy Port 29 | SOCKS_PROXY_PORT= 30 | 31 | # HTTPS PROXY 32 | HTTPS_PROXY= 33 | 34 | -------------------------------------------------------------------------------- /src/store/modules/app/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'appSetting' 4 | 5 | export type Theme = 'light' | 'dark' | 'auto' 6 | 7 | export type Language = 'zh-CN' | 'zh-TW' | 'en-US' 8 | 9 | export interface AppState { 10 | siderCollapsed: boolean 11 | theme: Theme 12 | language: Language 13 | } 14 | 15 | export function defaultSetting(): AppState { 16 | return { siderCollapsed: false, theme: 'light', language: 'zh-CN' } 17 | } 18 | 19 | export function getLocalSetting(): AppState { 20 | const localSetting: AppState | undefined = ss.get(LOCAL_NAME) 21 | return { ...defaultSetting(), ...localSetting } 22 | } 23 | 24 | export function setLocalSetting(setting: AppState): void { 25 | ss.set(LOCAL_NAME, setting) 26 | } 27 | -------------------------------------------------------------------------------- /service/src/middleware/limiter.ts: -------------------------------------------------------------------------------- 1 | import { rateLimit } from 'express-rate-limit' 2 | import { isNotEmptyString } from '../utils/is' 3 | 4 | const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR 5 | 6 | const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR))) 7 | ? parseInt(MAX_REQUEST_PER_HOUR) 8 | : 0 // 0 means unlimited 9 | 10 | const limiter = rateLimit({ 11 | windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour 12 | max: maxCount, 13 | statusCode: 200, // 200 means success,but the message is 'Too many request from this IP in 1 hour' 14 | message: async (req, res) => { 15 | res.send({ status: 'Fail', message: 'Too many request from this IP in 1 hour', data: null }) 16 | }, 17 | }) 18 | 19 | export { limiter } 20 | -------------------------------------------------------------------------------- /service/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { FetchFn } from 'chatgpt' 2 | 3 | export interface RequestProps { 4 | prompt: string 5 | options?: ChatContext 6 | systemMessage: string 7 | } 8 | 9 | export interface ChatContext { 10 | conversationId?: string 11 | parentMessageId?: string 12 | } 13 | 14 | export interface ChatGPTUnofficialProxyAPIOptions { 15 | accessToken: string 16 | apiReverseProxyUrl?: string 17 | model?: string 18 | debug?: boolean 19 | headers?: Record 20 | fetch?: FetchFn 21 | } 22 | 23 | export interface ModelConfig { 24 | apiModel?: ApiModel 25 | reverseProxy?: string 26 | timeoutMs?: number 27 | socksProxy?: string 28 | httpsProxy?: string 29 | balance?: string 30 | } 31 | 32 | export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined 33 | -------------------------------------------------------------------------------- /src/hooks/useIconRender.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { SvgIcon } from '@/components/common' 3 | 4 | export const useIconRender = () => { 5 | interface IconConfig { 6 | icon?: string 7 | color?: string 8 | fontSize?: number 9 | } 10 | 11 | interface IconStyle { 12 | color?: string 13 | fontSize?: string 14 | } 15 | 16 | const iconRender = (config: IconConfig) => { 17 | const { color, fontSize, icon } = config 18 | 19 | const style: IconStyle = {} 20 | 21 | if (color) 22 | style.color = color 23 | 24 | if (fontSize) 25 | style.fontSize = `${fontSize}px` 26 | 27 | if (!icon) 28 | window.console.warn('iconRender: icon is required') 29 | 30 | return () => h(SvgIcon, { icon, style }) 31 | } 32 | 33 | return { 34 | iconRender, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/router/permission.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | import { useAuthStoreWithout } from '@/store/modules/auth' 3 | 4 | export function setupPageGuard(router: Router) { 5 | router.beforeEach(async (to, from, next) => { 6 | const authStore = useAuthStoreWithout() 7 | if (!authStore.session) { 8 | try { 9 | const data = await authStore.getSession() 10 | if (String(data.auth) === 'false' && authStore.token) 11 | authStore.removeToken() 12 | if (to.path === '/500') 13 | next({ name: 'Root' }) 14 | else 15 | next() 16 | } 17 | catch (error) { 18 | if (to.path !== '/500') 19 | next({ name: '500' }) 20 | else 21 | next() 22 | } 23 | } 24 | else { 25 | next() 26 | } 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useChat.ts: -------------------------------------------------------------------------------- 1 | import { useChatStore } from '@/store' 2 | 3 | export function useChat() { 4 | const chatStore = useChatStore() 5 | 6 | const getChatByUuidAndIndex = (uuid: number, index: number) => { 7 | return chatStore.getChatByUuidAndIndex(uuid, index) 8 | } 9 | 10 | const addChat = (uuid: number, chat: Chat.Chat) => { 11 | chatStore.addChatByUuid(uuid, chat) 12 | } 13 | 14 | const updateChat = (uuid: number, index: number, chat: Chat.Chat) => { 15 | chatStore.updateChatByUuid(uuid, index, chat) 16 | } 17 | 18 | const updateChatSome = (uuid: number, index: number, chat: Partial) => { 19 | chatStore.updateChatSomeByUuid(uuid, index, chat) 20 | } 21 | 22 | return { 23 | addChat, 24 | updateChat, 25 | updateChatSome, 26 | getChatByUuidAndIndex, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/request/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosResponse } from 'axios' 2 | import { useAuthStore } from '@/store' 3 | 4 | const service = axios.create({ 5 | baseURL: import.meta.env.VITE_GLOB_API_URL, 6 | }) 7 | 8 | service.interceptors.request.use( 9 | (config) => { 10 | const token = useAuthStore().token 11 | if (token) 12 | config.headers.Authorization = `Bearer ${token}` 13 | return config 14 | }, 15 | (error) => { 16 | return Promise.reject(error.response) 17 | }, 18 | ) 19 | 20 | service.interceptors.response.use( 21 | (response: AxiosResponse): AxiosResponse => { 22 | if (response.status === 200) 23 | return response 24 | 25 | throw new Error(response.status.toString()) 26 | }, 27 | (error) => { 28 | return Promise.reject(error) 29 | }, 30 | ) 31 | 32 | export default service 33 | -------------------------------------------------------------------------------- /service/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export function isNumber(value: T | unknown): value is number { 2 | return Object.prototype.toString.call(value) === '[object Number]' 3 | } 4 | 5 | export function isString(value: T | unknown): value is string { 6 | return Object.prototype.toString.call(value) === '[object String]' 7 | } 8 | 9 | export function isNotEmptyString(value: any): boolean { 10 | return typeof value === 'string' && value.length > 0 11 | } 12 | 13 | export function isBoolean(value: T | unknown): value is boolean { 14 | return Object.prototype.toString.call(value) === '[object Boolean]' 15 | } 16 | 17 | export function isFunction any | void | never>(value: T | unknown): value is T { 18 | return Object.prototype.toString.call(value) === '[object Function]' 19 | } 20 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/Footer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createI18n } from 'vue-i18n' 3 | import enUS from './en-US' 4 | import zhCN from './zh-CN' 5 | import zhTW from './zh-TW' 6 | import { useAppStoreWithOut } from '@/store/modules/app' 7 | import type { Language } from '@/store/modules/app/helper' 8 | 9 | const appStore = useAppStoreWithOut() 10 | 11 | const defaultLocale = appStore.language || 'zh-CN' 12 | 13 | const i18n = createI18n({ 14 | locale: defaultLocale, 15 | fallbackLocale: 'en-US', 16 | allowComposition: true, 17 | messages: { 18 | 'en-US': enUS, 19 | 'zh-CN': zhCN, 20 | 'zh-TW': zhTW, 21 | }, 22 | }) 23 | 24 | export const t = i18n.global.t 25 | 26 | export function setLocale(locale: Language) { 27 | i18n.global.locale = locale 28 | } 29 | 30 | export function setupI18n(app: App) { 31 | app.use(i18n) 32 | } 33 | 34 | export default i18n 35 | -------------------------------------------------------------------------------- /src/store/modules/settings/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'settingsStorage' 4 | 5 | export interface SettingsState { 6 | systemMessage: string 7 | } 8 | 9 | export function defaultSetting(): SettingsState { 10 | const currentDate = new Date().toISOString().split('T')[0] 11 | return { 12 | systemMessage: `You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.\nKnowledge cutoff: 2021-09-01\nCurrent date: ${currentDate}`, 13 | } 14 | } 15 | 16 | export function getLocalState(): SettingsState { 17 | const localSetting: SettingsState | undefined = ss.get(LOCAL_NAME) 18 | return { ...defaultSetting(), ...localSetting } 19 | } 20 | 21 | export function setLocalState(setting: SettingsState): void { 22 | ss.set(LOCAL_NAME, setting) 23 | } 24 | 25 | export function removeLocalState() { 26 | ss.remove(LOCAL_NAME) 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Web App", 11 | "url": "http://localhost:1002", 12 | "webRoot": "${workspaceFolder}" 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Launch Service Server", 18 | "runtimeExecutable": "${workspaceFolder}/service/node_modules/.bin/esno", 19 | "skipFiles": ["/**"], 20 | "program": "${workspaceFolder}/service/src/index.ts", 21 | "outFiles": ["${workspaceFolder}/service/**/*.js"], 22 | "envFile": "${workspaceFolder}/service/.env" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useCopyCode.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUpdated } from 'vue' 2 | import { copyText } from '@/utils/format' 3 | 4 | export function useCopyCode() { 5 | function copyCodeBlock() { 6 | const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper') 7 | codeBlockWrapper.forEach((wrapper) => { 8 | const copyBtn = wrapper.querySelector('.code-block-header__copy') 9 | const codeBlock = wrapper.querySelector('.code-block-body') 10 | if (copyBtn && codeBlock) { 11 | copyBtn.addEventListener('click', () => { 12 | if (navigator.clipboard?.writeText) 13 | navigator.clipboard.writeText(codeBlock.textContent ?? '') 14 | else 15 | copyText({ text: codeBlock.textContent ?? '', origin: true }) 16 | }) 17 | } 18 | }) 19 | } 20 | 21 | onMounted(() => copyCodeBlock()) 22 | 23 | onUpdated(() => copyCodeBlock()) 24 | } 25 | -------------------------------------------------------------------------------- /src/store/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { AppState, Language, Theme } from './helper' 3 | import { getLocalSetting, setLocalSetting } from './helper' 4 | import { store } from '@/store' 5 | 6 | export const useAppStore = defineStore('app-store', { 7 | state: (): AppState => getLocalSetting(), 8 | actions: { 9 | setSiderCollapsed(collapsed: boolean) { 10 | this.siderCollapsed = collapsed 11 | this.recordState() 12 | }, 13 | 14 | setTheme(theme: Theme) { 15 | this.theme = theme 16 | this.recordState() 17 | }, 18 | 19 | setLanguage(language: Language) { 20 | if (this.language !== language) { 21 | this.language = language 22 | this.recordState() 23 | } 24 | }, 25 | 26 | recordState() { 27 | setLocalSetting(this.$state) 28 | }, 29 | }, 30 | }) 31 | 32 | export function useAppStoreWithOut() { 33 | return useAppStore(store) 34 | } 35 | -------------------------------------------------------------------------------- /src/store/modules/user/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'userStorage' 4 | 5 | export interface UserInfo { 6 | avatar: string 7 | name: string 8 | description: string 9 | } 10 | 11 | export interface UserState { 12 | userInfo: UserInfo 13 | } 14 | 15 | export function defaultSetting(): UserState { 16 | return { 17 | userInfo: { 18 | avatar: 'https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg', 19 | name: 'ChenZhaoYu', 20 | description: 'Star on Github', 21 | }, 22 | } 23 | } 24 | 25 | export function getLocalState(): UserState { 26 | const localSetting: UserState | undefined = ss.get(LOCAL_NAME) 27 | return { ...defaultSetting(), ...localSetting } 28 | } 29 | 30 | export function setLocalState(setting: UserState): void { 31 | ss.set(LOCAL_NAME, setting) 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18.x 21 | 22 | - name: Setup 23 | run: npm i -g @antfu/ni 24 | 25 | - name: Install 26 | run: nci 27 | 28 | - name: Lint 29 | run: nr lint:fix 30 | 31 | typecheck: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Set node 36 | uses: actions/setup-node@v3 37 | with: 38 | node-version: 18.x 39 | 40 | - name: Setup 41 | run: npm i -g @antfu/ni 42 | 43 | - name: Install 44 | run: nci 45 | 46 | - name: Typecheck 47 | run: nr type-check 48 | -------------------------------------------------------------------------------- /src/views/exception/500/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/exception/404/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build front-end 2 | FROM node:lts-alpine AS frontend 3 | 4 | RUN npm install pnpm -g 5 | 6 | WORKDIR /app 7 | 8 | COPY ./package.json /app 9 | 10 | COPY ./pnpm-lock.yaml /app 11 | 12 | RUN pnpm install 13 | 14 | COPY . /app 15 | 16 | RUN pnpm run build 17 | 18 | # build backend 19 | FROM node:lts-alpine as backend 20 | 21 | RUN npm install pnpm -g 22 | 23 | WORKDIR /app 24 | 25 | COPY /service/package.json /app 26 | 27 | COPY /service/pnpm-lock.yaml /app 28 | 29 | RUN pnpm install 30 | 31 | COPY /service /app 32 | 33 | RUN pnpm build 34 | 35 | # service 36 | FROM node:lts-alpine 37 | 38 | RUN npm install pnpm -g 39 | 40 | WORKDIR /app 41 | 42 | COPY /service/package.json /app 43 | 44 | COPY /service/pnpm-lock.yaml /app 45 | 46 | RUN pnpm install --production && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/* 47 | 48 | COPY /service /app 49 | 50 | COPY --from=frontend /app/dist /app/public 51 | 52 | COPY --from=backend /app/build /app/build 53 | 54 | EXPOSE 3002 55 | 56 | CMD ["pnpm", "run", "prod"] 57 | -------------------------------------------------------------------------------- /src/plugins/scrollbarStyle.ts: -------------------------------------------------------------------------------- 1 | import { darkTheme, lightTheme } from 'naive-ui' 2 | 3 | const setupScrollbarStyle = () => { 4 | const style = document.createElement('style') 5 | const styleContent = ` 6 | ::-webkit-scrollbar { 7 | background-color: transparent; 8 | width: ${lightTheme.Scrollbar.common?.scrollbarWidth}; 9 | } 10 | ::-webkit-scrollbar-thumb { 11 | background-color: ${lightTheme.Scrollbar.common?.scrollbarColor}; 12 | border-radius: ${lightTheme.Scrollbar.common?.scrollbarBorderRadius}; 13 | } 14 | html.dark ::-webkit-scrollbar { 15 | background-color: transparent; 16 | width: ${darkTheme.Scrollbar.common?.scrollbarWidth}; 17 | } 18 | html.dark ::-webkit-scrollbar-thumb { 19 | background-color: ${darkTheme.Scrollbar.common?.scrollbarColor}; 20 | border-radius: ${darkTheme.Scrollbar.common?.scrollbarBorderRadius}; 21 | } 22 | ` 23 | 24 | style.innerHTML = styleContent 25 | document.head.appendChild(style) 26 | } 27 | 28 | export default setupScrollbarStyle 29 | -------------------------------------------------------------------------------- /src/components/common/NaiveProvider/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 44 | -------------------------------------------------------------------------------- /src/components/common/HoverButton/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 47 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ChenZhaoYu 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 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalThemeOverrides } from 'naive-ui' 2 | import { computed, watch } from 'vue' 3 | import { darkTheme, useOsTheme } from 'naive-ui' 4 | import { useAppStore } from '@/store' 5 | 6 | export function useTheme() { 7 | const appStore = useAppStore() 8 | 9 | const OsTheme = useOsTheme() 10 | 11 | const isDark = computed(() => { 12 | if (appStore.theme === 'auto') 13 | return OsTheme.value === 'dark' 14 | else 15 | return appStore.theme === 'dark' 16 | }) 17 | 18 | const theme = computed(() => { 19 | return isDark.value ? darkTheme : undefined 20 | }) 21 | 22 | const themeOverrides = computed(() => { 23 | if (isDark.value) { 24 | return { 25 | common: {}, 26 | } 27 | } 28 | return {} 29 | }) 30 | 31 | watch( 32 | () => isDark.value, 33 | (dark) => { 34 | if (dark) 35 | document.documentElement.classList.add('dark') 36 | else 37 | document.documentElement.classList.remove('dark') 38 | }, 39 | { immediate: true }, 40 | ) 41 | 42 | return { theme, themeOverrides } 43 | } 44 | -------------------------------------------------------------------------------- /src/typings/chat.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Chat { 2 | 3 | interface Chat { 4 | dateTime: string 5 | text: string 6 | inversion?: boolean 7 | error?: boolean 8 | loading?: boolean 9 | conversationOptions?: ConversationRequest | null 10 | requestOptions: { prompt: string; options?: ConversationRequest | null } 11 | } 12 | 13 | interface History { 14 | title: string 15 | isEdit: boolean 16 | uuid: number 17 | } 18 | 19 | interface ChatState { 20 | active: number | null 21 | usingContext: boolean; 22 | history: History[] 23 | chat: { uuid: number; data: Chat[] }[] 24 | } 25 | 26 | interface ConversationRequest { 27 | conversationId?: string 28 | parentMessageId?: string 29 | } 30 | 31 | interface ConversationResponse { 32 | conversationId: string 33 | detail: { 34 | choices: { finish_reason: string; index: number; logprobs: any; text: string }[] 35 | created: number 36 | id: string 37 | model: string 38 | object: string 39 | usage: { completion_tokens: number; prompt_tokens: number; total_tokens: number } 40 | } 41 | id: string 42 | parentMessageId: string 43 | role: string 44 | text: string 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可 6 | ports: 7 | - 3002:3002 8 | environment: 9 | # 二选一 10 | OPENAI_API_KEY: sk-xxx 11 | # 二选一 12 | OPENAI_ACCESS_TOKEN: xxx 13 | # API接口地址,可选,设置 OPENAI_API_KEY 时可用 14 | OPENAI_API_BASE_URL: xxx 15 | # API模型,可选,设置 OPENAI_API_KEY 时可用 16 | OPENAI_API_MODEL: xxx 17 | # 反向代理,可选 18 | API_REVERSE_PROXY: xxx 19 | # 访问权限密钥,可选 20 | AUTH_SECRET_KEY: xxx 21 | # 每小时最大请求次数,可选,默认无限 22 | MAX_REQUEST_PER_HOUR: 0 23 | # 超时,单位毫秒,可选 24 | TIMEOUT_MS: 60000 25 | # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效 26 | SOCKS_PROXY_HOST: xxx 27 | # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效 28 | SOCKS_PROXY_PORT: xxx 29 | # HTTPS_PROXY 代理,可选 30 | HTTPS_PROXY: http://xxx:7890 31 | nginx: 32 | image: nginx:alpine 33 | ports: 34 | - '80:80' 35 | expose: 36 | - '80' 37 | volumes: 38 | - ./nginx/html:/usr/share/nginx/html 39 | - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf 40 | links: 41 | - app 42 | -------------------------------------------------------------------------------- /src/utils/format/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 转义 HTML 字符 3 | * @param source 4 | */ 5 | export function encodeHTML(source: string) { 6 | return source 7 | .replace(/&/g, '&') 8 | .replace(//g, '>') 10 | .replace(/"/g, '"') 11 | .replace(/'/g, ''') 12 | } 13 | 14 | /** 15 | * 判断是否为代码块 16 | * @param text 17 | */ 18 | export function includeCode(text: string | null | undefined) { 19 | const regexp = /^(?:\s{4}|\t).+/gm 20 | return !!(text?.includes(' = ') || text?.match(regexp)) 21 | } 22 | 23 | /** 24 | * 复制文本 25 | * @param options 26 | */ 27 | export function copyText(options: { text: string; origin?: boolean }) { 28 | const props = { origin: true, ...options } 29 | 30 | let input: HTMLInputElement | HTMLTextAreaElement 31 | 32 | if (props.origin) 33 | input = document.createElement('textarea') 34 | else 35 | input = document.createElement('input') 36 | 37 | input.setAttribute('readonly', 'readonly') 38 | input.value = props.text 39 | document.body.appendChild(input) 40 | input.select() 41 | if (document.execCommand('copy')) 42 | document.execCommand('copy') 43 | document.body.removeChild(input) 44 | } 45 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/style.less: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | background-color: transparent; 3 | font-size: 14px; 4 | 5 | p { 6 | white-space: pre-wrap; 7 | } 8 | 9 | ol { 10 | list-style-type: decimal; 11 | } 12 | 13 | ul { 14 | list-style-type: disc; 15 | } 16 | 17 | pre code, 18 | pre tt { 19 | line-height: 1.65; 20 | } 21 | 22 | .highlight pre, 23 | pre { 24 | background-color: #fff; 25 | } 26 | 27 | code.hljs { 28 | padding: 0; 29 | } 30 | 31 | .code-block { 32 | &-wrapper { 33 | position: relative; 34 | padding-top: 24px; 35 | } 36 | 37 | &-header { 38 | position: absolute; 39 | top: 5px; 40 | right: 0; 41 | width: 100%; 42 | padding: 0 1rem; 43 | display: flex; 44 | justify-content: flex-end; 45 | align-items: center; 46 | color: #b3b3b3; 47 | 48 | &__copy { 49 | cursor: pointer; 50 | margin-left: 0.5rem; 51 | user-select: none; 52 | 53 | &:hover { 54 | color: #65a665; 55 | } 56 | } 57 | } 58 | } 59 | 60 | } 61 | 62 | html.dark { 63 | 64 | .message-reply { 65 | .whitespace-pre-wrap { 66 | white-space: pre-wrap; 67 | color: var(--n-text-color); 68 | } 69 | } 70 | 71 | .highlight pre, 72 | pre { 73 | background-color: #282c34; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import type { RouteRecordRaw } from 'vue-router' 3 | import { createRouter, createWebHashHistory } from 'vue-router' 4 | import { setupPageGuard } from './permission' 5 | import { ChatLayout } from '@/views/chat/layout' 6 | 7 | const routes: RouteRecordRaw[] = [ 8 | { 9 | path: '/', 10 | name: 'Root', 11 | component: ChatLayout, 12 | redirect: '/chat', 13 | children: [ 14 | { 15 | path: '/chat/:uuid?', 16 | name: 'Chat', 17 | component: () => import('@/views/chat/index.vue'), 18 | }, 19 | ], 20 | }, 21 | 22 | { 23 | path: '/404', 24 | name: '404', 25 | component: () => import('@/views/exception/404/index.vue'), 26 | }, 27 | 28 | { 29 | path: '/500', 30 | name: '500', 31 | component: () => import('@/views/exception/500/index.vue'), 32 | }, 33 | 34 | { 35 | path: '/:pathMatch(.*)*', 36 | name: 'notFound', 37 | redirect: '/404', 38 | }, 39 | ] 40 | 41 | export const router = createRouter({ 42 | history: createWebHashHistory(), 43 | routes, 44 | scrollBehavior: () => ({ left: 0, top: 0 }), 45 | }) 46 | 47 | setupPageGuard(router) 48 | 49 | export async function setupRouter(app: App) { 50 | app.use(router) 51 | await router.isReady() 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/build_docker.yml: -------------------------------------------------------------------------------- 1 | name: build_docker 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | release: 7 | types: [created] # 表示在创建新的 Release 时触发 8 | 9 | jobs: 10 | build_docker: 11 | name: Build docker 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - run: | 18 | echo "本次构建的版本为:${GITHUB_REF_NAME} (但是这个变量目前上下文中无法获取到)" 19 | echo 本次构建的版本为:${{ github.ref_name }} 20 | env 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v2 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v2 26 | - name: Login to DockerHub 27 | uses: docker/login-action@v2 28 | with: 29 | username: ${{ secrets.DOCKERHUB_USERNAME }} 30 | password: ${{ secrets.DOCKERHUB_TOKEN }} 31 | - name: Build and push 32 | id: docker_build 33 | uses: docker/build-push-action@v4 34 | with: 35 | context: . 36 | push: true 37 | labels: ${{ steps.meta.outputs.labels }} 38 | platforms: linux/amd64,linux/arm64 39 | tags: | 40 | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:${{ github.ref_name }} 41 | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:latest 42 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useScroll.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { nextTick, ref } from 'vue' 3 | 4 | type ScrollElement = HTMLDivElement | null 5 | 6 | interface ScrollReturn { 7 | scrollRef: Ref 8 | scrollToBottom: () => Promise 9 | scrollToTop: () => Promise 10 | scrollToBottomIfAtBottom: () => Promise 11 | } 12 | 13 | export function useScroll(): ScrollReturn { 14 | const scrollRef = ref(null) 15 | 16 | const scrollToBottom = async () => { 17 | await nextTick() 18 | if (scrollRef.value) 19 | scrollRef.value.scrollTop = scrollRef.value.scrollHeight 20 | } 21 | 22 | const scrollToTop = async () => { 23 | await nextTick() 24 | if (scrollRef.value) 25 | scrollRef.value.scrollTop = 0 26 | } 27 | 28 | const scrollToBottomIfAtBottom = async () => { 29 | await nextTick() 30 | if (scrollRef.value) { 31 | const threshold = 100 // 阈值,表示滚动条到底部的距离阈值 32 | const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight 33 | if (distanceToBottom <= threshold) 34 | scrollRef.value.scrollTop = scrollRef.value.scrollHeight 35 | } 36 | } 37 | 38 | return { 39 | scrollRef, 40 | scrollToBottom, 41 | scrollToTop, 42 | scrollToBottomIfAtBottom, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | }, 7 | "eslint.validate": [ 8 | "javascript", 9 | "javascriptreact", 10 | "typescript", 11 | "typescriptreact", 12 | "vue", 13 | "html", 14 | "json", 15 | "jsonc", 16 | "json5", 17 | "yaml", 18 | "yml", 19 | "markdown" 20 | ], 21 | "cSpell.words": [ 22 | "antfu", 23 | "axios", 24 | "bumpp", 25 | "chatgpt", 26 | "chenzhaoyu", 27 | "commitlint", 28 | "davinci", 29 | "dockerhub", 30 | "esno", 31 | "GPTAPI", 32 | "highlightjs", 33 | "hljs", 34 | "iconify", 35 | "katex", 36 | "katexmath", 37 | "linkify", 38 | "logprobs", 39 | "mdhljs", 40 | "mila", 41 | "nodata", 42 | "OPENAI", 43 | "pinia", 44 | "Popconfirm", 45 | "rushstack", 46 | "Sider", 47 | "tailwindcss", 48 | "traptitech", 49 | "tsup", 50 | "Typecheck", 51 | "unplugin", 52 | "VITE", 53 | "vueuse", 54 | "Zhao" 55 | ], 56 | "i18n-ally.enabledParsers": [ 57 | "ts" 58 | ], 59 | "i18n-ally.sortKeys": true, 60 | "i18n-ally.keepFulfilled": true, 61 | "i18n-ally.localesPaths": [ 62 | "src/locales" 63 | ], 64 | "i18n-ally.keystyle": "nested" 65 | } 66 | -------------------------------------------------------------------------------- /src/store/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getToken, removeToken, setToken } from './helper' 3 | import { store } from '@/store' 4 | import { fetchSession } from '@/api' 5 | 6 | interface SessionResponse { 7 | auth: boolean 8 | model: 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' 9 | } 10 | 11 | export interface AuthState { 12 | token: string | undefined 13 | session: SessionResponse | null 14 | } 15 | 16 | export const useAuthStore = defineStore('auth-store', { 17 | state: (): AuthState => ({ 18 | token: getToken(), 19 | session: null, 20 | }), 21 | 22 | getters: { 23 | isChatGPTAPI(state): boolean { 24 | return state.session?.model === 'ChatGPTAPI' 25 | }, 26 | }, 27 | 28 | actions: { 29 | async getSession() { 30 | try { 31 | const { data } = await fetchSession() 32 | this.session = { ...data } 33 | return Promise.resolve(data) 34 | } 35 | catch (error) { 36 | return Promise.reject(error) 37 | } 38 | }, 39 | 40 | setToken(token: string) { 41 | this.token = token 42 | setToken(token) 43 | }, 44 | 45 | removeToken() { 46 | this.token = undefined 47 | removeToken() 48 | }, 49 | }, 50 | }) 51 | 52 | export function useAuthStoreWithout() { 53 | return useAuthStore(store) 54 | } 55 | -------------------------------------------------------------------------------- /service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-web-service", 3 | "version": "1.0.0", 4 | "private": false, 5 | "description": "ChatGPT Web Service", 6 | "author": "ChenZhaoYu ", 7 | "keywords": [ 8 | "chatgpt-web", 9 | "chatgpt", 10 | "chatbot", 11 | "express" 12 | ], 13 | "engines": { 14 | "node": "^16 || ^18 || ^19" 15 | }, 16 | "scripts": { 17 | "start": "esno ./src/index.ts", 18 | "dev": "esno watch ./src/index.ts", 19 | "prod": "node ./build/index.mjs", 20 | "build": "pnpm clean && tsup", 21 | "clean": "rimraf build", 22 | "lint": "eslint .", 23 | "lint:fix": "eslint . --fix", 24 | "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml" 25 | }, 26 | "dependencies": { 27 | "axios": "^1.3.4", 28 | "chatgpt": "^5.1.2", 29 | "dotenv": "^16.0.3", 30 | "esno": "^0.16.3", 31 | "express": "^4.18.2", 32 | "express-rate-limit": "^6.7.0", 33 | "https-proxy-agent": "^5.0.1", 34 | "isomorphic-fetch": "^3.0.0", 35 | "node-fetch": "^3.3.0", 36 | "socks-proxy-agent": "^7.0.0" 37 | }, 38 | "devDependencies": { 39 | "@antfu/eslint-config": "^0.35.3", 40 | "@types/express": "^4.17.17", 41 | "@types/node": "^18.14.6", 42 | "eslint": "^8.35.0", 43 | "rimraf": "^4.3.0", 44 | "tsup": "^6.6.3", 45 | "typescript": "^4.9.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。 3 | 4 | ## 语义化版本 5 | 该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的变更发布主版本号。 6 | 7 | 每个重大更改都将记录在 `changelog` 中。 8 | 9 | ## 提交 Pull Request 10 | 1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。 11 | 2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。 12 | 3. `vscode` 安装了 `Eslint` 插件,其它编辑器如 `webStorm` 打开了 `eslint` 功能。 13 | 4. 根目录下执行 `pnpm bootstrap`。 14 | 5. `/service/` 目录下执行 `pnpm install`。 15 | 6. 对代码库进行更改。如果适用的话,请确保进行了相应的测试。 16 | 7. 请在根目录下执行 `pnpm lint:fix` 进行代码格式检查。 17 | 8. 请在根目录下执行 `pnpm type-check` 进行类型检查。 18 | 9. 提交 git commit, 请同时遵守 [Commit 规范](#commit-指南) 19 | 10. 提交 `pull request`, 如果有对应的 `issue`,请进行[关联](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)。 20 | 21 | ## Commit 指南 22 | 23 | Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/): 24 | 25 | ```bash 26 | <类型>[可选 范围]: <描述> 27 | 28 | [可选 正文] 29 | 30 | [可选 脚注] 31 | ``` 32 | 33 | ### Commit 类型 34 | 35 | 以下是 commit 类型列表: 36 | 37 | - feat: 新特性或功能 38 | - fix: 缺陷修复 39 | - docs: 文档更新 40 | - style: 代码风格或者组件样式更新 41 | - refactor: 代码重构,不引入新功能和缺陷修复 42 | - perf: 性能优化 43 | - test: 单元测试 44 | - chore: 其他不修改 src 或测试文件的提交 45 | 46 | 47 | ## License 48 | 49 | [MIT](./license) 50 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosProgressEvent, GenericAbortSignal } from 'axios' 2 | import { post } from '@/utils/request' 3 | import { useSettingStore } from '@/store' 4 | 5 | export function fetchChatAPI( 6 | prompt: string, 7 | options?: { conversationId?: string; parentMessageId?: string }, 8 | signal?: GenericAbortSignal, 9 | ) { 10 | return post({ 11 | url: '/chat', 12 | data: { prompt, options }, 13 | signal, 14 | }) 15 | } 16 | 17 | export function fetchChatConfig() { 18 | return post({ 19 | url: '/config', 20 | }) 21 | } 22 | 23 | export function fetchChatAPIProcess( 24 | params: { 25 | prompt: string 26 | options?: { conversationId?: string; parentMessageId?: string } 27 | signal?: GenericAbortSignal 28 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void }, 29 | ) { 30 | const settingStore = useSettingStore() 31 | 32 | return post({ 33 | url: '/chat-process', 34 | data: { prompt: params.prompt, options: params.options, systemMessage: settingStore.systemMessage }, 35 | signal: params.signal, 36 | onDownloadProgress: params.onDownloadProgress, 37 | }) 38 | } 39 | 40 | export function fetchSession() { 41 | return post({ 42 | url: '/session', 43 | }) 44 | } 45 | 46 | export function fetchVerify(token: string) { 47 | return post({ 48 | url: '/verify', 49 | data: { token }, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/common/UserAvatar/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import type { PluginOption } from 'vite' 3 | import { defineConfig, loadEnv } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import { VitePWA } from 'vite-plugin-pwa' 6 | 7 | function setupPlugins(env: ImportMetaEnv): PluginOption[] { 8 | return [ 9 | vue(), 10 | env.VITE_GLOB_APP_PWA === 'true' && VitePWA({ 11 | injectRegister: 'auto', 12 | manifest: { 13 | name: 'chatGPT', 14 | short_name: 'chatGPT', 15 | icons: [ 16 | { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' }, 17 | { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' }, 18 | ], 19 | }, 20 | }), 21 | ] 22 | } 23 | 24 | export default defineConfig((env) => { 25 | const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv 26 | 27 | return { 28 | resolve: { 29 | alias: { 30 | '@': path.resolve(process.cwd(), 'src'), 31 | }, 32 | }, 33 | plugins: setupPlugins(viteEnv), 34 | server: { 35 | host: '0.0.0.0', 36 | port: 1002, 37 | open: false, 38 | proxy: { 39 | '/api': { 40 | target: viteEnv.VITE_APP_API_BASE_URL, 41 | changeOrigin: true, // 允许跨域 42 | rewrite: path => path.replace('/api/', '/'), 43 | }, 44 | }, 45 | }, 46 | build: { 47 | reportCompressedSize: false, 48 | sourcemap: false, 49 | commonjsOptions: { 50 | ignoreTryCatch: false, 51 | }, 52 | }, 53 | } 54 | }) 55 | -------------------------------------------------------------------------------- /src/components/common/Setting/Advanced.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 47 | -------------------------------------------------------------------------------- /src/utils/storage/local.ts: -------------------------------------------------------------------------------- 1 | import { deCrypto, enCrypto } from '../crypto' 2 | 3 | interface StorageData { 4 | data: T 5 | expire: number | null 6 | } 7 | 8 | export function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) { 9 | const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7 10 | 11 | const { expire, crypto } = Object.assign( 12 | { 13 | expire: DEFAULT_CACHE_TIME, 14 | crypto: true, 15 | }, 16 | options, 17 | ) 18 | 19 | function set(key: string, data: T) { 20 | const storageData: StorageData = { 21 | data, 22 | expire: expire !== null ? new Date().getTime() + expire * 1000 : null, 23 | } 24 | 25 | const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData) 26 | window.localStorage.setItem(key, json) 27 | } 28 | 29 | function get(key: string) { 30 | const json = window.localStorage.getItem(key) 31 | if (json) { 32 | let storageData: StorageData | null = null 33 | 34 | try { 35 | storageData = crypto ? deCrypto(json) : JSON.parse(json) 36 | } 37 | catch { 38 | // Prevent failure 39 | } 40 | 41 | if (storageData) { 42 | const { data, expire } = storageData 43 | if (expire === null || expire >= Date.now()) 44 | return data 45 | } 46 | 47 | remove(key) 48 | return null 49 | } 50 | } 51 | 52 | function remove(key: string) { 53 | window.localStorage.removeItem(key) 54 | } 55 | 56 | function clear() { 57 | window.localStorage.clear() 58 | } 59 | 60 | return { 61 | set, 62 | get, 63 | remove, 64 | clear, 65 | } 66 | } 67 | 68 | export const ls = createLocalStorage() 69 | 70 | export const ss = createLocalStorage({ expire: null, crypto: false }) 71 | -------------------------------------------------------------------------------- /src/views/chat/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 52 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/Avatar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | ChatGPT Web 11 | 12 | 13 | 14 |
15 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-web", 3 | "version": "2.10.8", 4 | "private": false, 5 | "description": "ChatGPT Web", 6 | "author": "ChenZhaoYu ", 7 | "keywords": [ 8 | "chatgpt-web", 9 | "chatgpt", 10 | "chatbot", 11 | "vue" 12 | ], 13 | "scripts": { 14 | "dev": "vite", 15 | "build": "run-p type-check build-only", 16 | "preview": "vite preview", 17 | "build-only": "vite build", 18 | "type-check": "vue-tsc --noEmit", 19 | "lint": "eslint .", 20 | "lint:fix": "eslint . --fix", 21 | "bootstrap": "pnpm install && pnpm run common:prepare", 22 | "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml", 23 | "common:prepare": "husky install" 24 | }, 25 | "dependencies": { 26 | "@traptitech/markdown-it-katex": "^3.6.0", 27 | "@vueuse/core": "^9.13.0", 28 | "highlight.js": "^11.7.0", 29 | "html2canvas": "^1.4.1", 30 | "katex": "^0.16.4", 31 | "markdown-it": "^13.0.1", 32 | "naive-ui": "^2.34.3", 33 | "pinia": "^2.0.33", 34 | "vue": "^3.2.47", 35 | "vue-i18n": "^9.2.2", 36 | "vue-router": "^4.1.6" 37 | }, 38 | "devDependencies": { 39 | "@antfu/eslint-config": "^0.35.3", 40 | "@commitlint/cli": "^17.4.4", 41 | "@commitlint/config-conventional": "^17.4.4", 42 | "@iconify/vue": "^4.1.0", 43 | "@types/crypto-js": "^4.1.1", 44 | "@types/katex": "^0.16.0", 45 | "@types/markdown-it": "^12.2.3", 46 | "@types/markdown-it-link-attributes": "^3.0.1", 47 | "@types/node": "^18.14.6", 48 | "@vitejs/plugin-vue": "^4.0.0", 49 | "autoprefixer": "^10.4.13", 50 | "axios": "^1.3.4", 51 | "crypto-js": "^4.1.1", 52 | "eslint": "^8.35.0", 53 | "husky": "^8.0.3", 54 | "less": "^4.1.3", 55 | "lint-staged": "^13.1.2", 56 | "markdown-it-link-attributes": "^4.0.1", 57 | "npm-run-all": "^4.1.5", 58 | "postcss": "^8.4.21", 59 | "rimraf": "^4.2.0", 60 | "tailwindcss": "^3.2.7", 61 | "typescript": "~4.9.5", 62 | "vite": "^4.2.0", 63 | "vite-plugin-pwa": "^0.14.4", 64 | "vue-tsc": "^1.2.0" 65 | }, 66 | "lint-staged": { 67 | "*.{ts,tsx,vue}": [ 68 | "pnpm lint:fix" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/common/Setting/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 71 | -------------------------------------------------------------------------------- /src/components/common/Setting/About.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 76 | -------------------------------------------------------------------------------- /src/views/chat/layout/Permission.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 81 | -------------------------------------------------------------------------------- /src/utils/is/index.ts: -------------------------------------------------------------------------------- 1 | export function isNumber(value: T | unknown): value is number { 2 | return Object.prototype.toString.call(value) === '[object Number]' 3 | } 4 | 5 | export function isString(value: T | unknown): value is string { 6 | return Object.prototype.toString.call(value) === '[object String]' 7 | } 8 | 9 | export function isBoolean(value: T | unknown): value is boolean { 10 | return Object.prototype.toString.call(value) === '[object Boolean]' 11 | } 12 | 13 | export function isNull(value: T | unknown): value is null { 14 | return Object.prototype.toString.call(value) === '[object Null]' 15 | } 16 | 17 | export function isUndefined(value: T | unknown): value is undefined { 18 | return Object.prototype.toString.call(value) === '[object Undefined]' 19 | } 20 | 21 | export function isObject(value: T | unknown): value is object { 22 | return Object.prototype.toString.call(value) === '[object Object]' 23 | } 24 | 25 | export function isArray(value: T | unknown): value is T { 26 | return Object.prototype.toString.call(value) === '[object Array]' 27 | } 28 | 29 | export function isFunction any | void | never>(value: T | unknown): value is T { 30 | return Object.prototype.toString.call(value) === '[object Function]' 31 | } 32 | 33 | export function isDate(value: T | unknown): value is T { 34 | return Object.prototype.toString.call(value) === '[object Date]' 35 | } 36 | 37 | export function isRegExp(value: T | unknown): value is T { 38 | return Object.prototype.toString.call(value) === '[object RegExp]' 39 | } 40 | 41 | export function isPromise>(value: T | unknown): value is T { 42 | return Object.prototype.toString.call(value) === '[object Promise]' 43 | } 44 | 45 | export function isSet>(value: T | unknown): value is T { 46 | return Object.prototype.toString.call(value) === '[object Set]' 47 | } 48 | 49 | export function isMap>(value: T | unknown): value is T { 50 | return Object.prototype.toString.call(value) === '[object Map]' 51 | } 52 | 53 | export function isFile(value: T | unknown): value is T { 54 | return Object.prototype.toString.call(value) === '[object File]' 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/request/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios' 2 | import request from './axios' 3 | import { useAuthStore } from '@/store' 4 | 5 | export interface HttpOption { 6 | url: string 7 | data?: any 8 | method?: string 9 | headers?: any 10 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void 11 | signal?: GenericAbortSignal 12 | beforeRequest?: () => void 13 | afterRequest?: () => void 14 | } 15 | 16 | export interface Response { 17 | data: T 18 | message: string | null 19 | status: string 20 | } 21 | 22 | function http( 23 | { url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 24 | ) { 25 | const successHandler = (res: AxiosResponse>) => { 26 | const authStore = useAuthStore() 27 | 28 | if (res.data.status === 'Success' || typeof res.data === 'string') 29 | return res.data 30 | 31 | if (res.data.status === 'Unauthorized') { 32 | authStore.removeToken() 33 | window.location.reload() 34 | } 35 | 36 | return Promise.reject(res.data) 37 | } 38 | 39 | const failHandler = (error: Response) => { 40 | afterRequest?.() 41 | throw new Error(error?.message || 'Error') 42 | } 43 | 44 | beforeRequest?.() 45 | 46 | method = method || 'GET' 47 | 48 | const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {}) 49 | 50 | return method === 'GET' 51 | ? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler) 52 | : request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler) 53 | } 54 | 55 | export function get( 56 | { url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 57 | ): Promise> { 58 | return http({ 59 | url, 60 | method, 61 | data, 62 | onDownloadProgress, 63 | signal, 64 | beforeRequest, 65 | afterRequest, 66 | }) 67 | } 68 | 69 | export function post( 70 | { url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 71 | ): Promise> { 72 | return http({ 73 | url, 74 | method, 75 | data, 76 | headers, 77 | onDownloadProgress, 78 | signal, 79 | beforeRequest, 80 | afterRequest, 81 | }) 82 | } 83 | 84 | export default post 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.en.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | Thank you for your valuable time. Your contributions will make this project better! Before submitting a contribution, please take some time to read the getting started guide below. 3 | 4 | ## Semantic Versioning 5 | This project follows semantic versioning. We release patch versions for important bug fixes, minor versions for new features or non-important changes, and major versions for significant and incompatible changes. 6 | 7 | Each major change will be recorded in the `changelog`. 8 | 9 | ## Submitting Pull Request 10 | 1. Fork [this repository](https://github.com/Chanzhaoyu/chatgpt-web) and create a branch from `main`. For new feature implementations, submit a pull request to the `feature` branch. For other changes, submit to the `main` branch. 11 | 2. Install the `pnpm` tool using `npm install pnpm -g`. 12 | 3. Install the `Eslint` plugin for `VSCode`, or enable `eslint` functionality for other editors such as `WebStorm`. 13 | 4. Execute `pnpm bootstrap` in the root directory. 14 | 5. Execute `pnpm install` in the `/service/` directory. 15 | 6. Make changes to the codebase. If applicable, ensure that appropriate testing has been done. 16 | 7. Execute `pnpm lint:fix` in the root directory to perform a code formatting check. 17 | 8. Execute `pnpm type-check` in the root directory to perform a type check. 18 | 9. Submit a git commit, following the [Commit Guidelines](#commit-guidelines). 19 | 10. Submit a `pull request`. If there is a corresponding `issue`, please link it using the [linking-a-pull-request-to-an-issue keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword). 20 | 21 | ## Commit Guidelines 22 | 23 | Commit messages should follow the [conventional-changelog standard](https://www.conventionalcommits.org/en/v1.0.0/): 24 | 25 | ```bash 26 | [optional scope]: 27 | 28 | [optional body] 29 | 30 | [optional footer] 31 | ``` 32 | 33 | ### Commit Types 34 | 35 | The following is a list of commit types: 36 | 37 | - feat: New feature or functionality 38 | - fix: Bug fix 39 | - docs: Documentation update 40 | - style: Code style or component style update 41 | - refactor: Code refactoring, no new features or bug fixes introduced 42 | - perf: Performance optimization 43 | - test: Unit test 44 | - chore: Other commits that do not modify src or test files 45 | 46 | 47 | ## License 48 | 49 | [MIT](./license) -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | add: '添加', 4 | addSuccess: '添加成功', 5 | edit: '编辑', 6 | editSuccess: '编辑成功', 7 | delete: '删除', 8 | deleteSuccess: '删除成功', 9 | save: '保存', 10 | saveSuccess: '保存成功', 11 | reset: '重置', 12 | action: '操作', 13 | export: '导出', 14 | exportSuccess: '导出成功', 15 | import: '导入', 16 | importSuccess: '导入成功', 17 | clear: '清空', 18 | clearSuccess: '清空成功', 19 | yes: '是', 20 | no: '否', 21 | confirm: '确定', 22 | download: '下载', 23 | noData: '暂无数据', 24 | wrong: '好像出错了,请稍后再试。', 25 | success: '操作成功', 26 | failed: '操作失败', 27 | verify: '验证', 28 | unauthorizedTips: '未经授权,请先进行验证。', 29 | }, 30 | chat: { 31 | newChatButton: '新建聊天', 32 | placeholder: '来说点什么吧...(Shift + Enter = 换行)', 33 | placeholderMobile: '来说点什么...', 34 | copy: '复制', 35 | copied: '复制成功', 36 | copyCode: '复制代码', 37 | clearChat: '清空会话', 38 | clearChatConfirm: '是否清空会话?', 39 | exportImage: '保存会话到图片', 40 | exportImageConfirm: '是否将会话保存为图片?', 41 | exportSuccess: '保存成功', 42 | exportFailed: '保存失败', 43 | usingContext: '上下文模式', 44 | turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录', 45 | turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录', 46 | deleteMessage: '删除消息', 47 | deleteMessageConfirm: '是否删除此消息?', 48 | deleteHistoryConfirm: '确定删除此记录?', 49 | clearHistoryConfirm: '确定清空聊天记录?', 50 | preview: '预览', 51 | showRawText: '显示原文', 52 | }, 53 | setting: { 54 | setting: '设置', 55 | general: '总览', 56 | advanced: '高级', 57 | config: '配置', 58 | avatarLink: '头像链接', 59 | name: '名称', 60 | description: '描述', 61 | role: '角色设定', 62 | resetUserInfo: '重置用户信息', 63 | chatHistory: '聊天记录', 64 | theme: '主题', 65 | language: '语言', 66 | api: 'API', 67 | reverseProxy: '反向代理', 68 | timeout: '超时', 69 | socks: 'Socks', 70 | httpsProxy: 'HTTPS Proxy', 71 | balance: 'API余额', 72 | }, 73 | store: { 74 | siderButton: '提示词商店', 75 | local: '本地', 76 | online: '在线', 77 | title: '标题', 78 | description: '描述', 79 | clearStoreConfirm: '是否清空数据?', 80 | importPlaceholder: '请粘贴 JSON 数据到此处', 81 | addRepeatTitleTips: '标题重复,请重新输入', 82 | addRepeatContentTips: '内容重复:{msg},请重新输入', 83 | editRepeatTitleTips: '标题冲突,请重新修改', 84 | editRepeatContentTips: '内容冲突{msg} ,请重新修改', 85 | importError: '键值不匹配', 86 | importRepeatTitle: '标题重复跳过:{msg}', 87 | importRepeatContent: '内容重复跳过:{msg}', 88 | onlineImportWarning: '注意:请检查 JSON 文件来源!', 89 | downloadError: '请检查网络状态与 JSON 文件有效性', 90 | }, 91 | } 92 | -------------------------------------------------------------------------------- /src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | add: '新增', 4 | addSuccess: '新增成功', 5 | edit: '編輯', 6 | editSuccess: '編輯成功', 7 | delete: '刪除', 8 | deleteSuccess: '刪除成功', 9 | save: '儲存', 10 | saveSuccess: '儲存成功', 11 | reset: '重設', 12 | action: '操作', 13 | export: '匯出', 14 | exportSuccess: '匯出成功', 15 | import: '匯入', 16 | importSuccess: '匯入成功', 17 | clear: '清除', 18 | clearSuccess: '清除成功', 19 | yes: '是', 20 | no: '否', 21 | confirm: '確認', 22 | download: '下載', 23 | noData: '目前無資料', 24 | wrong: '發生錯誤,請稍後再試。', 25 | success: '操作成功', 26 | failed: '操作失敗', 27 | verify: '驗證', 28 | unauthorizedTips: '未經授權,請先進行驗證。', 29 | }, 30 | chat: { 31 | newChatButton: '新建對話', 32 | placeholder: '來說點什麼...(Shift + Enter = 換行)', 33 | placeholderMobile: '來說點什麼...', 34 | copy: '複製', 35 | copied: '複製成功', 36 | copyCode: '複製代碼', 37 | clearChat: '清除對話', 38 | clearChatConfirm: '是否清空對話?', 39 | exportImage: '儲存對話為圖片', 40 | exportImageConfirm: '是否將對話儲存為圖片?', 41 | exportSuccess: '儲存成功', 42 | exportFailed: '儲存失敗', 43 | usingContext: '上下文模式', 44 | turnOnContext: '啟用上下文模式,在此模式下,發送訊息會包含之前的聊天記錄。', 45 | turnOffContext: '關閉上下文模式,在此模式下,發送訊息不會包含之前的聊天記錄。', 46 | deleteMessage: '刪除訊息', 47 | deleteMessageConfirm: '是否刪除此訊息?', 48 | deleteHistoryConfirm: '確定刪除此紀錄?', 49 | clearHistoryConfirm: '確定清除紀錄?', 50 | preview: '預覽', 51 | showRawText: '顯示原文', 52 | }, 53 | setting: { 54 | setting: '設定', 55 | general: '總覽', 56 | advanced: '高級', 57 | config: '設定', 58 | avatarLink: '頭貼連結', 59 | name: '名稱', 60 | description: '描述', 61 | role: '角色設定', 62 | resetUserInfo: '重設使用者資訊', 63 | chatHistory: '紀錄', 64 | theme: '主題', 65 | language: '語言', 66 | api: 'API', 67 | reverseProxy: '反向代理', 68 | timeout: '逾時', 69 | socks: 'Socks', 70 | httpsProxy: 'HTTPS Proxy', 71 | balance: 'API余額', 72 | }, 73 | store: { 74 | siderButton: '提示詞商店', 75 | local: '本機', 76 | online: '線上', 77 | title: '標題', 78 | description: '描述', 79 | clearStoreConfirm: '是否清除資料?', 80 | importPlaceholder: '請將 JSON 資料貼在此處', 81 | addRepeatTitleTips: '標題重複,請重新輸入', 82 | addRepeatContentTips: '內容重複:{msg},請重新輸入', 83 | editRepeatTitleTips: '標題衝突,請重新修改', 84 | editRepeatContentTips: '內容衝突{msg} ,請重新修改', 85 | importError: '鍵值不符合', 86 | importRepeatTitle: '因標題重複跳過:{msg}', 87 | importRepeatContent: '因內容重複跳過:{msg}', 88 | onlineImportWarning: '注意:請檢查 JSON 檔案來源!', 89 | downloadError: '請檢查網路狀態與 JSON 檔案有效性', 90 | }, 91 | } 92 | -------------------------------------------------------------------------------- /src/views/chat/components/Header/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 79 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/index.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 96 | -------------------------------------------------------------------------------- /service/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import type { RequestProps } from './types' 3 | import type { ChatMessage } from './chatgpt' 4 | import { chatConfig, chatReplyProcess, currentModel } from './chatgpt' 5 | import { auth } from './middleware/auth' 6 | import { limiter } from './middleware/limiter' 7 | import { isNotEmptyString } from './utils/is' 8 | 9 | const app = express() 10 | const router = express.Router() 11 | 12 | app.use(express.static('public')) 13 | app.use(express.json()) 14 | 15 | app.all('*', (_, res, next) => { 16 | res.header('Access-Control-Allow-Origin', '*') 17 | res.header('Access-Control-Allow-Headers', 'authorization, Content-Type') 18 | res.header('Access-Control-Allow-Methods', '*') 19 | next() 20 | }) 21 | 22 | router.post('/chat-process', [auth, limiter], async (req, res) => { 23 | res.setHeader('Content-type', 'application/octet-stream') 24 | 25 | try { 26 | const { prompt, options = {}, systemMessage } = req.body as RequestProps 27 | let firstChunk = true 28 | await chatReplyProcess({ 29 | message: prompt, 30 | lastContext: options, 31 | process: (chat: ChatMessage) => { 32 | res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`) 33 | firstChunk = false 34 | }, 35 | systemMessage, 36 | }) 37 | } 38 | catch (error) { 39 | res.write(JSON.stringify(error)) 40 | } 41 | finally { 42 | res.end() 43 | } 44 | }) 45 | 46 | router.post('/config', auth, async (req, res) => { 47 | try { 48 | const response = await chatConfig() 49 | res.send(response) 50 | } 51 | catch (error) { 52 | res.send(error) 53 | } 54 | }) 55 | 56 | router.post('/session', async (req, res) => { 57 | try { 58 | const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY 59 | const hasAuth = isNotEmptyString(AUTH_SECRET_KEY) 60 | res.send({ status: 'Success', message: '', data: { auth: hasAuth, model: currentModel() } }) 61 | } 62 | catch (error) { 63 | res.send({ status: 'Fail', message: error.message, data: null }) 64 | } 65 | }) 66 | 67 | router.post('/verify', async (req, res) => { 68 | try { 69 | const { token } = req.body as { token: string } 70 | if (!token) 71 | throw new Error('Secret key is empty') 72 | 73 | if (process.env.AUTH_SECRET_KEY !== token) 74 | throw new Error('密钥无效 | Secret key is invalid') 75 | 76 | res.send({ status: 'Success', message: 'Verify successfully', data: null }) 77 | } 78 | catch (error) { 79 | res.send({ status: 'Fail', message: error.message, data: null }) 80 | } 81 | }) 82 | 83 | app.use('', router) 84 | app.use('/api', router) 85 | app.set('trust proxy', 1) 86 | 87 | app.listen(3002, () => globalThis.console.log('Server is running on port 3002')) 88 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/Text.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 82 | 83 | 86 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | add: 'Add', 4 | addSuccess: 'Add Success', 5 | edit: 'Edit', 6 | editSuccess: 'Edit Success', 7 | delete: 'Delete', 8 | deleteSuccess: 'Delete Success', 9 | save: 'Save', 10 | saveSuccess: 'Save Success', 11 | reset: 'Reset', 12 | action: 'Action', 13 | export: 'Export', 14 | exportSuccess: 'Export Success', 15 | import: 'Import', 16 | importSuccess: 'Import Success', 17 | clear: 'Clear', 18 | clearSuccess: 'Clear Success', 19 | yes: 'Yes', 20 | no: 'No', 21 | confirm: 'Confirm', 22 | download: 'Download', 23 | noData: 'No Data', 24 | wrong: 'Something went wrong, please try again later.', 25 | success: 'Success', 26 | failed: 'Failed', 27 | verify: 'Verify', 28 | unauthorizedTips: 'Unauthorized, please verify first.', 29 | }, 30 | chat: { 31 | newChatButton: 'New Chat', 32 | placeholder: 'Ask me anything...(Shift + Enter = line break)', 33 | placeholderMobile: 'Ask me anything...', 34 | copy: 'Copy', 35 | copied: 'Copied', 36 | copyCode: 'Copy Code', 37 | clearChat: 'Clear Chat', 38 | clearChatConfirm: 'Are you sure to clear this chat?', 39 | exportImage: 'Export Image', 40 | exportImageConfirm: 'Are you sure to export this chat to png?', 41 | exportSuccess: 'Export Success', 42 | exportFailed: 'Export Failed', 43 | usingContext: 'Context Mode', 44 | turnOnContext: 'In the current mode, sending messages will carry previous chat records.', 45 | turnOffContext: 'In the current mode, sending messages will not carry previous chat records.', 46 | deleteMessage: 'Delete Message', 47 | deleteMessageConfirm: 'Are you sure to delete this message?', 48 | deleteHistoryConfirm: 'Are you sure to clear this history?', 49 | clearHistoryConfirm: 'Are you sure to clear chat history?', 50 | preview: 'Preview', 51 | showRawText: 'Show as raw text', 52 | }, 53 | setting: { 54 | setting: 'Setting', 55 | general: 'General', 56 | advanced: 'Advanced', 57 | config: 'Config', 58 | avatarLink: 'Avatar Link', 59 | name: 'Name', 60 | description: 'Description', 61 | role: 'Role', 62 | resetUserInfo: 'Reset UserInfo', 63 | chatHistory: 'ChatHistory', 64 | theme: 'Theme', 65 | language: 'Language', 66 | api: 'API', 67 | reverseProxy: 'Reverse Proxy', 68 | timeout: 'Timeout', 69 | socks: 'Socks', 70 | httpsProxy: 'HTTPS Proxy', 71 | balance: 'API Balance', 72 | }, 73 | store: { 74 | siderButton: 'Prompt Store', 75 | local: 'Local', 76 | online: 'Online', 77 | title: 'Title', 78 | description: 'Description', 79 | clearStoreConfirm: 'Whether to clear the data?', 80 | importPlaceholder: 'Please paste the JSON data here', 81 | addRepeatTitleTips: 'Title duplicate, please re-enter', 82 | addRepeatContentTips: 'Content duplicate: {msg}, please re-enter', 83 | editRepeatTitleTips: 'Title conflict, please revise', 84 | editRepeatContentTips: 'Content conflict {msg} , please re-modify', 85 | importError: 'Key value mismatch', 86 | importRepeatTitle: 'Title repeatedly skipped: {msg}', 87 | importRepeatContent: 'Content is repeatedly skipped: {msg}', 88 | onlineImportWarning: 'Note: Please check the JSON file source!', 89 | downloadError: 'Please check the network status and JSON file validity', 90 | }, 91 | } 92 | -------------------------------------------------------------------------------- /src/styles/lib/highlight.less: -------------------------------------------------------------------------------- 1 | html.dark { 2 | pre code.hljs { 3 | display: block; 4 | overflow-x: auto; 5 | padding: 1em 6 | } 7 | 8 | code.hljs { 9 | padding: 3px 5px 10 | } 11 | 12 | .hljs { 13 | color: #abb2bf; 14 | background: #282c34 15 | } 16 | 17 | .hljs-keyword, 18 | .hljs-operator, 19 | .hljs-pattern-match { 20 | color: #f92672 21 | } 22 | 23 | .hljs-function, 24 | .hljs-pattern-match .hljs-constructor { 25 | color: #61aeee 26 | } 27 | 28 | .hljs-function .hljs-params { 29 | color: #a6e22e 30 | } 31 | 32 | .hljs-function .hljs-params .hljs-typing { 33 | color: #fd971f 34 | } 35 | 36 | .hljs-module-access .hljs-module { 37 | color: #7e57c2 38 | } 39 | 40 | .hljs-constructor { 41 | color: #e2b93d 42 | } 43 | 44 | .hljs-constructor .hljs-string { 45 | color: #9ccc65 46 | } 47 | 48 | .hljs-comment, 49 | .hljs-quote { 50 | color: #b18eb1; 51 | font-style: italic 52 | } 53 | 54 | .hljs-doctag, 55 | .hljs-formula { 56 | color: #c678dd 57 | } 58 | 59 | .hljs-deletion, 60 | .hljs-name, 61 | .hljs-section, 62 | .hljs-selector-tag, 63 | .hljs-subst { 64 | color: #e06c75 65 | } 66 | 67 | .hljs-literal { 68 | color: #56b6c2 69 | } 70 | 71 | .hljs-addition, 72 | .hljs-attribute, 73 | .hljs-meta .hljs-string, 74 | .hljs-regexp, 75 | .hljs-string { 76 | color: #98c379 77 | } 78 | 79 | .hljs-built_in, 80 | .hljs-class .hljs-title, 81 | .hljs-title.class_ { 82 | color: #e6c07b 83 | } 84 | 85 | .hljs-attr, 86 | .hljs-number, 87 | .hljs-selector-attr, 88 | .hljs-selector-class, 89 | .hljs-selector-pseudo, 90 | .hljs-template-variable, 91 | .hljs-type, 92 | .hljs-variable { 93 | color: #d19a66 94 | } 95 | 96 | .hljs-bullet, 97 | .hljs-link, 98 | .hljs-meta, 99 | .hljs-selector-id, 100 | .hljs-symbol, 101 | .hljs-title { 102 | color: #61aeee 103 | } 104 | 105 | .hljs-emphasis { 106 | font-style: italic 107 | } 108 | 109 | .hljs-strong { 110 | font-weight: 700 111 | } 112 | 113 | .hljs-link { 114 | text-decoration: underline 115 | } 116 | } 117 | 118 | html { 119 | pre code.hljs { 120 | display: block; 121 | overflow-x: auto; 122 | padding: 1em 123 | } 124 | 125 | code.hljs { 126 | padding: 3px 5px 127 | } 128 | 129 | .hljs { 130 | color: #383a42; 131 | background: #fafafa 132 | } 133 | 134 | .hljs-comment, 135 | .hljs-quote { 136 | color: #a0a1a7; 137 | font-style: italic 138 | } 139 | 140 | .hljs-doctag, 141 | .hljs-formula, 142 | .hljs-keyword { 143 | color: #a626a4 144 | } 145 | 146 | .hljs-deletion, 147 | .hljs-name, 148 | .hljs-section, 149 | .hljs-selector-tag, 150 | .hljs-subst { 151 | color: #e45649 152 | } 153 | 154 | .hljs-literal { 155 | color: #0184bb 156 | } 157 | 158 | .hljs-addition, 159 | .hljs-attribute, 160 | .hljs-meta .hljs-string, 161 | .hljs-regexp, 162 | .hljs-string { 163 | color: #50a14f 164 | } 165 | 166 | .hljs-attr, 167 | .hljs-number, 168 | .hljs-selector-attr, 169 | .hljs-selector-class, 170 | .hljs-selector-pseudo, 171 | .hljs-template-variable, 172 | .hljs-type, 173 | .hljs-variable { 174 | color: #986801 175 | } 176 | 177 | .hljs-bullet, 178 | .hljs-link, 179 | .hljs-meta, 180 | .hljs-selector-id, 181 | .hljs-symbol, 182 | .hljs-title { 183 | color: #4078f2 184 | } 185 | 186 | .hljs-built_in, 187 | .hljs-class .hljs-title, 188 | .hljs-title.class_ { 189 | color: #c18401 190 | } 191 | 192 | .hljs-emphasis { 193 | font-style: italic 194 | } 195 | 196 | .hljs-strong { 197 | font-weight: 700 198 | } 199 | 200 | .hljs-link { 201 | text-decoration: underline 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/index.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 134 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/List.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 106 | -------------------------------------------------------------------------------- /src/store/modules/chat/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getLocalState, setLocalState } from './helper' 3 | import { router } from '@/router' 4 | 5 | export const useChatStore = defineStore('chat-store', { 6 | state: (): Chat.ChatState => getLocalState(), 7 | 8 | getters: { 9 | getChatHistoryByCurrentActive(state: Chat.ChatState) { 10 | const index = state.history.findIndex(item => item.uuid === state.active) 11 | if (index !== -1) 12 | return state.history[index] 13 | return null 14 | }, 15 | 16 | getChatByUuid(state: Chat.ChatState) { 17 | return (uuid?: number) => { 18 | if (uuid) 19 | return state.chat.find(item => item.uuid === uuid)?.data ?? [] 20 | return state.chat.find(item => item.uuid === state.active)?.data ?? [] 21 | } 22 | }, 23 | }, 24 | 25 | actions: { 26 | setUsingContext(context: boolean) { 27 | this.usingContext = context 28 | this.recordState() 29 | }, 30 | 31 | addHistory(history: Chat.History, chatData: Chat.Chat[] = []) { 32 | this.history.unshift(history) 33 | this.chat.unshift({ uuid: history.uuid, data: chatData }) 34 | this.active = history.uuid 35 | this.reloadRoute(history.uuid) 36 | }, 37 | 38 | updateHistory(uuid: number, edit: Partial) { 39 | const index = this.history.findIndex(item => item.uuid === uuid) 40 | if (index !== -1) { 41 | this.history[index] = { ...this.history[index], ...edit } 42 | this.recordState() 43 | } 44 | }, 45 | 46 | async deleteHistory(index: number) { 47 | this.history.splice(index, 1) 48 | this.chat.splice(index, 1) 49 | 50 | if (this.history.length === 0) { 51 | this.active = null 52 | this.reloadRoute() 53 | return 54 | } 55 | 56 | if (index > 0 && index <= this.history.length) { 57 | const uuid = this.history[index - 1].uuid 58 | this.active = uuid 59 | this.reloadRoute(uuid) 60 | return 61 | } 62 | 63 | if (index === 0) { 64 | if (this.history.length > 0) { 65 | const uuid = this.history[0].uuid 66 | this.active = uuid 67 | this.reloadRoute(uuid) 68 | } 69 | } 70 | 71 | if (index > this.history.length) { 72 | const uuid = this.history[this.history.length - 1].uuid 73 | this.active = uuid 74 | this.reloadRoute(uuid) 75 | } 76 | }, 77 | 78 | async setActive(uuid: number) { 79 | this.active = uuid 80 | return await this.reloadRoute(uuid) 81 | }, 82 | 83 | getChatByUuidAndIndex(uuid: number, index: number) { 84 | if (!uuid || uuid === 0) { 85 | if (this.chat.length) 86 | return this.chat[0].data[index] 87 | return null 88 | } 89 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 90 | if (chatIndex !== -1) 91 | return this.chat[chatIndex].data[index] 92 | return null 93 | }, 94 | 95 | addChatByUuid(uuid: number, chat: Chat.Chat) { 96 | if (!uuid || uuid === 0) { 97 | if (this.history.length === 0) { 98 | const uuid = Date.now() 99 | this.history.push({ uuid, title: chat.text, isEdit: false }) 100 | this.chat.push({ uuid, data: [chat] }) 101 | this.active = uuid 102 | this.recordState() 103 | } 104 | else { 105 | this.chat[0].data.push(chat) 106 | if (this.history[0].title === 'New Chat') 107 | this.history[0].title = chat.text 108 | this.recordState() 109 | } 110 | } 111 | 112 | const index = this.chat.findIndex(item => item.uuid === uuid) 113 | if (index !== -1) { 114 | this.chat[index].data.push(chat) 115 | if (this.history[index].title === 'New Chat') 116 | this.history[index].title = chat.text 117 | this.recordState() 118 | } 119 | }, 120 | 121 | updateChatByUuid(uuid: number, index: number, chat: Chat.Chat) { 122 | if (!uuid || uuid === 0) { 123 | if (this.chat.length) { 124 | this.chat[0].data[index] = chat 125 | this.recordState() 126 | } 127 | return 128 | } 129 | 130 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 131 | if (chatIndex !== -1) { 132 | this.chat[chatIndex].data[index] = chat 133 | this.recordState() 134 | } 135 | }, 136 | 137 | updateChatSomeByUuid(uuid: number, index: number, chat: Partial) { 138 | if (!uuid || uuid === 0) { 139 | if (this.chat.length) { 140 | this.chat[0].data[index] = { ...this.chat[0].data[index], ...chat } 141 | this.recordState() 142 | } 143 | return 144 | } 145 | 146 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 147 | if (chatIndex !== -1) { 148 | this.chat[chatIndex].data[index] = { ...this.chat[chatIndex].data[index], ...chat } 149 | this.recordState() 150 | } 151 | }, 152 | 153 | deleteChatByUuid(uuid: number, index: number) { 154 | if (!uuid || uuid === 0) { 155 | if (this.chat.length) { 156 | this.chat[0].data.splice(index, 1) 157 | this.recordState() 158 | } 159 | return 160 | } 161 | 162 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 163 | if (chatIndex !== -1) { 164 | this.chat[chatIndex].data.splice(index, 1) 165 | this.recordState() 166 | } 167 | }, 168 | 169 | clearChatByUuid(uuid: number) { 170 | if (!uuid || uuid === 0) { 171 | if (this.chat.length) { 172 | this.chat[0].data = [] 173 | this.recordState() 174 | } 175 | return 176 | } 177 | 178 | const index = this.chat.findIndex(item => item.uuid === uuid) 179 | if (index !== -1) { 180 | this.chat[index].data = [] 181 | this.recordState() 182 | } 183 | }, 184 | 185 | async reloadRoute(uuid?: number) { 186 | this.recordState() 187 | await router.push({ name: 'Chat', params: { uuid } }) 188 | }, 189 | 190 | recordState() { 191 | setLocalState(this.$state) 192 | }, 193 | }, 194 | }) 195 | -------------------------------------------------------------------------------- /service/src/chatgpt/index.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import 'isomorphic-fetch' 3 | import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt' 4 | import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt' 5 | import { SocksProxyAgent } from 'socks-proxy-agent' 6 | import httpsProxyAgent from 'https-proxy-agent' 7 | import fetch from 'node-fetch' 8 | import axios from 'axios' 9 | import { sendResponse } from '../utils' 10 | import { isNotEmptyString } from '../utils/is' 11 | import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types' 12 | import type { RequestOptions } from './types' 13 | 14 | const { HttpsProxyAgent } = httpsProxyAgent 15 | 16 | dotenv.config() 17 | 18 | const ErrorCodeMessage: Record = { 19 | 401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided', 20 | 403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later', 21 | 502: '[OpenAI] 错误的网关 | Bad Gateway', 22 | 503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later', 23 | 504: '[OpenAI] 网关超时 | Gateway Time-out', 24 | 500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error', 25 | } 26 | 27 | const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 30 * 1000 28 | 29 | let apiModel: ApiModel 30 | 31 | if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN)) 32 | throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable') 33 | 34 | let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI 35 | 36 | (async () => { 37 | // More Info: https://github.com/transitive-bullshit/chatgpt-api 38 | 39 | if (isNotEmptyString(process.env.OPENAI_API_KEY)) { 40 | const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL 41 | const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL 42 | const model = isNotEmptyString(OPENAI_API_MODEL) ? OPENAI_API_MODEL : 'gpt-3.5-turbo' 43 | 44 | const options: ChatGPTAPIOptions = { 45 | apiKey: process.env.OPENAI_API_KEY, 46 | completionParams: { model }, 47 | debug: true, 48 | } 49 | 50 | // increase max token limit if use gpt-4 51 | if (model.toLowerCase().includes('gpt-4')) { 52 | // if use 32k model 53 | if (model.toLowerCase().includes('32k')) { 54 | options.maxModelTokens = 32768 55 | options.maxResponseTokens = 8192 56 | } 57 | else { 58 | options.maxModelTokens = 8192 59 | options.maxResponseTokens = 2048 60 | } 61 | } 62 | 63 | if (isNotEmptyString(OPENAI_API_BASE_URL)) 64 | options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1` 65 | 66 | setupProxy(options) 67 | 68 | api = new ChatGPTAPI({ ...options }) 69 | apiModel = 'ChatGPTAPI' 70 | } 71 | else { 72 | const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL 73 | const options: ChatGPTUnofficialProxyAPIOptions = { 74 | accessToken: process.env.OPENAI_ACCESS_TOKEN, 75 | debug: true, 76 | } 77 | if (isNotEmptyString(OPENAI_API_MODEL)) 78 | options.model = OPENAI_API_MODEL 79 | 80 | if (isNotEmptyString(process.env.API_REVERSE_PROXY)) 81 | options.apiReverseProxyUrl = process.env.API_REVERSE_PROXY 82 | 83 | setupProxy(options) 84 | 85 | api = new ChatGPTUnofficialProxyAPI({ ...options }) 86 | apiModel = 'ChatGPTUnofficialProxyAPI' 87 | } 88 | })() 89 | 90 | async function chatReplyProcess(options: RequestOptions) { 91 | const { message, lastContext, process, systemMessage } = options 92 | try { 93 | let options: SendMessageOptions = { timeoutMs } 94 | 95 | if (apiModel === 'ChatGPTAPI') { 96 | if (isNotEmptyString(systemMessage)) 97 | options.systemMessage = systemMessage 98 | } 99 | 100 | if (lastContext != null) { 101 | if (apiModel === 'ChatGPTAPI') 102 | options.parentMessageId = lastContext.parentMessageId 103 | else 104 | options = { ...lastContext } 105 | } 106 | 107 | const response = await api.sendMessage(message, { 108 | ...options, 109 | onProgress: (partialResponse) => { 110 | process?.(partialResponse) 111 | }, 112 | }) 113 | 114 | return sendResponse({ type: 'Success', data: response }) 115 | } 116 | catch (error: any) { 117 | const code = error.statusCode 118 | global.console.log(error) 119 | if (Reflect.has(ErrorCodeMessage, code)) 120 | return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] }) 121 | return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' }) 122 | } 123 | } 124 | 125 | async function fetchBalance() { 126 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY 127 | const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL 128 | 129 | if (!isNotEmptyString(OPENAI_API_KEY)) 130 | return Promise.resolve('-') 131 | 132 | const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL) 133 | ? OPENAI_API_BASE_URL 134 | : 'https://api.openai.com' 135 | 136 | try { 137 | const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${OPENAI_API_KEY}` } 138 | const response = await axios.get(`${API_BASE_URL}/dashboard/billing/credit_grants`, { headers }) 139 | const balance = response.data.total_available ?? 0 140 | return Promise.resolve(balance.toFixed(3)) 141 | } 142 | catch { 143 | return Promise.resolve('-') 144 | } 145 | } 146 | 147 | async function chatConfig() { 148 | const balance = await fetchBalance() 149 | const reverseProxy = process.env.API_REVERSE_PROXY ?? '-' 150 | const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-' 151 | const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) 152 | ? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`) 153 | : '-' 154 | return sendResponse({ 155 | type: 'Success', 156 | data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, balance }, 157 | }) 158 | } 159 | 160 | function setupProxy(options: ChatGPTAPIOptions | ChatGPTUnofficialProxyAPIOptions) { 161 | if (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) { 162 | const agent = new SocksProxyAgent({ 163 | hostname: process.env.SOCKS_PROXY_HOST, 164 | port: process.env.SOCKS_PROXY_PORT, 165 | }) 166 | options.fetch = (url, options) => { 167 | return fetch(url, { agent, ...options }) 168 | } 169 | } 170 | else { 171 | if (process.env.HTTPS_PROXY || process.env.ALL_PROXY) { 172 | const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY 173 | if (httpsProxy) { 174 | const agent = new HttpsProxyAgent(httpsProxy) 175 | options.fetch = (url, options) => { 176 | return fetch(url, { agent, ...options }) 177 | } 178 | } 179 | } 180 | } 181 | } 182 | 183 | function currentModel(): ApiModel { 184 | return apiModel 185 | } 186 | 187 | export type { ChatContext, ChatMessage } 188 | 189 | export { chatReplyProcess, chatConfig, currentModel } 190 | -------------------------------------------------------------------------------- /src/components/common/Setting/General.vue: -------------------------------------------------------------------------------- 1 | 122 | 123 | 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Web 2 | 3 |
4 | 中文 | 5 | English 6 |
7 |
8 | 9 | > 声明:此项目只发布于 Github,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。 10 | 11 | ![cover](./docs/c1.png) 12 | ![cover2](./docs/c2.png) 13 | 14 | - [ChatGPT Web](#chatgpt-web) 15 | - [介绍](#介绍) 16 | - [待实现路线](#待实现路线) 17 | - [前置要求](#前置要求) 18 | - [Node](#node) 19 | - [PNPM](#pnpm) 20 | - [填写密钥](#填写密钥) 21 | - [安装依赖](#安装依赖) 22 | - [后端](#后端) 23 | - [前端](#前端) 24 | - [测试环境运行](#测试环境运行) 25 | - [后端服务](#后端服务) 26 | - [前端网页](#前端网页) 27 | - [环境变量](#环境变量) 28 | - [打包](#打包) 29 | - [使用 Docker](#使用-docker) 30 | - [Docker 参数示例](#docker-参数示例) 31 | - [Docker build \& Run](#docker-build--run) 32 | - [Docker compose](#docker-compose) 33 | - [使用 Railway 部署](#使用-railway-部署) 34 | - [Railway 环境变量](#railway-环境变量) 35 | - [手动打包](#手动打包) 36 | - [后端服务](#后端服务-1) 37 | - [前端网页](#前端网页-1) 38 | - [常见问题](#常见问题) 39 | - [参与贡献](#参与贡献) 40 | - [赞助](#赞助) 41 | - [License](#license) 42 | ## 介绍 43 | 44 | 支持双模型,提供了两种非官方 `ChatGPT API` 方法 45 | 46 | | 方式 | 免费? | 可靠性 | 质量 | 47 | | --------------------------------------------- | ------ | ---------- | ---- | 48 | | `ChatGPTAPI(gpt-3.5-turbo-0301)` | 否 | 可靠 | 相对较笨 | 49 | | `ChatGPTUnofficialProxyAPI(网页 accessToken)` | 是 | 相对不可靠 | 聪明 | 50 | 51 | 对比: 52 | 1. `ChatGPTAPI` 使用 `gpt-3.5-turbo-0301` 通过官方`OpenAI`补全`API`模拟`ChatGPT`(最稳健的方法,但它不是免费的,并且没有使用针对聊天进行微调的模型) 53 | 2. `ChatGPTUnofficialProxyAPI` 使用非官方代理服务器访问 `ChatGPT` 的后端`API`,绕过`Cloudflare`(使用真实的的`ChatGPT`,非常轻量级,但依赖于第三方服务器,并且有速率限制) 54 | 55 | 警告: 56 | 1. 你应该首先使用 `API` 方式 57 | 2. 使用 `API` 时,如果网络不通,那是国内被墙了,你需要自建代理,绝对不要使用别人的公开代理,那是危险的。 58 | 3. 使用 `accessToken` 方式时反向代理将向第三方暴露您的访问令牌,这样做应该不会产生任何不良影响,但在使用这种方法之前请考虑风险。 59 | 4. 使用 `accessToken` 时,不管你是国内还是国外的机器,都会使用代理。默认代理为 [acheong08](https://github.com/acheong08) 大佬的 `https://bypass.churchless.tech/api/conversation`,这不是后门也不是监听,除非你有能力自己翻过 `CF` 验证,用前请知悉。[社区代理](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) 60 | 5. 把项目发布到公共网络时,你应该设置 `AUTH_SECRET_KEY` 变量添加你的密码访问权限,你也应该修改 `index.html` 中的 `title`,防止被关键词搜索到。 61 | 62 | 切换方式: 63 | 1. 进入 `service/.env.example` 文件,复制内容到 `service/.env` 文件 64 | 2. 使用 `OpenAI API Key` 请填写 `OPENAI_API_KEY` 字段 [(获取 apiKey)](https://platform.openai.com/overview) 65 | 3. 使用 `Web API` 请填写 `OPENAI_ACCESS_TOKEN` 字段 [(获取 accessToken)](https://chat.openai.com/api/auth/session) 66 | 4. 同时存在时以 `OpenAI API Key` 优先 67 | 68 | 环境变量: 69 | 70 | 全部参数变量请查看或[这里](#环境变量) 71 | 72 | ``` 73 | /service/.env.example 74 | ``` 75 | 76 | ## 待实现路线 77 | [✓] 双模型 78 | 79 | [✓] 多会话储存和上下文逻辑 80 | 81 | [✓] 对代码等消息类型的格式化美化处理 82 | 83 | [✓] 访问权限控制 84 | 85 | [✓] 数据导入、导出 86 | 87 | [✓] 保存消息到本地图片 88 | 89 | [✓] 界面多语言 90 | 91 | [✓] 界面主题 92 | 93 | [✗] More... 94 | 95 | ## 前置要求 96 | 97 | ### Node 98 | 99 | `node` 需要 `^16 || ^18 || ^19` 版本(`node >= 14` 需要安装 [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill)),使用 [nvm](https://github.com/nvm-sh/nvm) 可管理本地多个 `node` 版本 100 | 101 | ```shell 102 | node -v 103 | ``` 104 | 105 | ### PNPM 106 | 如果你没有安装过 `pnpm` 107 | ```shell 108 | npm install pnpm -g 109 | ``` 110 | 111 | ### 填写密钥 112 | 获取 `Openai Api Key` 或 `accessToken` 并填写本地环境变量 [跳转](#介绍) 113 | 114 | ``` 115 | # service/.env 文件 116 | 117 | # OpenAI API Key - https://platform.openai.com/overview 118 | OPENAI_API_KEY= 119 | 120 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 121 | OPENAI_ACCESS_TOKEN= 122 | ``` 123 | 124 | ## 安装依赖 125 | 126 | > 为了简便 `后端开发人员` 的了解负担,所以并没有采用前端 `workspace` 模式,而是分文件夹存放。如果只需要前端页面做二次开发,删除 `service` 文件夹即可。 127 | 128 | ### 后端 129 | 130 | 进入文件夹 `/service` 运行以下命令 131 | 132 | ```shell 133 | pnpm install 134 | ``` 135 | 136 | ### 前端 137 | 根目录下运行以下命令 138 | ```shell 139 | pnpm bootstrap 140 | ``` 141 | 142 | ## 测试环境运行 143 | ### 后端服务 144 | 145 | 进入文件夹 `/service` 运行以下命令 146 | 147 | ```shell 148 | pnpm start 149 | ``` 150 | 151 | ### 前端网页 152 | 根目录下运行以下命令 153 | ```shell 154 | pnpm dev 155 | ``` 156 | 157 | ## 环境变量 158 | 159 | `API` 可用: 160 | 161 | - `OPENAI_API_KEY` 和 `OPENAI_ACCESS_TOKEN` 二选一 162 | - `OPENAI_API_MODEL` 设置模型,可选,默认:`gpt-3.5-turbo` 163 | - `OPENAI_API_BASE_URL` 设置接口地址,可选,默认:`https://api.openai.com` 164 | 165 | `ACCESS_TOKEN` 可用: 166 | 167 | - `OPENAI_ACCESS_TOKEN` 和 `OPENAI_API_KEY` 二选一,同时存在时,`OPENAI_API_KEY` 优先 168 | - `API_REVERSE_PROXY` 设置反向代理,可选,默认:`https://bypass.churchless.tech/api/conversation`,[社区](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) 169 | 170 | 通用: 171 | 172 | - `AUTH_SECRET_KEY` 访问权限密钥,可选 173 | - `MAX_REQUEST_PER_HOUR` 每小时最大请求次数,可选,默认无限 174 | - `TIMEOUT_MS` 超时,单位毫秒,可选 175 | - `SOCKS_PROXY_HOST` 和 `SOCKS_PROXY_PORT` 一起时生效,可选 176 | - `SOCKS_PROXY_PORT` 和 `SOCKS_PROXY_HOST` 一起时生效,可选 177 | - `HTTPS_PROXY` 支持 `http`,`https`, `socks5`,可选 178 | - `ALL_PROXY` 支持 `http`,`https`, `socks5`,可选 179 | 180 | ## 打包 181 | 182 | ### 使用 Docker 183 | 184 | #### Docker 参数示例 185 | 186 | ![docker](./docs/docker.png) 187 | 188 | #### Docker build & Run 189 | 190 | ```bash 191 | docker build -t chatgpt-web . 192 | 193 | # 前台运行 194 | docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 195 | 196 | # 后台运行 197 | docker run --name chatgpt-web -d -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 198 | 199 | # 运行地址 200 | http://localhost:3002/ 201 | ``` 202 | 203 | #### Docker compose 204 | 205 | [Hub 地址](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general) 206 | 207 | ```yml 208 | version: '3' 209 | 210 | services: 211 | app: 212 | image: chenzhaoyu94/chatgpt-web # 总是使用 latest ,更新时重新 pull 该 tag 镜像即可 213 | ports: 214 | - 127.0.0.1:3002:3002 215 | environment: 216 | # 二选一 217 | OPENAI_API_KEY: sk-xxx 218 | # 二选一 219 | OPENAI_ACCESS_TOKEN: xxx 220 | # API接口地址,可选,设置 OPENAI_API_KEY 时可用 221 | OPENAI_API_BASE_URL: xxx 222 | # API模型,可选,设置 OPENAI_API_KEY 时可用,https://platform.openai.com/docs/models 223 | # gpt-4, gpt-4-0314, gpt-4-32k, gpt-4-32k-0314, gpt-3.5-turbo, gpt-3.5-turbo-0301, text-davinci-003, text-davinci-002, code-davinci-002 224 | OPENAI_API_MODEL: xxx 225 | # 反向代理,可选 226 | API_REVERSE_PROXY: xxx 227 | # 访问权限密钥,可选 228 | AUTH_SECRET_KEY: xxx 229 | # 每小时最大请求次数,可选,默认无限 230 | MAX_REQUEST_PER_HOUR: 0 231 | # 超时,单位毫秒,可选 232 | TIMEOUT_MS: 60000 233 | # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效 234 | SOCKS_PROXY_HOST: xxx 235 | # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效 236 | SOCKS_PROXY_PORT: xxx 237 | # HTTPS 代理,可选,支持 http,https,socks5 238 | HTTPS_PROXY: http://xxx:7890 239 | ``` 240 | - `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用 241 | - `OPENAI_API_MODEL` 可选,设置 `OPENAI_API_KEY` 时可用 242 | ### 使用 Railway 部署 243 | 244 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc) 245 | 246 | #### Railway 环境变量 247 | 248 | | 环境变量名称 | 必填 | 备注 | 249 | | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- | 250 | | `PORT` | 必填 | 默认 `3002` 251 | | `AUTH_SECRET_KEY` | 可选 | 访问权限密钥 | 252 | | `MAX_REQUEST_PER_HOUR` | 可选 | 每小时最大请求次数,可选,默认无限 | 253 | | `TIMEOUT_MS` | 可选 | 超时时间,单位毫秒 | 254 | | `OPENAI_API_KEY` | `OpenAI API` 二选一 | 使用 `OpenAI API` 所需的 `apiKey` [(获取 apiKey)](https://platform.openai.com/overview) | 255 | | `OPENAI_ACCESS_TOKEN` | `Web API` 二选一 | 使用 `Web API` 所需的 `accessToken` [(获取 accessToken)](https://chat.openai.com/api/auth/session) | 256 | | `OPENAI_API_BASE_URL` | 可选,`OpenAI API` 时可用 | `API`接口地址 | 257 | | `OPENAI_API_MODEL` | 可选,`OpenAI API` 时可用 | `API`模型 | 258 | | `API_REVERSE_PROXY` | 可选,`Web API` 时可用 | `Web API` 反向代理地址 [详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | 259 | | `SOCKS_PROXY_HOST` | 可选,和 `SOCKS_PROXY_PORT` 一起时生效 | Socks代理 | 260 | | `SOCKS_PROXY_PORT` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理端口 | 261 | | `HTTPS_PROXY` | 可选 | HTTPS 代理,支持 http,https, socks5 | 262 | | `ALL_PROXY` | 可选 | 所有代理 代理,支持 http,https, socks5 | 263 | 264 | > 注意: `Railway` 修改环境变量会重新 `Deploy` 265 | 266 | ### 手动打包 267 | #### 后端服务 268 | > 如果你不需要本项目的 `node` 接口,可以省略如下操作 269 | 270 | 复制 `service` 文件夹到你有 `node` 服务环境的服务器上。 271 | 272 | ```shell 273 | # 安装 274 | pnpm install 275 | 276 | # 打包 277 | pnpm build 278 | 279 | # 运行 280 | pnpm prod 281 | ``` 282 | 283 | PS: 不进行打包,直接在服务器上运行 `pnpm start` 也可 284 | 285 | #### 前端网页 286 | 287 | 1、修改根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 为你的实际后端接口地址 288 | 289 | 2、根目录下运行以下命令,然后将 `dist` 文件夹内的文件复制到你网站服务的根目录下 290 | 291 | [参考信息](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app) 292 | 293 | ```shell 294 | pnpm build 295 | ``` 296 | 297 | ## 常见问题 298 | Q: 为什么 `Git` 提交总是报错? 299 | 300 | A: 因为有提交信息验证,请遵循 [Commit 指南](./CONTRIBUTING.md) 301 | 302 | Q: 如果只使用前端页面,在哪里改请求接口? 303 | 304 | A: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。 305 | 306 | Q: 文件保存时全部爆红? 307 | 308 | A: `vscode` 请安装项目推荐插件,或手动安装 `Eslint` 插件。 309 | 310 | Q: 前端没有打字机效果? 311 | 312 | A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer,则 Nginx 会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`,然后重载 Nginx。其他 web server 配置同理。 313 | 314 | ## 参与贡献 315 | 316 | 贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md) 317 | 318 | 感谢所有做过贡献的人! 319 | 320 | 321 | 322 | 323 | 324 | ## 赞助 325 | 326 | 如果你觉得这个项目对你有帮助,并且情况允许的话,可以给我一点点支持,总之非常感谢支持~ 327 | 328 |
329 |
330 | 微信 331 |

WeChat Pay

332 |
333 |
334 | 支付宝 335 |

Alipay

336 |
337 |
338 | 339 | ## License 340 | MIT © [ChenZhaoYu](./license) 341 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Web 2 | 3 |
4 | 中文 | 5 | English 6 |
7 |
8 | 9 | > Disclaimer: This project is only released on GitHub, under the MIT License, free and for open-source learning purposes. There will be no account selling, paid services, discussion groups, or forums. Beware of fraud. 10 | 11 | ![cover](./docs/c1.png) 12 | ![cover2](./docs/c2.png) 13 | 14 | - [ChatGPT Web](#chatgpt-web) 15 | - [Introduction](#introduction) 16 | - [Roadmap](#roadmap) 17 | - [Prerequisites](#prerequisites) 18 | - [Node](#node) 19 | - [PNPM](#pnpm) 20 | - [Fill in the Keys](#fill-in-the-keys) 21 | - [Install Dependencies](#install-dependencies) 22 | - [Backend](#backend) 23 | - [Frontend](#frontend) 24 | - [Run in Test Environment](#run-in-test-environment) 25 | - [Backend Service](#backend-service) 26 | - [Frontend Webpage](#frontend-webpage) 27 | - [Packaging](#packaging) 28 | - [Using Docker](#using-docker) 29 | - [Docker Parameter Example](#docker-parameter-example) 30 | - [Docker Build \& Run](#docker-build--run) 31 | - [Docker Compose](#docker-compose) 32 | - [Deployment with Railway](#deployment-with-railway) 33 | - [Railway Environment Variables](#railway-environment-variables) 34 | - [Manual packaging](#manual-packaging) 35 | - [Backend service](#backend-service-1) 36 | - [Frontend webpage](#frontend-webpage-1) 37 | - [Frequently Asked Questions](#frequently-asked-questions) 38 | - [Contributing](#contributing) 39 | - [Sponsorship](#sponsorship) 40 | - [License](#license) 41 | 42 | ## Introduction 43 | 44 | Supports dual models, provides two unofficial `ChatGPT API` methods: 45 | 46 | | Method | Free? | Reliability | Quality | 47 | | --------------------------------------------- | ------ | ----------- | ------- | 48 | | `ChatGPTAPI(gpt-3.5-turbo-0301)` | No | Reliable | Relatively clumsy | 49 | | `ChatGPTUnofficialProxyAPI(Web accessToken)` | Yes | Relatively unreliable | Smart | 50 | 51 | Comparison: 52 | 1. `ChatGPTAPI` uses `gpt-3.5-turbo-0301` to simulate `ChatGPT` through the official `OpenAI` completion `API` (the most reliable method, but it is not free and does not use models specifically tuned for chat). 53 | 2. `ChatGPTUnofficialProxyAPI` accesses `ChatGPT`'s backend `API` via an unofficial proxy server to bypass `Cloudflare` (uses the real `ChatGPT`, is very lightweight, but depends on third-party servers and has rate limits). 54 | 55 | [Details](https://github.com/Chanzhaoyu/chatgpt-web/issues/138) 56 | 57 | Switching Methods: 58 | 1. Go to the `service/.env.example` file and copy the contents to the `service/.env` file. 59 | 2. For `OpenAI API Key`, fill in the `OPENAI_API_KEY` field [(Get apiKey)](https://platform.openai.com/overview). 60 | 3. For `Web API`, fill in the `OPENAI_ACCESS_TOKEN` field [(Get accessToken)](https://chat.openai.com/api/auth/session). 61 | 4. When both are present, `OpenAI API Key` takes precedence. 62 | 63 | Reverse Proxy: 64 | 65 | Available when using `ChatGPTUnofficialProxyAPI`.[Details](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) 66 | 67 | ```shell 68 | # service/.env 69 | API_REVERSE_PROXY= 70 | ``` 71 | 72 | Environment Variables: 73 | 74 | For all parameter variables, check [here](#docker-parameter-example) or see: 75 | 76 | ``` 77 | /service/.env 78 | ``` 79 | 80 | ## Roadmap 81 | [✓] Dual models 82 | 83 | [✓] Multiple session storage and context logic 84 | 85 | [✓] Formatting and beautifying code-like message types 86 | 87 | [✓] Access rights control 88 | 89 | [✓] Data import and export 90 | 91 | [✓] Save message to local image 92 | 93 | [✓] Multilingual interface 94 | 95 | [✓] Interface themes 96 | 97 | [✗] More... 98 | 99 | ## Prerequisites 100 | 101 | ### Node 102 | 103 | `node` requires version `^16 || ^18` (`node >= 14` requires installation of [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill)), and multiple local `node` versions can be managed using [nvm](https://github.com/nvm-sh/nvm). 104 | 105 | ```shell 106 | node -v 107 | ``` 108 | 109 | ### PNPM 110 | If you have not installed `pnpm` before: 111 | ```shell 112 | npm install pnpm -g 113 | ``` 114 | 115 | ### Fill in the Keys 116 | 117 | Get `Openai Api Key` or `accessToken` and fill in the local environment variables [jump](#introduction) 118 | 119 | ``` 120 | # service/.env file 121 | 122 | # OpenAI API Key - https://platform.openai.com/overview 123 | OPENAI_API_KEY= 124 | 125 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 126 | OPENAI_ACCESS_TOKEN= 127 | ``` 128 | 129 | ## Install Dependencies 130 | 131 | > To make it easier for `backend developers` to understand, we did not use the front-end `workspace` mode, but stored it in different folders. If you only need to do secondary development of the front-end page, delete the `service` folder. 132 | 133 | ### Backend 134 | 135 | Enter the `/service` folder and run the following command 136 | 137 | ```shell 138 | pnpm install 139 | ``` 140 | 141 | ### Frontend 142 | Run the following command in the root directory 143 | ```shell 144 | pnpm bootstrap 145 | ``` 146 | 147 | ## Run in Test Environment 148 | ### Backend Service 149 | 150 | Enter the `/service` folder and run the following command 151 | 152 | ```shell 153 | pnpm start 154 | ``` 155 | 156 | ### Frontend Webpage 157 | Run the following command in the root directory 158 | ```shell 159 | pnpm dev 160 | ``` 161 | 162 | ## Packaging 163 | 164 | ### Using Docker 165 | 166 | #### Docker Parameter Example 167 | 168 | - `OPENAI_API_KEY` one of two 169 | - `OPENAI_ACCESS_TOKEN` one of two, `OPENAI_API_KEY` takes precedence when both are present 170 | - `OPENAI_API_BASE_URL` optional, available when `OPENAI_API_KEY` is set 171 | - `OPENAI_API_MODEL` optional, available when `OPENAI_API_KEY` is set 172 | - `API_REVERSE_PROXY` optional, available when `OPENAI_ACCESS_TOKEN` is set [Reference](#introduction) 173 | - `AUTH_SECRET_KEY` Access Password,optional 174 | - `TIMEOUT_MS` timeout, in milliseconds, optional 175 | - `SOCKS_PROXY_HOST` optional, effective with SOCKS_PROXY_PORT 176 | - `SOCKS_PROXY_PORT` optional, effective with SOCKS_PROXY_HOST 177 | - `HTTPS_PROXY` optional, support http,https, socks5 178 | - `ALL_PROXY` optional, support http,https, socks5 179 | 180 | ![docker](./docs/docker.png) 181 | 182 | #### Docker Build & Run 183 | 184 | ```bash 185 | docker build -t chatgpt-web . 186 | 187 | # foreground operation 188 | docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 189 | 190 | # background operation 191 | docker run --name chatgpt-web -d -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 192 | 193 | # running address 194 | http://localhost:3002/ 195 | ``` 196 | 197 | #### Docker Compose 198 | 199 | [Hub Address](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general) 200 | 201 | ```yml 202 | version: '3' 203 | 204 | services: 205 | app: 206 | image: chenzhaoyu94/chatgpt-web # always use latest, pull the tag image again when updating 207 | ports: 208 | - 127.0.0.1:3002:3002 209 | environment: 210 | # one of two 211 | OPENAI_API_KEY: xxxxxx 212 | # one of two 213 | OPENAI_ACCESS_TOKEN: xxxxxx 214 | # api interface url, optional, available when OPENAI_API_KEY is set 215 | OPENAI_API_BASE_URL: xxxx 216 | # api model, optional, available when OPENAI_API_KEY is set 217 | OPENAI_API_MODEL: xxxx 218 | # reverse proxy, optional 219 | API_REVERSE_PROXY: xxx 220 | # access password,optional 221 | AUTH_SECRET_KEY: xxx 222 | # timeout, in milliseconds, optional 223 | TIMEOUT_MS: 60000 224 | # socks proxy, optional, effective with SOCKS_PROXY_PORT 225 | SOCKS_PROXY_HOST: xxxx 226 | # socks proxy port, optional, effective with SOCKS_PROXY_HOST 227 | SOCKS_PROXY_PORT: xxxx 228 | # HTTPS Proxy,optional, support http, https, socks5 229 | HTTPS_PROXY: http://xxx:7890 230 | ``` 231 | The `OPENAI_API_BASE_URL` is optional and only used when setting the `OPENAI_API_KEY`. 232 | The `OPENAI_API_MODEL` is optional and only used when setting the `OPENAI_API_KEY`. 233 | 234 | ### Deployment with Railway 235 | 236 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc) 237 | 238 | #### Railway Environment Variables 239 | 240 | | Environment Variable | Required | Description | 241 | | -------------------- | -------- | ------------------------------------------------------------------------------------------------- | 242 | | `PORT` | Required | Default: `3002` | 243 | | `AUTH_SECRET_KEY` | Optional | access password | 244 | | `TIMEOUT_MS` | Optional | Timeout in milliseconds | 245 | | `OPENAI_API_KEY` | Optional | Required for `OpenAI API`. `apiKey` can be obtained from [here](https://platform.openai.com/overview). | 246 | | `OPENAI_ACCESS_TOKEN`| Optional | Required for `Web API`. `accessToken` can be obtained from [here](https://chat.openai.com/api/auth/session).| 247 | | `OPENAI_API_BASE_URL` | Optional, only for `OpenAI API` | API endpoint. | 248 | | `OPENAI_API_MODEL` | Optional, only for `OpenAI API` | API model. | 249 | | `API_REVERSE_PROXY` | Optional, only for `Web API` | Reverse proxy address for `Web API`. [Details](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | 250 | | `SOCKS_PROXY_HOST` | Optional, effective with `SOCKS_PROXY_PORT` | Socks proxy. | 251 | | `SOCKS_PROXY_PORT` | Optional, effective with `SOCKS_PROXY_HOST` | Socks proxy port. | 252 | | `HTTPS_PROXY` | Optional | HTTPS Proxy. | 253 | | `ALL_PROXY` | Optional | ALL Proxy. | 254 | 255 | > Note: Changing environment variables in Railway will cause re-deployment. 256 | 257 | ### Manual packaging 258 | 259 | #### Backend service 260 | 261 | > If you don't need the `node` interface of this project, you can skip the following steps. 262 | 263 | Copy the `service` folder to a server that has a `node` service environment. 264 | 265 | ```shell 266 | # Install 267 | pnpm install 268 | 269 | # Build 270 | pnpm build 271 | 272 | # Run 273 | pnpm prod 274 | ``` 275 | 276 | PS: You can also run `pnpm start` directly on the server without packaging. 277 | 278 | #### Frontend webpage 279 | 280 | 1. Refer to the root directory `.env.example` file content to create `.env` file, modify `VITE_GLOB_API_URL` in `.env` at the root directory to your actual backend interface address. 281 | 2. Run the following command in the root directory and then copy the files in the `dist` folder to the root directory of your website service. 282 | 283 | [Reference information](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app) 284 | 285 | ```shell 286 | pnpm build 287 | ``` 288 | 289 | ## Frequently Asked Questions 290 | 291 | Q: Why does Git always report an error when committing? 292 | 293 | A: Because there is submission information verification, please follow the [Commit Guidelines](./CONTRIBUTING.en.md). 294 | 295 | Q: Where to change the request interface if only the frontend page is used? 296 | 297 | A: The `VITE_GLOB_API_URL` field in the `.env` file at the root directory. 298 | 299 | Q: All red when saving the file? 300 | 301 | A: For `vscode`, please install the recommended plug-in of the project or manually install the `Eslint` plug-in. 302 | 303 | Q: Why doesn't the frontend have a typewriter effect? 304 | 305 | A: One possible reason is that after Nginx reverse proxying, buffering is turned on, and Nginx will try to buffer a certain amount of data from the backend before sending it to the browser. Please try adding `proxy_buffering off;` after the reverse proxy parameter and then reloading Nginx. Other web server configurations are similar. 306 | 307 | Q: The content returned is incomplete? 308 | 309 | A: There is a length limit for the content returned by the API each time. You can modify the `VITE_GLOB_OPEN_LONG_REPLY` field in the `.env` file under the root directory, set it to `true`, and rebuild the front-end to enable the long reply feature, which can return the full content. It should be noted that using this feature may bring more API usage fees. 310 | 311 | ## Contributing 312 | 313 | Please read the [Contributing Guidelines](./CONTRIBUTING.en.md) before contributing. 314 | 315 | Thanks to all the contributors! 316 | 317 | 318 | 319 | 320 | 321 | ## Sponsorship 322 | 323 | If you find this project helpful and circumstances permit, you can give me a little support. Thank you very much for your support~ 324 | 325 |
326 |
327 | WeChat 328 |

WeChat Pay

329 |
330 |
331 | Alipay 332 |

Alipay

333 |
334 |
335 | 336 | ## License 337 | MIT © [ChenZhaoYu](./license) 338 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.10.8 2 | 3 | `2023-03-23` 4 | 5 | 如遇问题,请删除 `node_modules` 重新安装依赖。 6 | 7 | ## Feature 8 | - 显示回复消息原文的选项 [[yilozt](https://github.com/Chanzhaoyu/chatgpt-web/pull/672)] 9 | - 添加单 `IP` 每小时请求限制。环境变量: `MAX_REQUEST_PER_HOUR` [[zhuxindong ](https://github.com/Chanzhaoyu/chatgpt-web/pull/718)] 10 | - 前端添加角色设定,仅 `API` 方式可见 [[quzard](https://github.com/Chanzhaoyu/chatgpt-web/pull/768)] 11 | - `OPENAI_API_MODEL` 变量现在对 `ChatGPTUnofficialProxyAPI` 也生效,注意:`Token` 和 `API` 的模型命名不一致,不能直接填入 `gpt-3.5` 或者 `gpt-4` [[hncboy](https://github.com/Chanzhaoyu/chatgpt-web/pull/632)] 12 | - 添加繁体中文 `Prompts` [[PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/796)] 13 | 14 | ## Enhancement 15 | - 重置回答时滚动定位至该回答 [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/781)] 16 | - 当 `API` 是 `gpt-4` 时增加可用的 `Max Tokens` [[simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/729)] 17 | - 判断和忽略回复字符 [[liut](https://github.com/Chanzhaoyu/chatgpt-web/pull/474)] 18 | - 切换会话时,自动聚焦输入框 [[JS-an](https://github.com/Chanzhaoyu/chatgpt-web/pull/735)] 19 | - 渲染的链接新窗口打开 20 | - 查询余额可选 `API_BASE_URL` 代理地址 21 | - `config` 接口添加验证防止被无限制调用 22 | - `PWA` 默认不开启,现在需手动修改 `.env` 文件 `VITE_GLOB_APP_PWA` 变量 23 | - 当网络连接时,刷新页面,`500` 错误页自动跳转到主页 24 | 25 | ## BugFix 26 | - `scrollToBottom` 调回 `scrollToBottomIfAtBottom` [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/771)] 27 | - 重置异常的 `loading` 会话 28 | 29 | ## Common 30 | - 创建 `start.cmd` 在 `windows` 下也可以运行 [vulgatecnn](https://github.com/Chanzhaoyu/chatgpt-web/pull/656)] 31 | - 添加 `visual-studio-code` 中调试配置 [[ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/296)] 32 | - 修复文档中 `docker` 端口为本地 [[kilvn](https://github.com/Chanzhaoyu/chatgpt-web/pull/802)] 33 | ## Other 34 | - 依赖更新 35 | 36 | 37 | ## v2.10.7 38 | 39 | `2023-03-17` 40 | 41 | ## BugFix 42 | - 回退 `chatgpt` 版本,原因:导致 `OPENAI_API_BASE_URL` 代理失效 43 | - 修复缺省状态的 `usingContext` 默认值 44 | 45 | ## v2.10.6 46 | 47 | `2023-03-17` 48 | 49 | ## Feature 50 | - 显示 `API` 余额 [[pzcn](https://github.com/Chanzhaoyu/chatgpt-web/pull/582)] 51 | 52 | ## Enhancement 53 | - 美化滚动条样式和 `UI` 保持一致 [[haydenull](https://github.com/Chanzhaoyu/chatgpt-web/pull/617)] 54 | - 优化移动端 `Prompt` 样式 [[CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/608)] 55 | - 上下文开关改为全局开关,现在记录在本地缓存中 56 | - 配置信息按接口类型显示 57 | 58 | ## Perf 59 | - 优化函数方法 [[kirklin](https://github.com/Chanzhaoyu/chatgpt-web/pull/583)] 60 | - 字符错误 [[pdsuwwz](https://github.com/Chanzhaoyu/chatgpt-web/pull/585)] 61 | - 文档描述错误 [[lizhongyuan3](https://github.com/Chanzhaoyu/chatgpt-web/pull/636)] 62 | 63 | ## BugFix 64 | - 修复 `Prompt` 导入、导出兼容性错误 65 | - 修复 `highlight.js` 控制台兼容性警告 66 | 67 | ## Other 68 | - 依赖更新 69 | 70 | ## v2.10.5 71 | 72 | `2023-03-13` 73 | 74 | 更新依赖,`access_token` 默认代理为 [acheong08](https://github.com/acheong08) 的 `https://bypass.duti.tech/api/conversation` 75 | 76 | ## Feature 77 | - `Prompt` 商店在线导入可以导入两种 `recommend.json`里提到的模板 [simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/521) 78 | - 支持 `HTTPS_PROXY` [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/308) 79 | - `Prompt` 添加查询筛选 80 | 81 | ## Enhancement 82 | - 调整输入框最大行数 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/502) 83 | - 优化 `docker` 打包 [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/520) 84 | - `Prompt` 添加翻译和优化布局 85 | - 「繁体中文」补全和审阅 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/542) 86 | - 语言选择调整为下路框形式 87 | - 权限输入框类型调整为密码形式 88 | 89 | ## BugFix 90 | - `JSON` 导入检查 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/523) 91 | - 修复 `AUTH_SECRET_KEY` 模式下跨域异常并添加对 `node.js 19` 版本的支持 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/499) 92 | - 确定清空上下文时不应该重置会话标题 93 | 94 | ## Other 95 | - 调整文档 96 | - 更新依赖 97 | 98 | ## v2.10.4 99 | 100 | `2023-03-11` 101 | 102 | ## Feature 103 | - 感谢 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/268) 添加 `Prompt` 模板和 `Prompt` 商店支持 104 | 105 | ## Enhancement 106 | - 设置添加关闭按钮[#495] 107 | 108 | ## Demo 109 | 110 | ![Prompt](https://camo.githubusercontent.com/6a51af751eb29238cb7ef4f8fbd89f63db837562f97f33273095424e62dc9194/68747470733a2f2f73312e6c6f63696d672e636f6d2f323032332f30332f30342f333036326665633163613562632e676966) 111 | 112 | ## v2.10.3 113 | 114 | `2023-03-10` 115 | 116 | > 声明:除 `ChatGPTUnofficialProxyAPI` 使用的非官方代理外,本项目代码包括上游引用包均开源在 `GitHub`,如果你觉得本项目有监控后门或有问题导致你的账号、API被封,那我很抱歉。我可能`BUG`写的多,但我不缺德。此次主要为前端界面调整,周末愉快。 117 | 118 | ## Feature 119 | - 支持长回复 [[yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)][[详情](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)] 120 | - 支持 `PWA` [[chenxch](https://github.com/Chanzhaoyu/chatgpt-web/pull/452)] 121 | 122 | ## Enhancement 123 | - 调整移动端按钮和优化布局 124 | - 调整 `iOS` 上安全距离 125 | - 简化 `docker-compose` 部署 [[cloudGrin](https://github.com/Chanzhaoyu/chatgpt-web/pull/466)] 126 | 127 | ## BugFix 128 | - 修复清空会话侧边栏标题不会重置的问题 [[RyanXinOne](https://github.com/Chanzhaoyu/chatgpt-web/pull/453)] 129 | - 修复设置文字过长时导致的设置按钮消失的问题 130 | 131 | ## Other 132 | - 更新依赖 133 | 134 | ## v2.10.2 135 | 136 | `2023-03-09` 137 | 138 | 衔接 `2.10.1` 版本[详情](https://github.com/Chanzhaoyu/chatgpt-web/releases/tag/v2.10.1) 139 | 140 | ## Enhancement 141 | - 移动端下输入框获得焦点时左侧按钮隐藏 142 | 143 | ## BugFix 144 | - 修复 `2.10.1` 中添加 `OPENAI_API_MODEL` 变量的判断错误,会导致默认模型指定失效,抱歉 145 | - 回退 `2.10.1` 中前端变量影响 `Docker` 打包 146 | 147 | ## v2.10.1 148 | 149 | `2023-03-09` 150 | 151 | 注意:删除了 `.env` 文件改用 `.env.example` 代替,如果是手动部署的同学现在需要手动创建 `.env` 文件并从 `.env.example` 中复制需要的变量,并且 `.env` 文件现在会在 `Git` 提交中被忽略,原因如下: 152 | 153 | - 在项目中添加 `.env` 从一开始就是个错误的示范 154 | - 如果是 `Fork` 项目进行修改测试总是会被 `Git` 修改提示给打扰 155 | - 感谢 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/395) 的提醒和修改 156 | 157 | 158 | 这两天开始,官方已经开始对第三方代理进行了拉闸, `accessToken` 即将或已经开始可能会不可使用。异常 `API` 使用也开始封号,封号缘由不明,如果出现使用 `API` 提示错误,请查看后端控制台信息,或留意邮箱。 159 | 160 | ## Feature 161 | - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/393) 添加是否发送上下文开关功能 162 | 163 | ## Enhancement 164 | - 感谢 [nagaame](https://github.com/Chanzhaoyu/chatgpt-web/pull/415) 优化`docker`打包镜像文件过大的问题 165 | - 感谢 [xieccc](https://github.com/Chanzhaoyu/chatgpt-web/pull/404) 新增 `API` 模型配置变量 `OPENAI_API_MODEL` 166 | - 感谢 [acongee](https://github.com/Chanzhaoyu/chatgpt-web/pull/394) 优化输出时滚动条问题 167 | 168 | ## BugFix 169 | - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/392) 修复导出图片会丢失头像的问题 170 | - 修复深色模式导出图片的样式问题 171 | 172 | 173 | ## v2.10.0 174 | 175 | `2023-03-07` 176 | 177 | - 老规矩,手动部署的同学需要删除 `node_modules` 安装包重新安装降低出错概率,其他部署不受影响,但是可能会有缓存问题。 178 | - 虽然说了更新放缓,但是 `issues` 不看, `PR` 不改我睡不着,我的邮箱从每天早上`8`点到凌晨`12`永远在滴滴滴,所以求求各位,超时的`issues`自己关闭下哈,我真的需要缓冲一下。 179 | - 演示图片请看最后 180 | 181 | ## Feature 182 | - 添加权限功能,用法:`service/.env` 中的 `AUTH_SECRET_KEY` 变量添加密码 183 | - 感谢 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/348) 添加「繁体中文」翻译 184 | - 感谢 [GermMC](https://github.com/Chanzhaoyu/chatgpt-web/pull/369) 添加聊天记录导入、导出、清空的功能 185 | - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/374) 添加会话保存为本地图片的功能 186 | 187 | 188 | ## Enhancement 189 | - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/363) 添加 `ctrl+enter` 发送消息 190 | - 现在新消息只有在结束了之后才滚动到底部,而不是之前的强制性 191 | - 优化部分代码 192 | 193 | ## BugFix 194 | - 转义状态码前端显示,防止直接暴露 `key`(我可能需要更多的状态码补充) 195 | 196 | ## Other 197 | - 更新依赖到最新 198 | 199 | ## 演示 200 | > 不是界面最新效果,有美化改动 201 | 202 | 权限 203 | 204 | ![权限](https://user-images.githubusercontent.com/24789441/223438518-80d58d42-e344-4e39-b87c-251ff73925ed.png) 205 | 206 | 聊天记录导出 207 | 208 | ![聊天记录导出](https://user-images.githubusercontent.com/57023771/223372153-6d8e9ec1-d82c-42af-b4bd-232e50504a25.gif) 209 | 210 | 保存图片到本地 211 | 212 | ![保存图片到本地](https://user-images.githubusercontent.com/13901424/223423555-b69b95ef-8bcf-4951-a7c9-98aff2677e18.gif) 213 | 214 | ## v2.9.3 215 | 216 | `2023-03-06` 217 | 218 | ## Enhancement 219 | - 感谢 [ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/305) 使用 `markdown-it` 替换 `marked`,解决代码块闪烁的问题 220 | - 感谢 [shansing](https://github.com/Chanzhaoyu/chatgpt-web/pull/277) 改善文档 221 | - 感谢 [nalf3in](https://github.com/Chanzhaoyu/chatgpt-web/pull/293) 添加英文翻译 222 | 223 | ## BugFix 224 | - 感谢[sepcnt ](https://github.com/Chanzhaoyu/chatgpt-web/pull/279) 修复切换记录时编辑状态未关闭的问题 225 | - 修复复制代码的兼容性报错问题 226 | - 修复部分优化小问题 227 | 228 | ## v2.9.2 229 | 230 | `2023-03-04` 231 | 232 | 手动部署的同学,务必删除根目录和`service`中的`node_modules`重新安装依赖,降低出现问题的概率,自动部署的不需要做改动。 233 | 234 | ### Feature 235 | - 感谢 [hyln9](https://github.com/Chanzhaoyu/chatgpt-web/pull/247) 添加对渲染 `LaTex` 数学公式的支持 236 | - 感谢 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/227) 添加支持 `webAPP` (苹果添加到主页书签访问)支持 237 | - 添加 `OPENAI_API_BASE_URL` 可选环境变量[#249] 238 | ## Enhancement 239 | - 优化在高分屏上主题内容的最大宽度[#257] 240 | - 现在文字按单词截断[#215][#225] 241 | ### BugFix 242 | - 修复动态生成时代码块不能被复制的问题[#251][#260] 243 | - 修复 `iOS` 移动端输入框不会被键盘顶起的问题[#256] 244 | - 修复控制台渲染警告 245 | ## Other 246 | - 更新依赖至最新 247 | - 修改 `README` 内容 248 | 249 | ## v2.9.1 250 | 251 | `2023-03-02` 252 | 253 | ### Feature 254 | - 代码块添加当前代码语言显示和复制功能[#197][#196] 255 | - 完善多语言,现在可以切换中英文显示 256 | 257 | ## Enhancement 258 | - 由[Zo3i](https://github.com/Chanzhaoyu/chatgpt-web/pull/187) 完善 `docker-compose` 部署文档 259 | 260 | ### BugFix 261 | - 由 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/200) 修复头像修改不同步的问题 262 | ## Other 263 | - 更新依赖至最新 264 | - 修改 `README` 内容 265 | ## v2.9.0 266 | 267 | `2023-03-02` 268 | 269 | ### Feature 270 | - 现在能复制带格式的消息文本 271 | - 新设计的设定页面,可以自定义姓名、描述、头像(链接方式) 272 | - 新增`403`和`404`页面以便扩展 273 | 274 | ## Enhancement 275 | - 更新 `chatgpt` 使 `ChatGPTAPI` 支持 `gpt-3.5-turbo-0301`(默认) 276 | - 取消了前端超时限制设定 277 | 278 | ## v2.8.3 279 | 280 | `2023-03-01` 281 | 282 | ### Feature 283 | - 消息已输出内容不会因为中断而消失[#167] 284 | - 添加复制消息按钮[#133] 285 | 286 | ### Other 287 | - `README` 添加声明内容 288 | 289 | ## v2.8.2 290 | 291 | `2023-02-28` 292 | ### Enhancement 293 | - 代码主题调整为 `One Dark - light|dark` 适配深色模式 294 | ### BugFix 295 | - 修复普通文本代码渲染和深色模式下的问题[#139][#154] 296 | 297 | ## v2.8.1 298 | 299 | `2023-02-27` 300 | 301 | ### BugFix 302 | - 修复 `API` 版本不是 `Markdown` 时,普通 `HTML` 代码会被渲染的问题 [#146] 303 | 304 | ## v2.8.0 305 | 306 | `2023-02-27` 307 | 308 | - 感谢 [puppywang](https://github.com/Chanzhaoyu/chatgpt-web/commit/628187f5c3348bda0d0518f90699a86525d19018) 修复了 `2.7.0` 版本中关于流输出数据的问题(使用 `nginx` 需要自行配置 `octet-stream` 相关内容) 309 | 310 | - 关于为什么使用 `octet-stream` 而不是 `sse`,是因为更好的兼容之前的模式。 311 | 312 | - 建议更新到此版本获得比较完整的体验 313 | 314 | ### Enhancement 315 | - 优化了部份代码和类型提示 316 | - 输入框添加换行提示 317 | - 移动端输入框现在回车为换行,而不是直接提交 318 | - 移动端双击标题返回顶部,箭头返回底部 319 | 320 | ### BugFix 321 | - 流输出数据下的问题[#122] 322 | - 修复了 `API Key` 下部份代码不换行的问题 323 | - 修复移动端深色模式部份样式问题[#123][#126] 324 | - 修复主题模式图标不一致的问题[#126] 325 | 326 | ## v2.7.3 327 | 328 | `2023-02-25` 329 | 330 | ### Feature 331 | - 适配系统深色模式 [#118](https://github.com/Chanzhaoyu/chatgpt-web/issues/103) 332 | ### BugFix 333 | - 修复用户消息能被渲染为 `HTML` 问题 [#117](https://github.com/Chanzhaoyu/chatgpt-web/issues/117) 334 | 335 | ## v2.7.2 336 | 337 | `2023-02-24` 338 | ### Enhancement 339 | - 消息使用 [github-markdown-css](https://www.npmjs.com/package/github-markdown-css) 进行美化,现在支持全语法 340 | - 移除测试无用函数 341 | 342 | ## v2.7.1 343 | 344 | `2023-02-23` 345 | 346 | 因为消息流在 `accessToken` 中存在解析失败和消息不完整等一系列的问题,调整回正常消息形式 347 | 348 | ### Feature 349 | - 现在可以中断请求过长没有答复的消息 350 | - 现在可以删除单条消息 351 | - 设置中显示当前版本信息 352 | 353 | ### BugFix 354 | - 回退 `2.7.0` 的消息不稳定的问题 355 | 356 | ## v2.7.0 357 | 358 | `2023-02-23` 359 | 360 | ### Feature 361 | - 使用消息流返回信息,反应更迅速 362 | 363 | ### Enhancement 364 | - 样式的一点小改动 365 | 366 | ## v2.6.2 367 | 368 | `2023-02-22` 369 | ### BugFix 370 | - 还原修改代理导致的异常问题 371 | 372 | ## v2.6.1 373 | 374 | `2023-02-22` 375 | 376 | ### Feature 377 | - 新增 `Railway` 部署模版 378 | 379 | ### BugFix 380 | - 手动打包 `Proxy` 问题 381 | 382 | ## v2.6.0 383 | 384 | `2023-02-21` 385 | ### Feature 386 | - 新增对 `网页 accessToken` 调用 `ChatGPT`,更智能不过不太稳定 [#51](https://github.com/Chanzhaoyu/chatgpt-web/issues/51) 387 | - 前端页面设置按钮显示查看当前后端服务配置 388 | 389 | ### Enhancement 390 | - 新增 `TIMEOUT_MS` 环境变量设定后端超时时常(单位:毫秒)[#62](https://github.com/Chanzhaoyu/chatgpt-web/issues/62) 391 | 392 | ## v2.5.2 393 | 394 | `2023-02-21` 395 | ### Feature 396 | - 增加对 `markdown` 格式的支持 [Demo](https://github.com/Chanzhaoyu/chatgpt-web/pull/77) 397 | ### BugFix 398 | - 重载会话时滚动条保持 399 | 400 | ## v2.5.1 401 | 402 | `2023-02-21` 403 | 404 | ### Enhancement 405 | - 调整路由模式为 `hash` 406 | - 调整新增会话添加到 407 | - 调整移动端样式 408 | 409 | 410 | ## v2.5.0 411 | 412 | `2023-02-20` 413 | 414 | ### Feature 415 | - 会话 `loading` 现在显示为光标动画 416 | - 会话现在可以再次生成回复 417 | - 会话异常可以再次进行请求 418 | - 所有删除选项添加确认操作 419 | 420 | ### Enhancement 421 | - 调整 `chat` 为路由页面而不是组件形式 422 | - 更新依赖至最新 423 | - 调整移动端体验 424 | 425 | ### BugFix 426 | - 修复移动端左侧菜单显示不完整的问题 427 | 428 | ## v2.4.1 429 | 430 | `2023-02-18` 431 | 432 | ### Enhancement 433 | - 调整部份移动端上的样式 434 | - 输入框支持换行 435 | 436 | ## v2.4.0 437 | 438 | `2023-02-17` 439 | 440 | ### Feature 441 | - 响应式支持移动端 442 | ### Enhancement 443 | - 修改部份描述错误 444 | 445 | ## v2.3.3 446 | 447 | `2023-02-16` 448 | 449 | ### Feature 450 | - 添加 `README` 部份说明和贡献列表 451 | - 添加 `docker` 镜像 452 | - 添加 `GitHub Action` 自动化构建 453 | 454 | ### BugFix 455 | - 回退依赖更新导致的 [Eslint 报错](https://github.com/eslint/eslint/issues/16896) 456 | 457 | ## v2.3.2 458 | 459 | `2023-02-16` 460 | 461 | ### Enhancement 462 | - 更新依赖至最新 463 | - 优化部份内容 464 | 465 | ## v2.3.1 466 | 467 | `2023-02-15` 468 | 469 | ### BugFix 470 | - 修复多会话状态下一些意想不到的问题 471 | 472 | ## v2.3.0 473 | 474 | `2023-02-15` 475 | ### Feature 476 | - 代码类型信息高亮显示 477 | - 支持 `node ^16` 版本 478 | - 移动端响应式初步支持 479 | - `vite` 中 `proxy` 代理 480 | 481 | ### Enhancement 482 | - 调整超时处理范围 483 | 484 | ### BugFix 485 | - 修复取消请求错误提示会添加到信息中 486 | - 修复部份情况下提交请求不可用 487 | - 修复侧边栏宽度变化闪烁的问题 488 | 489 | ## v2.2.0 490 | 491 | `2023-02-14` 492 | ### Feature 493 | - 会话和上下文本地储存 494 | - 侧边栏本地储存 495 | 496 | ## v2.1.0 497 | 498 | `2023-02-14` 499 | ### Enhancement 500 | - 更新依赖至最新 501 | - 联想功能移动至前端提交,后端只做转发 502 | 503 | ### BugFix 504 | - 修复部份项目检测有关 `Bug` 505 | - 修复清除上下文按钮失效 506 | 507 | ## v2.0.0 508 | 509 | `2023-02-13` 510 | ### Refactor 511 | 重构并优化大部分内容 512 | 513 | ## v1.0.5 514 | 515 | `2023-02-12` 516 | 517 | ### Enhancement 518 | - 输入框焦点,连续提交 519 | 520 | ### BugFix 521 | - 修复信息框样式问题 522 | - 修复中文输入法提交问题 523 | 524 | ## v1.0.4 525 | 526 | `2023-02-11` 527 | 528 | ### Feature 529 | - 支持上下文联想 530 | 531 | ## v1.0.3 532 | 533 | `2023-02-11` 534 | 535 | ### Enhancement 536 | - 拆分 `service` 文件以便扩展 537 | - 调整 `Eslint` 相关验证 538 | 539 | ### BugFix 540 | - 修复部份控制台报错 541 | 542 | ## v1.0.2 543 | 544 | `2023-02-10` 545 | 546 | ### BugFix 547 | - 修复新增信息容器不会自动滚动到问题 548 | - 修复文本过长不换行到问题 [#1](https://github.com/Chanzhaoyu/chatgpt-web/issues/1) 549 | -------------------------------------------------------------------------------- /src/components/common/PromptStore/index.vue: -------------------------------------------------------------------------------- 1 | 328 | 329 | 481 | --------------------------------------------------------------------------------