├── .commitlintrc.json ├── .devcontainer └── devcontainer.json ├── .dockerignore ├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .github └── workflows │ ├── build_docker.yml │ ├── ci.yml │ └── issues_close.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.en.md ├── CONTRIBUTING.md ├── Dockerfile ├── README.md ├── README.zh.md ├── docker-compose ├── README.md ├── docker-compose.yml └── nginx │ └── nginx.conf ├── docs ├── alipay.png ├── c1-2.8.0.png ├── c1-2.9.0.png ├── c1.png ├── c2-2.8.0.png ├── c2-2.9.0.png ├── c2.png ├── docker.png └── wechat.png ├── index.html ├── kubernetes ├── README.md ├── deploy.yaml └── ingress.yaml ├── license ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── favicon.ico ├── favicon.svg ├── pwa-192x192.png └── pwa-512x512.png ├── service ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .vscode │ ├── extensions.json │ └── settings.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── src │ ├── chatgpt │ │ ├── index.ts │ │ └── types.ts │ ├── index.ts │ ├── middleware │ │ ├── auth.ts │ │ └── limiter.ts │ ├── types.ts │ └── utils │ │ ├── index.ts │ │ └── is.ts ├── tsconfig.json └── tsup.config.ts ├── src ├── App.vue ├── api │ └── index.ts ├── assets │ ├── avatar.jpg │ └── recommend.json ├── components │ ├── common │ │ ├── HoverButton │ │ │ ├── Button.vue │ │ │ └── index.vue │ │ ├── NaiveProvider │ │ │ └── index.vue │ │ ├── PromptStore │ │ │ └── index.vue │ │ ├── Setting │ │ │ ├── About.vue │ │ │ ├── Advanced.vue │ │ │ ├── General.vue │ │ │ └── index.vue │ │ ├── SvgIcon │ │ │ └── index.vue │ │ ├── UserAvatar │ │ │ └── index.vue │ │ └── index.ts │ └── custom │ │ ├── GithubSite.vue │ │ └── index.ts ├── hooks │ ├── useBasicLayout.ts │ ├── useIconRender.ts │ ├── useLanguage.ts │ └── useTheme.ts ├── icons │ ├── 403.vue │ ├── 404.svg │ └── 500.vue ├── locales │ ├── en-US.ts │ ├── es-ES.ts │ ├── index.ts │ ├── ko-KR.ts │ ├── ru-RU.ts │ ├── vi-VN.ts │ ├── zh-CN.ts │ └── zh-TW.ts ├── main.ts ├── plugins │ ├── assets.ts │ ├── index.ts │ └── scrollbarStyle.ts ├── router │ ├── index.ts │ └── permission.ts ├── store │ ├── helper.ts │ ├── index.ts │ └── modules │ │ ├── app │ │ ├── helper.ts │ │ └── index.ts │ │ ├── auth │ │ ├── helper.ts │ │ └── index.ts │ │ ├── chat │ │ ├── helper.ts │ │ └── index.ts │ │ ├── index.ts │ │ ├── prompt │ │ ├── helper.ts │ │ └── index.ts │ │ ├── settings │ │ ├── helper.ts │ │ └── index.ts │ │ └── user │ │ ├── helper.ts │ │ └── index.ts ├── styles │ ├── global.less │ └── lib │ │ ├── github-markdown.less │ │ ├── highlight.less │ │ └── tailwind.css ├── typings │ ├── chat.d.ts │ ├── env.d.ts │ └── global.d.ts ├── utils │ ├── copy.ts │ ├── functions │ │ ├── debounce.ts │ │ └── index.ts │ ├── is │ │ └── index.ts │ ├── request │ │ ├── axios.ts │ │ └── index.ts │ └── storage │ │ └── index.ts └── views │ ├── chat │ ├── components │ │ ├── Header │ │ │ └── index.vue │ │ ├── Message │ │ │ ├── Avatar.vue │ │ │ ├── Text.vue │ │ │ ├── index.vue │ │ │ └── style.less │ │ └── index.ts │ ├── hooks │ │ ├── useChat.ts │ │ ├── useScroll.ts │ │ └── useUsingContext.ts │ ├── index.vue │ └── layout │ │ ├── Layout.vue │ │ ├── Permission.vue │ │ ├── index.ts │ │ └── sider │ │ ├── Footer.vue │ │ ├── List.vue │ │ └── index.vue │ └── exception │ ├── 404 │ └── index.vue │ └── 500 │ └── index.vue ├── start.cmd ├── start.sh ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye" 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | // "postCreateCommand": "yarn install", 16 | 17 | // Configure tool-specific properties. 18 | // "customizations": {}, 19 | 20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 21 | // "remoteUser": "root" 22 | } 23 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | */node_modules 3 | node_modules 4 | Dockerfile 5 | .* 6 | */.* 7 | !.env 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | docker-compose 2 | kubernetes 3 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@antfu'], 4 | } 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/issues_close.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: 10 16 | days-before-issue-close: 2 17 | stale-issue-label: stale 18 | stale-issue-message: This issue is stale because it has been open for 10 days with no activity. 19 | close-issue-message: This issue was closed because it has been inactive for 2 days since being marked as stale. 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 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 | -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Web 2 | 3 | > Disclaimer: This project is only published on GitHub, based on the MIT license, free and for open source learning usage. And there will be no any form of account selling, paid service, discussion group, discussion group and other behaviors. Beware of being deceived. 4 | 5 | [中文](README.zh.md) 6 | 7 | ![cover](./docs/c1.png) 8 | ![cover2](./docs/c2.png) 9 | 10 | - [ChatGPT Web](#chatgpt-web) 11 | - [Introduction](#introduction) 12 | - [Roadmap](#roadmap) 13 | - [Prerequisites](#prerequisites) 14 | - [Node](#node) 15 | - [PNPM](#pnpm) 16 | - [Filling in the Key](#filling-in-the-key) 17 | - [Install Dependencies](#install-dependencies) 18 | - [Backend](#backend) 19 | - [Frontend](#frontend) 20 | - [Run in Test Environment](#run-in-test-environment) 21 | - [Backend Service](#backend-service) 22 | - [Frontend Webpage](#frontend-webpage) 23 | - [Environment Variables](#environment-variables) 24 | - [Packaging](#packaging) 25 | - [Use Docker](#use-docker) 26 | - [Docker Parameter Examples](#docker-parameter-examples) 27 | - [Docker build \& Run](#docker-build--run) 28 | - [Docker compose](#docker-compose) 29 | - [Prevent Crawlers](#prevent-crawlers) 30 | - [Deploy with Railway](#deploy-with-railway) 31 | - [Railway Environment Variables](#railway-environment-variables) 32 | - [Deploy with Sealos](#deploy-with-sealos) 33 | - [Package Manually](#package-manually) 34 | - [Backend Service](#backend-service-1) 35 | - [Frontend Webpage](#frontend-webpage-1) 36 | - [FAQ](#faq) 37 | - [Contributing](#contributing) 38 | - [Acknowledgements](#acknowledgements) 39 | - [Sponsors](#sponsors) 40 | - [License](#license) 41 | ## Introduction 42 | 43 | Supports dual models and provides two unofficial `ChatGPT API` methods 44 | 45 | | Method | Free? | Reliability | Quality | 46 | | ---------------------------------- | ----- | ----------- | ------- | 47 | | `ChatGPTAPI(gpt-3.5-turbo-0301)` | No | Reliable | Relatively stupid | 48 | | `ChatGPTUnofficialProxyAPI(web accessToken)` | Yes | Relatively unreliable | Smart | 49 | 50 | Comparison: 51 | 1. `ChatGPTAPI` uses `gpt-3.5-turbo` through `OpenAI` official `API` to call `ChatGPT` 52 | 2. `ChatGPTUnofficialProxyAPI` uses unofficial proxy server to access `ChatGPT`'s backend `API`, bypass `Cloudflare` (dependent on third-party servers, and has rate limits) 53 | 54 | Warnings: 55 | 1. You should first use the `API` method 56 | 2. When using the `API`, if the network is not working, it is blocked in China, you need to build your own proxy, never use someone else's public proxy, which is dangerous. 57 | 3. When using the `accessToken` method, the reverse proxy will expose your access token to third parties. This should not have any adverse effects, but please consider the risks before using this method. 58 | 4. When using `accessToken`, whether you are a domestic or foreign machine, proxies will be used. The default proxy is [pengzhile](https://github.com/pengzhile)'s `https://ai.fakeopen.com/api/conversation`. This is not a backdoor or monitoring unless you have the ability to flip over `CF` verification yourself. Use beforehand acknowledge. [Community Proxy](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) (Note: Only these two are recommended, other third-party sources, please identify for yourself) 59 | 5. When publishing the project to public network, you should set the `AUTH_SECRET_KEY` variable to add your password access, you should also modify the `title` in `index. html` to prevent it from being searched by keywords. 60 | 61 | Switching methods: 62 | 1. Enter the `service/.env.example` file, copy the contents to the `service/.env` file 63 | 2. To use `OpenAI API Key`, fill in the `OPENAI_API_KEY` field [(get apiKey)](https://platform.openai.com/overview) 64 | 3. To use `Web API`, fill in the `OPENAI_ACCESS_TOKEN` field [(get accessToken)](https://chat.openai.com/api/auth/session) 65 | 4. `OpenAI API Key` takes precedence when both exist 66 | 67 | Environment variables: 68 | 69 | See all parameter variables [here](#environment-variables) 70 | 71 | ## Roadmap 72 | [✓] Dual models 73 | 74 | [✓] Multi-session storage and context logic 75 | 76 | [✓] Formatting and beautification of code and other message types 77 | 78 | [✓] Access control 79 | 80 | [✓] Data import/export 81 | 82 | [✓] Save messages as local images 83 | 84 | [✓] Multilingual interface 85 | 86 | [✓] Interface themes 87 | 88 | [✗] More... 89 | 90 | ## Prerequisites 91 | 92 | ### Node 93 | 94 | `node` requires version `^16 || ^18 || ^19` (`node >= 14` needs [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill) installation), use [nvm](https://github.com/nvm-sh/nvm) to manage multiple local `node` versions 95 | 96 | ```shell 97 | node -v 98 | ``` 99 | 100 | ### PNPM 101 | If you haven't installed `pnpm` 102 | ```shell 103 | npm install pnpm -g 104 | ``` 105 | 106 | ### Filling in the Key 107 | Get `Openai Api Key` or `accessToken` and fill in the local environment variables [Go to Introduction](#introduction) 108 | 109 | ``` 110 | # service/.env file 111 | 112 | # OpenAI API Key - https://platform.openai.com/overview 113 | OPENAI_API_KEY= 114 | 115 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 116 | OPENAI_ACCESS_TOKEN= 117 | ``` 118 | 119 | ## Install Dependencies 120 | 121 | > For the convenience of "backend developers" to understand the burden, the front-end "workspace" mode is not adopted, but separate folders are used to store them. If you only need to do secondary development of the front-end page, delete the `service` folder. 122 | 123 | ### Backend 124 | 125 | Enter the folder `/service` and run the following commands 126 | 127 | ```shell 128 | pnpm install 129 | ``` 130 | 131 | ### Frontend 132 | Run the following commands at the root directory 133 | ```shell 134 | pnpm bootstrap 135 | ``` 136 | 137 | ## Run in Test Environment 138 | ### Backend Service 139 | 140 | Enter the folder `/service` and run the following commands 141 | 142 | ```shell 143 | pnpm start 144 | ``` 145 | 146 | ### Frontend Webpage 147 | Run the following commands at the root directory 148 | ```shell 149 | pnpm dev 150 | ``` 151 | 152 | ## Environment Variables 153 | 154 | `API` available: 155 | 156 | - `OPENAI_API_KEY` and `OPENAI_ACCESS_TOKEN` choose one 157 | - `OPENAI_API_MODEL` Set model, optional, default: `gpt-3.5-turbo` 158 | - `OPENAI_API_BASE_URL` Set interface address, optional, default: `https://api.openai.com` 159 | - `OPENAI_API_DISABLE_DEBUG` Set interface to close debug logs, optional, default: empty does not close 160 | 161 | `ACCESS_TOKEN` available: 162 | 163 | - `OPENAI_ACCESS_TOKEN` and `OPENAI_API_KEY` choose one, `OPENAI_API_KEY` takes precedence when both exist 164 | - `API_REVERSE_PROXY` Set reverse proxy, optional, default: `https://ai.fakeopen.com/api/conversation`, [Community](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) (Note: Only these two are recommended, other third party sources, please identify for yourself) 165 | 166 | Common: 167 | 168 | - `AUTH_SECRET_KEY` Access permission key, optional 169 | - `MAX_REQUEST_PER_HOUR` Maximum number of requests per hour, optional, unlimited by default 170 | - `TIMEOUT_MS` Timeout, unit milliseconds, optional 171 | - `SOCKS_PROXY_HOST` and `SOCKS_PROXY_PORT` take effect together, optional 172 | - `SOCKS_PROXY_PORT` and `SOCKS_PROXY_HOST` take effect together, optional 173 | - `HTTPS_PROXY` Support `http`, `https`, `socks5`, optional 174 | - `ALL_PROXY` Support `http`, `https`, `socks5`, optional 175 | 176 | ## Packaging 177 | 178 | ### Use Docker 179 | 180 | #### Docker Parameter Examples 181 | 182 | ![docker](./docs/docker.png) 183 | 184 | #### Docker build & Run 185 | 186 | ```bash 187 | docker build -t chatgpt-web . 188 | 189 | # Foreground running 190 | docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 191 | 192 | # Background running 193 | docker run --name chatgpt-web -d -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 194 | 195 | # Run address 196 | http://localhost:3002/ 197 | ``` 198 | 199 | #### Docker compose 200 | 201 | [Hub address](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general) 202 | 203 | ```yml 204 | version: '3' 205 | 206 | services: 207 | app: 208 | image: chenzhaoyu94/chatgpt-web # always use latest, pull the tag image again to update 209 | ports: 210 | - 127.0.0.1:3002:3002 211 | environment: 212 | # choose one 213 | OPENAI_API_KEY: sk-xxx 214 | # choose one 215 | OPENAI_ACCESS_TOKEN: xxx 216 | # API interface address, optional, available when OPENAI_API_KEY is set 217 | OPENAI_API_BASE_URL: xxx 218 | # API model, optional, available when OPENAI_API_KEY is set, https://platform.openai.com/docs/models 219 | # gpt-4, gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4-turbo-preview, gpt-4-0125-preview, gpt-4-1106-preview, gpt-4-0314, gpt-4-0613, gpt-4-32k, gpt-4-32k-0314, gpt-4-32k-0613, gpt-3.5-turbo-16k, gpt-3.5-turbo-16k-0613, gpt-3.5-turbo, gpt-3.5-turbo-0301, gpt-3.5-turbo-0613, text-davinci-003, text-davinci-002, code-davinci-002 220 | OPENAI_API_MODEL: xxx 221 | # reverse proxy, optional 222 | API_REVERSE_PROXY: xxx 223 | # access permission key, optional 224 | AUTH_SECRET_KEY: xxx 225 | # maximum number of requests per hour, optional, unlimited by default 226 | MAX_REQUEST_PER_HOUR: 0 227 | # timeout, unit milliseconds, optional 228 | TIMEOUT_MS: 60000 229 | # Socks proxy, optional, take effect with SOCKS_PROXY_PORT 230 | SOCKS_PROXY_HOST: xxx 231 | # Socks proxy port, optional, take effect with SOCKS_PROXY_HOST 232 | SOCKS_PROXY_PORT: xxx 233 | # HTTPS proxy, optional, support http,https,socks5 234 | HTTPS_PROXY: http://xxx:7890 235 | ``` 236 | 237 | - `OPENAI_API_BASE_URL` Optional, available when `OPENAI_API_KEY` is set 238 | - `OPENAI_API_MODEL` Optional, available when `OPENAI_API_KEY` is set 239 | 240 | #### Prevent Crawlers 241 | 242 | **nginx** 243 | 244 | Fill in the following configuration in the nginx configuration file to prevent crawlers. You can refer to the `docker-compose/nginx/nginx.conf` file to add anti-crawler methods 245 | 246 | ``` 247 | # Prevent crawlers 248 | if ($http_user_agent ~* "360Spider|JikeSpider|Spider|spider|bot|Bot|2345Explorer|curl|wget|webZIP|qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot|NSPlayer|bingbot") 249 | { 250 | return 403; 251 | } 252 | ``` 253 | 254 | ### Deploy with Railway 255 | 256 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc) 257 | 258 | #### Railway Environment Variables 259 | 260 | | Environment variable name | Required | Remarks | 261 | | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- | 262 | | `PORT` | Required | Default `3002` | 263 | | `AUTH_SECRET_KEY` | Optional | Access permission key | 264 | | `MAX_REQUEST_PER_HOUR` | Optional | Maximum number of requests per hour, optional, unlimited by default | 265 | | `TIMEOUT_MS` | Optional | Timeout, unit milliseconds | 266 | | `OPENAI_API_KEY` | `OpenAI API` choose one | `apiKey` required for `OpenAI API` [(get apiKey)](https://platform.openai.com/overview) | 267 | | `OPENAI_ACCESS_TOKEN` | `Web API` choose one | `accessToken` required for `Web API` [(get accessToken)](https://chat.openai.com/api/auth/session) | 268 | | `OPENAI_API_BASE_URL` | Optional, available when `OpenAI API` | `API` interface address | 269 | | `OPENAI_API_MODEL` | Optional, available when `OpenAI API` | `API` model | 270 | | `API_REVERSE_PROXY` | Optional, available when `Web API` | `Web API` reverse proxy address [Details](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | 271 | | `SOCKS_PROXY_HOST` | Optional, take effect with `SOCKS_PROXY_PORT` | Socks proxy | 272 | | `SOCKS_PROXY_PORT` | Optional, take effect with `SOCKS_PROXY_HOST` | Socks proxy port | 273 | | `SOCKS_PROXY_USERNAME` | Optional, take effect with `SOCKS_PROXY_HOST` | Socks proxy username | 274 | | `SOCKS_PROXY_PASSWORD` | Optional, take effect with `SOCKS_PROXY_HOST` | Socks proxy password | 275 | | `HTTPS_PROXY` | Optional | HTTPS proxy, support http,https, socks5 | 276 | | `ALL_PROXY` | Optional | All proxies, support http,https, socks5 | 277 | 278 | > Note: Modifying environment variables on `Railway` will re-`Deploy` 279 | 280 | ### Deploy with Sealos 281 | 282 | [![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dchatgpt-web) 283 | 284 | > Environment variables are consistent with Docker environment variables 285 | 286 | ### Package Manually 287 | #### Backend Service 288 | > If you don't need the `node` interface of this project, you can omit the following operations 289 | 290 | Copy the `service` folder to the server where you have the `node` service environment. 291 | 292 | ```shell 293 | # Install 294 | pnpm install 295 | 296 | # Pack 297 | pnpm build 298 | 299 | # Run 300 | pnpm prod 301 | ``` 302 | 303 | PS: It is also okay to run `pnpm start` directly on the server without packing 304 | 305 | #### Frontend Webpage 306 | 307 | 1. Modify the `VITE_GLOB_API_URL` field in the `.env` file at the root directory to your actual backend interface address 308 | 309 | 2. Run the following commands at the root directory, then copy the files in the `dist` folder to the root directory of your website service 310 | 311 | [Reference](https://cn.vitejs.dev/guide/static -deploy.html#building-the-app) 312 | 313 | ```shell 314 | pnpm build 315 | ``` 316 | 317 | ## FAQ 318 | Q: Why does `Git` commit always report errors? 319 | 320 | A: Because there is a commit message verification, please follow the [Commit Guide](./CONTRIBUTING.md) 321 | 322 | Q: Where to change the request interface if only the front-end page is used? 323 | 324 | A: The `VITE_GLOB_API_URL` field in the `.env` file at the root directory. 325 | 326 | Q: All files explode red when saving? 327 | 328 | A: `vscode` please install the recommended plug-ins for the project, or manually install the `Eslint` plug-in. 329 | 330 | Q: No typewriter effect on the front end? 331 | 332 | A: One possible reason is that after Nginx reverse proxy, buffer is turned on, then Nginx will try to buffer some data from the backend before sending it to the browser. Please try adding `proxy_buffering off; ` after the reverse proxy parameter, then reload Nginx. Other web server configurations are similar. 333 | 334 | ## Contributing 335 | 336 | Please read the [Contributing Guide](./CONTRIBUTING.md) before contributing 337 | 338 | Thanks to everyone who has contributed! 339 | 340 | 341 | 342 | 343 | 344 | ## Acknowledgements 345 | 346 | Thanks to [JetBrains](https://www.jetbrains.com/) SoftWare for providing free Open Source license for this project. 347 | 348 | ## Sponsors 349 | 350 | If you find this project helpful and can afford it, you can give me a little support. Anyway, thanks for your support~ 351 | 352 |
353 |
354 | WeChat 355 |

WeChat Pay

356 |
357 |
358 | Alipay 359 |

Alipay

360 |
361 |
362 | 363 | ## License 364 | MIT © [ChenZhaoYu] 365 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Web 2 | 3 | > 声明:此项目只发布于 GitHub,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。 4 | 5 | [English](README.md) 6 | 7 | ![cover](./docs/c1.png) 8 | ![cover2](./docs/c2.png) 9 | 10 | - [ChatGPT Web](#chatgpt-web) 11 | - [介绍](#介绍) 12 | - [待实现路线](#待实现路线) 13 | - [前置要求](#前置要求) 14 | - [Node](#node) 15 | - [PNPM](#pnpm) 16 | - [填写密钥](#填写密钥) 17 | - [安装依赖](#安装依赖) 18 | - [后端](#后端) 19 | - [前端](#前端) 20 | - [测试环境运行](#测试环境运行) 21 | - [后端服务](#后端服务) 22 | - [前端网页](#前端网页) 23 | - [环境变量](#环境变量) 24 | - [打包](#打包) 25 | - [使用 Docker](#使用-docker) 26 | - [Docker 参数示例](#docker-参数示例) 27 | - [Docker build \& Run](#docker-build--run) 28 | - [Docker compose](#docker-compose) 29 | - [防止爬虫抓取](#防止爬虫抓取) 30 | - [使用 Railway 部署](#使用-railway-部署) 31 | - [Railway 环境变量](#railway-环境变量) 32 | - [使用 Sealos 部署](#使用-sealos-部署) 33 | - [手动打包](#手动打包) 34 | - [后端服务](#后端服务-1) 35 | - [前端网页](#前端网页-1) 36 | - [常见问题](#常见问题) 37 | - [参与贡献](#参与贡献) 38 | - [致谢](#致谢) 39 | - [赞助](#赞助) 40 | - [License](#license) 41 | ## 介绍 42 | 43 | 支持双模型,提供了两种非官方 `ChatGPT API` 方法 44 | 45 | | 方式 | 免费? | 可靠性 | 质量 | 46 | | --------------------------------------------- | ------ | ---------- | ---- | 47 | | `ChatGPTAPI(gpt-3.5-turbo-0301)` | 否 | 可靠 | 相对较笨 | 48 | | `ChatGPTUnofficialProxyAPI(网页 accessToken)` | 是 | 相对不可靠 | 聪明 | 49 | 50 | 对比: 51 | 1. `ChatGPTAPI` 使用 `gpt-3.5-turbo` 通过 `OpenAI` 官方 `API` 调用 `ChatGPT` 52 | 2. `ChatGPTUnofficialProxyAPI` 使用非官方代理服务器访问 `ChatGPT` 的后端`API`,绕过`Cloudflare`(依赖于第三方服务器,并且有速率限制) 53 | 54 | 警告: 55 | 1. 你应该首先使用 `API` 方式 56 | 2. 使用 `API` 时,如果网络不通,那是国内被墙了,你需要自建代理,绝对不要使用别人的公开代理,那是危险的。 57 | 3. 使用 `accessToken` 方式时反向代理将向第三方暴露您的访问令牌,这样做应该不会产生任何不良影响,但在使用这种方法之前请考虑风险。 58 | 4. 使用 `accessToken` 时,不管你是国内还是国外的机器,都会使用代理。默认代理为 [pengzhile](https://github.com/pengzhile) 大佬的 `https://ai.fakeopen.com/api/conversation`,这不是后门也不是监听,除非你有能力自己翻过 `CF` 验证,用前请知悉。[社区代理](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) 59 | 5. 把项目发布到公共网络时,你应该设置 `AUTH_SECRET_KEY` 变量添加你的密码访问权限,你也应该修改 `index.html` 中的 `title`,防止被关键词搜索到。 60 | 61 | 切换方式: 62 | 1. 进入 `service/.env.example` 文件,复制内容到 `service/.env` 文件 63 | 2. 使用 `OpenAI API Key` 请填写 `OPENAI_API_KEY` 字段 [(获取 apiKey)](https://platform.openai.com/overview) 64 | 3. 使用 `Web API` 请填写 `OPENAI_ACCESS_TOKEN` 字段 [(获取 accessToken)](https://chat.openai.com/api/auth/session) 65 | 4. 同时存在时以 `OpenAI API Key` 优先 66 | 67 | 环境变量: 68 | 69 | 全部参数变量请查看或[这里](#环境变量) 70 | 71 | ``` 72 | /service/.env.example 73 | ``` 74 | 75 | ## 待实现路线 76 | [✓] 双模型 77 | 78 | [✓] 多会话储存和上下文逻辑 79 | 80 | [✓] 对代码等消息类型的格式化美化处理 81 | 82 | [✓] 访问权限控制 83 | 84 | [✓] 数据导入、导出 85 | 86 | [✓] 保存消息到本地图片 87 | 88 | [✓] 界面多语言 89 | 90 | [✓] 界面主题 91 | 92 | [✗] More... 93 | 94 | ## 前置要求 95 | 96 | ### Node 97 | 98 | `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` 版本 99 | 100 | ```shell 101 | node -v 102 | ``` 103 | 104 | ### PNPM 105 | 如果你没有安装过 `pnpm` 106 | ```shell 107 | npm install pnpm -g 108 | ``` 109 | 110 | ### 填写密钥 111 | 获取 `Openai Api Key` 或 `accessToken` 并填写本地环境变量 [跳转](#介绍) 112 | 113 | ``` 114 | # service/.env 文件 115 | 116 | # OpenAI API Key - https://platform.openai.com/overview 117 | OPENAI_API_KEY= 118 | 119 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 120 | OPENAI_ACCESS_TOKEN= 121 | ``` 122 | 123 | ## 安装依赖 124 | 125 | > 为了简便 `后端开发人员` 的了解负担,所以并没有采用前端 `workspace` 模式,而是分文件夹存放。如果只需要前端页面做二次开发,删除 `service` 文件夹即可。 126 | 127 | ### 后端 128 | 129 | 进入文件夹 `/service` 运行以下命令 130 | 131 | ```shell 132 | pnpm install 133 | ``` 134 | 135 | ### 前端 136 | 根目录下运行以下命令 137 | ```shell 138 | pnpm bootstrap 139 | ``` 140 | 141 | ## 测试环境运行 142 | ### 后端服务 143 | 144 | 进入文件夹 `/service` 运行以下命令 145 | 146 | ```shell 147 | pnpm start 148 | ``` 149 | 150 | ### 前端网页 151 | 根目录下运行以下命令 152 | ```shell 153 | pnpm dev 154 | ``` 155 | 156 | ## 环境变量 157 | 158 | `API` 可用: 159 | 160 | - `OPENAI_API_KEY` 和 `OPENAI_ACCESS_TOKEN` 二选一 161 | - `OPENAI_API_MODEL` 设置模型,可选,默认:`gpt-3.5-turbo` 162 | - `OPENAI_API_BASE_URL` 设置接口地址,可选,默认:`https://api.openai.com` 163 | - `OPENAI_API_DISABLE_DEBUG` 设置接口关闭 debug 日志,可选,默认:empty 不关闭 164 | 165 | `ACCESS_TOKEN` 可用: 166 | 167 | - `OPENAI_ACCESS_TOKEN` 和 `OPENAI_API_KEY` 二选一,同时存在时,`OPENAI_API_KEY` 优先 168 | - `API_REVERSE_PROXY` 设置反向代理,可选,默认:`https://ai.fakeopen.com/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-4o, gpt-4o-mini, gpt-4-turbo, gpt-4-turbo-preview, gpt-4-0125-preview, gpt-4-1106-preview, gpt-4-0314, gpt-4-0613, gpt-4-32k, gpt-4-32k-0314, gpt-4-32k-0613, gpt-3.5-turbo-16k, gpt-3.5-turbo-16k-0613, gpt-3.5-turbo, gpt-3.5-turbo-0301, gpt-3.5-turbo-0613, 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 | 243 | #### 防止爬虫抓取 244 | 245 | **nginx** 246 | 247 | 将下面配置填入nginx配置文件中,可以参考 `docker-compose/nginx/nginx.conf` 文件中添加反爬虫的方法 248 | 249 | ``` 250 | # 防止爬虫抓取 251 | if ($http_user_agent ~* "360Spider|JikeSpider|Spider|spider|bot|Bot|2345Explorer|curl|wget|webZIP|qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot|NSPlayer|bingbot") 252 | { 253 | return 403; 254 | } 255 | ``` 256 | 257 | ### 使用 Railway 部署 258 | 259 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc) 260 | 261 | #### Railway 环境变量 262 | 263 | | 环境变量名称 | 必填 | 备注 | 264 | | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- | 265 | | `PORT` | 必填 | 默认 `3002` 266 | | `AUTH_SECRET_KEY` | 可选 | 访问权限密钥 | 267 | | `MAX_REQUEST_PER_HOUR` | 可选 | 每小时最大请求次数,可选,默认无限 | 268 | | `TIMEOUT_MS` | 可选 | 超时时间,单位毫秒 | 269 | | `OPENAI_API_KEY` | `OpenAI API` 二选一 | 使用 `OpenAI API` 所需的 `apiKey` [(获取 apiKey)](https://platform.openai.com/overview) | 270 | | `OPENAI_ACCESS_TOKEN` | `Web API` 二选一 | 使用 `Web API` 所需的 `accessToken` [(获取 accessToken)](https://chat.openai.com/api/auth/session) | 271 | | `OPENAI_API_BASE_URL` | 可选,`OpenAI API` 时可用 | `API`接口地址 | 272 | | `OPENAI_API_MODEL` | 可选,`OpenAI API` 时可用 | `API`模型 | 273 | | `API_REVERSE_PROXY` | 可选,`Web API` 时可用 | `Web API` 反向代理地址 [详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | 274 | | `SOCKS_PROXY_HOST` | 可选,和 `SOCKS_PROXY_PORT` 一起时生效 | Socks代理 | 275 | | `SOCKS_PROXY_PORT` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理端口 | 276 | | `SOCKS_PROXY_USERNAME` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理用户名 | 277 | | `SOCKS_PROXY_PASSWORD` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理密码 | 278 | | `HTTPS_PROXY` | 可选 | HTTPS 代理,支持 http,https, socks5 | 279 | | `ALL_PROXY` | 可选 | 所有代理 代理,支持 http,https, socks5 | 280 | 281 | > 注意: `Railway` 修改环境变量会重新 `Deploy` 282 | 283 | ### 使用 Sealos 部署 284 | 285 | [![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dchatgpt-web) 286 | 287 | > 环境变量与 Docker 环境变量一致 288 | 289 | ### 手动打包 290 | #### 后端服务 291 | > 如果你不需要本项目的 `node` 接口,可以省略如下操作 292 | 293 | 复制 `service` 文件夹到你有 `node` 服务环境的服务器上。 294 | 295 | ```shell 296 | # 安装 297 | pnpm install 298 | 299 | # 打包 300 | pnpm build 301 | 302 | # 运行 303 | pnpm prod 304 | ``` 305 | 306 | PS: 不进行打包,直接在服务器上运行 `pnpm start` 也可 307 | 308 | #### 前端网页 309 | 310 | 1、修改根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 为你的实际后端接口地址 311 | 312 | 2、根目录下运行以下命令,然后将 `dist` 文件夹内的文件复制到你网站服务的根目录下 313 | 314 | [参考信息](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app) 315 | 316 | ```shell 317 | pnpm build 318 | ``` 319 | 320 | ## 常见问题 321 | Q: 为什么 `Git` 提交总是报错? 322 | 323 | A: 因为有提交信息验证,请遵循 [Commit 指南](./CONTRIBUTING.md) 324 | 325 | Q: 如果只使用前端页面,在哪里改请求接口? 326 | 327 | A: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。 328 | 329 | Q: 文件保存时全部爆红? 330 | 331 | A: `vscode` 请安装项目推荐插件,或手动安装 `Eslint` 插件。 332 | 333 | Q: 前端没有打字机效果? 334 | 335 | A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer,则 Nginx 会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`,然后重载 Nginx。其他 web server 配置同理。 336 | 337 | ## 参与贡献 338 | 339 | 贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md) 340 | 341 | 感谢所有做过贡献的人! 342 | 343 | 344 | 345 | 346 | 347 | ## 致谢 348 | 349 | 感谢 [JetBrains](https://www.jetbrains.com/) 为这个项目提供免费开源许可的软件。 350 | 351 | ## 赞助 352 | 353 | 如果你觉得这个项目对你有帮助,并且情况允许的话,可以给我一点点支持,总之非常感谢支持~ 354 | 355 |
356 |
357 | 微信 358 |

WeChat Pay

359 |
360 |
361 | 支付宝 362 |

Alipay

363 |
364 |
365 | 366 | ## License 367 | MIT © [ChenZhaoYu](./license) 368 | -------------------------------------------------------------------------------- /docker-compose/README.md: -------------------------------------------------------------------------------- 1 | ### docker-compose Deployment Tutorial 2 | -Put the packaged front-end files in the `nginx/html` directory 3 | - ```shell 4 | # start up 5 | docker-compose up -d 6 | ``` 7 | - ```shell 8 | # Check the running status 9 | docker ps 10 | ``` 11 | - ```shell 12 | # end run 13 | docker-compose down 14 | ``` 15 | -------------------------------------------------------------------------------- /docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | container_name: chatgpt-web 6 | image: chenzhaoyu94/chatgpt-web # Always use latest, just pull the tag image again when updating 7 | ports: 8 | - 3002:3002 9 | environment: 10 | # pick one of two 11 | OPENAI_API_KEY: 12 | # pick one of two 13 | OPENAI_ACCESS_TOKEN: 14 | # API interface address, optional, available when OPENAI_API_KEY is set 15 | OPENAI_API_BASE_URL: 16 | # API model, optional, available when OPENAI_API_KEY is set 17 | OPENAI_API_MODEL: 18 | # reverse proxy, optional 19 | API_REVERSE_PROXY: 20 | # Access permission key, optional 21 | AUTH_SECRET_KEY: 22 | # The maximum number of requests per hour, optional, default unlimited 23 | MAX_REQUEST_PER_HOUR: 0 24 | # timeout in milliseconds, optional 25 | TIMEOUT_MS: 60000 26 | # Socks proxy, optional, works with SOCKS_PROXY_PORT 27 | SOCKS_PROXY_HOST: 28 | # Socks proxy port, optional, effective when combined with SOCKS_PROXY_HOST 29 | SOCKS_PROXY_PORT: 30 | # Socks proxy username, optional, effective when combined with SOCKS_PROXY_HOST & SOCKS_PROXY_PORT 31 | SOCKS_PROXY_USERNAME: 32 | # Socks proxy password, optional, effective when combined with SOCKS_PROXY_HOST & SOCKS_PROXY_PORT 33 | SOCKS_PROXY_PASSWORD: 34 | # HTTPS_PROXY proxy, optional 35 | HTTPS_PROXY: 36 | nginx: 37 | container_name: nginx 38 | image: nginx:alpine 39 | ports: 40 | - '80:80' 41 | expose: 42 | - '80' 43 | volumes: 44 | - ./nginx/html:/usr/share/nginx/html 45 | - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf 46 | links: 47 | - app 48 | -------------------------------------------------------------------------------- /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 | 7 | # Prevent crawlers from crawling 8 | if ($http_user_agent ~* "360Spider|JikeSpider|Spider|spider|bot|Bot|2345Explorer|curl|wget|webZIP|qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot|NSPlayer|bingbot") 9 | { 10 | return 403; 11 | } 12 | 13 | location / { 14 | root /usr/share/nginx/html; 15 | try_files $uri /index.html; 16 | } 17 | 18 | location /api { 19 | proxy_set_header X-Real-IP $remote_addr; #Forward user IP 20 | proxy_pass http://app:3002; 21 | } 22 | 23 | proxy_set_header Host $host; 24 | proxy_set_header X-Real-IP $remote_addr; 25 | proxy_set_header REMOTE-HOST $remote_addr; 26 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 27 | } 28 | -------------------------------------------------------------------------------- /docs/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/docs/alipay.png -------------------------------------------------------------------------------- /docs/c1-2.8.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/docs/c1-2.8.0.png -------------------------------------------------------------------------------- /docs/c1-2.9.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/docs/c1-2.9.0.png -------------------------------------------------------------------------------- /docs/c1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/docs/c1.png -------------------------------------------------------------------------------- /docs/c2-2.8.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/docs/c2-2.8.0.png -------------------------------------------------------------------------------- /docs/c2-2.9.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/docs/c2-2.9.0.png -------------------------------------------------------------------------------- /docs/c2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/docs/c2.png -------------------------------------------------------------------------------- /docs/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/docs/docker.png -------------------------------------------------------------------------------- /docs/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/docs/wechat.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /kubernetes/README.md: -------------------------------------------------------------------------------- 1 | ## 增加一个Kubernetes的部署方式 2 | ``` 3 | kubectl apply -f deploy.yaml 4 | ``` 5 | 6 | ### 如果需要Ingress域名接入 7 | ``` 8 | kubectl apply -f ingress.yaml 9 | ``` 10 | -------------------------------------------------------------------------------- /kubernetes/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: chatgpt-web 5 | labels: 6 | app: chatgpt-web 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: chatgpt-web 12 | strategy: 13 | type: RollingUpdate 14 | template: 15 | metadata: 16 | labels: 17 | app: chatgpt-web 18 | spec: 19 | containers: 20 | - image: chenzhaoyu94/chatgpt-web 21 | name: chatgpt-web 22 | imagePullPolicy: Always 23 | ports: 24 | - containerPort: 3002 25 | env: 26 | - name: OPENAI_API_KEY 27 | value: sk-xxx 28 | - name: OPENAI_API_BASE_URL 29 | value: 'https://api.openai.com' 30 | - name: OPENAI_API_MODEL 31 | value: gpt-3.5-turbo 32 | - name: API_REVERSE_PROXY 33 | value: https://ai.fakeopen.com/api/conversation 34 | - name: AUTH_SECRET_KEY 35 | value: '123456' 36 | - name: TIMEOUT_MS 37 | value: '60000' 38 | - name: SOCKS_PROXY_HOST 39 | value: '' 40 | - name: SOCKS_PROXY_PORT 41 | value: '' 42 | - name: HTTPS_PROXY 43 | value: '' 44 | resources: 45 | limits: 46 | cpu: 500m 47 | memory: 500Mi 48 | requests: 49 | cpu: 300m 50 | memory: 300Mi 51 | --- 52 | apiVersion: v1 53 | kind: Service 54 | metadata: 55 | labels: 56 | app: chatgpt-web 57 | name: chatgpt-web 58 | spec: 59 | ports: 60 | - name: chatgpt-web 61 | port: 3002 62 | protocol: TCP 63 | targetPort: 3002 64 | selector: 65 | app: chatgpt-web 66 | type: ClusterIP 67 | -------------------------------------------------------------------------------- /kubernetes/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | annotations: 5 | kubernetes.io/ingress.class: nginx 6 | nginx.ingress.kubernetes.io/proxy-connect-timeout: '5' 7 | name: chatgpt-web 8 | spec: 9 | rules: 10 | - host: chatgpt.example.com 11 | http: 12 | paths: 13 | - backend: 14 | service: 15 | name: chatgpt-web 16 | port: 17 | number: 3002 18 | path: / 19 | pathType: ImplementationSpecific 20 | tls: 21 | - secretName: chatgpt-web-tls 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-web", 3 | "version": "2.11.1", 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 | "@vscode/markdown-it-katex": "^1.0.3", 27 | "@vueuse/core": "^9.13.0", 28 | "highlight.js": "^11.7.0", 29 | "html-to-image": "^1.11.11", 30 | "katex": "^0.16.4", 31 | "markdown-it": "^13.0.1", 32 | "mermaid-it-markdown": "^1.0.8", 33 | "naive-ui": "^2.34.3", 34 | "pinia": "^2.0.33", 35 | "vue": "^3.2.47", 36 | "vue-i18n": "^9.2.2", 37 | "vue-router": "^4.1.6" 38 | }, 39 | "devDependencies": { 40 | "@antfu/eslint-config": "^0.35.3", 41 | "@commitlint/cli": "^17.4.4", 42 | "@commitlint/config-conventional": "^17.4.4", 43 | "@iconify/vue": "^4.1.0", 44 | "@types/crypto-js": "^4.1.1", 45 | "@types/katex": "^0.16.0", 46 | "@types/markdown-it": "^12.2.3", 47 | "@types/markdown-it-link-attributes": "^3.0.1", 48 | "@types/node": "^18.14.6", 49 | "@vitejs/plugin-vue": "^4.0.0", 50 | "autoprefixer": "^10.4.13", 51 | "axios": "^1.3.4", 52 | "crypto-js": "^4.1.1", 53 | "eslint": "^8.35.0", 54 | "husky": "^8.0.3", 55 | "less": "^4.1.3", 56 | "lint-staged": "^13.1.2", 57 | "markdown-it-link-attributes": "^4.0.1", 58 | "npm-run-all": "^4.1.5", 59 | "postcss": "^8.4.21", 60 | "rimraf": "^4.3.0", 61 | "tailwindcss": "^3.2.7", 62 | "typescript": "~4.9.5", 63 | "vite": "^4.2.0", 64 | "vite-plugin-pwa": "^0.14.4", 65 | "vue-tsc": "^1.2.0" 66 | }, 67 | "lint-staged": { 68 | "*.{ts,tsx,vue}": [ 69 | "pnpm lint:fix" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/public/pwa-192x192.png -------------------------------------------------------------------------------- /public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/public/pwa-512x512.png -------------------------------------------------------------------------------- /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 | # set `true` to disable OpenAI API debug log 14 | OPENAI_API_DISABLE_DEBUG= 15 | 16 | # Reverse Proxy - Available on accessToken 17 | # Default: https://ai.fakeopen.com/api/conversation 18 | # More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy 19 | API_REVERSE_PROXY= 20 | 21 | # timeout 22 | TIMEOUT_MS=100000 23 | 24 | # Rate Limit 25 | MAX_REQUEST_PER_HOUR= 26 | 27 | # Secret key 28 | AUTH_SECRET_KEY= 29 | 30 | # Socks Proxy Host 31 | SOCKS_PROXY_HOST= 32 | 33 | # Socks Proxy Port 34 | SOCKS_PROXY_PORT= 35 | 36 | # Socks Proxy Username 37 | SOCKS_PROXY_USERNAME= 38 | 39 | # Socks Proxy Password 40 | SOCKS_PROXY_PASSWORD= 41 | 42 | # HTTPS PROXY 43 | HTTPS_PROXY= 44 | 45 | -------------------------------------------------------------------------------- /service/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["build"], 4 | "extends": ["@antfu"] 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /service/.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | -------------------------------------------------------------------------------- /service/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /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/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 || ^20" 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": "^4.7.0", 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 | -------------------------------------------------------------------------------- /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 { sendResponse } from '../utils' 9 | import { isNotEmptyString } from '../utils/is' 10 | import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types' 11 | import type { RequestOptions, SetProxyOptions, UsageResponse } from './types' 12 | 13 | const { HttpsProxyAgent } = httpsProxyAgent 14 | 15 | dotenv.config() 16 | 17 | const ErrorCodeMessage: Record = { 18 | 401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided', 19 | 403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later', 20 | 502: '[OpenAI] 错误的网关 | Bad Gateway', 21 | 503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later', 22 | 504: '[OpenAI] 网关超时 | Gateway Time-out', 23 | 500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error', 24 | } 25 | 26 | const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 100 * 1000 27 | const disableDebug: boolean = process.env.OPENAI_API_DISABLE_DEBUG === 'true' 28 | 29 | let apiModel: ApiModel 30 | const model = isNotEmptyString(process.env.OPENAI_API_MODEL) ? process.env.OPENAI_API_MODEL : 'gpt-3.5-turbo' 31 | 32 | if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN)) 33 | throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable') 34 | 35 | let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI 36 | 37 | (async () => { 38 | // More Info: https://github.com/transitive-bullshit/chatgpt-api 39 | 40 | if (isNotEmptyString(process.env.OPENAI_API_KEY)) { 41 | const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL 42 | 43 | const options: ChatGPTAPIOptions = { 44 | apiKey: process.env.OPENAI_API_KEY, 45 | completionParams: { model }, 46 | debug: !disableDebug, 47 | } 48 | 49 | // increase max token limit if use gpt-4 50 | if (model.toLowerCase().includes('gpt-4')) { 51 | // if use 32k model 52 | if (model.toLowerCase().includes('32k')) { 53 | options.maxModelTokens = 32768 54 | options.maxResponseTokens = 8192 55 | } 56 | else if (/-4o-mini/.test(model.toLowerCase())) { 57 | options.maxModelTokens = 128000 58 | options.maxResponseTokens = 16384 59 | } 60 | // if use GPT-4 Turbo or GPT-4o 61 | else if (/-preview|-turbo|o/.test(model.toLowerCase())) { 62 | options.maxModelTokens = 128000 63 | options.maxResponseTokens = 4096 64 | } 65 | else { 66 | options.maxModelTokens = 8192 67 | options.maxResponseTokens = 2048 68 | } 69 | } 70 | else if (model.toLowerCase().includes('gpt-3.5')) { 71 | if (/16k|1106|0125/.test(model.toLowerCase())) { 72 | options.maxModelTokens = 16384 73 | options.maxResponseTokens = 4096 74 | } 75 | } 76 | 77 | if (isNotEmptyString(OPENAI_API_BASE_URL)) { 78 | // if find /v1 in OPENAI_API_BASE_URL then use it 79 | if (OPENAI_API_BASE_URL.includes('/v1')) 80 | options.apiBaseUrl = `${OPENAI_API_BASE_URL}` 81 | else 82 | options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1` 83 | } 84 | 85 | setupProxy(options) 86 | 87 | api = new ChatGPTAPI({ ...options }) 88 | apiModel = 'ChatGPTAPI' 89 | } 90 | else { 91 | const options: ChatGPTUnofficialProxyAPIOptions = { 92 | accessToken: process.env.OPENAI_ACCESS_TOKEN, 93 | apiReverseProxyUrl: isNotEmptyString(process.env.API_REVERSE_PROXY) ? process.env.API_REVERSE_PROXY : 'https://ai.fakeopen.com/api/conversation', 94 | model, 95 | debug: !disableDebug, 96 | } 97 | 98 | setupProxy(options) 99 | 100 | api = new ChatGPTUnofficialProxyAPI({ ...options }) 101 | apiModel = 'ChatGPTUnofficialProxyAPI' 102 | } 103 | })() 104 | 105 | async function chatReplyProcess(options: RequestOptions) { 106 | const { message, lastContext, process, systemMessage, temperature, top_p } = options 107 | try { 108 | let options: SendMessageOptions = { timeoutMs } 109 | 110 | if (apiModel === 'ChatGPTAPI') { 111 | if (isNotEmptyString(systemMessage)) 112 | options.systemMessage = systemMessage 113 | options.completionParams = { model, temperature, top_p } 114 | } 115 | 116 | if (lastContext != null) { 117 | if (apiModel === 'ChatGPTAPI') 118 | options.parentMessageId = lastContext.parentMessageId 119 | else 120 | options = { ...lastContext } 121 | } 122 | 123 | const response = await api.sendMessage(message, { 124 | ...options, 125 | onProgress: (partialResponse) => { 126 | process?.(partialResponse) 127 | }, 128 | }) 129 | 130 | return sendResponse({ type: 'Success', data: response }) 131 | } 132 | catch (error: any) { 133 | const code = error.statusCode 134 | global.console.log(error) 135 | if (Reflect.has(ErrorCodeMessage, code)) 136 | return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] }) 137 | return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' }) 138 | } 139 | } 140 | 141 | async function fetchUsage() { 142 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY 143 | const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL 144 | 145 | if (!isNotEmptyString(OPENAI_API_KEY)) 146 | return Promise.resolve('-') 147 | 148 | const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL) 149 | ? OPENAI_API_BASE_URL 150 | : 'https://api.openai.com' 151 | 152 | const [startDate, endDate] = formatDate() 153 | 154 | // 每月使用量 155 | const urlUsage = `${API_BASE_URL}/v1/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}` 156 | 157 | const headers = { 158 | 'Authorization': `Bearer ${OPENAI_API_KEY}`, 159 | 'Content-Type': 'application/json', 160 | } 161 | 162 | const options = {} as SetProxyOptions 163 | 164 | setupProxy(options) 165 | 166 | try { 167 | // 获取已使用量 168 | const useResponse = await options.fetch(urlUsage, { headers }) 169 | if (!useResponse.ok) 170 | throw new Error('获取使用量失败') 171 | const usageData = await useResponse.json() as UsageResponse 172 | const usage = Math.round(usageData.total_usage) / 100 173 | return Promise.resolve(usage ? `$${usage}` : '-') 174 | } 175 | catch (error) { 176 | global.console.log(error) 177 | return Promise.resolve('-') 178 | } 179 | } 180 | 181 | function formatDate(): string[] { 182 | const today = new Date() 183 | const year = today.getFullYear() 184 | const month = today.getMonth() + 1 185 | const lastDay = new Date(year, month, 0) 186 | const formattedFirstDay = `${year}-${month.toString().padStart(2, '0')}-01` 187 | const formattedLastDay = `${year}-${month.toString().padStart(2, '0')}-${lastDay.getDate().toString().padStart(2, '0')}` 188 | return [formattedFirstDay, formattedLastDay] 189 | } 190 | 191 | async function chatConfig() { 192 | const usage = await fetchUsage() 193 | const reverseProxy = process.env.API_REVERSE_PROXY ?? '-' 194 | const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-' 195 | const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) 196 | ? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`) 197 | : '-' 198 | return sendResponse({ 199 | type: 'Success', 200 | data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, usage }, 201 | }) 202 | } 203 | 204 | function setupProxy(options: SetProxyOptions) { 205 | if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) { 206 | const agent = new SocksProxyAgent({ 207 | hostname: process.env.SOCKS_PROXY_HOST, 208 | port: process.env.SOCKS_PROXY_PORT, 209 | userId: isNotEmptyString(process.env.SOCKS_PROXY_USERNAME) ? process.env.SOCKS_PROXY_USERNAME : undefined, 210 | password: isNotEmptyString(process.env.SOCKS_PROXY_PASSWORD) ? process.env.SOCKS_PROXY_PASSWORD : undefined, 211 | }) 212 | options.fetch = (url, options) => { 213 | return fetch(url, { agent, ...options }) 214 | } 215 | } 216 | else if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(process.env.ALL_PROXY)) { 217 | const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY 218 | if (httpsProxy) { 219 | const agent = new HttpsProxyAgent(httpsProxy) 220 | options.fetch = (url, options) => { 221 | return fetch(url, { agent, ...options }) 222 | } 223 | } 224 | } 225 | else { 226 | options.fetch = (url, options) => { 227 | return fetch(url, { ...options }) 228 | } 229 | } 230 | } 231 | 232 | function currentModel(): ApiModel { 233 | return apiModel 234 | } 235 | 236 | export type { ChatContext, ChatMessage } 237 | 238 | export { chatReplyProcess, chatConfig, currentModel } 239 | -------------------------------------------------------------------------------- /service/src/chatgpt/types.ts: -------------------------------------------------------------------------------- 1 | import type { ChatMessage } from 'chatgpt' 2 | import type fetch from 'node-fetch' 3 | 4 | export interface RequestOptions { 5 | message: string 6 | lastContext?: { conversationId?: string; parentMessageId?: string } 7 | process?: (chat: ChatMessage) => void 8 | systemMessage?: string 9 | temperature?: number 10 | top_p?: number 11 | } 12 | 13 | export interface SetProxyOptions { 14 | fetch?: typeof fetch 15 | } 16 | 17 | export interface UsageResponse { 18 | total_usage: number 19 | } 20 | -------------------------------------------------------------------------------- /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, temperature, top_p } = 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 | temperature, 37 | top_p, 38 | }) 39 | } 40 | catch (error) { 41 | res.write(JSON.stringify(error)) 42 | } 43 | finally { 44 | res.end() 45 | } 46 | }) 47 | 48 | router.post('/config', auth, async (req, res) => { 49 | try { 50 | const response = await chatConfig() 51 | res.send(response) 52 | } 53 | catch (error) { 54 | res.send(error) 55 | } 56 | }) 57 | 58 | router.post('/session', async (req, res) => { 59 | try { 60 | const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY 61 | const hasAuth = isNotEmptyString(AUTH_SECRET_KEY) 62 | res.send({ status: 'Success', message: '', data: { auth: hasAuth, model: currentModel() } }) 63 | } 64 | catch (error) { 65 | res.send({ status: 'Fail', message: error.message, data: null }) 66 | } 67 | }) 68 | 69 | router.post('/verify', async (req, res) => { 70 | try { 71 | const { token } = req.body as { token: string } 72 | if (!token) 73 | throw new Error('Secret key is empty') 74 | 75 | if (process.env.AUTH_SECRET_KEY !== token) 76 | throw new Error('密钥无效 | Secret key is invalid') 77 | 78 | res.send({ status: 'Success', message: 'Verify successfully', data: null }) 79 | } 80 | catch (error) { 81 | res.send({ status: 'Fail', message: error.message, data: null }) 82 | } 83 | }) 84 | 85 | app.use('', router) 86 | app.use('/api', router) 87 | app.set('trust proxy', 1) 88 | 89 | app.listen(3002, () => globalThis.console.log('Server is running on port 3002')) 90 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | temperature?: number 8 | top_p?: number 9 | } 10 | 11 | export interface ChatContext { 12 | conversationId?: string 13 | parentMessageId?: string 14 | } 15 | 16 | export interface ChatGPTUnofficialProxyAPIOptions { 17 | accessToken: string 18 | apiReverseProxyUrl?: string 19 | model?: string 20 | debug?: boolean 21 | headers?: Record 22 | fetch?: FetchFn 23 | } 24 | 25 | export interface ModelConfig { 26 | apiModel?: ApiModel 27 | reverseProxy?: string 28 | timeoutMs?: number 29 | socksProxy?: string 30 | httpsProxy?: string 31 | usage?: string 32 | } 33 | 34 | export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosProgressEvent, GenericAbortSignal } from 'axios' 2 | import { post } from '@/utils/request' 3 | import { useAuthStore, 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 | const authStore = useAuthStore() 32 | 33 | let data: Record = { 34 | prompt: params.prompt, 35 | options: params.options, 36 | } 37 | 38 | if (authStore.isChatGPTAPI) { 39 | data = { 40 | ...data, 41 | systemMessage: settingStore.systemMessage, 42 | temperature: settingStore.temperature, 43 | top_p: settingStore.top_p, 44 | } 45 | } 46 | 47 | return post({ 48 | url: '/chat-process', 49 | data, 50 | signal: params.signal, 51 | onDownloadProgress: params.onDownloadProgress, 52 | }) 53 | } 54 | 55 | export function fetchSession() { 56 | return post({ 57 | url: '/session', 58 | }) 59 | } 60 | 61 | export function fetchVerify(token: string) { 62 | return post({ 63 | url: '/verify', 64 | data: { token }, 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/assets/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/7474b2c14971d2d9584c4b129396f9332ed87b7b/src/assets/avatar.jpg -------------------------------------------------------------------------------- /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/components/common/HoverButton/Button.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /src/components/common/HoverButton/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 47 | -------------------------------------------------------------------------------- /src/components/common/NaiveProvider/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 44 | -------------------------------------------------------------------------------- /src/components/common/Setting/About.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 76 | -------------------------------------------------------------------------------- /src/components/common/Setting/Advanced.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 71 | -------------------------------------------------------------------------------- /src/components/common/Setting/General.vue: -------------------------------------------------------------------------------- 1 | 126 | 127 | 228 | -------------------------------------------------------------------------------- /src/components/common/Setting/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 71 | -------------------------------------------------------------------------------- /src/components/common/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /src/components/common/UserAvatar/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/custom/GithubSite.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/components/custom/index.ts: -------------------------------------------------------------------------------- 1 | import GithubSite from './GithubSite.vue' 2 | 3 | export { GithubSite } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/hooks/useLanguage.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { enUS, esAR, koKR, ruRU, viVN, 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 | setLocale(appStore.language) 11 | switch (appStore.language) { 12 | case 'en-US': 13 | return enUS 14 | case 'es-ES': 15 | return esAR 16 | case 'ko-KR': 17 | return koKR 18 | case 'vi-VN': 19 | return viVN 20 | case 'ru-RU': 21 | return ruRU 22 | case 'zh-CN': 23 | return zhCN 24 | case 'zh-TW': 25 | return zhTW 26 | default: 27 | return enUS 28 | } 29 | }) 30 | 31 | return { language } 32 | } 33 | -------------------------------------------------------------------------------- /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/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 | stopResponding: 'Stop Responding', 30 | }, 31 | chat: { 32 | newChatButton: 'New Chat', 33 | newChatTitle: 'New Chat', 34 | placeholder: 'Ask me anything...(Shift + Enter = line break, "/" to trigger prompts)', 35 | placeholderMobile: 'Ask me anything...', 36 | copy: 'Copy', 37 | copied: 'Copied', 38 | copyCode: 'Copy Code', 39 | copyFailed: 'Copy Failed', 40 | clearChat: 'Clear Chat', 41 | clearChatConfirm: 'Are you sure to clear this chat?', 42 | exportImage: 'Export Image', 43 | exportImageConfirm: 'Are you sure to export this chat to png?', 44 | exportSuccess: 'Export Success', 45 | exportFailed: 'Export Failed', 46 | usingContext: 'Context Mode', 47 | turnOnContext: 'In the current mode, sending messages will carry previous chat records.', 48 | turnOffContext: 'In the current mode, sending messages will not carry previous chat records.', 49 | deleteMessage: 'Delete Message', 50 | deleteMessageConfirm: 'Are you sure to delete this message?', 51 | deleteHistoryConfirm: 'Are you sure to clear this history?', 52 | clearHistoryConfirm: 'Are you sure to clear chat history?', 53 | preview: 'Preview', 54 | showRawText: 'Show as raw text', 55 | thinking: 'Thinking...', 56 | }, 57 | setting: { 58 | setting: 'Setting', 59 | general: 'General', 60 | advanced: 'Advanced', 61 | config: 'Config', 62 | avatarLink: 'Avatar Link', 63 | name: 'Name', 64 | description: 'Description', 65 | role: 'Role', 66 | temperature: 'Temperature', 67 | top_p: 'Top_p', 68 | resetUserInfo: 'Reset UserInfo', 69 | chatHistory: 'ChatHistory', 70 | theme: 'Theme', 71 | language: 'Language', 72 | api: 'API', 73 | reverseProxy: 'Reverse Proxy', 74 | timeout: 'Timeout', 75 | socks: 'Socks', 76 | httpsProxy: 'HTTPS Proxy', 77 | balance: 'API Balance', 78 | monthlyUsage: 'Monthly Usage', 79 | openSource: 'This project is open sourced at', 80 | freeMIT: 'free and based on the MIT license, without any form of paid behavior!', 81 | stars: 'If you find this project helpful, please give me a Star on GitHub or give a little sponsorship, thank you!', 82 | }, 83 | store: { 84 | siderButton: 'Prompt Store', 85 | local: 'Local', 86 | online: 'Online', 87 | title: 'Title', 88 | description: 'Description', 89 | clearStoreConfirm: 'Whether to clear the data?', 90 | importPlaceholder: 'Please paste the JSON data here', 91 | addRepeatTitleTips: 'Title duplicate, please re-enter', 92 | addRepeatContentTips: 'Content duplicate: {msg}, please re-enter', 93 | editRepeatTitleTips: 'Title conflict, please revise', 94 | editRepeatContentTips: 'Content conflict {msg} , please re-modify', 95 | importError: 'Key value mismatch', 96 | importRepeatTitle: 'Title repeatedly skipped: {msg}', 97 | importRepeatContent: 'Content is repeatedly skipped: {msg}', 98 | onlineImportWarning: 'Note: Please check the JSON file source!', 99 | downloadError: 'Please check the network status and JSON file validity', 100 | }, 101 | } 102 | -------------------------------------------------------------------------------- /src/locales/es-ES.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | add: 'Agregar', 4 | addSuccess: 'Agregado con éxito', 5 | edit: 'Editar', 6 | editSuccess: 'Edición exitosa', 7 | delete: 'Borrar', 8 | deleteSuccess: 'Borrado con éxito', 9 | save: 'Guardar', 10 | saveSuccess: 'Guardado con éxito', 11 | reset: 'Reiniciar', 12 | action: 'Acción', 13 | export: 'Exportar', 14 | exportSuccess: 'Exportación exitosa', 15 | import: 'Importar', 16 | importSuccess: 'Importación exitosa', 17 | clear: 'Limpiar', 18 | clearSuccess: 'Limpieza exitosa', 19 | yes: 'Sí', 20 | no: 'No', 21 | confirm: 'Confirmar', 22 | download: 'Descargar', 23 | noData: 'Sin datos', 24 | wrong: 'Algo salió mal, inténtalo de nuevo más tarde.', 25 | success: 'Exitoso', 26 | failed: 'Fallido', 27 | verify: 'Verificar', 28 | unauthorizedTips: 'No autorizado, por favor verifique primero.', 29 | stopResponding: 'No responde', 30 | }, 31 | chat: { 32 | newChatButton: 'Nueva conversación', 33 | newChatTitle: 'Nueva conversación', 34 | placeholder: 'Pregúntame lo que sea...(Shift + Enter = salto de línea, "/" para activar avisos)', 35 | placeholderMobile: 'Pregúntame lo que sea...', 36 | copy: 'Copiar', 37 | copied: 'Copiado', 38 | copyCode: 'Copiar código', 39 | copyFailed: 'Copia fallida', 40 | clearChat: 'Limpiar chat', 41 | clearChatConfirm: '¿Estás seguro de borrar este chat?', 42 | exportImage: 'Exportar imagen', 43 | exportImageConfirm: '¿Estás seguro de exportar este chat a png?', 44 | exportSuccess: 'Exportación exitosa', 45 | exportFailed: 'Exportación fallida', 46 | usingContext: 'Modo de contexto', 47 | turnOnContext: 'En el modo actual, el envío de mensajes llevará registros de chat anteriores.', 48 | turnOffContext: 'En el modo actual, el envío de mensajes no incluirá registros de conversaciones anteriores.', 49 | deleteMessage: 'Borrar mensaje', 50 | deleteMessageConfirm: '¿Estás seguro de eliminar este mensaje?', 51 | deleteHistoryConfirm: '¿Estás seguro de borrar esta historia?', 52 | clearHistoryConfirm: '¿Estás seguro de borrar el historial de chat?', 53 | preview: 'Avance', 54 | showRawText: 'Mostrar como texto sin formato', 55 | }, 56 | setting: { 57 | setting: 'Configuración', 58 | general: 'General', 59 | advanced: 'Avanzado', 60 | config: 'Configurar', 61 | avatarLink: 'Enlace de avatar', 62 | name: 'Nombre', 63 | description: 'Descripción', 64 | role: 'Rol', 65 | temperature: 'Temperatura', 66 | top_p: 'Top_p', 67 | resetUserInfo: 'Restablecer información de usuario', 68 | chatHistory: 'Historial de chat', 69 | theme: 'Tema', 70 | language: 'Idioma', 71 | api: 'API', 72 | reverseProxy: 'Reverse Proxy', 73 | timeout: 'Tiempo de espera', 74 | socks: 'Socks', 75 | httpsProxy: 'HTTPS Proxy', 76 | balance: 'Saldo de API', 77 | monthlyUsage: 'Uso mensual de API', 78 | openSource: 'Este proyecto es de código abierto en', 79 | freeMIT: 'gratis y basado en la licencia MIT, ¡sin ningún tipo de comportamiento de pago!', 80 | stars: 'Si encuentras este proyecto útil, por favor dame una Estrella en GitHub o da un pequeño patrocinio, ¡gracias!', 81 | }, 82 | store: { 83 | siderButton: 'Tienda rápida', 84 | local: 'Local', 85 | online: 'En línea', 86 | title: 'Título', 87 | description: 'Descripción', 88 | clearStoreConfirm: '¿Estás seguro de borrar los datos?', 89 | importPlaceholder: 'Pegue los datos JSON aquí', 90 | addRepeatTitleTips: 'Título duplicado, vuelva a ingresar', 91 | addRepeatContentTips: 'Contenido duplicado: {msg}, por favor vuelva a entrar', 92 | editRepeatTitleTips: 'Conflicto de título, revíselo', 93 | editRepeatContentTips: 'Conflicto de contenido {msg} , por favor vuelva a modificar', 94 | importError: 'Discrepancia de valor clave', 95 | importRepeatTitle: 'Título saltado repetidamente: {msg}', 96 | importRepeatContent: 'El contenido se salta repetidamente: {msg}', 97 | onlineImportWarning: 'Nota: ¡Compruebe la fuente del archivo JSON!', 98 | downloadError: 'Verifique el estado de la red y la validez del archivo JSON', 99 | }, 100 | } 101 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createI18n } from 'vue-i18n' 3 | import enUS from './en-US' 4 | import esES from './es-ES' 5 | import koKR from './ko-KR' 6 | import ruRU from './ru-RU' 7 | import viVN from './vi-VN' 8 | import zhCN from './zh-CN' 9 | import zhTW from './zh-TW' 10 | import { useAppStoreWithOut } from '@/store/modules/app' 11 | import type { Language } from '@/store/modules/app/helper' 12 | 13 | const appStore = useAppStoreWithOut() 14 | 15 | const defaultLocale = appStore.language || 'zh-CN' 16 | 17 | const i18n = createI18n({ 18 | locale: defaultLocale, 19 | fallbackLocale: 'en-US', 20 | allowComposition: true, 21 | messages: { 22 | 'en-US': enUS, 23 | 'es-ES': esES, 24 | 'ko-KR': koKR, 25 | 'ru-RU': ruRU, 26 | 'vi-VN': viVN, 27 | 'zh-CN': zhCN, 28 | 'zh-TW': zhTW, 29 | }, 30 | }) 31 | 32 | export const t = i18n.global.t 33 | 34 | export function setLocale(locale: Language) { 35 | i18n.global.locale = locale 36 | } 37 | 38 | export function setupI18n(app: App) { 39 | app.use(i18n) 40 | } 41 | 42 | export default i18n 43 | -------------------------------------------------------------------------------- /src/locales/ko-KR.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 | stopResponding: '응답 중지', 30 | }, 31 | chat: { 32 | newChatButton: '새로운 채팅', 33 | newChatTitle: '새로운 채팅', 34 | placeholder: '무엇이든 물어보세요...(Shift + Enter = 줄바꿈, "/"를 눌러서 힌트를 보세요)', 35 | placeholderMobile: '무엇이든 물어보세요...', 36 | copy: '복사', 37 | copied: '복사됨', 38 | copyCode: '코드 복사', 39 | copyFailed: '복사 실패', 40 | clearChat: '채팅 비우기', 41 | clearChatConfirm: '이 채팅을 비우시겠습니까?', 42 | exportImage: '이미지 내보내기', 43 | exportImageConfirm: '이 채팅을 png로 내보내시겠습니까?', 44 | exportSuccess: '내보내기 성공', 45 | exportFailed: '내보내기 실패', 46 | usingContext: '컨텍스트 모드', 47 | turnOnContext: '현재 모드에서는 이전 대화 기록을 포함하여 메시지를 보낼 수 있습니다.', 48 | turnOffContext: '현재 모드에서는 이전 대화 기록을 포함하지 않고 메시지를 보낼 수 있습니다.', 49 | deleteMessage: '메시지 삭제', 50 | deleteMessageConfirm: '이 메시지를 삭제하시겠습니까?', 51 | deleteHistoryConfirm: '이 기록을 삭제하시겠습니까?', 52 | clearHistoryConfirm: '채팅 기록을 삭제하시겠습니까?', 53 | preview: '미리보기', 54 | showRawText: '원본 텍스트로 보기', 55 | thinking: '생각 중...', 56 | }, 57 | setting: { 58 | setting: '설정', 59 | general: '일반', 60 | advanced: '고급', 61 | config: '설정', 62 | avatarLink: '아바타 링크', 63 | name: '이름', 64 | description: '설명', 65 | role: '역할', 66 | temperature: '온도', 67 | top_p: 'Top_p', 68 | resetUserInfo: '사용자 정보 초기화', 69 | chatHistory: '채팅 기록', 70 | theme: '테마', 71 | language: '언어', 72 | api: 'API', 73 | reverseProxy: '리버스 프록시', 74 | timeout: '타임아웃', 75 | socks: 'Socks', 76 | httpsProxy: 'HTTPS 프록시', 77 | balance: 'API 잔액', 78 | monthlyUsage: '월 사용량', 79 | openSource: '이 프로젝트는 다음에서 오픈 소스로 제공됩니다:', 80 | freeMIT: '무료이며 MIT 라이선스에 기반하며, 어떠한 형태의 유료 행동도 없습니다!', 81 | stars: '이 프로젝트가 도움이 되었다면, GitHub에서 별을 주거나 조금의 후원을 해주시면 감사하겠습니다!', 82 | }, 83 | store: { 84 | siderButton: '프롬프트 저장소', 85 | local: '로컬', 86 | online: '온라인', 87 | title: '제목', 88 | description: '설명', 89 | clearStoreConfirm: '데이터를 삭제하시겠습니까?', 90 | importPlaceholder: '여기에 JSON 데이터를 붙여넣으십시오', 91 | addRepeatTitleTips: '제목 중복됨, 다시 입력하십시오', 92 | addRepeatContentTips: '내용 중복됨: {msg}, 다시 입력하십시오', 93 | editRepeatTitleTips: '제목 충돌, 수정하십시오', 94 | editRepeatContentTips: '내용 충돌 {msg} , 수정하십시오', 95 | importError: '키 값 불일치', 96 | importRepeatTitle: '제목이 반복되어 건너뜀: {msg}', 97 | importRepeatContent: '내용이 반복되어 건너뜀: {msg}', 98 | onlineImportWarning: '참고: JSON 파일 소스를 확인하십시오!', 99 | }, 100 | } 101 | -------------------------------------------------------------------------------- /src/locales/ru-RU.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 | stopResponding: 'Прекращение отклика', 30 | }, 31 | chat: { 32 | newChatButton: 'Новый чат', 33 | newChatTitle: 'Новый чат', 34 | placeholder: 'Спросите меня о чем-нибудь ... (Shift + Enter = перенос строки, "/" для вызова подсказок)', 35 | placeholderMobile: 'Спросите меня о чем-нибудь ...', 36 | copy: 'Копировать', 37 | copied: 'Скопировано', 38 | copyCode: 'Копировать код', 39 | copyFailed: 'Не удалось скопировать', 40 | clearChat: 'Очистить чат', 41 | clearChatConfirm: 'Вы уверены, что хотите очистить этот чат?', 42 | exportImage: 'Экспорт в изображение', 43 | exportImageConfirm: 'Вы уверены, что хотите экспортировать этот чат в формате PNG?', 44 | exportSuccess: 'Экспортировано успешно', 45 | exportFailed: 'Не удалось выполнить экспорт', 46 | usingContext: 'Режим контекста', 47 | turnOnContext: 'В текущем режиме отправка сообщений будет включать предыдущие записи чата.', 48 | turnOffContext: 'В текущем режиме отправка сообщений не будет включать предыдущие записи чата.', 49 | deleteMessage: 'Удалить сообщение', 50 | deleteMessageConfirm: 'Вы уверены, что хотите удалить это сообщение?', 51 | deleteHistoryConfirm: 'Вы уверены, что хотите очистить эту историю?', 52 | clearHistoryConfirm: 'Вы уверены, что хотите очистить историю чата?', 53 | preview: 'Предварительный просмотр', 54 | showRawText: 'Показать как обычный текст', 55 | thinking: 'Думаю...', 56 | }, 57 | setting: { 58 | setting: 'Настройки', 59 | general: 'Общее', 60 | advanced: 'Дополнительно', 61 | config: 'Конфигурация', 62 | avatarLink: 'Ссылка на аватар', 63 | name: 'Имя', 64 | description: 'Описание', 65 | role: 'Роль', 66 | temperature: 'Температура', 67 | top_p: 'Top_p', 68 | resetUserInfo: 'Сбросить информацию о пользователе', 69 | chatHistory: 'История чата', 70 | theme: 'Тема', 71 | language: 'Язык', 72 | api: 'API', 73 | reverseProxy: 'Обратный прокси-сервер', 74 | timeout: 'Время ожидания', 75 | socks: 'Socks', 76 | httpsProxy: 'HTTPS-прокси', 77 | balance: 'Баланс API', 78 | monthlyUsage: 'Ежемесячное использование', 79 | openSource: 'Этот проект опубликован в открытом доступе на', 80 | freeMIT: 'бесплатно и основан на лицензии MIT, без каких-либо форм оплаты!', 81 | stars: 'Если вы считаете этот проект полезным, пожалуйста, поставьте мне звезду на GitHub или сделайте небольшое пожертвование, спасибо!', 82 | }, 83 | store: { 84 | siderButton: 'Хранилище подсказок', 85 | local: 'Локальное', 86 | online: 'Онлайн', 87 | title: 'Название', 88 | description: 'Описание', 89 | clearStoreConfirm: 'Вы действительно хотите очистить данные?', 90 | importPlaceholder: 'Пожалуйста, вставьте здесь JSON-данные', 91 | addRepeatTitleTips: 'Дубликат названия, пожалуйста, введите другое название', 92 | addRepeatContentTips: 'Дубликат содержимого: {msg}, пожалуйста, введите другой текст', 93 | editRepeatTitleTips: 'Конфликт названий, пожалуйста, измените название', 94 | editRepeatContentTips: 'Конфликт содержимого {msg}, пожалуйста, измените текст', 95 | importError: 'Не совпадает ключ-значение', 96 | importRepeatTitle: 'Название повторяющееся, пропускается: {msg}', 97 | importRepeatContent: 'Содержание повторяющееся, пропускается: {msg}', 98 | onlineImportWarning: 'Внимание! Проверьте источник JSON-файла!', 99 | downloadError: 'Проверьте состояние сети и правильность JSON-файла', 100 | }, 101 | } 102 | -------------------------------------------------------------------------------- /src/locales/vi-VN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | add: 'Thêm', 4 | addSuccess: 'Thêm thành công', 5 | edit: 'Sửa', 6 | editSuccess: 'Sửa thành công', 7 | delete: 'Xóa', 8 | deleteSuccess: 'Xóa thành công', 9 | save: 'Lưu', 10 | saveSuccess: 'Lưu thành công', 11 | reset: 'Đặt lại', 12 | action: 'Hành động', 13 | export: 'Xuất', 14 | exportSuccess: 'Xuất thành công', 15 | import: 'Nhập', 16 | importSuccess: 'Nhập thành công', 17 | clear: 'Dọn dẹp', 18 | clearSuccess: 'Dọn dẹp thành công', 19 | yes: 'Có', 20 | no: 'Không', 21 | confirm: 'Xác nhận', 22 | download: 'Tải xuống', 23 | noData: 'Không có dữ liệu', 24 | wrong: 'Đã xảy ra lỗi, vui lòng thử lại sau.', 25 | success: 'Thành công', 26 | failed: 'Thất bại', 27 | verify: 'Xác minh', 28 | unauthorizedTips: 'Không được ủy quyền, vui lòng xác minh trước.', 29 | }, 30 | chat: { 31 | newChatButton: 'Tạo hội thoại', 32 | newChatTitle: 'Tạo hội thoại', 33 | placeholder: 'Hỏi tôi bất cứ điều gì...(Shift + Enter = ngắt dòng, "/" to trigger prompts)', 34 | placeholderMobile: 'Hỏi tôi bất cứ điều gì...', 35 | copy: 'Sao chép', 36 | copied: 'Đã sao chép', 37 | copyCode: 'Sao chép Code', 38 | copyFailed: 'Sao chép thất bại', 39 | clearChat: 'Clear Chat', 40 | clearChatConfirm: 'Bạn có chắc chắn xóa cuộc trò chuyện này?', 41 | exportImage: 'Xuất hình ảnh', 42 | exportImageConfirm: 'Bạn có chắc chắn xuất cuộc trò chuyện này sang png không?', 43 | exportSuccess: 'Xuất thành công', 44 | exportFailed: 'Xuất thất bại', 45 | usingContext: 'Context Mode', 46 | turnOnContext: 'Ở chế độ hiện tại, việc gửi tin nhắn sẽ mang theo các bản ghi trò chuyện trước đó.', 47 | turnOffContext: 'Ở chế độ hiện tại, việc gửi tin nhắn sẽ không mang theo các bản ghi trò chuyện trước đó.', 48 | deleteMessage: 'Xóa tin nhắn', 49 | deleteMessageConfirm: 'Bạn có chắc chắn xóa tin nhắn này?', 50 | deleteHistoryConfirm: 'Bạn có chắc chắn để xóa lịch sử này?', 51 | clearHistoryConfirm: 'Bạn có chắc chắn để xóa lịch sử trò chuyện?', 52 | preview: 'Xem trước', 53 | showRawText: 'Hiển thị dưới dạng văn bản thô', 54 | thinking: 'Đang suy nghĩ...', 55 | }, 56 | setting: { 57 | setting: 'Cài đặt', 58 | general: 'Chung', 59 | advanced: 'Nâng cao', 60 | config: 'Cấu hình', 61 | avatarLink: 'Avatar Link', 62 | name: 'Tên', 63 | description: 'Miêu tả', 64 | role: 'Vai trò', 65 | temperature: 'Nhiệt độ', 66 | top_p: 'Top_p', 67 | resetUserInfo: 'Đặt lại thông tin người dùng', 68 | chatHistory: 'Lịch sử trò chuyện', 69 | theme: 'Giao diện', 70 | language: 'Ngôn ngữ', 71 | api: 'API', 72 | reverseProxy: 'Reverse Proxy', 73 | timeout: 'Timeout', 74 | socks: 'Socks', 75 | httpsProxy: 'HTTPS Proxy', 76 | balance: 'API Balance', 77 | monthlyUsage: 'Sử dụng hàng tháng', 78 | openSource: 'Dự án này được mở nguồn tại', 79 | freeMIT: 'miễn phí và dựa trên giấy phép MIT, không có bất kỳ hình thức hành vi trả phí nào!', 80 | stars: 'Nếu bạn thấy dự án này hữu ích, vui lòng cho tôi một Star trên GitHub hoặc tài trợ một chút, cảm ơn bạn!', 81 | }, 82 | store: { 83 | siderButton: 'Prompt Store', 84 | local: 'Local', 85 | online: 'Online', 86 | title: 'Tiêu đề', 87 | description: 'Miêu tả', 88 | clearStoreConfirm: 'Cho dù để xóa dữ liệu?', 89 | importPlaceholder: 'Vui lòng dán dữ liệu JSON vào đây', 90 | addRepeatTitleTips: 'Tiêu đề trùng lặp, vui lòng nhập lại', 91 | addRepeatContentTips: 'Nội dung trùng lặp: {msg}, vui lòng nhập lại', 92 | editRepeatTitleTips: 'Xung đột tiêu đề, vui lòng sửa lại', 93 | editRepeatContentTips: 'Xung đột nội dung {msg} , vui lòng sửa đổi lại', 94 | importError: 'Key value mismatch', 95 | importRepeatTitle: 'Tiêu đề liên tục bị bỏ qua: {msg}', 96 | importRepeatContent: 'Nội dung liên tục bị bỏ qua: {msg}', 97 | onlineImportWarning: 'Lưu ý: Vui lòng kiểm tra nguồn tệp JSON!', 98 | downloadError: 'Vui lòng kiểm tra trạng thái mạng và tính hợp lệ của tệp JSON', 99 | }, 100 | } 101 | -------------------------------------------------------------------------------- /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 | stopResponding: '停止响应', 30 | }, 31 | chat: { 32 | newChatButton: '新建聊天', 33 | newChatTitle: '新建聊天', 34 | placeholder: '来说点什么吧...(Shift + Enter = 换行,"/" 触发提示词)', 35 | placeholderMobile: '来说点什么...', 36 | copy: '复制', 37 | copied: '复制成功', 38 | copyCode: '复制代码', 39 | copyFailed: '复制失败', 40 | clearChat: '清空会话', 41 | clearChatConfirm: '是否清空会话?', 42 | exportImage: '保存会话到图片', 43 | exportImageConfirm: '是否将会话保存为图片?', 44 | exportSuccess: '保存成功', 45 | exportFailed: '保存失败', 46 | usingContext: '上下文模式', 47 | turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录', 48 | turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录', 49 | deleteMessage: '删除消息', 50 | deleteMessageConfirm: '是否删除此消息?', 51 | deleteHistoryConfirm: '确定删除此记录?', 52 | clearHistoryConfirm: '确定清空记录?', 53 | preview: '预览', 54 | showRawText: '显示原文', 55 | thinking: '思考中...', 56 | }, 57 | setting: { 58 | setting: '设置', 59 | general: '总览', 60 | advanced: '高级', 61 | config: '配置', 62 | avatarLink: '头像链接', 63 | name: '名称', 64 | description: '描述', 65 | role: '角色设定', 66 | temperature: 'Temperature', 67 | top_p: 'Top_p', 68 | resetUserInfo: '重置用户信息', 69 | chatHistory: '聊天记录', 70 | theme: '主题', 71 | language: '语言', 72 | api: 'API', 73 | reverseProxy: '反向代理', 74 | timeout: '超时', 75 | socks: 'Socks', 76 | httpsProxy: 'HTTPS Proxy', 77 | balance: 'API余额', 78 | monthlyUsage: '本月使用量', 79 | openSource: '此项目开源于', 80 | freeMIT: '免费且基于 MIT 协议,没有任何形式的付费行为', 81 | stars: '如果你觉得此项目对你有帮助,请在 GitHub 上给我一个星星或者给予一点赞助,谢谢!', 82 | }, 83 | store: { 84 | siderButton: '提示词商店', 85 | local: '本地', 86 | online: '在线', 87 | title: '标题', 88 | description: '描述', 89 | clearStoreConfirm: '是否清空数据?', 90 | importPlaceholder: '请粘贴 JSON 数据到此处', 91 | addRepeatTitleTips: '标题重复,请重新输入', 92 | addRepeatContentTips: '内容重复:{msg},请重新输入', 93 | editRepeatTitleTips: '标题冲突,请重新修改', 94 | editRepeatContentTips: '内容冲突{msg} ,请重新修改', 95 | importError: '键值不匹配', 96 | importRepeatTitle: '标题重复跳过:{msg}', 97 | importRepeatContent: '内容重复跳过:{msg}', 98 | onlineImportWarning: '注意:请检查 JSON 文件来源!', 99 | downloadError: '请检查网络状态与 JSON 文件有效性', 100 | }, 101 | } 102 | -------------------------------------------------------------------------------- /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 | stopResponding: '停止回應', 30 | }, 31 | chat: { 32 | newChatButton: '新增對話', 33 | newChatTitle: '新增對話', 34 | placeholder: '來說點什麼...(Shift + Enter = 換行,"/" 觸發提示詞)', 35 | placeholderMobile: '來說點什麼...', 36 | copy: '複製', 37 | copied: '複製成功', 38 | copyCode: '複製代碼', 39 | copyFailed: '複製失敗', 40 | clearChat: '清除對話', 41 | clearChatConfirm: '是否清空對話?', 42 | exportImage: '儲存對話為圖片', 43 | exportImageConfirm: '是否將對話儲存為圖片?', 44 | exportSuccess: '儲存成功', 45 | exportFailed: '儲存失敗', 46 | usingContext: '上下文模式', 47 | turnOnContext: '啟用上下文模式,在此模式下,發送訊息會包含之前的聊天記錄。', 48 | turnOffContext: '關閉上下文模式,在此模式下,發送訊息不會包含之前的聊天記錄。', 49 | deleteMessage: '刪除訊息', 50 | deleteMessageConfirm: '是否刪除此訊息?', 51 | deleteHistoryConfirm: '確定刪除此紀錄?', 52 | clearHistoryConfirm: '確定清除紀錄?', 53 | preview: '預覽', 54 | showRawText: '顯示原文', 55 | thinking: '思考中...', 56 | }, 57 | setting: { 58 | setting: '設定', 59 | general: '總覽', 60 | advanced: '進階', 61 | config: '設定', 62 | avatarLink: '頭貼連結', 63 | name: '名稱', 64 | description: '描述', 65 | role: '角色設定', 66 | temperature: 'Temperature', 67 | top_p: 'Top_p', 68 | resetUserInfo: '重設使用者資訊', 69 | chatHistory: '紀錄', 70 | theme: '主題', 71 | language: '語言', 72 | api: 'API', 73 | reverseProxy: '反向代理', 74 | timeout: '逾時', 75 | socks: 'Socks', 76 | httpsProxy: 'HTTPS Proxy', 77 | balance: 'API Credit 餘額', 78 | monthlyUsage: '本月使用量', 79 | openSource: '此專案在此開源:', 80 | freeMIT: '免費且基於 MIT 授權,沒有任何形式的付費行為!', 81 | stars: '如果你覺得此專案對你有幫助,請在 GitHub 上給我一顆星,或者贊助我,謝謝!', 82 | }, 83 | store: { 84 | siderButton: '提示詞商店', 85 | local: '本機', 86 | online: '線上', 87 | title: '標題', 88 | description: '描述', 89 | clearStoreConfirm: '是否清除資料?', 90 | importPlaceholder: '請將 JSON 資料貼在此處', 91 | addRepeatTitleTips: '標題重複,請重新輸入', 92 | addRepeatContentTips: '內容重複:{msg},請重新輸入', 93 | editRepeatTitleTips: '標題衝突,請重新修改', 94 | editRepeatContentTips: '內容衝突{msg} ,請重新修改', 95 | importError: '鍵值不符合', 96 | importRepeatTitle: '因標題重複跳過:{msg}', 97 | importRepeatContent: '因內容重複跳過:{msg}', 98 | onlineImportWarning: '注意:請檢查 JSON 檔案來源!', 99 | downloadError: '請檢查網路狀態與 JSON 檔案有效性', 100 | }, 101 | } 102 | -------------------------------------------------------------------------------- /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/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import setupAssets from './assets' 2 | import setupScrollbarStyle from './scrollbarStyle' 3 | 4 | export { setupAssets, setupScrollbarStyle } 5 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/store/helper.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | export const store = createPinia() 4 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { store } from './helper' 3 | 4 | export function setupStore(app: App) { 5 | app.use(store) 6 | } 7 | 8 | export * from './modules' 9 | -------------------------------------------------------------------------------- /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 = 'en-US' | 'es-ES' | 'ko-KR' | 'ru-RU' | 'vi-VN' | 'zh-CN' | 'zh-TW' 8 | 9 | const languageMap: { [key: string]: Language } = { 10 | 'en': 'en-US', 11 | 'en-US': 'en-US', 12 | 'es': 'es-ES', 13 | 'es-ES': 'es-ES', 14 | 'ko': 'ko-KR', 15 | 'ko-KR': 'ko-KR', 16 | 'ru': 'ru-RU', 17 | 'ru-RU': 'ru-RU', 18 | 'vi': 'vi-VN', 19 | 'vi-VN': 'vi-VN', 20 | 'zh': 'zh-CN', 21 | 'zh-CN': 'zh-CN', 22 | 'zh-TW': 'zh-TW', 23 | } 24 | 25 | export interface AppState { 26 | siderCollapsed: boolean 27 | theme: Theme 28 | language: Language 29 | } 30 | 31 | export function defaultSetting(): AppState { 32 | const language = languageMap[navigator.language] 33 | return { siderCollapsed: false, theme: 'light', language } 34 | } 35 | 36 | export function getLocalSetting(): AppState { 37 | const localSetting: AppState | undefined = ss.get(LOCAL_NAME) 38 | return { ...defaultSetting(), ...localSetting } 39 | } 40 | 41 | export function setLocalSetting(setting: AppState): void { 42 | ss.set(LOCAL_NAME, setting) 43 | } 44 | -------------------------------------------------------------------------------- /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/helper' 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/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 | -------------------------------------------------------------------------------- /src/store/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getToken, removeToken, setToken } from './helper' 3 | import { store } from '@/store/helper' 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 | -------------------------------------------------------------------------------- /src/store/modules/chat/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | import { t } from '@/locales' 3 | 4 | const LOCAL_NAME = 'chatStorage' 5 | 6 | export function defaultState(): Chat.ChatState { 7 | const uuid = 1002 8 | return { 9 | active: uuid, 10 | usingContext: true, 11 | history: [{ uuid, title: t('chat.newChatTitle'), isEdit: false }], 12 | chat: [{ uuid, data: [] }], 13 | } 14 | } 15 | 16 | export function getLocalState(): Chat.ChatState { 17 | const localState = ss.get(LOCAL_NAME) 18 | return { ...defaultState(), ...localState } 19 | } 20 | 21 | export function setLocalState(state: Chat.ChatState) { 22 | ss.set(LOCAL_NAME, state) 23 | } 24 | -------------------------------------------------------------------------------- /src/store/modules/chat/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { defaultState, getLocalState, setLocalState } from './helper' 3 | import { router } from '@/router' 4 | import { t } from '@/locales' 5 | 6 | export const useChatStore = defineStore('chat-store', { 7 | state: (): Chat.ChatState => getLocalState(), 8 | 9 | getters: { 10 | getChatHistoryByCurrentActive(state: Chat.ChatState) { 11 | const index = state.history.findIndex(item => item.uuid === state.active) 12 | if (index !== -1) 13 | return state.history[index] 14 | return null 15 | }, 16 | 17 | getChatByUuid(state: Chat.ChatState) { 18 | return (uuid?: number) => { 19 | if (uuid) 20 | return state.chat.find(item => item.uuid === uuid)?.data ?? [] 21 | return state.chat.find(item => item.uuid === state.active)?.data ?? [] 22 | } 23 | }, 24 | }, 25 | 26 | actions: { 27 | setUsingContext(context: boolean) { 28 | this.usingContext = context 29 | this.recordState() 30 | }, 31 | 32 | addHistory(history: Chat.History, chatData: Chat.Chat[] = []) { 33 | this.history.unshift(history) 34 | this.chat.unshift({ uuid: history.uuid, data: chatData }) 35 | this.active = history.uuid 36 | this.reloadRoute(history.uuid) 37 | }, 38 | 39 | updateHistory(uuid: number, edit: Partial) { 40 | const index = this.history.findIndex(item => item.uuid === uuid) 41 | if (index !== -1) { 42 | this.history[index] = { ...this.history[index], ...edit } 43 | this.recordState() 44 | } 45 | }, 46 | 47 | async deleteHistory(index: number) { 48 | this.history.splice(index, 1) 49 | this.chat.splice(index, 1) 50 | 51 | if (this.history.length === 0) { 52 | this.active = null 53 | this.reloadRoute() 54 | return 55 | } 56 | 57 | if (index > 0 && index <= this.history.length) { 58 | const uuid = this.history[index - 1].uuid 59 | this.active = uuid 60 | this.reloadRoute(uuid) 61 | return 62 | } 63 | 64 | if (index === 0) { 65 | if (this.history.length > 0) { 66 | const uuid = this.history[0].uuid 67 | this.active = uuid 68 | this.reloadRoute(uuid) 69 | } 70 | } 71 | 72 | if (index > this.history.length) { 73 | const uuid = this.history[this.history.length - 1].uuid 74 | this.active = uuid 75 | this.reloadRoute(uuid) 76 | } 77 | }, 78 | 79 | async setActive(uuid: number) { 80 | this.active = uuid 81 | return await this.reloadRoute(uuid) 82 | }, 83 | 84 | getChatByUuidAndIndex(uuid: number, index: number) { 85 | if (!uuid || uuid === 0) { 86 | if (this.chat.length) 87 | return this.chat[0].data[index] 88 | return null 89 | } 90 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 91 | if (chatIndex !== -1) 92 | return this.chat[chatIndex].data[index] 93 | return null 94 | }, 95 | 96 | addChatByUuid(uuid: number, chat: Chat.Chat) { 97 | if (!uuid || uuid === 0) { 98 | if (this.history.length === 0) { 99 | const uuid = Date.now() 100 | this.history.push({ uuid, title: chat.text, isEdit: false }) 101 | this.chat.push({ uuid, data: [chat] }) 102 | this.active = uuid 103 | this.recordState() 104 | } 105 | else { 106 | this.chat[0].data.push(chat) 107 | if (this.history[0].title === t('chat.newChatTitle')) 108 | this.history[0].title = chat.text 109 | this.recordState() 110 | } 111 | } 112 | 113 | const index = this.chat.findIndex(item => item.uuid === uuid) 114 | if (index !== -1) { 115 | this.chat[index].data.push(chat) 116 | if (this.history[index].title === t('chat.newChatTitle')) 117 | this.history[index].title = chat.text 118 | this.recordState() 119 | } 120 | }, 121 | 122 | updateChatByUuid(uuid: number, index: number, chat: Chat.Chat) { 123 | if (!uuid || uuid === 0) { 124 | if (this.chat.length) { 125 | this.chat[0].data[index] = chat 126 | this.recordState() 127 | } 128 | return 129 | } 130 | 131 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 132 | if (chatIndex !== -1) { 133 | this.chat[chatIndex].data[index] = chat 134 | this.recordState() 135 | } 136 | }, 137 | 138 | updateChatSomeByUuid(uuid: number, index: number, chat: Partial) { 139 | if (!uuid || uuid === 0) { 140 | if (this.chat.length) { 141 | this.chat[0].data[index] = { ...this.chat[0].data[index], ...chat } 142 | this.recordState() 143 | } 144 | return 145 | } 146 | 147 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 148 | if (chatIndex !== -1) { 149 | this.chat[chatIndex].data[index] = { ...this.chat[chatIndex].data[index], ...chat } 150 | this.recordState() 151 | } 152 | }, 153 | 154 | deleteChatByUuid(uuid: number, index: number) { 155 | if (!uuid || uuid === 0) { 156 | if (this.chat.length) { 157 | this.chat[0].data.splice(index, 1) 158 | this.recordState() 159 | } 160 | return 161 | } 162 | 163 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 164 | if (chatIndex !== -1) { 165 | this.chat[chatIndex].data.splice(index, 1) 166 | this.recordState() 167 | } 168 | }, 169 | 170 | clearChatByUuid(uuid: number) { 171 | if (!uuid || uuid === 0) { 172 | if (this.chat.length) { 173 | this.chat[0].data = [] 174 | this.recordState() 175 | } 176 | return 177 | } 178 | 179 | const index = this.chat.findIndex(item => item.uuid === uuid) 180 | if (index !== -1) { 181 | this.chat[index].data = [] 182 | this.recordState() 183 | } 184 | }, 185 | 186 | clearHistory() { 187 | this.$state = { ...defaultState() } 188 | this.recordState() 189 | }, 190 | 191 | async reloadRoute(uuid?: number) { 192 | this.recordState() 193 | await router.push({ name: 'Chat', params: { uuid } }) 194 | }, 195 | 196 | recordState() { 197 | setLocalState(this.$state) 198 | }, 199 | }, 200 | }) 201 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | temperature: number 8 | top_p: number 9 | } 10 | 11 | export function defaultSetting(): SettingsState { 12 | return { 13 | systemMessage: 'You are ChatGPT, a large language model trained by OpenAI. Follow the user\'s instructions carefully. Respond using markdown.', 14 | temperature: 0.8, 15 | top_p: 1, 16 | } 17 | } 18 | 19 | export function getLocalState(): SettingsState { 20 | const localSetting: SettingsState | undefined = ss.get(LOCAL_NAME) 21 | return { ...defaultSetting(), ...localSetting } 22 | } 23 | 24 | export function setLocalState(setting: SettingsState): void { 25 | ss.set(LOCAL_NAME, setting) 26 | } 27 | 28 | export function removeLocalState() { 29 | ss.remove(LOCAL_NAME) 30 | } 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | &::-webkit-scrollbar { 128 | height: 4px; 129 | } 130 | } 131 | 132 | .hljs { 133 | color: #383a42; 134 | background: #fafafa 135 | } 136 | 137 | .hljs-comment, 138 | .hljs-quote { 139 | color: #a0a1a7; 140 | font-style: italic 141 | } 142 | 143 | .hljs-doctag, 144 | .hljs-formula, 145 | .hljs-keyword { 146 | color: #a626a4 147 | } 148 | 149 | .hljs-deletion, 150 | .hljs-name, 151 | .hljs-section, 152 | .hljs-selector-tag, 153 | .hljs-subst { 154 | color: #e45649 155 | } 156 | 157 | .hljs-literal { 158 | color: #0184bb 159 | } 160 | 161 | .hljs-addition, 162 | .hljs-attribute, 163 | .hljs-meta .hljs-string, 164 | .hljs-regexp, 165 | .hljs-string { 166 | color: #50a14f 167 | } 168 | 169 | .hljs-attr, 170 | .hljs-number, 171 | .hljs-selector-attr, 172 | .hljs-selector-class, 173 | .hljs-selector-pseudo, 174 | .hljs-template-variable, 175 | .hljs-type, 176 | .hljs-variable { 177 | color: #986801 178 | } 179 | 180 | .hljs-bullet, 181 | .hljs-link, 182 | .hljs-meta, 183 | .hljs-selector-id, 184 | .hljs-symbol, 185 | .hljs-title { 186 | color: #4078f2 187 | } 188 | 189 | .hljs-built_in, 190 | .hljs-class .hljs-title, 191 | .hljs-title.class_ { 192 | color: #c18401 193 | } 194 | 195 | .hljs-emphasis { 196 | font-style: italic 197 | } 198 | 199 | .hljs-strong { 200 | font-weight: 700 201 | } 202 | 203 | .hljs-link { 204 | text-decoration: underline 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/styles/lib/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/utils/copy.ts: -------------------------------------------------------------------------------- 1 | export function copyToClip(text: string) { 2 | return new Promise((resolve, reject) => { 3 | try { 4 | const input: HTMLTextAreaElement = document.createElement('textarea') 5 | input.setAttribute('readonly', 'readonly') 6 | input.value = text 7 | document.body.appendChild(input) 8 | input.select() 9 | if (document.execCommand('copy')) 10 | document.execCommand('copy') 11 | document.body.removeChild(input) 12 | resolve(text) 13 | } 14 | catch (error) { 15 | reject(error) 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | interface StorageData { 2 | data: T 3 | expire: number | null 4 | } 5 | 6 | export function createLocalStorage(options?: { expire?: number | null }) { 7 | const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7 8 | 9 | const { expire } = Object.assign({ expire: DEFAULT_CACHE_TIME }, options) 10 | 11 | function set(key: string, data: T) { 12 | const storageData: StorageData = { 13 | data, 14 | expire: expire !== null ? new Date().getTime() + expire * 1000 : null, 15 | } 16 | 17 | const json = JSON.stringify(storageData) 18 | window.localStorage.setItem(key, json) 19 | } 20 | 21 | function get(key: string) { 22 | const json = window.localStorage.getItem(key) 23 | if (json) { 24 | let storageData: StorageData | null = null 25 | 26 | try { 27 | storageData = JSON.parse(json) 28 | } 29 | catch { 30 | // Prevent failure 31 | } 32 | 33 | if (storageData) { 34 | const { data, expire } = storageData 35 | if (expire === null || expire >= Date.now()) 36 | return data 37 | } 38 | 39 | remove(key) 40 | return null 41 | } 42 | } 43 | 44 | function remove(key: string) { 45 | window.localStorage.removeItem(key) 46 | } 47 | 48 | function clear() { 49 | window.localStorage.clear() 50 | } 51 | 52 | return { set, get, remove, clear } 53 | } 54 | 55 | export const ls = createLocalStorage() 56 | 57 | export const ss = createLocalStorage({ expire: null }) 58 | -------------------------------------------------------------------------------- /src/views/chat/components/Header/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 79 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/Avatar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/Text.vue: -------------------------------------------------------------------------------- 1 | 137 | 138 | 149 | 150 | 153 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/index.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 146 | -------------------------------------------------------------------------------- /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 | // Mermaid 61 | div[id^='mermaid-container'] { 62 | padding: 4px; 63 | border-radius: 4px; 64 | overflow-x: auto !important; 65 | background-color: #fff; 66 | border: 1px solid #e5e5e5; 67 | } 68 | 69 | &.markdown-body-generate>dd:last-child:after, 70 | &.markdown-body-generate>dl:last-child:after, 71 | &.markdown-body-generate>dt:last-child:after, 72 | &.markdown-body-generate>h1:last-child:after, 73 | &.markdown-body-generate>h2:last-child:after, 74 | &.markdown-body-generate>h3:last-child:after, 75 | &.markdown-body-generate>h4:last-child:after, 76 | &.markdown-body-generate>h5:last-child:after, 77 | &.markdown-body-generate>h6:last-child:after, 78 | &.markdown-body-generate>li:last-child:after, 79 | &.markdown-body-generate>ol:last-child li:last-child:after, 80 | &.markdown-body-generate>p:last-child:after, 81 | &.markdown-body-generate>pre:last-child code:after, 82 | &.markdown-body-generate>td:last-child:after, 83 | &.markdown-body-generate>ul:last-child li:last-child:after { 84 | animation: blink 1s steps(5, start) infinite; 85 | color: #000; 86 | content: '_'; 87 | font-weight: 700; 88 | margin-left: 3px; 89 | vertical-align: baseline; 90 | } 91 | 92 | @keyframes blink { 93 | to { 94 | visibility: hidden; 95 | } 96 | } 97 | } 98 | 99 | html.dark { 100 | 101 | .markdown-body { 102 | 103 | &.markdown-body-generate>dd:last-child:after, 104 | &.markdown-body-generate>dl:last-child:after, 105 | &.markdown-body-generate>dt:last-child:after, 106 | &.markdown-body-generate>h1:last-child:after, 107 | &.markdown-body-generate>h2:last-child:after, 108 | &.markdown-body-generate>h3:last-child:after, 109 | &.markdown-body-generate>h4:last-child:after, 110 | &.markdown-body-generate>h5:last-child:after, 111 | &.markdown-body-generate>h6:last-child:after, 112 | &.markdown-body-generate>li:last-child:after, 113 | &.markdown-body-generate>ol:last-child li:last-child:after, 114 | &.markdown-body-generate>p:last-child:after, 115 | &.markdown-body-generate>pre:last-child code:after, 116 | &.markdown-body-generate>td:last-child:after, 117 | &.markdown-body-generate>ul:last-child li:last-child:after { 118 | color: #65a665; 119 | } 120 | } 121 | 122 | .message-reply { 123 | .whitespace-pre-wrap { 124 | white-space: pre-wrap; 125 | color: var(--n-text-color); 126 | } 127 | } 128 | 129 | .highlight pre, 130 | pre { 131 | background-color: #282c34; 132 | } 133 | } 134 | 135 | @media screen and (max-width: 533px) { 136 | .markdown-body .code-block-wrapper { 137 | padding: unset; 138 | 139 | code { 140 | padding: 24px 16px 16px 16px; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/views/chat/components/index.ts: -------------------------------------------------------------------------------- 1 | import Message from './Message/index.vue' 2 | 3 | export { Message } 4 | -------------------------------------------------------------------------------- /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/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 // Threshold, indicating the distance threshold to the bottom of the scroll bar. 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 | -------------------------------------------------------------------------------- /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/views/chat/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 52 | -------------------------------------------------------------------------------- /src/views/chat/layout/Permission.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 81 | -------------------------------------------------------------------------------- /src/views/chat/layout/index.ts: -------------------------------------------------------------------------------- 1 | import ChatLayout from './Layout.vue' 2 | 3 | export { ChatLayout } 4 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/Footer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/List.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 106 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/index.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 118 | -------------------------------------------------------------------------------- /src/views/exception/404/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /src/views/exception/500/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------