Repository: Chanzhaoyu/chatgpt-web Branch: main Commit: 7474b2c14971 Files: 136 Total size: 288.7 KB Directory structure: gitextract_4kuwrg29/ ├── .commitlintrc.json ├── .devcontainer/ │ └── devcontainer.json ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── build_docker.yml │ ├── ci.yml │ └── issues_close.yml ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── .npmrc ├── .vscode/ │ ├── extensions.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 ├── index.html ├── kubernetes/ │ ├── README.md │ ├── deploy.yaml │ └── ingress.yaml ├── license ├── package.json ├── postcss.config.js ├── service/ │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmrc │ ├── .vscode/ │ │ ├── extensions.json │ │ └── settings.json │ ├── package.json │ ├── 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/ │ │ └── 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 │ │ └── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .commitlintrc.json ================================================ { "extends": ["@commitlint/config-conventional"] } ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node { "name": "Node.js & TypeScript", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye" // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "yarn install", // Configure tool-specific properties. // "customizations": {}, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" } ================================================ FILE: .dockerignore ================================================ **/node_modules */node_modules node_modules Dockerfile .* */.* !.env ================================================ FILE: .editorconfig ================================================ # Editor configuration, see http://editorconfig.org root = true [*] charset = utf-8 indent_style = tab indent_size = 2 end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: .eslintignore ================================================ docker-compose kubernetes ================================================ FILE: .eslintrc.cjs ================================================ module.exports = { root: true, extends: ['@antfu'], } ================================================ FILE: .gitattributes ================================================ "*.vue" eol=lf "*.js" eol=lf "*.ts" eol=lf "*.jsx" eol=lf "*.tsx" eol=lf "*.cjs" eol=lf "*.cts" eol=lf "*.mjs" eol=lf "*.mts" eol=lf "*.json" eol=lf "*.html" eol=lf "*.css" eol=lf "*.less" eol=lf "*.scss" eol=lf "*.sass" eol=lf "*.styl" eol=lf "*.md" eol=lf ================================================ FILE: .github/workflows/build_docker.yml ================================================ name: build_docker on: push: branches: [main] release: types: [created] # 表示在创建新的 Release 时触发 jobs: build_docker: name: Build docker runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - run: | echo "本次构建的版本为:${GITHUB_REF_NAME} (但是这个变量目前上下文中无法获取到)" echo 本次构建的版本为:${{ github.ref_name }} env - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build uses: docker/build-push-action@v4 with: context: . push: true labels: ${{ steps.meta.outputs.labels }} platforms: linux/amd64,linux/arm64 tags: | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:${{ github.ref_name }} ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:latest ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main pull_request: branches: - main jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set node uses: actions/setup-node@v3 with: node-version: 18.x - name: Setup run: npm i -g @antfu/ni - name: Install run: nci - name: Lint run: nr lint:fix typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set node uses: actions/setup-node@v3 with: node-version: 18.x - name: Setup run: npm i -g @antfu/ni - name: Install run: nci - name: Typecheck run: nr type-check ================================================ FILE: .github/workflows/issues_close.yml ================================================ name: Close inactive issues on: schedule: - cron: '30 1 * * *' jobs: close-issues: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v5 with: days-before-issue-stale: 10 days-before-issue-close: 2 stale-issue-label: stale stale-issue-message: This issue is stale because it has been open for 10 days with no activity. close-issue-message: This issue was closed because it has been inactive for 2 days since being marked as stale. days-before-pr-stale: -1 days-before-pr-close: -1 repo-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules .DS_Store dist dist-ssr coverage *.local /cypress/videos/ /cypress/screenshots/ # Editor directories and files .vscode/* !.vscode/settings.json !.vscode/extensions.json .idea *.suo *.ntvs* *.njsproj *.sln *.sw? # Environment variables files /service/.env ================================================ FILE: .husky/commit-msg ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx --no -- commitlint --edit ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx lint-staged ================================================ FILE: .npmrc ================================================ strict-peer-dependencies=false ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"] } ================================================ FILE: .vscode/settings.json ================================================ { "prettier.enable": false, "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "html", "json", "jsonc", "json5", "yaml", "yml", "markdown" ], "cSpell.words": [ "antfu", "axios", "bumpp", "chatgpt", "chenzhaoyu", "commitlint", "davinci", "dockerhub", "esno", "GPTAPI", "highlightjs", "hljs", "iconify", "katex", "katexmath", "linkify", "logprobs", "mdhljs", "mila", "nodata", "OPENAI", "pinia", "Popconfirm", "rushstack", "Sider", "tailwindcss", "traptitech", "tsup", "Typecheck", "unplugin", "VITE", "vueuse", "Zhao" ], "i18n-ally.enabledParsers": [ "ts" ], "i18n-ally.sortKeys": true, "i18n-ally.keepFulfilled": true, "i18n-ally.localesPaths": [ "src/locales" ], "i18n-ally.keystyle": "nested" } ================================================ FILE: CHANGELOG.md ================================================ ## v2.11.1 `2023-10-11` ## Enhancement - 优化打字机光标效果 - 清空聊天历史按钮 - 更新文档 ## BugFix - 修复移动端上的问题 - 修复不规范的引入导致的问题 ## v2.11.0 `2023-04-26` > [chatgpt-web-plus](https://github.com/Chanzhaoyu/chatgpt-web-plus) 新界面、完整用户管理 ## Enhancement - 更新默认 `accessToken` 反代地址为 [[pengzhile](https://github.com/pengzhile)] 的 `https://ai.fakeopen.com/api/conversation` [[24min](https://github.com/Chanzhaoyu/chatgpt-web/pull/1567/files)] - 添加自定义 `temperature` 和 `top_p` [[quzard](https://github.com/Chanzhaoyu/chatgpt-web/pull/1260)] - 优化代码 [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/1328)] - 优化复制代码反馈效果 ## BugFix - 修复余额查询和文案 [[luckywangxi](https://github.com/Chanzhaoyu/chatgpt-web/pull/1174)][[zuoning777](https://github.com/Chanzhaoyu/chatgpt-web/pull/1296)] - 修复默认语言错误 [[idawnwon](https://github.com/Chanzhaoyu/chatgpt-web/pull/1352)] - 修复 `onRegenerate` 下问题 [[leafsummer](https://github.com/Chanzhaoyu/chatgpt-web/pull/1188)] ## Other - 引导用户触发提示词 [[RyanXinOne](https://github.com/Chanzhaoyu/chatgpt-web/pull/1183)] - 添加韩语翻译 [[Kamilake](https://github.com/Chanzhaoyu/chatgpt-web/pull/1372)] - 添加俄语翻译 [[aquaratixc](https://github.com/Chanzhaoyu/chatgpt-web/pull/1571)] - 优化翻译和文本检查 [[PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/1460)] - 移除无用文件 ## v2.10.9 `2023-04-03` > 更新默认 `accessToken` 反代地址为 [[pengzhile](https://github.com/pengzhile)] 的 `https://ai.fakeopen.com/api/conversation` ## Enhancement - 添加 `socks5` 代理认证 [[yimiaoxiehou](https://github.com/Chanzhaoyu/chatgpt-web/pull/999)] - 添加 `socks` 代理用户名密码的配置 [[hank-cp](https://github.com/Chanzhaoyu/chatgpt-web/pull/890)] - 添加可选日志打印 [[zcong1993](https://github.com/Chanzhaoyu/chatgpt-web/pull/1041)] - 更新侧边栏按钮本地化[[simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/911)] - 优化代码块滚动条高度 [[Fog3211](https://github.com/Chanzhaoyu/chatgpt-web/pull/1153)] ## BugFix - 修复 `PWA` 问题 [[bingo235](https://github.com/Chanzhaoyu/chatgpt-web/pull/807)] - 修复 `ESM` 错误 [[kidonng](https://github.com/Chanzhaoyu/chatgpt-web/pull/826)] - 修复反向代理开启时限流失效的问题 [[gitgitgogogo](https://github.com/Chanzhaoyu/chatgpt-web/pull/863)] - 修复 `docker` 构建时 `.env` 可能被忽略的问题 [[zaiMoe](https://github.com/Chanzhaoyu/chatgpt-web/pull/877)] - 修复导出异常错误 [[KingTwinkle](https://github.com/Chanzhaoyu/chatgpt-web/pull/938)] - 修复空值异常 [[vchenpeng](https://github.com/Chanzhaoyu/chatgpt-web/pull/1103)] - 移动端上的体验问题 ## Other - `Docker` 容器名字名义 [[LOVECHEN](https://github.com/Chanzhaoyu/chatgpt-web/pull/1035)] - `kubernetes` 部署配置 [[CaoYunzhou](https://github.com/Chanzhaoyu/chatgpt-web/pull/1001)] - 感谢 [[assassinliujie](https://github.com/Chanzhaoyu/chatgpt-web/pull/962)] 和 [[puppywang](https://github.com/Chanzhaoyu/chatgpt-web/pull/1017)] 的某些贡献 - 更新 `kubernetes/deploy.yaml` [[idawnwon](https://github.com/Chanzhaoyu/chatgpt-web/pull/1085)] - 文档更新 [[#yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/883)] - 文档更新 [[weifeng12x](https://github.com/Chanzhaoyu/chatgpt-web/pull/880)] - 依赖更新 ## v2.10.8 `2023-03-23` 如遇问题,请删除 `node_modules` 重新安装依赖。 ## Feature - 显示回复消息原文的选项 [[yilozt](https://github.com/Chanzhaoyu/chatgpt-web/pull/672)] - 添加单 `IP` 每小时请求限制。环境变量: `MAX_REQUEST_PER_HOUR` [[zhuxindong ](https://github.com/Chanzhaoyu/chatgpt-web/pull/718)] - 前端添加角色设定,仅 `API` 方式可见 [[quzard](https://github.com/Chanzhaoyu/chatgpt-web/pull/768)] - `OPENAI_API_MODEL` 变量现在对 `ChatGPTUnofficialProxyAPI` 也生效,注意:`Token` 和 `API` 的模型命名不一致,不能直接填入 `gpt-3.5` 或者 `gpt-4` [[hncboy](https://github.com/Chanzhaoyu/chatgpt-web/pull/632)] - 添加繁体中文 `Prompts` [[PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/796)] ## Enhancement - 重置回答时滚动定位至该回答 [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/781)] - 当 `API` 是 `gpt-4` 时增加可用的 `Max Tokens` [[simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/729)] - 判断和忽略回复字符 [[liut](https://github.com/Chanzhaoyu/chatgpt-web/pull/474)] - 切换会话时,自动聚焦输入框 [[JS-an](https://github.com/Chanzhaoyu/chatgpt-web/pull/735)] - 渲染的链接新窗口打开 - 查询余额可选 `API_BASE_URL` 代理地址 - `config` 接口添加验证防止被无限制调用 - `PWA` 默认不开启,现在需手动修改 `.env` 文件 `VITE_GLOB_APP_PWA` 变量 - 当网络连接时,刷新页面,`500` 错误页自动跳转到主页 ## BugFix - `scrollToBottom` 调回 `scrollToBottomIfAtBottom` [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/771)] - 重置异常的 `loading` 会话 ## Common - 创建 `start.cmd` 在 `windows` 下也可以运行 [vulgatecnn](https://github.com/Chanzhaoyu/chatgpt-web/pull/656)] - 添加 `visual-studio-code` 中调试配置 [[ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/296)] - 修复文档中 `docker` 端口为本地 [[kilvn](https://github.com/Chanzhaoyu/chatgpt-web/pull/802)] ## Other - 依赖更新 ## v2.10.7 `2023-03-17` ## BugFix - 回退 `chatgpt` 版本,原因:导致 `OPENAI_API_BASE_URL` 代理失效 - 修复缺省状态的 `usingContext` 默认值 ## v2.10.6 `2023-03-17` ## Feature - 显示 `API` 余额 [[pzcn](https://github.com/Chanzhaoyu/chatgpt-web/pull/582)] ## Enhancement - 美化滚动条样式和 `UI` 保持一致 [[haydenull](https://github.com/Chanzhaoyu/chatgpt-web/pull/617)] - 优化移动端 `Prompt` 样式 [[CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/608)] - 上下文开关改为全局开关,现在记录在本地缓存中 - 配置信息按接口类型显示 ## Perf - 优化函数方法 [[kirklin](https://github.com/Chanzhaoyu/chatgpt-web/pull/583)] - 字符错误 [[pdsuwwz](https://github.com/Chanzhaoyu/chatgpt-web/pull/585)] - 文档描述错误 [[lizhongyuan3](https://github.com/Chanzhaoyu/chatgpt-web/pull/636)] ## BugFix - 修复 `Prompt` 导入、导出兼容性错误 - 修复 `highlight.js` 控制台兼容性警告 ## Other - 依赖更新 ## v2.10.5 `2023-03-13` 更新依赖,`access_token` 默认代理为 [pengzhile](https://github.com/pengzhile) 的 `https://bypass.duti.tech/api/conversation` ## Feature - `Prompt` 商店在线导入可以导入两种 `recommend.json`里提到的模板 [simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/521) - 支持 `HTTPS_PROXY` [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/308) - `Prompt` 添加查询筛选 ## Enhancement - 调整输入框最大行数 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/502) - 优化 `docker` 打包 [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/520) - `Prompt` 添加翻译和优化布局 - 「繁体中文」补全和审阅 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/542) - 语言选择调整为下路框形式 - 权限输入框类型调整为密码形式 ## BugFix - `JSON` 导入检查 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/523) - 修复 `AUTH_SECRET_KEY` 模式下跨域异常并添加对 `node.js 19` 版本的支持 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/499) - 确定清空上下文时不应该重置会话标题 ## Other - 调整文档 - 更新依赖 ## v2.10.4 `2023-03-11` ## Feature - 感谢 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/268) 添加 `Prompt` 模板和 `Prompt` 商店支持 ## Enhancement - 设置添加关闭按钮[#495] ## Demo ![Prompt](https://camo.githubusercontent.com/6a51af751eb29238cb7ef4f8fbd89f63db837562f97f33273095424e62dc9194/68747470733a2f2f73312e6c6f63696d672e636f6d2f323032332f30332f30342f333036326665633163613562632e676966) ## v2.10.3 `2023-03-10` > 声明:除 `ChatGPTUnofficialProxyAPI` 使用的非官方代理外,本项目代码包括上游引用包均开源在 `GitHub`,如果你觉得本项目有监控后门或有问题导致你的账号、API被封,那我很抱歉。我可能`BUG`写的多,但我不缺德。此次主要为前端界面调整,周末愉快。 ## Feature - 支持长回复 [[yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)][[详情](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)] - 支持 `PWA` [[chenxch](https://github.com/Chanzhaoyu/chatgpt-web/pull/452)] ## Enhancement - 调整移动端按钮和优化布局 - 调整 `iOS` 上安全距离 - 简化 `docker-compose` 部署 [[cloudGrin](https://github.com/Chanzhaoyu/chatgpt-web/pull/466)] ## BugFix - 修复清空会话侧边栏标题不会重置的问题 [[RyanXinOne](https://github.com/Chanzhaoyu/chatgpt-web/pull/453)] - 修复设置文字过长时导致的设置按钮消失的问题 ## Other - 更新依赖 ## v2.10.2 `2023-03-09` 衔接 `2.10.1` 版本[详情](https://github.com/Chanzhaoyu/chatgpt-web/releases/tag/v2.10.1) ## Enhancement - 移动端下输入框获得焦点时左侧按钮隐藏 ## BugFix - 修复 `2.10.1` 中添加 `OPENAI_API_MODEL` 变量的判断错误,会导致默认模型指定失效,抱歉 - 回退 `2.10.1` 中前端变量影响 `Docker` 打包 ## v2.10.1 `2023-03-09` 注意:删除了 `.env` 文件改用 `.env.example` 代替,如果是手动部署的同学现在需要手动创建 `.env` 文件并从 `.env.example` 中复制需要的变量,并且 `.env` 文件现在会在 `Git` 提交中被忽略,原因如下: - 在项目中添加 `.env` 从一开始就是个错误的示范 - 如果是 `Fork` 项目进行修改测试总是会被 `Git` 修改提示给打扰 - 感谢 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/395) 的提醒和修改 这两天开始,官方已经开始对第三方代理进行了拉闸, `accessToken` 即将或已经开始可能会不可使用。异常 `API` 使用也开始封号,封号缘由不明,如果出现使用 `API` 提示错误,请查看后端控制台信息,或留意邮箱。 ## Feature - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/393) 添加是否发送上下文开关功能 ## Enhancement - 感谢 [nagaame](https://github.com/Chanzhaoyu/chatgpt-web/pull/415) 优化`docker`打包镜像文件过大的问题 - 感谢 [xieccc](https://github.com/Chanzhaoyu/chatgpt-web/pull/404) 新增 `API` 模型配置变量 `OPENAI_API_MODEL` - 感谢 [acongee](https://github.com/Chanzhaoyu/chatgpt-web/pull/394) 优化输出时滚动条问题 ## BugFix - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/392) 修复导出图片会丢失头像的问题 - 修复深色模式导出图片的样式问题 ## v2.10.0 `2023-03-07` - 老规矩,手动部署的同学需要删除 `node_modules` 安装包重新安装降低出错概率,其他部署不受影响,但是可能会有缓存问题。 - 虽然说了更新放缓,但是 `issues` 不看, `PR` 不改我睡不着,我的邮箱从每天早上`8`点到凌晨`12`永远在滴滴滴,所以求求各位,超时的`issues`自己关闭下哈,我真的需要缓冲一下。 - 演示图片请看最后 ## Feature - 添加权限功能,用法:`service/.env` 中的 `AUTH_SECRET_KEY` 变量添加密码 - 感谢 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/348) 添加「繁体中文」翻译 - 感谢 [GermMC](https://github.com/Chanzhaoyu/chatgpt-web/pull/369) 添加聊天记录导入、导出、清空的功能 - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/374) 添加会话保存为本地图片的功能 ## Enhancement - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/363) 添加 `ctrl+enter` 发送消息 - 现在新消息只有在结束了之后才滚动到底部,而不是之前的强制性 - 优化部分代码 ## BugFix - 转义状态码前端显示,防止直接暴露 `key`(我可能需要更多的状态码补充) ## Other - 更新依赖到最新 ## 演示 > 不是界面最新效果,有美化改动 权限 ![权限](https://user-images.githubusercontent.com/24789441/223438518-80d58d42-e344-4e39-b87c-251ff73925ed.png) 聊天记录导出 ![聊天记录导出](https://user-images.githubusercontent.com/57023771/223372153-6d8e9ec1-d82c-42af-b4bd-232e50504a25.gif) 保存图片到本地 ![保存图片到本地](https://user-images.githubusercontent.com/13901424/223423555-b69b95ef-8bcf-4951-a7c9-98aff2677e18.gif) ## v2.9.3 `2023-03-06` ## Enhancement - 感谢 [ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/305) 使用 `markdown-it` 替换 `marked`,解决代码块闪烁的问题 - 感谢 [shansing](https://github.com/Chanzhaoyu/chatgpt-web/pull/277) 改善文档 - 感谢 [nalf3in](https://github.com/Chanzhaoyu/chatgpt-web/pull/293) 添加英文翻译 ## BugFix - 感谢[sepcnt ](https://github.com/Chanzhaoyu/chatgpt-web/pull/279) 修复切换记录时编辑状态未关闭的问题 - 修复复制代码的兼容性报错问题 - 修复部分优化小问题 ## v2.9.2 `2023-03-04` 手动部署的同学,务必删除根目录和`service`中的`node_modules`重新安装依赖,降低出现问题的概率,自动部署的不需要做改动。 ### Feature - 感谢 [hyln9](https://github.com/Chanzhaoyu/chatgpt-web/pull/247) 添加对渲染 `LaTex` 数学公式的支持 - 感谢 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/227) 添加支持 `webAPP` (苹果添加到主页书签访问)支持 - 添加 `OPENAI_API_BASE_URL` 可选环境变量[#249] ## Enhancement - 优化在高分屏上主题内容的最大宽度[#257] - 现在文字按单词截断[#215][#225] ### BugFix - 修复动态生成时代码块不能被复制的问题[#251][#260] - 修复 `iOS` 移动端输入框不会被键盘顶起的问题[#256] - 修复控制台渲染警告 ## Other - 更新依赖至最新 - 修改 `README` 内容 ## v2.9.1 `2023-03-02` ### Feature - 代码块添加当前代码语言显示和复制功能[#197][#196] - 完善多语言,现在可以切换中英文显示 ## Enhancement - 由[Zo3i](https://github.com/Chanzhaoyu/chatgpt-web/pull/187) 完善 `docker-compose` 部署文档 ### BugFix - 由 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/200) 修复头像修改不同步的问题 ## Other - 更新依赖至最新 - 修改 `README` 内容 ## v2.9.0 `2023-03-02` ### Feature - 现在能复制带格式的消息文本 - 新设计的设定页面,可以自定义姓名、描述、头像(链接方式) - 新增`403`和`404`页面以便扩展 ## Enhancement - 更新 `chatgpt` 使 `ChatGPTAPI` 支持 `gpt-3.5-turbo-0301`(默认) - 取消了前端超时限制设定 ## v2.8.3 `2023-03-01` ### Feature - 消息已输出内容不会因为中断而消失[#167] - 添加复制消息按钮[#133] ### Other - `README` 添加声明内容 ## v2.8.2 `2023-02-28` ### Enhancement - 代码主题调整为 `One Dark - light|dark` 适配深色模式 ### BugFix - 修复普通文本代码渲染和深色模式下的问题[#139][#154] ## v2.8.1 `2023-02-27` ### BugFix - 修复 `API` 版本不是 `Markdown` 时,普通 `HTML` 代码会被渲染的问题 [#146] ## v2.8.0 `2023-02-27` - 感谢 [puppywang](https://github.com/Chanzhaoyu/chatgpt-web/commit/628187f5c3348bda0d0518f90699a86525d19018) 修复了 `2.7.0` 版本中关于流输出数据的问题(使用 `nginx` 需要自行配置 `octet-stream` 相关内容) - 关于为什么使用 `octet-stream` 而不是 `sse`,是因为更好的兼容之前的模式。 - 建议更新到此版本获得比较完整的体验 ### Enhancement - 优化了部份代码和类型提示 - 输入框添加换行提示 - 移动端输入框现在回车为换行,而不是直接提交 - 移动端双击标题返回顶部,箭头返回底部 ### BugFix - 流输出数据下的问题[#122] - 修复了 `API Key` 下部份代码不换行的问题 - 修复移动端深色模式部份样式问题[#123][#126] - 修复主题模式图标不一致的问题[#126] ## v2.7.3 `2023-02-25` ### Feature - 适配系统深色模式 [#118](https://github.com/Chanzhaoyu/chatgpt-web/issues/103) ### BugFix - 修复用户消息能被渲染为 `HTML` 问题 [#117](https://github.com/Chanzhaoyu/chatgpt-web/issues/117) ## v2.7.2 `2023-02-24` ### Enhancement - 消息使用 [github-markdown-css](https://www.npmjs.com/package/github-markdown-css) 进行美化,现在支持全语法 - 移除测试无用函数 ## v2.7.1 `2023-02-23` 因为消息流在 `accessToken` 中存在解析失败和消息不完整等一系列的问题,调整回正常消息形式 ### Feature - 现在可以中断请求过长没有答复的消息 - 现在可以删除单条消息 - 设置中显示当前版本信息 ### BugFix - 回退 `2.7.0` 的消息不稳定的问题 ## v2.7.0 `2023-02-23` ### Feature - 使用消息流返回信息,反应更迅速 ### Enhancement - 样式的一点小改动 ## v2.6.2 `2023-02-22` ### BugFix - 还原修改代理导致的异常问题 ## v2.6.1 `2023-02-22` ### Feature - 新增 `Railway` 部署模版 ### BugFix - 手动打包 `Proxy` 问题 ## v2.6.0 `2023-02-21` ### Feature - 新增对 `网页 accessToken` 调用 `ChatGPT`,更智能不过不太稳定 [#51](https://github.com/Chanzhaoyu/chatgpt-web/issues/51) - 前端页面设置按钮显示查看当前后端服务配置 ### Enhancement - 新增 `TIMEOUT_MS` 环境变量设定后端超时时常(单位:毫秒)[#62](https://github.com/Chanzhaoyu/chatgpt-web/issues/62) ## v2.5.2 `2023-02-21` ### Feature - 增加对 `markdown` 格式的支持 [Demo](https://github.com/Chanzhaoyu/chatgpt-web/pull/77) ### BugFix - 重载会话时滚动条保持 ## v2.5.1 `2023-02-21` ### Enhancement - 调整路由模式为 `hash` - 调整新增会话添加到 - 调整移动端样式 ## v2.5.0 `2023-02-20` ### Feature - 会话 `loading` 现在显示为光标动画 - 会话现在可以再次生成回复 - 会话异常可以再次进行请求 - 所有删除选项添加确认操作 ### Enhancement - 调整 `chat` 为路由页面而不是组件形式 - 更新依赖至最新 - 调整移动端体验 ### BugFix - 修复移动端左侧菜单显示不完整的问题 ## v2.4.1 `2023-02-18` ### Enhancement - 调整部份移动端上的样式 - 输入框支持换行 ## v2.4.0 `2023-02-17` ### Feature - 响应式支持移动端 ### Enhancement - 修改部份描述错误 ## v2.3.3 `2023-02-16` ### Feature - 添加 `README` 部份说明和贡献列表 - 添加 `docker` 镜像 - 添加 `GitHub Action` 自动化构建 ### BugFix - 回退依赖更新导致的 [Eslint 报错](https://github.com/eslint/eslint/issues/16896) ## v2.3.2 `2023-02-16` ### Enhancement - 更新依赖至最新 - 优化部份内容 ## v2.3.1 `2023-02-15` ### BugFix - 修复多会话状态下一些意想不到的问题 ## v2.3.0 `2023-02-15` ### Feature - 代码类型信息高亮显示 - 支持 `node ^16` 版本 - 移动端响应式初步支持 - `vite` 中 `proxy` 代理 ### Enhancement - 调整超时处理范围 ### BugFix - 修复取消请求错误提示会添加到信息中 - 修复部份情况下提交请求不可用 - 修复侧边栏宽度变化闪烁的问题 ## v2.2.0 `2023-02-14` ### Feature - 会话和上下文本地储存 - 侧边栏本地储存 ## v2.1.0 `2023-02-14` ### Enhancement - 更新依赖至最新 - 联想功能移动至前端提交,后端只做转发 ### BugFix - 修复部份项目检测有关 `Bug` - 修复清除上下文按钮失效 ## v2.0.0 `2023-02-13` ### Refactor 重构并优化大部分内容 ## v1.0.5 `2023-02-12` ### Enhancement - 输入框焦点,连续提交 ### BugFix - 修复信息框样式问题 - 修复中文输入法提交问题 ## v1.0.4 `2023-02-11` ### Feature - 支持上下文联想 ## v1.0.3 `2023-02-11` ### Enhancement - 拆分 `service` 文件以便扩展 - 调整 `Eslint` 相关验证 ### BugFix - 修复部份控制台报错 ## v1.0.2 `2023-02-10` ### BugFix - 修复新增信息容器不会自动滚动到问题 - 修复文本过长不换行到问题 [#1](https://github.com/Chanzhaoyu/chatgpt-web/issues/1) ================================================ FILE: CONTRIBUTING.en.md ================================================ # Contribution Guide 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. ## Semantic Versioning 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. Each major change will be recorded in the `changelog`. ## Submitting Pull Request 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. 2. Install the `pnpm` tool using `npm install pnpm -g`. 3. Install the `Eslint` plugin for `VSCode`, or enable `eslint` functionality for other editors such as `WebStorm`. 4. Execute `pnpm bootstrap` in the root directory. 5. Execute `pnpm install` in the `/service/` directory. 6. Make changes to the codebase. If applicable, ensure that appropriate testing has been done. 7. Execute `pnpm lint:fix` in the root directory to perform a code formatting check. 8. Execute `pnpm type-check` in the root directory to perform a type check. 9. Submit a git commit, following the [Commit Guidelines](#commit-guidelines). 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). ## Commit Guidelines Commit messages should follow the [conventional-changelog standard](https://www.conventionalcommits.org/en/v1.0.0/): ```bash [optional scope]: [optional body] [optional footer] ``` ### Commit Types The following is a list of commit types: - feat: New feature or functionality - fix: Bug fix - docs: Documentation update - style: Code style or component style update - refactor: Code refactoring, no new features or bug fixes introduced - perf: Performance optimization - test: Unit test - chore: Other commits that do not modify src or test files ## License [MIT](./license) ================================================ FILE: CONTRIBUTING.md ================================================ # 贡献指南 感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。 ## 语义化版本 该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的变更发布主版本号。 每个重大更改都将记录在 `changelog` 中。 ## 提交 Pull Request 1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。 2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。 3. `vscode` 安装了 `Eslint` 插件,其它编辑器如 `webStorm` 打开了 `eslint` 功能。 4. 根目录下执行 `pnpm bootstrap`。 5. `/service/` 目录下执行 `pnpm install`。 6. 对代码库进行更改。如果适用的话,请确保进行了相应的测试。 7. 请在根目录下执行 `pnpm lint:fix` 进行代码格式检查。 8. 请在根目录下执行 `pnpm type-check` 进行类型检查。 9. 提交 git commit, 请同时遵守 [Commit 规范](#commit-指南) 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)。 ## Commit 指南 Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/): ```bash <类型>[可选 范围]: <描述> [可选 正文] [可选 脚注] ``` ### Commit 类型 以下是 commit 类型列表: - feat: 新特性或功能 - fix: 缺陷修复 - docs: 文档更新 - style: 代码风格或者组件样式更新 - refactor: 代码重构,不引入新功能和缺陷修复 - perf: 性能优化 - test: 单元测试 - chore: 其他不修改 src 或测试文件的提交 ## License [MIT](./license) ================================================ FILE: Dockerfile ================================================ # build front-end FROM node:lts-alpine AS frontend RUN npm install pnpm -g WORKDIR /app COPY ./package.json /app COPY ./pnpm-lock.yaml /app RUN pnpm install COPY . /app RUN pnpm run build # build backend FROM node:lts-alpine as backend RUN npm install pnpm -g WORKDIR /app COPY /service/package.json /app COPY /service/pnpm-lock.yaml /app RUN pnpm install COPY /service /app RUN pnpm build # service FROM node:lts-alpine RUN npm install pnpm -g WORKDIR /app COPY /service/package.json /app COPY /service/pnpm-lock.yaml /app RUN pnpm install --production && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/* COPY /service /app COPY --from=frontend /app/dist /app/public COPY --from=backend /app/build /app/build EXPOSE 3002 CMD ["pnpm", "run", "prod"] ================================================ FILE: README.md ================================================ # ChatGPT Web > 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. [中文](README.zh.md) ![cover](./docs/c1.png) ![cover2](./docs/c2.png) - [ChatGPT Web](#chatgpt-web) - [Introduction](#introduction) - [Roadmap](#roadmap) - [Prerequisites](#prerequisites) - [Node](#node) - [PNPM](#pnpm) - [Filling in the Key](#filling-in-the-key) - [Install Dependencies](#install-dependencies) - [Backend](#backend) - [Frontend](#frontend) - [Run in Test Environment](#run-in-test-environment) - [Backend Service](#backend-service) - [Frontend Webpage](#frontend-webpage) - [Environment Variables](#environment-variables) - [Packaging](#packaging) - [Use Docker](#use-docker) - [Docker Parameter Examples](#docker-parameter-examples) - [Docker build \& Run](#docker-build--run) - [Docker compose](#docker-compose) - [Prevent Crawlers](#prevent-crawlers) - [Deploy with Railway](#deploy-with-railway) - [Railway Environment Variables](#railway-environment-variables) - [Deploy with Sealos](#deploy-with-sealos) - [Package Manually](#package-manually) - [Backend Service](#backend-service-1) - [Frontend Webpage](#frontend-webpage-1) - [FAQ](#faq) - [Contributing](#contributing) - [Acknowledgements](#acknowledgements) - [Sponsors](#sponsors) - [License](#license) ## Introduction Supports dual models and provides two unofficial `ChatGPT API` methods | Method | Free? | Reliability | Quality | | ---------------------------------- | ----- | ----------- | ------- | | `ChatGPTAPI(gpt-3.5-turbo-0301)` | No | Reliable | Relatively stupid | | `ChatGPTUnofficialProxyAPI(web accessToken)` | Yes | Relatively unreliable | Smart | Comparison: 1. `ChatGPTAPI` uses `gpt-3.5-turbo` through `OpenAI` official `API` to call `ChatGPT` 2. `ChatGPTUnofficialProxyAPI` uses unofficial proxy server to access `ChatGPT`'s backend `API`, bypass `Cloudflare` (dependent on third-party servers, and has rate limits) Warnings: 1. You should first use the `API` method 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. 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. 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) 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. Switching methods: 1. Enter the `service/.env.example` file, copy the contents to the `service/.env` file 2. To use `OpenAI API Key`, fill in the `OPENAI_API_KEY` field [(get apiKey)](https://platform.openai.com/overview) 3. To use `Web API`, fill in the `OPENAI_ACCESS_TOKEN` field [(get accessToken)](https://chat.openai.com/api/auth/session) 4. `OpenAI API Key` takes precedence when both exist Environment variables: See all parameter variables [here](#environment-variables) ## Roadmap [✓] Dual models [✓] Multi-session storage and context logic [✓] Formatting and beautification of code and other message types [✓] Access control [✓] Data import/export [✓] Save messages as local images [✓] Multilingual interface [✓] Interface themes [✗] More... ## Prerequisites ### Node `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 ```shell node -v ``` ### PNPM If you haven't installed `pnpm` ```shell npm install pnpm -g ``` ### Filling in the Key Get `Openai Api Key` or `accessToken` and fill in the local environment variables [Go to Introduction](#introduction) ``` # service/.env file # OpenAI API Key - https://platform.openai.com/overview OPENAI_API_KEY= # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response OPENAI_ACCESS_TOKEN= ``` ## Install Dependencies > 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. ### Backend Enter the folder `/service` and run the following commands ```shell pnpm install ``` ### Frontend Run the following commands at the root directory ```shell pnpm bootstrap ``` ## Run in Test Environment ### Backend Service Enter the folder `/service` and run the following commands ```shell pnpm start ``` ### Frontend Webpage Run the following commands at the root directory ```shell pnpm dev ``` ## Environment Variables `API` available: - `OPENAI_API_KEY` and `OPENAI_ACCESS_TOKEN` choose one - `OPENAI_API_MODEL` Set model, optional, default: `gpt-3.5-turbo` - `OPENAI_API_BASE_URL` Set interface address, optional, default: `https://api.openai.com` - `OPENAI_API_DISABLE_DEBUG` Set interface to close debug logs, optional, default: empty does not close `ACCESS_TOKEN` available: - `OPENAI_ACCESS_TOKEN` and `OPENAI_API_KEY` choose one, `OPENAI_API_KEY` takes precedence when both exist - `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) Common: - `AUTH_SECRET_KEY` Access permission key, optional - `MAX_REQUEST_PER_HOUR` Maximum number of requests per hour, optional, unlimited by default - `TIMEOUT_MS` Timeout, unit milliseconds, optional - `SOCKS_PROXY_HOST` and `SOCKS_PROXY_PORT` take effect together, optional - `SOCKS_PROXY_PORT` and `SOCKS_PROXY_HOST` take effect together, optional - `HTTPS_PROXY` Support `http`, `https`, `socks5`, optional - `ALL_PROXY` Support `http`, `https`, `socks5`, optional ## Packaging ### Use Docker #### Docker Parameter Examples ![docker](./docs/docker.png) #### Docker build & Run ```bash docker build -t chatgpt-web . # Foreground running docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web # Background running docker run --name chatgpt-web -d -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web # Run address http://localhost:3002/ ``` #### Docker compose [Hub address](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general) ```yml version: '3' services: app: image: chenzhaoyu94/chatgpt-web # always use latest, pull the tag image again to update ports: - 127.0.0.1:3002:3002 environment: # choose one OPENAI_API_KEY: sk-xxx # choose one OPENAI_ACCESS_TOKEN: xxx # API interface address, optional, available when OPENAI_API_KEY is set OPENAI_API_BASE_URL: xxx # API model, optional, available when OPENAI_API_KEY is set, https://platform.openai.com/docs/models # 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 OPENAI_API_MODEL: xxx # reverse proxy, optional API_REVERSE_PROXY: xxx # access permission key, optional AUTH_SECRET_KEY: xxx # maximum number of requests per hour, optional, unlimited by default MAX_REQUEST_PER_HOUR: 0 # timeout, unit milliseconds, optional TIMEOUT_MS: 60000 # Socks proxy, optional, take effect with SOCKS_PROXY_PORT SOCKS_PROXY_HOST: xxx # Socks proxy port, optional, take effect with SOCKS_PROXY_HOST SOCKS_PROXY_PORT: xxx # HTTPS proxy, optional, support http,https,socks5 HTTPS_PROXY: http://xxx:7890 ``` - `OPENAI_API_BASE_URL` Optional, available when `OPENAI_API_KEY` is set - `OPENAI_API_MODEL` Optional, available when `OPENAI_API_KEY` is set #### Prevent Crawlers **nginx** 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 ``` # Prevent crawlers 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") { return 403; } ``` ### Deploy with Railway [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc) #### Railway Environment Variables | Environment variable name | Required | Remarks | | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- | | `PORT` | Required | Default `3002` | | `AUTH_SECRET_KEY` | Optional | Access permission key | | `MAX_REQUEST_PER_HOUR` | Optional | Maximum number of requests per hour, optional, unlimited by default | | `TIMEOUT_MS` | Optional | Timeout, unit milliseconds | | `OPENAI_API_KEY` | `OpenAI API` choose one | `apiKey` required for `OpenAI API` [(get apiKey)](https://platform.openai.com/overview) | | `OPENAI_ACCESS_TOKEN` | `Web API` choose one | `accessToken` required for `Web API` [(get accessToken)](https://chat.openai.com/api/auth/session) | | `OPENAI_API_BASE_URL` | Optional, available when `OpenAI API` | `API` interface address | | `OPENAI_API_MODEL` | Optional, available when `OpenAI API` | `API` model | | `API_REVERSE_PROXY` | Optional, available when `Web API` | `Web API` reverse proxy address [Details](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | | `SOCKS_PROXY_HOST` | Optional, take effect with `SOCKS_PROXY_PORT` | Socks proxy | | `SOCKS_PROXY_PORT` | Optional, take effect with `SOCKS_PROXY_HOST` | Socks proxy port | | `SOCKS_PROXY_USERNAME` | Optional, take effect with `SOCKS_PROXY_HOST` | Socks proxy username | | `SOCKS_PROXY_PASSWORD` | Optional, take effect with `SOCKS_PROXY_HOST` | Socks proxy password | | `HTTPS_PROXY` | Optional | HTTPS proxy, support http,https, socks5 | | `ALL_PROXY` | Optional | All proxies, support http,https, socks5 | > Note: Modifying environment variables on `Railway` will re-`Deploy` ### Deploy with Sealos [![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dchatgpt-web) > Environment variables are consistent with Docker environment variables ### Package Manually #### Backend Service > If you don't need the `node` interface of this project, you can omit the following operations Copy the `service` folder to the server where you have the `node` service environment. ```shell # Install pnpm install # Pack pnpm build # Run pnpm prod ``` PS: It is also okay to run `pnpm start` directly on the server without packing #### Frontend Webpage 1. Modify the `VITE_GLOB_API_URL` field in the `.env` file at the root directory to your actual backend interface address 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 [Reference](https://cn.vitejs.dev/guide/static -deploy.html#building-the-app) ```shell pnpm build ``` ## FAQ Q: Why does `Git` commit always report errors? A: Because there is a commit message verification, please follow the [Commit Guide](./CONTRIBUTING.md) Q: Where to change the request interface if only the front-end page is used? A: The `VITE_GLOB_API_URL` field in the `.env` file at the root directory. Q: All files explode red when saving? A: `vscode` please install the recommended plug-ins for the project, or manually install the `Eslint` plug-in. Q: No typewriter effect on the front end? 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. ## Contributing Please read the [Contributing Guide](./CONTRIBUTING.md) before contributing Thanks to everyone who has contributed! ## Acknowledgements Thanks to [JetBrains](https://www.jetbrains.com/) SoftWare for providing free Open Source license for this project. ## Sponsors If you find this project helpful and can afford it, you can give me a little support. Anyway, thanks for your support~
WeChat

WeChat Pay

Alipay

Alipay

## License MIT © [ChenZhaoYu] ================================================ FILE: README.zh.md ================================================ # ChatGPT Web > 声明:此项目只发布于 GitHub,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。 [English](README.md) ![cover](./docs/c1.png) ![cover2](./docs/c2.png) - [ChatGPT Web](#chatgpt-web) - [介绍](#介绍) - [待实现路线](#待实现路线) - [前置要求](#前置要求) - [Node](#node) - [PNPM](#pnpm) - [填写密钥](#填写密钥) - [安装依赖](#安装依赖) - [后端](#后端) - [前端](#前端) - [测试环境运行](#测试环境运行) - [后端服务](#后端服务) - [前端网页](#前端网页) - [环境变量](#环境变量) - [打包](#打包) - [使用 Docker](#使用-docker) - [Docker 参数示例](#docker-参数示例) - [Docker build \& Run](#docker-build--run) - [Docker compose](#docker-compose) - [防止爬虫抓取](#防止爬虫抓取) - [使用 Railway 部署](#使用-railway-部署) - [Railway 环境变量](#railway-环境变量) - [使用 Sealos 部署](#使用-sealos-部署) - [手动打包](#手动打包) - [后端服务](#后端服务-1) - [前端网页](#前端网页-1) - [常见问题](#常见问题) - [参与贡献](#参与贡献) - [致谢](#致谢) - [赞助](#赞助) - [License](#license) ## 介绍 支持双模型,提供了两种非官方 `ChatGPT API` 方法 | 方式 | 免费? | 可靠性 | 质量 | | --------------------------------------------- | ------ | ---------- | ---- | | `ChatGPTAPI(gpt-3.5-turbo-0301)` | 否 | 可靠 | 相对较笨 | | `ChatGPTUnofficialProxyAPI(网页 accessToken)` | 是 | 相对不可靠 | 聪明 | 对比: 1. `ChatGPTAPI` 使用 `gpt-3.5-turbo` 通过 `OpenAI` 官方 `API` 调用 `ChatGPT` 2. `ChatGPTUnofficialProxyAPI` 使用非官方代理服务器访问 `ChatGPT` 的后端`API`,绕过`Cloudflare`(依赖于第三方服务器,并且有速率限制) 警告: 1. 你应该首先使用 `API` 方式 2. 使用 `API` 时,如果网络不通,那是国内被墙了,你需要自建代理,绝对不要使用别人的公开代理,那是危险的。 3. 使用 `accessToken` 方式时反向代理将向第三方暴露您的访问令牌,这样做应该不会产生任何不良影响,但在使用这种方法之前请考虑风险。 4. 使用 `accessToken` 时,不管你是国内还是国外的机器,都会使用代理。默认代理为 [pengzhile](https://github.com/pengzhile) 大佬的 `https://ai.fakeopen.com/api/conversation`,这不是后门也不是监听,除非你有能力自己翻过 `CF` 验证,用前请知悉。[社区代理](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) 5. 把项目发布到公共网络时,你应该设置 `AUTH_SECRET_KEY` 变量添加你的密码访问权限,你也应该修改 `index.html` 中的 `title`,防止被关键词搜索到。 切换方式: 1. 进入 `service/.env.example` 文件,复制内容到 `service/.env` 文件 2. 使用 `OpenAI API Key` 请填写 `OPENAI_API_KEY` 字段 [(获取 apiKey)](https://platform.openai.com/overview) 3. 使用 `Web API` 请填写 `OPENAI_ACCESS_TOKEN` 字段 [(获取 accessToken)](https://chat.openai.com/api/auth/session) 4. 同时存在时以 `OpenAI API Key` 优先 环境变量: 全部参数变量请查看或[这里](#环境变量) ``` /service/.env.example ``` ## 待实现路线 [✓] 双模型 [✓] 多会话储存和上下文逻辑 [✓] 对代码等消息类型的格式化美化处理 [✓] 访问权限控制 [✓] 数据导入、导出 [✓] 保存消息到本地图片 [✓] 界面多语言 [✓] 界面主题 [✗] More... ## 前置要求 ### Node `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` 版本 ```shell node -v ``` ### PNPM 如果你没有安装过 `pnpm` ```shell npm install pnpm -g ``` ### 填写密钥 获取 `Openai Api Key` 或 `accessToken` 并填写本地环境变量 [跳转](#介绍) ``` # service/.env 文件 # OpenAI API Key - https://platform.openai.com/overview OPENAI_API_KEY= # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response OPENAI_ACCESS_TOKEN= ``` ## 安装依赖 > 为了简便 `后端开发人员` 的了解负担,所以并没有采用前端 `workspace` 模式,而是分文件夹存放。如果只需要前端页面做二次开发,删除 `service` 文件夹即可。 ### 后端 进入文件夹 `/service` 运行以下命令 ```shell pnpm install ``` ### 前端 根目录下运行以下命令 ```shell pnpm bootstrap ``` ## 测试环境运行 ### 后端服务 进入文件夹 `/service` 运行以下命令 ```shell pnpm start ``` ### 前端网页 根目录下运行以下命令 ```shell pnpm dev ``` ## 环境变量 `API` 可用: - `OPENAI_API_KEY` 和 `OPENAI_ACCESS_TOKEN` 二选一 - `OPENAI_API_MODEL` 设置模型,可选,默认:`gpt-3.5-turbo` - `OPENAI_API_BASE_URL` 设置接口地址,可选,默认:`https://api.openai.com` - `OPENAI_API_DISABLE_DEBUG` 设置接口关闭 debug 日志,可选,默认:empty 不关闭 `ACCESS_TOKEN` 可用: - `OPENAI_ACCESS_TOKEN` 和 `OPENAI_API_KEY` 二选一,同时存在时,`OPENAI_API_KEY` 优先 - `API_REVERSE_PROXY` 设置反向代理,可选,默认:`https://ai.fakeopen.com/api/conversation`,[社区](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) 通用: - `AUTH_SECRET_KEY` 访问权限密钥,可选 - `MAX_REQUEST_PER_HOUR` 每小时最大请求次数,可选,默认无限 - `TIMEOUT_MS` 超时,单位毫秒,可选 - `SOCKS_PROXY_HOST` 和 `SOCKS_PROXY_PORT` 一起时生效,可选 - `SOCKS_PROXY_PORT` 和 `SOCKS_PROXY_HOST` 一起时生效,可选 - `HTTPS_PROXY` 支持 `http`,`https`, `socks5`,可选 - `ALL_PROXY` 支持 `http`,`https`, `socks5`,可选 ## 打包 ### 使用 Docker #### Docker 参数示例 ![docker](./docs/docker.png) #### Docker build & Run ```bash docker build -t chatgpt-web . # 前台运行 docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web # 后台运行 docker run --name chatgpt-web -d -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web # 运行地址 http://localhost:3002/ ``` #### Docker compose [Hub 地址](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general) ```yml version: '3' services: app: image: chenzhaoyu94/chatgpt-web # 总是使用 latest ,更新时重新 pull 该 tag 镜像即可 ports: - 127.0.0.1:3002:3002 environment: # 二选一 OPENAI_API_KEY: sk-xxx # 二选一 OPENAI_ACCESS_TOKEN: xxx # API接口地址,可选,设置 OPENAI_API_KEY 时可用 OPENAI_API_BASE_URL: xxx # API模型,可选,设置 OPENAI_API_KEY 时可用,https://platform.openai.com/docs/models # 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 OPENAI_API_MODEL: xxx # 反向代理,可选 API_REVERSE_PROXY: xxx # 访问权限密钥,可选 AUTH_SECRET_KEY: xxx # 每小时最大请求次数,可选,默认无限 MAX_REQUEST_PER_HOUR: 0 # 超时,单位毫秒,可选 TIMEOUT_MS: 60000 # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效 SOCKS_PROXY_HOST: xxx # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效 SOCKS_PROXY_PORT: xxx # HTTPS 代理,可选,支持 http,https,socks5 HTTPS_PROXY: http://xxx:7890 ``` - `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用 - `OPENAI_API_MODEL` 可选,设置 `OPENAI_API_KEY` 时可用 #### 防止爬虫抓取 **nginx** 将下面配置填入nginx配置文件中,可以参考 `docker-compose/nginx/nginx.conf` 文件中添加反爬虫的方法 ``` # 防止爬虫抓取 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") { return 403; } ``` ### 使用 Railway 部署 [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc) #### Railway 环境变量 | 环境变量名称 | 必填 | 备注 | | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- | | `PORT` | 必填 | 默认 `3002` | `AUTH_SECRET_KEY` | 可选 | 访问权限密钥 | | `MAX_REQUEST_PER_HOUR` | 可选 | 每小时最大请求次数,可选,默认无限 | | `TIMEOUT_MS` | 可选 | 超时时间,单位毫秒 | | `OPENAI_API_KEY` | `OpenAI API` 二选一 | 使用 `OpenAI API` 所需的 `apiKey` [(获取 apiKey)](https://platform.openai.com/overview) | | `OPENAI_ACCESS_TOKEN` | `Web API` 二选一 | 使用 `Web API` 所需的 `accessToken` [(获取 accessToken)](https://chat.openai.com/api/auth/session) | | `OPENAI_API_BASE_URL` | 可选,`OpenAI API` 时可用 | `API`接口地址 | | `OPENAI_API_MODEL` | 可选,`OpenAI API` 时可用 | `API`模型 | | `API_REVERSE_PROXY` | 可选,`Web API` 时可用 | `Web API` 反向代理地址 [详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | | `SOCKS_PROXY_HOST` | 可选,和 `SOCKS_PROXY_PORT` 一起时生效 | Socks代理 | | `SOCKS_PROXY_PORT` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理端口 | | `SOCKS_PROXY_USERNAME` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理用户名 | | `SOCKS_PROXY_PASSWORD` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理密码 | | `HTTPS_PROXY` | 可选 | HTTPS 代理,支持 http,https, socks5 | | `ALL_PROXY` | 可选 | 所有代理 代理,支持 http,https, socks5 | > 注意: `Railway` 修改环境变量会重新 `Deploy` ### 使用 Sealos 部署 [![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dchatgpt-web) > 环境变量与 Docker 环境变量一致 ### 手动打包 #### 后端服务 > 如果你不需要本项目的 `node` 接口,可以省略如下操作 复制 `service` 文件夹到你有 `node` 服务环境的服务器上。 ```shell # 安装 pnpm install # 打包 pnpm build # 运行 pnpm prod ``` PS: 不进行打包,直接在服务器上运行 `pnpm start` 也可 #### 前端网页 1、修改根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 为你的实际后端接口地址 2、根目录下运行以下命令,然后将 `dist` 文件夹内的文件复制到你网站服务的根目录下 [参考信息](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app) ```shell pnpm build ``` ## 常见问题 Q: 为什么 `Git` 提交总是报错? A: 因为有提交信息验证,请遵循 [Commit 指南](./CONTRIBUTING.md) Q: 如果只使用前端页面,在哪里改请求接口? A: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。 Q: 文件保存时全部爆红? A: `vscode` 请安装项目推荐插件,或手动安装 `Eslint` 插件。 Q: 前端没有打字机效果? A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer,则 Nginx 会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`,然后重载 Nginx。其他 web server 配置同理。 ## 参与贡献 贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md) 感谢所有做过贡献的人! ## 致谢 感谢 [JetBrains](https://www.jetbrains.com/) 为这个项目提供免费开源许可的软件。 ## 赞助 如果你觉得这个项目对你有帮助,并且情况允许的话,可以给我一点点支持,总之非常感谢支持~
微信

WeChat Pay

支付宝

Alipay

## License MIT © [ChenZhaoYu](./license) ================================================ FILE: docker-compose/README.md ================================================ ### docker-compose Deployment Tutorial -Put the packaged front-end files in the `nginx/html` directory - ```shell # start up docker-compose up -d ``` - ```shell # Check the running status docker ps ``` - ```shell # end run docker-compose down ``` ================================================ FILE: docker-compose/docker-compose.yml ================================================ version: '3' services: app: container_name: chatgpt-web image: chenzhaoyu94/chatgpt-web # Always use latest, just pull the tag image again when updating ports: - 3002:3002 environment: # pick one of two OPENAI_API_KEY: # pick one of two OPENAI_ACCESS_TOKEN: # API interface address, optional, available when OPENAI_API_KEY is set OPENAI_API_BASE_URL: # API model, optional, available when OPENAI_API_KEY is set OPENAI_API_MODEL: # reverse proxy, optional API_REVERSE_PROXY: # Access permission key, optional AUTH_SECRET_KEY: # The maximum number of requests per hour, optional, default unlimited MAX_REQUEST_PER_HOUR: 0 # timeout in milliseconds, optional TIMEOUT_MS: 60000 # Socks proxy, optional, works with SOCKS_PROXY_PORT SOCKS_PROXY_HOST: # Socks proxy port, optional, effective when combined with SOCKS_PROXY_HOST SOCKS_PROXY_PORT: # Socks proxy username, optional, effective when combined with SOCKS_PROXY_HOST & SOCKS_PROXY_PORT SOCKS_PROXY_USERNAME: # Socks proxy password, optional, effective when combined with SOCKS_PROXY_HOST & SOCKS_PROXY_PORT SOCKS_PROXY_PASSWORD: # HTTPS_PROXY proxy, optional HTTPS_PROXY: nginx: container_name: nginx image: nginx:alpine ports: - '80:80' expose: - '80' volumes: - ./nginx/html:/usr/share/nginx/html - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf links: - app ================================================ FILE: docker-compose/nginx/nginx.conf ================================================ server { listen 80; server_name localhost; charset utf-8; error_page 500 502 503 504 /50x.html; # Prevent crawlers from crawling 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") { return 403; } location / { root /usr/share/nginx/html; try_files $uri /index.html; } location /api { proxy_set_header X-Real-IP $remote_addr; #Forward user IP proxy_pass http://app:3002; } proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } ================================================ FILE: index.html ================================================ ChatGPT Web
================================================ FILE: kubernetes/README.md ================================================ ## 增加一个Kubernetes的部署方式 ``` kubectl apply -f deploy.yaml ``` ### 如果需要Ingress域名接入 ``` kubectl apply -f ingress.yaml ``` ================================================ FILE: kubernetes/deploy.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: chatgpt-web labels: app: chatgpt-web spec: replicas: 1 selector: matchLabels: app: chatgpt-web strategy: type: RollingUpdate template: metadata: labels: app: chatgpt-web spec: containers: - image: chenzhaoyu94/chatgpt-web name: chatgpt-web imagePullPolicy: Always ports: - containerPort: 3002 env: - name: OPENAI_API_KEY value: sk-xxx - name: OPENAI_API_BASE_URL value: 'https://api.openai.com' - name: OPENAI_API_MODEL value: gpt-3.5-turbo - name: API_REVERSE_PROXY value: https://ai.fakeopen.com/api/conversation - name: AUTH_SECRET_KEY value: '123456' - name: TIMEOUT_MS value: '60000' - name: SOCKS_PROXY_HOST value: '' - name: SOCKS_PROXY_PORT value: '' - name: HTTPS_PROXY value: '' resources: limits: cpu: 500m memory: 500Mi requests: cpu: 300m memory: 300Mi --- apiVersion: v1 kind: Service metadata: labels: app: chatgpt-web name: chatgpt-web spec: ports: - name: chatgpt-web port: 3002 protocol: TCP targetPort: 3002 selector: app: chatgpt-web type: ClusterIP ================================================ FILE: kubernetes/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/proxy-connect-timeout: '5' name: chatgpt-web spec: rules: - host: chatgpt.example.com http: paths: - backend: service: name: chatgpt-web port: number: 3002 path: / pathType: ImplementationSpecific tls: - secretName: chatgpt-web-tls ================================================ FILE: license ================================================ MIT License Copyright (c) 2023 ChenZhaoYu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: package.json ================================================ { "name": "chatgpt-web", "version": "2.11.1", "private": false, "description": "ChatGPT Web", "author": "ChenZhaoYu ", "keywords": [ "chatgpt-web", "chatgpt", "chatbot", "vue" ], "scripts": { "dev": "vite", "build": "run-p type-check build-only", "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --noEmit", "lint": "eslint .", "lint:fix": "eslint . --fix", "bootstrap": "pnpm install && pnpm run common:prepare", "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml", "common:prepare": "husky install" }, "dependencies": { "@vscode/markdown-it-katex": "^1.0.3", "@vueuse/core": "^9.13.0", "highlight.js": "^11.7.0", "html-to-image": "^1.11.11", "katex": "^0.16.4", "markdown-it": "^13.0.1", "mermaid-it-markdown": "^1.0.8", "naive-ui": "^2.34.3", "pinia": "^2.0.33", "vue": "^3.2.47", "vue-i18n": "^9.2.2", "vue-router": "^4.1.6" }, "devDependencies": { "@antfu/eslint-config": "^0.35.3", "@commitlint/cli": "^17.4.4", "@commitlint/config-conventional": "^17.4.4", "@iconify/vue": "^4.1.0", "@types/crypto-js": "^4.1.1", "@types/katex": "^0.16.0", "@types/markdown-it": "^12.2.3", "@types/markdown-it-link-attributes": "^3.0.1", "@types/node": "^18.14.6", "@vitejs/plugin-vue": "^4.0.0", "autoprefixer": "^10.4.13", "axios": "^1.3.4", "crypto-js": "^4.1.1", "eslint": "^8.35.0", "husky": "^8.0.3", "less": "^4.1.3", "lint-staged": "^13.1.2", "markdown-it-link-attributes": "^4.0.1", "npm-run-all": "^4.1.5", "postcss": "^8.4.21", "rimraf": "^4.3.0", "tailwindcss": "^3.2.7", "typescript": "~4.9.5", "vite": "^4.2.0", "vite-plugin-pwa": "^0.14.4", "vue-tsc": "^1.2.0" }, "lint-staged": { "*.{ts,tsx,vue}": [ "pnpm lint:fix" ] } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: service/.eslintrc.json ================================================ { "root": true, "ignorePatterns": ["build"], "extends": ["@antfu"] } ================================================ FILE: service/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules .DS_Store dist dist-ssr coverage *.local /cypress/videos/ /cypress/screenshots/ # Editor directories and files .vscode/* !.vscode/settings.json !.vscode/extensions.json .idea *.suo *.ntvs* *.njsproj *.sln *.sw? build ================================================ FILE: service/.npmrc ================================================ enable-pre-post-scripts=true ================================================ FILE: service/.vscode/extensions.json ================================================ { "recommendations": ["dbaeumer.vscode-eslint"] } ================================================ FILE: service/.vscode/settings.json ================================================ { "prettier.enable": false, "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "eslint.validate": [ "javascript", "typescript", "json", "jsonc", "json5", "yaml" ], "cSpell.words": [ "antfu", "chatgpt", "esno", "GPTAPI", "OPENAI" ] } ================================================ FILE: service/package.json ================================================ { "name": "chatgpt-web-service", "version": "1.0.0", "private": false, "description": "ChatGPT Web Service", "author": "ChenZhaoYu ", "keywords": [ "chatgpt-web", "chatgpt", "chatbot", "express" ], "engines": { "node": "^16 || ^18 || ^20" }, "scripts": { "start": "esno ./src/index.ts", "dev": "esno watch ./src/index.ts", "prod": "node ./build/index.mjs", "build": "pnpm clean && tsup", "clean": "rimraf build", "lint": "eslint .", "lint:fix": "eslint . --fix", "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml" }, "dependencies": { "axios": "^1.3.4", "chatgpt": "^5.1.2", "dotenv": "^16.0.3", "esno": "^4.7.0", "express": "^4.18.2", "express-rate-limit": "^6.7.0", "https-proxy-agent": "^5.0.1", "isomorphic-fetch": "^3.0.0", "node-fetch": "^3.3.0", "socks-proxy-agent": "^7.0.0" }, "devDependencies": { "@antfu/eslint-config": "^0.35.3", "@types/express": "^4.17.17", "@types/node": "^18.14.6", "eslint": "^8.35.0", "rimraf": "^4.3.0", "tsup": "^6.6.3", "typescript": "^4.9.5" } } ================================================ FILE: service/src/chatgpt/index.ts ================================================ import * as dotenv from 'dotenv' import 'isomorphic-fetch' import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt' import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt' import { SocksProxyAgent } from 'socks-proxy-agent' import httpsProxyAgent from 'https-proxy-agent' import fetch from 'node-fetch' import { sendResponse } from '../utils' import { isNotEmptyString } from '../utils/is' import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types' import type { RequestOptions, SetProxyOptions, UsageResponse } from './types' const { HttpsProxyAgent } = httpsProxyAgent dotenv.config() const ErrorCodeMessage: Record = { 401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided', 403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later', 502: '[OpenAI] 错误的网关 | Bad Gateway', 503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later', 504: '[OpenAI] 网关超时 | Gateway Time-out', 500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error', } const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 100 * 1000 const disableDebug: boolean = process.env.OPENAI_API_DISABLE_DEBUG === 'true' let apiModel: ApiModel const model = isNotEmptyString(process.env.OPENAI_API_MODEL) ? process.env.OPENAI_API_MODEL : 'gpt-3.5-turbo' if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN)) throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable') let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI (async () => { // More Info: https://github.com/transitive-bullshit/chatgpt-api if (isNotEmptyString(process.env.OPENAI_API_KEY)) { const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL const options: ChatGPTAPIOptions = { apiKey: process.env.OPENAI_API_KEY, completionParams: { model }, debug: !disableDebug, } // increase max token limit if use gpt-4 if (model.toLowerCase().includes('gpt-4')) { // if use 32k model if (model.toLowerCase().includes('32k')) { options.maxModelTokens = 32768 options.maxResponseTokens = 8192 } else if (/-4o-mini/.test(model.toLowerCase())) { options.maxModelTokens = 128000 options.maxResponseTokens = 16384 } // if use GPT-4 Turbo or GPT-4o else if (/-preview|-turbo|o/.test(model.toLowerCase())) { options.maxModelTokens = 128000 options.maxResponseTokens = 4096 } else { options.maxModelTokens = 8192 options.maxResponseTokens = 2048 } } else if (model.toLowerCase().includes('gpt-3.5')) { if (/16k|1106|0125/.test(model.toLowerCase())) { options.maxModelTokens = 16384 options.maxResponseTokens = 4096 } } if (isNotEmptyString(OPENAI_API_BASE_URL)) { // if find /v1 in OPENAI_API_BASE_URL then use it if (OPENAI_API_BASE_URL.includes('/v1')) options.apiBaseUrl = `${OPENAI_API_BASE_URL}` else options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1` } setupProxy(options) api = new ChatGPTAPI({ ...options }) apiModel = 'ChatGPTAPI' } else { const options: ChatGPTUnofficialProxyAPIOptions = { accessToken: process.env.OPENAI_ACCESS_TOKEN, apiReverseProxyUrl: isNotEmptyString(process.env.API_REVERSE_PROXY) ? process.env.API_REVERSE_PROXY : 'https://ai.fakeopen.com/api/conversation', model, debug: !disableDebug, } setupProxy(options) api = new ChatGPTUnofficialProxyAPI({ ...options }) apiModel = 'ChatGPTUnofficialProxyAPI' } })() async function chatReplyProcess(options: RequestOptions) { const { message, lastContext, process, systemMessage, temperature, top_p } = options try { let options: SendMessageOptions = { timeoutMs } if (apiModel === 'ChatGPTAPI') { if (isNotEmptyString(systemMessage)) options.systemMessage = systemMessage options.completionParams = { model, temperature, top_p } } if (lastContext != null) { if (apiModel === 'ChatGPTAPI') options.parentMessageId = lastContext.parentMessageId else options = { ...lastContext } } const response = await api.sendMessage(message, { ...options, onProgress: (partialResponse) => { process?.(partialResponse) }, }) return sendResponse({ type: 'Success', data: response }) } catch (error: any) { const code = error.statusCode global.console.log(error) if (Reflect.has(ErrorCodeMessage, code)) return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] }) return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' }) } } async function fetchUsage() { const OPENAI_API_KEY = process.env.OPENAI_API_KEY const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL if (!isNotEmptyString(OPENAI_API_KEY)) return Promise.resolve('-') const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL) ? OPENAI_API_BASE_URL : 'https://api.openai.com' const [startDate, endDate] = formatDate() // 每月使用量 const urlUsage = `${API_BASE_URL}/v1/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}` const headers = { 'Authorization': `Bearer ${OPENAI_API_KEY}`, 'Content-Type': 'application/json', } const options = {} as SetProxyOptions setupProxy(options) try { // 获取已使用量 const useResponse = await options.fetch(urlUsage, { headers }) if (!useResponse.ok) throw new Error('获取使用量失败') const usageData = await useResponse.json() as UsageResponse const usage = Math.round(usageData.total_usage) / 100 return Promise.resolve(usage ? `$${usage}` : '-') } catch (error) { global.console.log(error) return Promise.resolve('-') } } function formatDate(): string[] { const today = new Date() const year = today.getFullYear() const month = today.getMonth() + 1 const lastDay = new Date(year, month, 0) const formattedFirstDay = `${year}-${month.toString().padStart(2, '0')}-01` const formattedLastDay = `${year}-${month.toString().padStart(2, '0')}-${lastDay.getDate().toString().padStart(2, '0')}` return [formattedFirstDay, formattedLastDay] } async function chatConfig() { const usage = await fetchUsage() const reverseProxy = process.env.API_REVERSE_PROXY ?? '-' const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-' const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) ? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`) : '-' return sendResponse({ type: 'Success', data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, usage }, }) } function setupProxy(options: SetProxyOptions) { if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) { const agent = new SocksProxyAgent({ hostname: process.env.SOCKS_PROXY_HOST, port: process.env.SOCKS_PROXY_PORT, userId: isNotEmptyString(process.env.SOCKS_PROXY_USERNAME) ? process.env.SOCKS_PROXY_USERNAME : undefined, password: isNotEmptyString(process.env.SOCKS_PROXY_PASSWORD) ? process.env.SOCKS_PROXY_PASSWORD : undefined, }) options.fetch = (url, options) => { return fetch(url, { agent, ...options }) } } else if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(process.env.ALL_PROXY)) { const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY if (httpsProxy) { const agent = new HttpsProxyAgent(httpsProxy) options.fetch = (url, options) => { return fetch(url, { agent, ...options }) } } } else { options.fetch = (url, options) => { return fetch(url, { ...options }) } } } function currentModel(): ApiModel { return apiModel } export type { ChatContext, ChatMessage } export { chatReplyProcess, chatConfig, currentModel } ================================================ FILE: service/src/chatgpt/types.ts ================================================ import type { ChatMessage } from 'chatgpt' import type fetch from 'node-fetch' export interface RequestOptions { message: string lastContext?: { conversationId?: string; parentMessageId?: string } process?: (chat: ChatMessage) => void systemMessage?: string temperature?: number top_p?: number } export interface SetProxyOptions { fetch?: typeof fetch } export interface UsageResponse { total_usage: number } ================================================ FILE: service/src/index.ts ================================================ import express from 'express' import type { RequestProps } from './types' import type { ChatMessage } from './chatgpt' import { chatConfig, chatReplyProcess, currentModel } from './chatgpt' import { auth } from './middleware/auth' import { limiter } from './middleware/limiter' import { isNotEmptyString } from './utils/is' const app = express() const router = express.Router() app.use(express.static('public')) app.use(express.json()) app.all('*', (_, res, next) => { res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Headers', 'authorization, Content-Type') res.header('Access-Control-Allow-Methods', '*') next() }) router.post('/chat-process', [auth, limiter], async (req, res) => { res.setHeader('Content-type', 'application/octet-stream') try { const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps let firstChunk = true await chatReplyProcess({ message: prompt, lastContext: options, process: (chat: ChatMessage) => { res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`) firstChunk = false }, systemMessage, temperature, top_p, }) } catch (error) { res.write(JSON.stringify(error)) } finally { res.end() } }) router.post('/config', auth, async (req, res) => { try { const response = await chatConfig() res.send(response) } catch (error) { res.send(error) } }) router.post('/session', async (req, res) => { try { const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY const hasAuth = isNotEmptyString(AUTH_SECRET_KEY) res.send({ status: 'Success', message: '', data: { auth: hasAuth, model: currentModel() } }) } catch (error) { res.send({ status: 'Fail', message: error.message, data: null }) } }) router.post('/verify', async (req, res) => { try { const { token } = req.body as { token: string } if (!token) throw new Error('Secret key is empty') if (process.env.AUTH_SECRET_KEY !== token) throw new Error('密钥无效 | Secret key is invalid') res.send({ status: 'Success', message: 'Verify successfully', data: null }) } catch (error) { res.send({ status: 'Fail', message: error.message, data: null }) } }) app.use('', router) app.use('/api', router) app.set('trust proxy', 1) app.listen(3002, () => globalThis.console.log('Server is running on port 3002')) ================================================ FILE: service/src/middleware/auth.ts ================================================ import { isNotEmptyString } from '../utils/is' const auth = async (req, res, next) => { const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY if (isNotEmptyString(AUTH_SECRET_KEY)) { try { const Authorization = req.header('Authorization') if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim()) throw new Error('Error: 无访问权限 | No access rights') next() } catch (error) { res.send({ status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null }) } } else { next() } } export { auth } ================================================ FILE: service/src/middleware/limiter.ts ================================================ import { rateLimit } from 'express-rate-limit' import { isNotEmptyString } from '../utils/is' const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR))) ? parseInt(MAX_REQUEST_PER_HOUR) : 0 // 0 means unlimited const limiter = rateLimit({ windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour max: maxCount, statusCode: 200, // 200 means success,but the message is 'Too many request from this IP in 1 hour' message: async (req, res) => { res.send({ status: 'Fail', message: 'Too many request from this IP in 1 hour', data: null }) }, }) export { limiter } ================================================ FILE: service/src/types.ts ================================================ import type { FetchFn } from 'chatgpt' export interface RequestProps { prompt: string options?: ChatContext systemMessage: string temperature?: number top_p?: number } export interface ChatContext { conversationId?: string parentMessageId?: string } export interface ChatGPTUnofficialProxyAPIOptions { accessToken: string apiReverseProxyUrl?: string model?: string debug?: boolean headers?: Record fetch?: FetchFn } export interface ModelConfig { apiModel?: ApiModel reverseProxy?: string timeoutMs?: number socksProxy?: string httpsProxy?: string usage?: string } export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined ================================================ FILE: service/src/utils/index.ts ================================================ interface SendResponseOptions { type: 'Success' | 'Fail' message?: string data?: T } export function sendResponse(options: SendResponseOptions) { if (options.type === 'Success') { return Promise.resolve({ message: options.message ?? null, data: options.data ?? null, status: options.type, }) } // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject({ message: options.message ?? 'Failed', data: options.data ?? null, status: options.type, }) } ================================================ FILE: service/src/utils/is.ts ================================================ export function isNumber(value: T | unknown): value is number { return Object.prototype.toString.call(value) === '[object Number]' } export function isString(value: T | unknown): value is string { return Object.prototype.toString.call(value) === '[object String]' } export function isNotEmptyString(value: any): boolean { return typeof value === 'string' && value.length > 0 } export function isBoolean(value: T | unknown): value is boolean { return Object.prototype.toString.call(value) === '[object Boolean]' } export function isFunction any | void | never>(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Function]' } ================================================ FILE: service/tsconfig.json ================================================ { "compilerOptions": { "target": "es2020", "lib": [ "esnext" ], "allowJs": true, "skipLibCheck": true, "strict": false, "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "baseUrl": ".", "outDir": "build", "noEmit": true }, "exclude": [ "node_modules", "build" ], "include": [ "**/*.ts" ] } ================================================ FILE: service/tsup.config.ts ================================================ import { defineConfig } from 'tsup' export default defineConfig({ entry: ['src/index.ts'], outDir: 'build', target: 'es2020', format: ['esm'], splitting: false, sourcemap: true, minify: false, shims: true, dts: false, }) ================================================ FILE: src/App.vue ================================================ ================================================ FILE: src/api/index.ts ================================================ import type { AxiosProgressEvent, GenericAbortSignal } from 'axios' import { post } from '@/utils/request' import { useAuthStore, useSettingStore } from '@/store' export function fetchChatAPI( prompt: string, options?: { conversationId?: string; parentMessageId?: string }, signal?: GenericAbortSignal, ) { return post({ url: '/chat', data: { prompt, options }, signal, }) } export function fetchChatConfig() { return post({ url: '/config', }) } export function fetchChatAPIProcess( params: { prompt: string options?: { conversationId?: string; parentMessageId?: string } signal?: GenericAbortSignal onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void }, ) { const settingStore = useSettingStore() const authStore = useAuthStore() let data: Record = { prompt: params.prompt, options: params.options, } if (authStore.isChatGPTAPI) { data = { ...data, systemMessage: settingStore.systemMessage, temperature: settingStore.temperature, top_p: settingStore.top_p, } } return post({ url: '/chat-process', data, signal: params.signal, onDownloadProgress: params.onDownloadProgress, }) } export function fetchSession() { return post({ url: '/session', }) } export function fetchVerify(token: string) { return post({ url: '/verify', data: { token }, }) } ================================================ FILE: src/assets/recommend.json ================================================ [ { "key": "awesome-chatgpt-prompts-zh", "desc": "ChatGPT 中文调教指南", "downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json", "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh" }, { "key": "awesome-chatgpt-prompts-zh-TW", "desc": "ChatGPT 中文調教指南 (透過 OpenAI / OpenCC 協助,從簡體中文轉換為繁體中文的版本)", "downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh-TW.json", "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh" } ] ================================================ FILE: src/components/common/HoverButton/Button.vue ================================================ ================================================ FILE: src/components/common/HoverButton/index.vue ================================================ ================================================ FILE: src/components/common/NaiveProvider/index.vue ================================================ ================================================ FILE: src/components/common/PromptStore/index.vue ================================================ ================================================ FILE: src/components/common/Setting/About.vue ================================================ ================================================ FILE: src/components/common/Setting/Advanced.vue ================================================ ================================================ FILE: src/components/common/Setting/General.vue ================================================ ================================================ FILE: src/components/common/Setting/index.vue ================================================ ================================================ FILE: src/components/common/SvgIcon/index.vue ================================================ ================================================ FILE: src/components/common/UserAvatar/index.vue ================================================ ================================================ FILE: src/components/common/index.ts ================================================ import HoverButton from './HoverButton/index.vue' import NaiveProvider from './NaiveProvider/index.vue' import SvgIcon from './SvgIcon/index.vue' import UserAvatar from './UserAvatar/index.vue' import Setting from './Setting/index.vue' import PromptStore from './PromptStore/index.vue' export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore } ================================================ FILE: src/components/custom/GithubSite.vue ================================================ ================================================ FILE: src/components/custom/index.ts ================================================ import GithubSite from './GithubSite.vue' export { GithubSite } ================================================ FILE: src/hooks/useBasicLayout.ts ================================================ import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' export function useBasicLayout() { const breakpoints = useBreakpoints(breakpointsTailwind) const isMobile = breakpoints.smaller('sm') return { isMobile } } ================================================ FILE: src/hooks/useIconRender.ts ================================================ import { h } from 'vue' import { SvgIcon } from '@/components/common' export const useIconRender = () => { interface IconConfig { icon?: string color?: string fontSize?: number } interface IconStyle { color?: string fontSize?: string } const iconRender = (config: IconConfig) => { const { color, fontSize, icon } = config const style: IconStyle = {} if (color) style.color = color if (fontSize) style.fontSize = `${fontSize}px` if (!icon) window.console.warn('iconRender: icon is required') return () => h(SvgIcon, { icon, style }) } return { iconRender, } } ================================================ FILE: src/hooks/useLanguage.ts ================================================ import { computed } from 'vue' import { enUS, esAR, koKR, ruRU, viVN, zhCN, zhTW } from 'naive-ui' import { useAppStore } from '@/store' import { setLocale } from '@/locales' export function useLanguage() { const appStore = useAppStore() const language = computed(() => { setLocale(appStore.language) switch (appStore.language) { case 'en-US': return enUS case 'es-ES': return esAR case 'ko-KR': return koKR case 'vi-VN': return viVN case 'ru-RU': return ruRU case 'zh-CN': return zhCN case 'zh-TW': return zhTW default: return enUS } }) return { language } } ================================================ FILE: src/hooks/useTheme.ts ================================================ import type { GlobalThemeOverrides } from 'naive-ui' import { computed, watch } from 'vue' import { darkTheme, useOsTheme } from 'naive-ui' import { useAppStore } from '@/store' export function useTheme() { const appStore = useAppStore() const OsTheme = useOsTheme() const isDark = computed(() => { if (appStore.theme === 'auto') return OsTheme.value === 'dark' else return appStore.theme === 'dark' }) const theme = computed(() => { return isDark.value ? darkTheme : undefined }) const themeOverrides = computed(() => { if (isDark.value) { return { common: {}, } } return {} }) watch( () => isDark.value, (dark) => { if (dark) document.documentElement.classList.add('dark') else document.documentElement.classList.remove('dark') }, { immediate: true }, ) return { theme, themeOverrides } } ================================================ FILE: src/icons/403.vue ================================================ ================================================ FILE: src/icons/500.vue ================================================ ================================================ FILE: src/locales/en-US.ts ================================================ export default { common: { add: 'Add', addSuccess: 'Add Success', edit: 'Edit', editSuccess: 'Edit Success', delete: 'Delete', deleteSuccess: 'Delete Success', save: 'Save', saveSuccess: 'Save Success', reset: 'Reset', action: 'Action', export: 'Export', exportSuccess: 'Export Success', import: 'Import', importSuccess: 'Import Success', clear: 'Clear', clearSuccess: 'Clear Success', yes: 'Yes', no: 'No', confirm: 'Confirm', download: 'Download', noData: 'No Data', wrong: 'Something went wrong, please try again later.', success: 'Success', failed: 'Failed', verify: 'Verify', unauthorizedTips: 'Unauthorized, please verify first.', stopResponding: 'Stop Responding', }, chat: { newChatButton: 'New Chat', newChatTitle: 'New Chat', placeholder: 'Ask me anything...(Shift + Enter = line break, "/" to trigger prompts)', placeholderMobile: 'Ask me anything...', copy: 'Copy', copied: 'Copied', copyCode: 'Copy Code', copyFailed: 'Copy Failed', clearChat: 'Clear Chat', clearChatConfirm: 'Are you sure to clear this chat?', exportImage: 'Export Image', exportImageConfirm: 'Are you sure to export this chat to png?', exportSuccess: 'Export Success', exportFailed: 'Export Failed', usingContext: 'Context Mode', turnOnContext: 'In the current mode, sending messages will carry previous chat records.', turnOffContext: 'In the current mode, sending messages will not carry previous chat records.', deleteMessage: 'Delete Message', deleteMessageConfirm: 'Are you sure to delete this message?', deleteHistoryConfirm: 'Are you sure to clear this history?', clearHistoryConfirm: 'Are you sure to clear chat history?', preview: 'Preview', showRawText: 'Show as raw text', thinking: 'Thinking...', }, setting: { setting: 'Setting', general: 'General', advanced: 'Advanced', config: 'Config', avatarLink: 'Avatar Link', name: 'Name', description: 'Description', role: 'Role', temperature: 'Temperature', top_p: 'Top_p', resetUserInfo: 'Reset UserInfo', chatHistory: 'ChatHistory', theme: 'Theme', language: 'Language', api: 'API', reverseProxy: 'Reverse Proxy', timeout: 'Timeout', socks: 'Socks', httpsProxy: 'HTTPS Proxy', balance: 'API Balance', monthlyUsage: 'Monthly Usage', openSource: 'This project is open sourced at', freeMIT: 'free and based on the MIT license, without any form of paid behavior!', stars: 'If you find this project helpful, please give me a Star on GitHub or give a little sponsorship, thank you!', }, store: { siderButton: 'Prompt Store', local: 'Local', online: 'Online', title: 'Title', description: 'Description', clearStoreConfirm: 'Whether to clear the data?', importPlaceholder: 'Please paste the JSON data here', addRepeatTitleTips: 'Title duplicate, please re-enter', addRepeatContentTips: 'Content duplicate: {msg}, please re-enter', editRepeatTitleTips: 'Title conflict, please revise', editRepeatContentTips: 'Content conflict {msg} , please re-modify', importError: 'Key value mismatch', importRepeatTitle: 'Title repeatedly skipped: {msg}', importRepeatContent: 'Content is repeatedly skipped: {msg}', onlineImportWarning: 'Note: Please check the JSON file source!', downloadError: 'Please check the network status and JSON file validity', }, } ================================================ FILE: src/locales/es-ES.ts ================================================ export default { common: { add: 'Agregar', addSuccess: 'Agregado con éxito', edit: 'Editar', editSuccess: 'Edición exitosa', delete: 'Borrar', deleteSuccess: 'Borrado con éxito', save: 'Guardar', saveSuccess: 'Guardado con éxito', reset: 'Reiniciar', action: 'Acción', export: 'Exportar', exportSuccess: 'Exportación exitosa', import: 'Importar', importSuccess: 'Importación exitosa', clear: 'Limpiar', clearSuccess: 'Limpieza exitosa', yes: 'Sí', no: 'No', confirm: 'Confirmar', download: 'Descargar', noData: 'Sin datos', wrong: 'Algo salió mal, inténtalo de nuevo más tarde.', success: 'Exitoso', failed: 'Fallido', verify: 'Verificar', unauthorizedTips: 'No autorizado, por favor verifique primero.', stopResponding: 'No responde', }, chat: { newChatButton: 'Nueva conversación', newChatTitle: 'Nueva conversación', placeholder: 'Pregúntame lo que sea...(Shift + Enter = salto de línea, "/" para activar avisos)', placeholderMobile: 'Pregúntame lo que sea...', copy: 'Copiar', copied: 'Copiado', copyCode: 'Copiar código', copyFailed: 'Copia fallida', clearChat: 'Limpiar chat', clearChatConfirm: '¿Estás seguro de borrar este chat?', exportImage: 'Exportar imagen', exportImageConfirm: '¿Estás seguro de exportar este chat a png?', exportSuccess: 'Exportación exitosa', exportFailed: 'Exportación fallida', usingContext: 'Modo de contexto', turnOnContext: 'En el modo actual, el envío de mensajes llevará registros de chat anteriores.', turnOffContext: 'En el modo actual, el envío de mensajes no incluirá registros de conversaciones anteriores.', deleteMessage: 'Borrar mensaje', deleteMessageConfirm: '¿Estás seguro de eliminar este mensaje?', deleteHistoryConfirm: '¿Estás seguro de borrar esta historia?', clearHistoryConfirm: '¿Estás seguro de borrar el historial de chat?', preview: 'Avance', showRawText: 'Mostrar como texto sin formato', }, setting: { setting: 'Configuración', general: 'General', advanced: 'Avanzado', config: 'Configurar', avatarLink: 'Enlace de avatar', name: 'Nombre', description: 'Descripción', role: 'Rol', temperature: 'Temperatura', top_p: 'Top_p', resetUserInfo: 'Restablecer información de usuario', chatHistory: 'Historial de chat', theme: 'Tema', language: 'Idioma', api: 'API', reverseProxy: 'Reverse Proxy', timeout: 'Tiempo de espera', socks: 'Socks', httpsProxy: 'HTTPS Proxy', balance: 'Saldo de API', monthlyUsage: 'Uso mensual de API', openSource: 'Este proyecto es de código abierto en', freeMIT: 'gratis y basado en la licencia MIT, ¡sin ningún tipo de comportamiento de pago!', stars: 'Si encuentras este proyecto útil, por favor dame una Estrella en GitHub o da un pequeño patrocinio, ¡gracias!', }, store: { siderButton: 'Tienda rápida', local: 'Local', online: 'En línea', title: 'Título', description: 'Descripción', clearStoreConfirm: '¿Estás seguro de borrar los datos?', importPlaceholder: 'Pegue los datos JSON aquí', addRepeatTitleTips: 'Título duplicado, vuelva a ingresar', addRepeatContentTips: 'Contenido duplicado: {msg}, por favor vuelva a entrar', editRepeatTitleTips: 'Conflicto de título, revíselo', editRepeatContentTips: 'Conflicto de contenido {msg} , por favor vuelva a modificar', importError: 'Discrepancia de valor clave', importRepeatTitle: 'Título saltado repetidamente: {msg}', importRepeatContent: 'El contenido se salta repetidamente: {msg}', onlineImportWarning: 'Nota: ¡Compruebe la fuente del archivo JSON!', downloadError: 'Verifique el estado de la red y la validez del archivo JSON', }, } ================================================ FILE: src/locales/index.ts ================================================ import type { App } from 'vue' import { createI18n } from 'vue-i18n' import enUS from './en-US' import esES from './es-ES' import koKR from './ko-KR' import ruRU from './ru-RU' import viVN from './vi-VN' import zhCN from './zh-CN' import zhTW from './zh-TW' import { useAppStoreWithOut } from '@/store/modules/app' import type { Language } from '@/store/modules/app/helper' const appStore = useAppStoreWithOut() const defaultLocale = appStore.language || 'zh-CN' const i18n = createI18n({ locale: defaultLocale, fallbackLocale: 'en-US', allowComposition: true, messages: { 'en-US': enUS, 'es-ES': esES, 'ko-KR': koKR, 'ru-RU': ruRU, 'vi-VN': viVN, 'zh-CN': zhCN, 'zh-TW': zhTW, }, }) export const t = i18n.global.t export function setLocale(locale: Language) { i18n.global.locale = locale } export function setupI18n(app: App) { app.use(i18n) } export default i18n ================================================ FILE: src/locales/ko-KR.ts ================================================ export default { common: { add: '추가', addSuccess: '추가 성공', edit: '편집', editSuccess: '편집 성공', delete: '삭제', deleteSuccess: '삭제 성공', save: '저장', saveSuccess: '저장 성공', reset: '초기화', action: '액션', export: '내보내기', exportSuccess: '내보내기 성공', import: '가져오기', importSuccess: '가져오기 성공', clear: '비우기', clearSuccess: '비우기 성공', yes: '예', no: '아니오', confirm: '확인', download: '다운로드', noData: '데이터 없음', wrong: '문제가 발생했습니다. 나중에 다시 시도하십시오.', success: '성공', failed: '실패', verify: '검증', unauthorizedTips: '인증되지 않았습니다. 먼저 확인하십시오.', stopResponding: '응답 중지', }, chat: { newChatButton: '새로운 채팅', newChatTitle: '새로운 채팅', placeholder: '무엇이든 물어보세요...(Shift + Enter = 줄바꿈, "/"를 눌러서 힌트를 보세요)', placeholderMobile: '무엇이든 물어보세요...', copy: '복사', copied: '복사됨', copyCode: '코드 복사', copyFailed: '복사 실패', clearChat: '채팅 비우기', clearChatConfirm: '이 채팅을 비우시겠습니까?', exportImage: '이미지 내보내기', exportImageConfirm: '이 채팅을 png로 내보내시겠습니까?', exportSuccess: '내보내기 성공', exportFailed: '내보내기 실패', usingContext: '컨텍스트 모드', turnOnContext: '현재 모드에서는 이전 대화 기록을 포함하여 메시지를 보낼 수 있습니다.', turnOffContext: '현재 모드에서는 이전 대화 기록을 포함하지 않고 메시지를 보낼 수 있습니다.', deleteMessage: '메시지 삭제', deleteMessageConfirm: '이 메시지를 삭제하시겠습니까?', deleteHistoryConfirm: '이 기록을 삭제하시겠습니까?', clearHistoryConfirm: '채팅 기록을 삭제하시겠습니까?', preview: '미리보기', showRawText: '원본 텍스트로 보기', thinking: '생각 중...', }, setting: { setting: '설정', general: '일반', advanced: '고급', config: '설정', avatarLink: '아바타 링크', name: '이름', description: '설명', role: '역할', temperature: '온도', top_p: 'Top_p', resetUserInfo: '사용자 정보 초기화', chatHistory: '채팅 기록', theme: '테마', language: '언어', api: 'API', reverseProxy: '리버스 프록시', timeout: '타임아웃', socks: 'Socks', httpsProxy: 'HTTPS 프록시', balance: 'API 잔액', monthlyUsage: '월 사용량', openSource: '이 프로젝트는 다음에서 오픈 소스로 제공됩니다:', freeMIT: '무료이며 MIT 라이선스에 기반하며, 어떠한 형태의 유료 행동도 없습니다!', stars: '이 프로젝트가 도움이 되었다면, GitHub에서 별을 주거나 조금의 후원을 해주시면 감사하겠습니다!', }, store: { siderButton: '프롬프트 저장소', local: '로컬', online: '온라인', title: '제목', description: '설명', clearStoreConfirm: '데이터를 삭제하시겠습니까?', importPlaceholder: '여기에 JSON 데이터를 붙여넣으십시오', addRepeatTitleTips: '제목 중복됨, 다시 입력하십시오', addRepeatContentTips: '내용 중복됨: {msg}, 다시 입력하십시오', editRepeatTitleTips: '제목 충돌, 수정하십시오', editRepeatContentTips: '내용 충돌 {msg} , 수정하십시오', importError: '키 값 불일치', importRepeatTitle: '제목이 반복되어 건너뜀: {msg}', importRepeatContent: '내용이 반복되어 건너뜀: {msg}', onlineImportWarning: '참고: JSON 파일 소스를 확인하십시오!', }, } ================================================ FILE: src/locales/ru-RU.ts ================================================ export default { common: { add: 'Добавить', addSuccess: 'Добавлено успешно', edit: 'Редактировать', editSuccess: 'Изменено успешно', delete: 'Удалить', deleteSuccess: 'Удалено успешно', save: 'Сохранить', saveSuccess: 'Сохранено успешно', reset: 'Сбросить', action: 'Действие', export: 'Экспортировать', exportSuccess: 'Экспорт выполнен успешно', import: 'Импортировать', importSuccess: 'Импорт выполнен успешно', clear: 'Очистить', clearSuccess: 'Очищено успешно', yes: 'Да', no: 'Нет', confirm: 'Подтвердить', download: 'Загрузить', noData: 'Нет данных', wrong: 'Что-то пошло не так, пожалуйста, повторите попытку позже.', success: 'Успех', failed: 'Не удалось', verify: 'Проверить', unauthorizedTips: 'Не авторизован, сначала подтвердите свою личность.', stopResponding: 'Прекращение отклика', }, chat: { newChatButton: 'Новый чат', newChatTitle: 'Новый чат', placeholder: 'Спросите меня о чем-нибудь ... (Shift + Enter = перенос строки, "/" для вызова подсказок)', placeholderMobile: 'Спросите меня о чем-нибудь ...', copy: 'Копировать', copied: 'Скопировано', copyCode: 'Копировать код', copyFailed: 'Не удалось скопировать', clearChat: 'Очистить чат', clearChatConfirm: 'Вы уверены, что хотите очистить этот чат?', exportImage: 'Экспорт в изображение', exportImageConfirm: 'Вы уверены, что хотите экспортировать этот чат в формате PNG?', exportSuccess: 'Экспортировано успешно', exportFailed: 'Не удалось выполнить экспорт', usingContext: 'Режим контекста', turnOnContext: 'В текущем режиме отправка сообщений будет включать предыдущие записи чата.', turnOffContext: 'В текущем режиме отправка сообщений не будет включать предыдущие записи чата.', deleteMessage: 'Удалить сообщение', deleteMessageConfirm: 'Вы уверены, что хотите удалить это сообщение?', deleteHistoryConfirm: 'Вы уверены, что хотите очистить эту историю?', clearHistoryConfirm: 'Вы уверены, что хотите очистить историю чата?', preview: 'Предварительный просмотр', showRawText: 'Показать как обычный текст', thinking: 'Думаю...', }, setting: { setting: 'Настройки', general: 'Общее', advanced: 'Дополнительно', config: 'Конфигурация', avatarLink: 'Ссылка на аватар', name: 'Имя', description: 'Описание', role: 'Роль', temperature: 'Температура', top_p: 'Top_p', resetUserInfo: 'Сбросить информацию о пользователе', chatHistory: 'История чата', theme: 'Тема', language: 'Язык', api: 'API', reverseProxy: 'Обратный прокси-сервер', timeout: 'Время ожидания', socks: 'Socks', httpsProxy: 'HTTPS-прокси', balance: 'Баланс API', monthlyUsage: 'Ежемесячное использование', openSource: 'Этот проект опубликован в открытом доступе на', freeMIT: 'бесплатно и основан на лицензии MIT, без каких-либо форм оплаты!', stars: 'Если вы считаете этот проект полезным, пожалуйста, поставьте мне звезду на GitHub или сделайте небольшое пожертвование, спасибо!', }, store: { siderButton: 'Хранилище подсказок', local: 'Локальное', online: 'Онлайн', title: 'Название', description: 'Описание', clearStoreConfirm: 'Вы действительно хотите очистить данные?', importPlaceholder: 'Пожалуйста, вставьте здесь JSON-данные', addRepeatTitleTips: 'Дубликат названия, пожалуйста, введите другое название', addRepeatContentTips: 'Дубликат содержимого: {msg}, пожалуйста, введите другой текст', editRepeatTitleTips: 'Конфликт названий, пожалуйста, измените название', editRepeatContentTips: 'Конфликт содержимого {msg}, пожалуйста, измените текст', importError: 'Не совпадает ключ-значение', importRepeatTitle: 'Название повторяющееся, пропускается: {msg}', importRepeatContent: 'Содержание повторяющееся, пропускается: {msg}', onlineImportWarning: 'Внимание! Проверьте источник JSON-файла!', downloadError: 'Проверьте состояние сети и правильность JSON-файла', }, } ================================================ FILE: src/locales/vi-VN.ts ================================================ export default { common: { add: 'Thêm', addSuccess: 'Thêm thành công', edit: 'Sửa', editSuccess: 'Sửa thành công', delete: 'Xóa', deleteSuccess: 'Xóa thành công', save: 'Lưu', saveSuccess: 'Lưu thành công', reset: 'Đặt lại', action: 'Hành động', export: 'Xuất', exportSuccess: 'Xuất thành công', import: 'Nhập', importSuccess: 'Nhập thành công', clear: 'Dọn dẹp', clearSuccess: 'Dọn dẹp thành công', yes: 'Có', no: 'Không', confirm: 'Xác nhận', download: 'Tải xuống', noData: 'Không có dữ liệu', wrong: 'Đã xảy ra lỗi, vui lòng thử lại sau.', success: 'Thành công', failed: 'Thất bại', verify: 'Xác minh', unauthorizedTips: 'Không được ủy quyền, vui lòng xác minh trước.', }, chat: { newChatButton: 'Tạo hội thoại', newChatTitle: 'Tạo hội thoại', placeholder: 'Hỏi tôi bất cứ điều gì...(Shift + Enter = ngắt dòng, "/" to trigger prompts)', placeholderMobile: 'Hỏi tôi bất cứ điều gì...', copy: 'Sao chép', copied: 'Đã sao chép', copyCode: 'Sao chép Code', copyFailed: 'Sao chép thất bại', clearChat: 'Clear Chat', clearChatConfirm: 'Bạn có chắc chắn xóa cuộc trò chuyện này?', exportImage: 'Xuất hình ảnh', exportImageConfirm: 'Bạn có chắc chắn xuất cuộc trò chuyện này sang png không?', exportSuccess: 'Xuất thành công', exportFailed: 'Xuất thất bại', usingContext: 'Context Mode', 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 đó.', 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 đó.', deleteMessage: 'Xóa tin nhắn', deleteMessageConfirm: 'Bạn có chắc chắn xóa tin nhắn này?', deleteHistoryConfirm: 'Bạn có chắc chắn để xóa lịch sử này?', clearHistoryConfirm: 'Bạn có chắc chắn để xóa lịch sử trò chuyện?', preview: 'Xem trước', showRawText: 'Hiển thị dưới dạng văn bản thô', thinking: 'Đang suy nghĩ...', }, setting: { setting: 'Cài đặt', general: 'Chung', advanced: 'Nâng cao', config: 'Cấu hình', avatarLink: 'Avatar Link', name: 'Tên', description: 'Miêu tả', role: 'Vai trò', temperature: 'Nhiệt độ', top_p: 'Top_p', resetUserInfo: 'Đặt lại thông tin người dùng', chatHistory: 'Lịch sử trò chuyện', theme: 'Giao diện', language: 'Ngôn ngữ', api: 'API', reverseProxy: 'Reverse Proxy', timeout: 'Timeout', socks: 'Socks', httpsProxy: 'HTTPS Proxy', balance: 'API Balance', monthlyUsage: 'Sử dụng hàng tháng', openSource: 'Dự án này được mở nguồn tại', 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!', 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!', }, store: { siderButton: 'Prompt Store', local: 'Local', online: 'Online', title: 'Tiêu đề', description: 'Miêu tả', clearStoreConfirm: 'Cho dù để xóa dữ liệu?', importPlaceholder: 'Vui lòng dán dữ liệu JSON vào đây', addRepeatTitleTips: 'Tiêu đề trùng lặp, vui lòng nhập lại', addRepeatContentTips: 'Nội dung trùng lặp: {msg}, vui lòng nhập lại', editRepeatTitleTips: 'Xung đột tiêu đề, vui lòng sửa lại', editRepeatContentTips: 'Xung đột nội dung {msg} , vui lòng sửa đổi lại', importError: 'Key value mismatch', importRepeatTitle: 'Tiêu đề liên tục bị bỏ qua: {msg}', importRepeatContent: 'Nội dung liên tục bị bỏ qua: {msg}', onlineImportWarning: 'Lưu ý: Vui lòng kiểm tra nguồn tệp JSON!', downloadError: 'Vui lòng kiểm tra trạng thái mạng và tính hợp lệ của tệp JSON', }, } ================================================ FILE: src/locales/zh-CN.ts ================================================ export default { common: { add: '添加', addSuccess: '添加成功', edit: '编辑', editSuccess: '编辑成功', delete: '删除', deleteSuccess: '删除成功', save: '保存', saveSuccess: '保存成功', reset: '重置', action: '操作', export: '导出', exportSuccess: '导出成功', import: '导入', importSuccess: '导入成功', clear: '清空', clearSuccess: '清空成功', yes: '是', no: '否', confirm: '确定', download: '下载', noData: '暂无数据', wrong: '好像出错了,请稍后再试。', success: '操作成功', failed: '操作失败', verify: '验证', unauthorizedTips: '未经授权,请先进行验证。', stopResponding: '停止响应', }, chat: { newChatButton: '新建聊天', newChatTitle: '新建聊天', placeholder: '来说点什么吧...(Shift + Enter = 换行,"/" 触发提示词)', placeholderMobile: '来说点什么...', copy: '复制', copied: '复制成功', copyCode: '复制代码', copyFailed: '复制失败', clearChat: '清空会话', clearChatConfirm: '是否清空会话?', exportImage: '保存会话到图片', exportImageConfirm: '是否将会话保存为图片?', exportSuccess: '保存成功', exportFailed: '保存失败', usingContext: '上下文模式', turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录', turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录', deleteMessage: '删除消息', deleteMessageConfirm: '是否删除此消息?', deleteHistoryConfirm: '确定删除此记录?', clearHistoryConfirm: '确定清空记录?', preview: '预览', showRawText: '显示原文', thinking: '思考中...', }, setting: { setting: '设置', general: '总览', advanced: '高级', config: '配置', avatarLink: '头像链接', name: '名称', description: '描述', role: '角色设定', temperature: 'Temperature', top_p: 'Top_p', resetUserInfo: '重置用户信息', chatHistory: '聊天记录', theme: '主题', language: '语言', api: 'API', reverseProxy: '反向代理', timeout: '超时', socks: 'Socks', httpsProxy: 'HTTPS Proxy', balance: 'API余额', monthlyUsage: '本月使用量', openSource: '此项目开源于', freeMIT: '免费且基于 MIT 协议,没有任何形式的付费行为', stars: '如果你觉得此项目对你有帮助,请在 GitHub 上给我一个星星或者给予一点赞助,谢谢!', }, store: { siderButton: '提示词商店', local: '本地', online: '在线', title: '标题', description: '描述', clearStoreConfirm: '是否清空数据?', importPlaceholder: '请粘贴 JSON 数据到此处', addRepeatTitleTips: '标题重复,请重新输入', addRepeatContentTips: '内容重复:{msg},请重新输入', editRepeatTitleTips: '标题冲突,请重新修改', editRepeatContentTips: '内容冲突{msg} ,请重新修改', importError: '键值不匹配', importRepeatTitle: '标题重复跳过:{msg}', importRepeatContent: '内容重复跳过:{msg}', onlineImportWarning: '注意:请检查 JSON 文件来源!', downloadError: '请检查网络状态与 JSON 文件有效性', }, } ================================================ FILE: src/locales/zh-TW.ts ================================================ export default { common: { add: '新增', addSuccess: '新增成功', edit: '編輯', editSuccess: '編輯成功', delete: '刪除', deleteSuccess: '刪除成功', save: '儲存', saveSuccess: '儲存成功', reset: '重設', action: '操作', export: '匯出', exportSuccess: '匯出成功', import: '匯入', importSuccess: '匯入成功', clear: '清除', clearSuccess: '清除成功', yes: '是', no: '否', confirm: '確認', download: '下載', noData: '目前無資料', wrong: '發生錯誤,請稍後再試。', success: '操作成功', failed: '操作失敗', verify: '驗證', unauthorizedTips: '未經授權,請先進行驗證。', stopResponding: '停止回應', }, chat: { newChatButton: '新增對話', newChatTitle: '新增對話', placeholder: '來說點什麼...(Shift + Enter = 換行,"/" 觸發提示詞)', placeholderMobile: '來說點什麼...', copy: '複製', copied: '複製成功', copyCode: '複製代碼', copyFailed: '複製失敗', clearChat: '清除對話', clearChatConfirm: '是否清空對話?', exportImage: '儲存對話為圖片', exportImageConfirm: '是否將對話儲存為圖片?', exportSuccess: '儲存成功', exportFailed: '儲存失敗', usingContext: '上下文模式', turnOnContext: '啟用上下文模式,在此模式下,發送訊息會包含之前的聊天記錄。', turnOffContext: '關閉上下文模式,在此模式下,發送訊息不會包含之前的聊天記錄。', deleteMessage: '刪除訊息', deleteMessageConfirm: '是否刪除此訊息?', deleteHistoryConfirm: '確定刪除此紀錄?', clearHistoryConfirm: '確定清除紀錄?', preview: '預覽', showRawText: '顯示原文', thinking: '思考中...', }, setting: { setting: '設定', general: '總覽', advanced: '進階', config: '設定', avatarLink: '頭貼連結', name: '名稱', description: '描述', role: '角色設定', temperature: 'Temperature', top_p: 'Top_p', resetUserInfo: '重設使用者資訊', chatHistory: '紀錄', theme: '主題', language: '語言', api: 'API', reverseProxy: '反向代理', timeout: '逾時', socks: 'Socks', httpsProxy: 'HTTPS Proxy', balance: 'API Credit 餘額', monthlyUsage: '本月使用量', openSource: '此專案在此開源:', freeMIT: '免費且基於 MIT 授權,沒有任何形式的付費行為!', stars: '如果你覺得此專案對你有幫助,請在 GitHub 上給我一顆星,或者贊助我,謝謝!', }, store: { siderButton: '提示詞商店', local: '本機', online: '線上', title: '標題', description: '描述', clearStoreConfirm: '是否清除資料?', importPlaceholder: '請將 JSON 資料貼在此處', addRepeatTitleTips: '標題重複,請重新輸入', addRepeatContentTips: '內容重複:{msg},請重新輸入', editRepeatTitleTips: '標題衝突,請重新修改', editRepeatContentTips: '內容衝突{msg} ,請重新修改', importError: '鍵值不符合', importRepeatTitle: '因標題重複跳過:{msg}', importRepeatContent: '因內容重複跳過:{msg}', onlineImportWarning: '注意:請檢查 JSON 檔案來源!', downloadError: '請檢查網路狀態與 JSON 檔案有效性', }, } ================================================ FILE: src/main.ts ================================================ import { createApp } from 'vue' import App from './App.vue' import { setupI18n } from './locales' import { setupAssets, setupScrollbarStyle } from './plugins' import { setupStore } from './store' import { setupRouter } from './router' async function bootstrap() { const app = createApp(App) setupAssets() setupScrollbarStyle() setupStore(app) setupI18n(app) await setupRouter(app) app.mount('#app') } bootstrap() ================================================ FILE: src/plugins/assets.ts ================================================ import 'katex/dist/katex.min.css' import '@/styles/lib/tailwind.css' import '@/styles/lib/highlight.less' import '@/styles/lib/github-markdown.less' import '@/styles/global.less' /** Tailwind's Preflight Style Override */ function naiveStyleOverride() { const meta = document.createElement('meta') meta.name = 'naive-ui-style' document.head.appendChild(meta) } function setupAssets() { naiveStyleOverride() } export default setupAssets ================================================ FILE: src/plugins/index.ts ================================================ import setupAssets from './assets' import setupScrollbarStyle from './scrollbarStyle' export { setupAssets, setupScrollbarStyle } ================================================ FILE: src/plugins/scrollbarStyle.ts ================================================ import { darkTheme, lightTheme } from 'naive-ui' const setupScrollbarStyle = () => { const style = document.createElement('style') const styleContent = ` ::-webkit-scrollbar { background-color: transparent; width: ${lightTheme.Scrollbar.common?.scrollbarWidth}; } ::-webkit-scrollbar-thumb { background-color: ${lightTheme.Scrollbar.common?.scrollbarColor}; border-radius: ${lightTheme.Scrollbar.common?.scrollbarBorderRadius}; } html.dark ::-webkit-scrollbar { background-color: transparent; width: ${darkTheme.Scrollbar.common?.scrollbarWidth}; } html.dark ::-webkit-scrollbar-thumb { background-color: ${darkTheme.Scrollbar.common?.scrollbarColor}; border-radius: ${darkTheme.Scrollbar.common?.scrollbarBorderRadius}; } ` style.innerHTML = styleContent document.head.appendChild(style) } export default setupScrollbarStyle ================================================ FILE: src/router/index.ts ================================================ import type { App } from 'vue' import type { RouteRecordRaw } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router' import { setupPageGuard } from './permission' import { ChatLayout } from '@/views/chat/layout' const routes: RouteRecordRaw[] = [ { path: '/', name: 'Root', component: ChatLayout, redirect: '/chat', children: [ { path: '/chat/:uuid?', name: 'Chat', component: () => import('@/views/chat/index.vue'), }, ], }, { path: '/404', name: '404', component: () => import('@/views/exception/404/index.vue'), }, { path: '/500', name: '500', component: () => import('@/views/exception/500/index.vue'), }, { path: '/:pathMatch(.*)*', name: 'notFound', redirect: '/404', }, ] export const router = createRouter({ history: createWebHashHistory(), routes, scrollBehavior: () => ({ left: 0, top: 0 }), }) setupPageGuard(router) export async function setupRouter(app: App) { app.use(router) await router.isReady() } ================================================ FILE: src/router/permission.ts ================================================ import type { Router } from 'vue-router' import { useAuthStoreWithout } from '@/store/modules/auth' export function setupPageGuard(router: Router) { router.beforeEach(async (to, from, next) => { const authStore = useAuthStoreWithout() if (!authStore.session) { try { const data = await authStore.getSession() if (String(data.auth) === 'false' && authStore.token) authStore.removeToken() if (to.path === '/500') next({ name: 'Root' }) else next() } catch (error) { if (to.path !== '/500') next({ name: '500' }) else next() } } else { next() } }) } ================================================ FILE: src/store/helper.ts ================================================ import { createPinia } from 'pinia' export const store = createPinia() ================================================ FILE: src/store/index.ts ================================================ import type { App } from 'vue' import { store } from './helper' export function setupStore(app: App) { app.use(store) } export * from './modules' ================================================ FILE: src/store/modules/app/helper.ts ================================================ import { ss } from '@/utils/storage' const LOCAL_NAME = 'appSetting' export type Theme = 'light' | 'dark' | 'auto' export type Language = 'en-US' | 'es-ES' | 'ko-KR' | 'ru-RU' | 'vi-VN' | 'zh-CN' | 'zh-TW' const languageMap: { [key: string]: Language } = { 'en': 'en-US', 'en-US': 'en-US', 'es': 'es-ES', 'es-ES': 'es-ES', 'ko': 'ko-KR', 'ko-KR': 'ko-KR', 'ru': 'ru-RU', 'ru-RU': 'ru-RU', 'vi': 'vi-VN', 'vi-VN': 'vi-VN', 'zh': 'zh-CN', 'zh-CN': 'zh-CN', 'zh-TW': 'zh-TW', } export interface AppState { siderCollapsed: boolean theme: Theme language: Language } export function defaultSetting(): AppState { const language = languageMap[navigator.language] return { siderCollapsed: false, theme: 'light', language } } export function getLocalSetting(): AppState { const localSetting: AppState | undefined = ss.get(LOCAL_NAME) return { ...defaultSetting(), ...localSetting } } export function setLocalSetting(setting: AppState): void { ss.set(LOCAL_NAME, setting) } ================================================ FILE: src/store/modules/app/index.ts ================================================ import { defineStore } from 'pinia' import type { AppState, Language, Theme } from './helper' import { getLocalSetting, setLocalSetting } from './helper' import { store } from '@/store/helper' export const useAppStore = defineStore('app-store', { state: (): AppState => getLocalSetting(), actions: { setSiderCollapsed(collapsed: boolean) { this.siderCollapsed = collapsed this.recordState() }, setTheme(theme: Theme) { this.theme = theme this.recordState() }, setLanguage(language: Language) { if (this.language !== language) { this.language = language this.recordState() } }, recordState() { setLocalSetting(this.$state) }, }, }) export function useAppStoreWithOut() { return useAppStore(store) } ================================================ FILE: src/store/modules/auth/helper.ts ================================================ import { ss } from '@/utils/storage' const LOCAL_NAME = 'SECRET_TOKEN' export function getToken() { return ss.get(LOCAL_NAME) } export function setToken(token: string) { return ss.set(LOCAL_NAME, token) } export function removeToken() { return ss.remove(LOCAL_NAME) } ================================================ FILE: src/store/modules/auth/index.ts ================================================ import { defineStore } from 'pinia' import { getToken, removeToken, setToken } from './helper' import { store } from '@/store/helper' import { fetchSession } from '@/api' interface SessionResponse { auth: boolean model: 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' } export interface AuthState { token: string | undefined session: SessionResponse | null } export const useAuthStore = defineStore('auth-store', { state: (): AuthState => ({ token: getToken(), session: null, }), getters: { isChatGPTAPI(state): boolean { return state.session?.model === 'ChatGPTAPI' }, }, actions: { async getSession() { try { const { data } = await fetchSession() this.session = { ...data } return Promise.resolve(data) } catch (error) { return Promise.reject(error) } }, setToken(token: string) { this.token = token setToken(token) }, removeToken() { this.token = undefined removeToken() }, }, }) export function useAuthStoreWithout() { return useAuthStore(store) } ================================================ FILE: src/store/modules/chat/helper.ts ================================================ import { ss } from '@/utils/storage' import { t } from '@/locales' const LOCAL_NAME = 'chatStorage' export function defaultState(): Chat.ChatState { const uuid = 1002 return { active: uuid, usingContext: true, history: [{ uuid, title: t('chat.newChatTitle'), isEdit: false }], chat: [{ uuid, data: [] }], } } export function getLocalState(): Chat.ChatState { const localState = ss.get(LOCAL_NAME) return { ...defaultState(), ...localState } } export function setLocalState(state: Chat.ChatState) { ss.set(LOCAL_NAME, state) } ================================================ FILE: src/store/modules/chat/index.ts ================================================ import { defineStore } from 'pinia' import { defaultState, getLocalState, setLocalState } from './helper' import { router } from '@/router' import { t } from '@/locales' export const useChatStore = defineStore('chat-store', { state: (): Chat.ChatState => getLocalState(), getters: { getChatHistoryByCurrentActive(state: Chat.ChatState) { const index = state.history.findIndex(item => item.uuid === state.active) if (index !== -1) return state.history[index] return null }, getChatByUuid(state: Chat.ChatState) { return (uuid?: number) => { if (uuid) return state.chat.find(item => item.uuid === uuid)?.data ?? [] return state.chat.find(item => item.uuid === state.active)?.data ?? [] } }, }, actions: { setUsingContext(context: boolean) { this.usingContext = context this.recordState() }, addHistory(history: Chat.History, chatData: Chat.Chat[] = []) { this.history.unshift(history) this.chat.unshift({ uuid: history.uuid, data: chatData }) this.active = history.uuid this.reloadRoute(history.uuid) }, updateHistory(uuid: number, edit: Partial) { const index = this.history.findIndex(item => item.uuid === uuid) if (index !== -1) { this.history[index] = { ...this.history[index], ...edit } this.recordState() } }, async deleteHistory(index: number) { this.history.splice(index, 1) this.chat.splice(index, 1) if (this.history.length === 0) { this.active = null this.reloadRoute() return } if (index > 0 && index <= this.history.length) { const uuid = this.history[index - 1].uuid this.active = uuid this.reloadRoute(uuid) return } if (index === 0) { if (this.history.length > 0) { const uuid = this.history[0].uuid this.active = uuid this.reloadRoute(uuid) } } if (index > this.history.length) { const uuid = this.history[this.history.length - 1].uuid this.active = uuid this.reloadRoute(uuid) } }, async setActive(uuid: number) { this.active = uuid return await this.reloadRoute(uuid) }, getChatByUuidAndIndex(uuid: number, index: number) { if (!uuid || uuid === 0) { if (this.chat.length) return this.chat[0].data[index] return null } const chatIndex = this.chat.findIndex(item => item.uuid === uuid) if (chatIndex !== -1) return this.chat[chatIndex].data[index] return null }, addChatByUuid(uuid: number, chat: Chat.Chat) { if (!uuid || uuid === 0) { if (this.history.length === 0) { const uuid = Date.now() this.history.push({ uuid, title: chat.text, isEdit: false }) this.chat.push({ uuid, data: [chat] }) this.active = uuid this.recordState() } else { this.chat[0].data.push(chat) if (this.history[0].title === t('chat.newChatTitle')) this.history[0].title = chat.text this.recordState() } } const index = this.chat.findIndex(item => item.uuid === uuid) if (index !== -1) { this.chat[index].data.push(chat) if (this.history[index].title === t('chat.newChatTitle')) this.history[index].title = chat.text this.recordState() } }, updateChatByUuid(uuid: number, index: number, chat: Chat.Chat) { if (!uuid || uuid === 0) { if (this.chat.length) { this.chat[0].data[index] = chat this.recordState() } return } const chatIndex = this.chat.findIndex(item => item.uuid === uuid) if (chatIndex !== -1) { this.chat[chatIndex].data[index] = chat this.recordState() } }, updateChatSomeByUuid(uuid: number, index: number, chat: Partial) { if (!uuid || uuid === 0) { if (this.chat.length) { this.chat[0].data[index] = { ...this.chat[0].data[index], ...chat } this.recordState() } return } const chatIndex = this.chat.findIndex(item => item.uuid === uuid) if (chatIndex !== -1) { this.chat[chatIndex].data[index] = { ...this.chat[chatIndex].data[index], ...chat } this.recordState() } }, deleteChatByUuid(uuid: number, index: number) { if (!uuid || uuid === 0) { if (this.chat.length) { this.chat[0].data.splice(index, 1) this.recordState() } return } const chatIndex = this.chat.findIndex(item => item.uuid === uuid) if (chatIndex !== -1) { this.chat[chatIndex].data.splice(index, 1) this.recordState() } }, clearChatByUuid(uuid: number) { if (!uuid || uuid === 0) { if (this.chat.length) { this.chat[0].data = [] this.recordState() } return } const index = this.chat.findIndex(item => item.uuid === uuid) if (index !== -1) { this.chat[index].data = [] this.recordState() } }, clearHistory() { this.$state = { ...defaultState() } this.recordState() }, async reloadRoute(uuid?: number) { this.recordState() await router.push({ name: 'Chat', params: { uuid } }) }, recordState() { setLocalState(this.$state) }, }, }) ================================================ FILE: src/store/modules/index.ts ================================================ export * from './app' export * from './chat' export * from './user' export * from './prompt' export * from './settings' export * from './auth' ================================================ FILE: src/store/modules/prompt/helper.ts ================================================ import { ss } from '@/utils/storage' const LOCAL_NAME = 'promptStore' export type PromptList = [] export interface PromptStore { promptList: PromptList } export function getLocalPromptList(): PromptStore { const promptStore: PromptStore | undefined = ss.get(LOCAL_NAME) return promptStore ?? { promptList: [] } } export function setLocalPromptList(promptStore: PromptStore): void { ss.set(LOCAL_NAME, promptStore) } ================================================ FILE: src/store/modules/prompt/index.ts ================================================ import { defineStore } from 'pinia' import type { PromptStore } from './helper' import { getLocalPromptList, setLocalPromptList } from './helper' export const usePromptStore = defineStore('prompt-store', { state: (): PromptStore => getLocalPromptList(), actions: { updatePromptList(promptList: []) { this.$patch({ promptList }) setLocalPromptList({ promptList }) }, getPromptList() { return this.$state }, }, }) ================================================ FILE: src/store/modules/settings/helper.ts ================================================ import { ss } from '@/utils/storage' const LOCAL_NAME = 'settingsStorage' export interface SettingsState { systemMessage: string temperature: number top_p: number } export function defaultSetting(): SettingsState { return { systemMessage: 'You are ChatGPT, a large language model trained by OpenAI. Follow the user\'s instructions carefully. Respond using markdown.', temperature: 0.8, top_p: 1, } } export function getLocalState(): SettingsState { const localSetting: SettingsState | undefined = ss.get(LOCAL_NAME) return { ...defaultSetting(), ...localSetting } } export function setLocalState(setting: SettingsState): void { ss.set(LOCAL_NAME, setting) } export function removeLocalState() { ss.remove(LOCAL_NAME) } ================================================ FILE: src/store/modules/settings/index.ts ================================================ import { defineStore } from 'pinia' import type { SettingsState } from './helper' import { defaultSetting, getLocalState, removeLocalState, setLocalState } from './helper' export const useSettingStore = defineStore('setting-store', { state: (): SettingsState => getLocalState(), actions: { updateSetting(settings: Partial) { this.$state = { ...this.$state, ...settings } this.recordState() }, resetSetting() { this.$state = defaultSetting() removeLocalState() }, recordState() { setLocalState(this.$state) }, }, }) ================================================ FILE: src/store/modules/user/helper.ts ================================================ import { ss } from '@/utils/storage' const LOCAL_NAME = 'userStorage' export interface UserInfo { avatar: string name: string description: string } export interface UserState { userInfo: UserInfo } export function defaultSetting(): UserState { return { userInfo: { avatar: 'https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg', name: 'ChenZhaoYu', description: 'Star on GitHub', }, } } export function getLocalState(): UserState { const localSetting: UserState | undefined = ss.get(LOCAL_NAME) return { ...defaultSetting(), ...localSetting } } export function setLocalState(setting: UserState): void { ss.set(LOCAL_NAME, setting) } ================================================ FILE: src/store/modules/user/index.ts ================================================ import { defineStore } from 'pinia' import type { UserInfo, UserState } from './helper' import { defaultSetting, getLocalState, setLocalState } from './helper' export const useUserStore = defineStore('user-store', { state: (): UserState => getLocalState(), actions: { updateUserInfo(userInfo: Partial) { this.userInfo = { ...this.userInfo, ...userInfo } this.recordState() }, resetUserInfo() { this.userInfo = { ...defaultSetting().userInfo } this.recordState() }, recordState() { setLocalState(this.$state) }, }, }) ================================================ FILE: src/styles/global.less ================================================ html, body, #app { height: 100%; } body { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); } ================================================ FILE: src/styles/lib/github-markdown.less ================================================ html.dark { .markdown-body { color-scheme: dark; --color-prettylights-syntax-comment: #8b949e; --color-prettylights-syntax-constant: #79c0ff; --color-prettylights-syntax-entity: #d2a8ff; --color-prettylights-syntax-storage-modifier-import: #c9d1d9; --color-prettylights-syntax-entity-tag: #7ee787; --color-prettylights-syntax-keyword: #ff7b72; --color-prettylights-syntax-string: #a5d6ff; --color-prettylights-syntax-variable: #ffa657; --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; --color-prettylights-syntax-invalid-illegal-bg: #8e1519; --color-prettylights-syntax-carriage-return-text: #f0f6fc; --color-prettylights-syntax-carriage-return-bg: #b62324; --color-prettylights-syntax-string-regexp: #7ee787; --color-prettylights-syntax-markup-list: #f2cc60; --color-prettylights-syntax-markup-heading: #1f6feb; --color-prettylights-syntax-markup-italic: #c9d1d9; --color-prettylights-syntax-markup-bold: #c9d1d9; --color-prettylights-syntax-markup-deleted-text: #ffdcd7; --color-prettylights-syntax-markup-deleted-bg: #67060c; --color-prettylights-syntax-markup-inserted-text: #aff5b4; --color-prettylights-syntax-markup-inserted-bg: #033a16; --color-prettylights-syntax-markup-changed-text: #ffdfb6; --color-prettylights-syntax-markup-changed-bg: #5a1e02; --color-prettylights-syntax-markup-ignored-text: #c9d1d9; --color-prettylights-syntax-markup-ignored-bg: #1158c7; --color-prettylights-syntax-meta-diff-range: #d2a8ff; --color-prettylights-syntax-brackethighlighter-angle: #8b949e; --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; --color-fg-default: #c9d1d9; --color-fg-muted: #8b949e; --color-fg-subtle: #6e7681; --color-canvas-default: #0d1117; --color-canvas-subtle: #161b22; --color-border-default: #30363d; --color-border-muted: #21262d; --color-neutral-muted: rgba(110,118,129,0.4); --color-accent-fg: #58a6ff; --color-accent-emphasis: #1f6feb; --color-attention-subtle: rgba(187,128,9,0.15); --color-danger-fg: #f85149; } } html { .markdown-body { color-scheme: light; --color-prettylights-syntax-comment: #6e7781; --color-prettylights-syntax-constant: #0550ae; --color-prettylights-syntax-entity: #8250df; --color-prettylights-syntax-storage-modifier-import: #24292f; --color-prettylights-syntax-entity-tag: #116329; --color-prettylights-syntax-keyword: #cf222e; --color-prettylights-syntax-string: #0a3069; --color-prettylights-syntax-variable: #953800; --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; --color-prettylights-syntax-invalid-illegal-bg: #82071e; --color-prettylights-syntax-carriage-return-text: #f6f8fa; --color-prettylights-syntax-carriage-return-bg: #cf222e; --color-prettylights-syntax-string-regexp: #116329; --color-prettylights-syntax-markup-list: #3b2300; --color-prettylights-syntax-markup-heading: #0550ae; --color-prettylights-syntax-markup-italic: #24292f; --color-prettylights-syntax-markup-bold: #24292f; --color-prettylights-syntax-markup-deleted-text: #82071e; --color-prettylights-syntax-markup-deleted-bg: #ffebe9; --color-prettylights-syntax-markup-inserted-text: #116329; --color-prettylights-syntax-markup-inserted-bg: #dafbe1; --color-prettylights-syntax-markup-changed-text: #953800; --color-prettylights-syntax-markup-changed-bg: #ffd8b5; --color-prettylights-syntax-markup-ignored-text: #eaeef2; --color-prettylights-syntax-markup-ignored-bg: #0550ae; --color-prettylights-syntax-meta-diff-range: #8250df; --color-prettylights-syntax-brackethighlighter-angle: #57606a; --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; --color-prettylights-syntax-constant-other-reference-link: #0a3069; --color-fg-default: #24292f; --color-fg-muted: #57606a; --color-fg-subtle: #6e7781; --color-canvas-default: #ffffff; --color-canvas-subtle: #f6f8fa; --color-border-default: #d0d7de; --color-border-muted: hsla(210,18%,87%,1); --color-neutral-muted: rgba(175,184,193,0.2); --color-accent-fg: #0969da; --color-accent-emphasis: #0969da; --color-attention-subtle: #fff8c5; --color-danger-fg: #cf222e; } } .markdown-body { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; margin: 0; color: var(--color-fg-default); background-color: var(--color-canvas-default); font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; font-size: 16px; line-height: 1.5; word-wrap: break-word; } .markdown-body .octicon { display: inline-block; fill: currentColor; vertical-align: text-bottom; } .markdown-body h1:hover .anchor .octicon-link:before, .markdown-body h2:hover .anchor .octicon-link:before, .markdown-body h3:hover .anchor .octicon-link:before, .markdown-body h4:hover .anchor .octicon-link:before, .markdown-body h5:hover .anchor .octicon-link:before, .markdown-body h6:hover .anchor .octicon-link:before { width: 16px; height: 16px; content: ' '; display: inline-block; background-color: currentColor; -webkit-mask-image: url("data:image/svg+xml,"); mask-image: url("data:image/svg+xml,"); } .markdown-body details, .markdown-body figcaption, .markdown-body figure { display: block; } .markdown-body summary { display: list-item; } .markdown-body [hidden] { display: none !important; } .markdown-body a { background-color: transparent; color: var(--color-accent-fg); text-decoration: none; } .markdown-body abbr[title] { border-bottom: none; text-decoration: underline dotted; } .markdown-body b, .markdown-body strong { font-weight: var(--base-text-weight-semibold, 600); } .markdown-body dfn { font-style: italic; } .markdown-body h1 { margin: .67em 0; font-weight: var(--base-text-weight-semibold, 600); padding-bottom: .3em; font-size: 2em; border-bottom: 1px solid var(--color-border-muted); } .markdown-body mark { background-color: var(--color-attention-subtle); color: var(--color-fg-default); } .markdown-body small { font-size: 90%; } .markdown-body sub, .markdown-body sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } .markdown-body sub { bottom: -0.25em; } .markdown-body sup { top: -0.5em; } .markdown-body img { border-style: none; max-width: 100%; box-sizing: content-box; background-color: var(--color-canvas-default); } .markdown-body code, .markdown-body kbd, .markdown-body pre, .markdown-body samp { font-family: monospace; font-size: 1em; } .markdown-body figure { margin: 1em 40px; } .markdown-body hr { box-sizing: content-box; overflow: hidden; background: transparent; border-bottom: 1px solid var(--color-border-muted); height: .25em; padding: 0; margin: 24px 0; background-color: var(--color-border-default); border: 0; } .markdown-body input { font: inherit; margin: 0; overflow: visible; font-family: inherit; font-size: inherit; line-height: inherit; } .markdown-body [type=button], .markdown-body [type=reset], .markdown-body [type=submit] { -webkit-appearance: button; } .markdown-body [type=checkbox], .markdown-body [type=radio] { box-sizing: border-box; padding: 0; } .markdown-body [type=number]::-webkit-inner-spin-button, .markdown-body [type=number]::-webkit-outer-spin-button { height: auto; } .markdown-body [type=search]::-webkit-search-cancel-button, .markdown-body [type=search]::-webkit-search-decoration { -webkit-appearance: none; } .markdown-body ::-webkit-input-placeholder { color: inherit; opacity: .54; } .markdown-body ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; } .markdown-body a:hover { text-decoration: underline; } .markdown-body ::placeholder { color: var(--color-fg-subtle); opacity: 1; } .markdown-body hr::before { display: table; content: ""; } .markdown-body hr::after { display: table; clear: both; content: ""; } .markdown-body table { border-spacing: 0; border-collapse: collapse; display: block; width: max-content; max-width: 100%; overflow: auto; } .markdown-body td, .markdown-body th { padding: 0; } .markdown-body details summary { cursor: pointer; } .markdown-body details:not([open])>*:not(summary) { display: none !important; } .markdown-body a:focus, .markdown-body [role=button]:focus, .markdown-body input[type=radio]:focus, .markdown-body input[type=checkbox]:focus { outline: 2px solid var(--color-accent-fg); outline-offset: -2px; box-shadow: none; } .markdown-body a:focus:not(:focus-visible), .markdown-body [role=button]:focus:not(:focus-visible), .markdown-body input[type=radio]:focus:not(:focus-visible), .markdown-body input[type=checkbox]:focus:not(:focus-visible) { outline: solid 1px transparent; } .markdown-body a:focus-visible, .markdown-body [role=button]:focus-visible, .markdown-body input[type=radio]:focus-visible, .markdown-body input[type=checkbox]:focus-visible { outline: 2px solid var(--color-accent-fg); outline-offset: -2px; box-shadow: none; } .markdown-body a:not([class]):focus, .markdown-body a:not([class]):focus-visible, .markdown-body input[type=radio]:focus, .markdown-body input[type=radio]:focus-visible, .markdown-body input[type=checkbox]:focus, .markdown-body input[type=checkbox]:focus-visible { outline-offset: 0; } .markdown-body kbd { display: inline-block; padding: 3px 5px; font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; line-height: 10px; color: var(--color-fg-default); vertical-align: middle; background-color: var(--color-canvas-subtle); border: solid 1px var(--color-neutral-muted); border-bottom-color: var(--color-neutral-muted); border-radius: 6px; box-shadow: inset 0 -1px 0 var(--color-neutral-muted); } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25; } .markdown-body h2 { font-weight: var(--base-text-weight-semibold, 600); padding-bottom: .3em; font-size: 1.5em; border-bottom: 1px solid var(--color-border-muted); } .markdown-body h3 { font-weight: var(--base-text-weight-semibold, 600); font-size: 1.25em; } .markdown-body h4 { font-weight: var(--base-text-weight-semibold, 600); font-size: 1em; } .markdown-body h5 { font-weight: var(--base-text-weight-semibold, 600); font-size: .875em; } .markdown-body h6 { font-weight: var(--base-text-weight-semibold, 600); font-size: .85em; color: var(--color-fg-muted); } .markdown-body p { margin-top: 0; margin-bottom: 10px; } .markdown-body blockquote { margin: 0; padding: 0 1em; color: var(--color-fg-muted); border-left: .25em solid var(--color-border-default); } .markdown-body ul, .markdown-body ol { margin-top: 0; margin-bottom: 0; padding-left: 2em; } .markdown-body ol ol, .markdown-body ul ol { list-style-type: lower-roman; } .markdown-body ul ul ol, .markdown-body ul ol ol, .markdown-body ol ul ol, .markdown-body ol ol ol { list-style-type: lower-alpha; } .markdown-body dd { margin-left: 0; } .markdown-body tt, .markdown-body code, .markdown-body samp { font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; font-size: 12px; } .markdown-body pre { margin-top: 0; margin-bottom: 0; font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; font-size: 12px; word-wrap: normal; } .markdown-body .octicon { display: inline-block; overflow: visible !important; vertical-align: text-bottom; fill: currentColor; } .markdown-body input::-webkit-outer-spin-button, .markdown-body input::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none; appearance: none; } .markdown-body::before { display: table; content: ""; } .markdown-body::after { display: table; clear: both; content: ""; } .markdown-body>*:first-child { margin-top: 0 !important; } .markdown-body>*:last-child { margin-bottom: 0 !important; } .markdown-body a:not([href]) { color: inherit; text-decoration: none; } .markdown-body .absent { color: var(--color-danger-fg); } .markdown-body .anchor { float: left; padding-right: 4px; margin-left: -20px; line-height: 1; } .markdown-body .anchor:focus { outline: none; } .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre, .markdown-body details { margin-top: 0; margin-bottom: 16px; } .markdown-body blockquote>:first-child { margin-top: 0; } .markdown-body blockquote>:last-child { margin-bottom: 0; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: var(--color-fg-default); vertical-align: middle; visibility: hidden; } .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor { text-decoration: none; } .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link { visibility: visible; } .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { padding: 0 .2em; font-size: inherit; } .markdown-body summary h1, .markdown-body summary h2, .markdown-body summary h3, .markdown-body summary h4, .markdown-body summary h5, .markdown-body summary h6 { display: inline-block; } .markdown-body summary h1 .anchor, .markdown-body summary h2 .anchor, .markdown-body summary h3 .anchor, .markdown-body summary h4 .anchor, .markdown-body summary h5 .anchor, .markdown-body summary h6 .anchor { margin-left: -40px; } .markdown-body summary h1, .markdown-body summary h2 { padding-bottom: 0; border-bottom: 0; } .markdown-body ul.no-list, .markdown-body ol.no-list { padding: 0; list-style-type: none; } .markdown-body ol[type=a] { list-style-type: lower-alpha; } .markdown-body ol[type=A] { list-style-type: upper-alpha; } .markdown-body ol[type=i] { list-style-type: lower-roman; } .markdown-body ol[type=I] { list-style-type: upper-roman; } .markdown-body ol[type="1"] { list-style-type: decimal; } .markdown-body div>ol:not([type]) { list-style-type: decimal; } .markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul { margin-top: 0; margin-bottom: 0; } .markdown-body li>p { margin-top: 16px; } .markdown-body li+li { margin-top: .25em; } .markdown-body dl { padding: 0; } .markdown-body dl dt { padding: 0; margin-top: 16px; font-size: 1em; font-style: italic; font-weight: var(--base-text-weight-semibold, 600); } .markdown-body dl dd { padding: 0 16px; margin-bottom: 16px; } .markdown-body table th { font-weight: var(--base-text-weight-semibold, 600); } .markdown-body table th, .markdown-body table td { padding: 6px 13px; border: 1px solid var(--color-border-default); } .markdown-body table tr { background-color: var(--color-canvas-default); border-top: 1px solid var(--color-border-muted); } .markdown-body table tr:nth-child(2n) { background-color: var(--color-canvas-subtle); } .markdown-body table img { background-color: transparent; } .markdown-body img[align=right] { padding-left: 20px; } .markdown-body img[align=left] { padding-right: 20px; } .markdown-body .emoji { max-width: none; vertical-align: text-top; background-color: transparent; } .markdown-body span.frame { display: block; overflow: hidden; } .markdown-body span.frame>span { display: block; float: left; width: auto; padding: 7px; margin: 13px 0 0; overflow: hidden; border: 1px solid var(--color-border-default); } .markdown-body span.frame span img { display: block; float: left; } .markdown-body span.frame span span { display: block; padding: 5px 0 0; clear: both; color: var(--color-fg-default); } .markdown-body span.align-center { display: block; overflow: hidden; clear: both; } .markdown-body span.align-center>span { display: block; margin: 13px auto 0; overflow: hidden; text-align: center; } .markdown-body span.align-center span img { margin: 0 auto; text-align: center; } .markdown-body span.align-right { display: block; overflow: hidden; clear: both; } .markdown-body span.align-right>span { display: block; margin: 13px 0 0; overflow: hidden; text-align: right; } .markdown-body span.align-right span img { margin: 0; text-align: right; } .markdown-body span.float-left { display: block; float: left; margin-right: 13px; overflow: hidden; } .markdown-body span.float-left span { margin: 13px 0 0; } .markdown-body span.float-right { display: block; float: right; margin-left: 13px; overflow: hidden; } .markdown-body span.float-right>span { display: block; margin: 13px auto 0; overflow: hidden; text-align: right; } .markdown-body code, .markdown-body tt { padding: .2em .4em; margin: 0; font-size: 85%; white-space: break-spaces; background-color: var(--color-neutral-muted); border-radius: 6px; } .markdown-body code br, .markdown-body tt br { display: none; } .markdown-body del code { text-decoration: inherit; } .markdown-body samp { font-size: 85%; } .markdown-body pre code { font-size: 100%; } .markdown-body pre>code { padding: 0; margin: 0; word-break: normal; white-space: pre; background: transparent; border: 0; } .markdown-body .highlight { margin-bottom: 16px; } .markdown-body .highlight pre { margin-bottom: 0; word-break: normal; } .markdown-body .highlight pre, .markdown-body pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: var(--color-canvas-subtle); border-radius: 6px; } .markdown-body pre code, .markdown-body pre tt { display: inline; max-width: auto; padding: 0; margin: 0; overflow: visible; line-height: inherit; word-wrap: normal; background-color: transparent; border: 0; } .markdown-body .csv-data td, .markdown-body .csv-data th { padding: 5px; overflow: hidden; font-size: 12px; line-height: 1; text-align: left; white-space: nowrap; } .markdown-body .csv-data .blob-num { padding: 10px 8px 9px; text-align: right; background: var(--color-canvas-default); border: 0; } .markdown-body .csv-data tr { border-top: 0; } .markdown-body .csv-data th { font-weight: var(--base-text-weight-semibold, 600); background: var(--color-canvas-subtle); border-top: 0; } .markdown-body [data-footnote-ref]::before { content: "["; } .markdown-body [data-footnote-ref]::after { content: "]"; } .markdown-body .footnotes { font-size: 12px; color: var(--color-fg-muted); border-top: 1px solid var(--color-border-default); } .markdown-body .footnotes ol { padding-left: 16px; } .markdown-body .footnotes ol ul { display: inline-block; padding-left: 16px; margin-top: 16px; } .markdown-body .footnotes li { position: relative; } .markdown-body .footnotes li:target::before { position: absolute; top: -8px; right: -8px; bottom: -8px; left: -24px; pointer-events: none; content: ""; border: 2px solid var(--color-accent-emphasis); border-radius: 6px; } .markdown-body .footnotes li:target { color: var(--color-fg-default); } .markdown-body .footnotes .data-footnote-backref g-emoji { font-family: monospace; } .markdown-body .pl-c { color: var(--color-prettylights-syntax-comment); } .markdown-body .pl-c1, .markdown-body .pl-s .pl-v { color: var(--color-prettylights-syntax-constant); } .markdown-body .pl-e, .markdown-body .pl-en { color: var(--color-prettylights-syntax-entity); } .markdown-body .pl-smi, .markdown-body .pl-s .pl-s1 { color: var(--color-prettylights-syntax-storage-modifier-import); } .markdown-body .pl-ent { color: var(--color-prettylights-syntax-entity-tag); } .markdown-body .pl-k { color: var(--color-prettylights-syntax-keyword); } .markdown-body .pl-s, .markdown-body .pl-pds, .markdown-body .pl-s .pl-pse .pl-s1, .markdown-body .pl-sr, .markdown-body .pl-sr .pl-cce, .markdown-body .pl-sr .pl-sre, .markdown-body .pl-sr .pl-sra { color: var(--color-prettylights-syntax-string); } .markdown-body .pl-v, .markdown-body .pl-smw { color: var(--color-prettylights-syntax-variable); } .markdown-body .pl-bu { color: var(--color-prettylights-syntax-brackethighlighter-unmatched); } .markdown-body .pl-ii { color: var(--color-prettylights-syntax-invalid-illegal-text); background-color: var(--color-prettylights-syntax-invalid-illegal-bg); } .markdown-body .pl-c2 { color: var(--color-prettylights-syntax-carriage-return-text); background-color: var(--color-prettylights-syntax-carriage-return-bg); } .markdown-body .pl-sr .pl-cce { font-weight: bold; color: var(--color-prettylights-syntax-string-regexp); } .markdown-body .pl-ml { color: var(--color-prettylights-syntax-markup-list); } .markdown-body .pl-mh, .markdown-body .pl-mh .pl-en, .markdown-body .pl-ms { font-weight: bold; color: var(--color-prettylights-syntax-markup-heading); } .markdown-body .pl-mi { font-style: italic; color: var(--color-prettylights-syntax-markup-italic); } .markdown-body .pl-mb { font-weight: bold; color: var(--color-prettylights-syntax-markup-bold); } .markdown-body .pl-md { color: var(--color-prettylights-syntax-markup-deleted-text); background-color: var(--color-prettylights-syntax-markup-deleted-bg); } .markdown-body .pl-mi1 { color: var(--color-prettylights-syntax-markup-inserted-text); background-color: var(--color-prettylights-syntax-markup-inserted-bg); } .markdown-body .pl-mc { color: var(--color-prettylights-syntax-markup-changed-text); background-color: var(--color-prettylights-syntax-markup-changed-bg); } .markdown-body .pl-mi2 { color: var(--color-prettylights-syntax-markup-ignored-text); background-color: var(--color-prettylights-syntax-markup-ignored-bg); } .markdown-body .pl-mdr { font-weight: bold; color: var(--color-prettylights-syntax-meta-diff-range); } .markdown-body .pl-ba { color: var(--color-prettylights-syntax-brackethighlighter-angle); } .markdown-body .pl-sg { color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); } .markdown-body .pl-corl { text-decoration: underline; color: var(--color-prettylights-syntax-constant-other-reference-link); } .markdown-body g-emoji { display: inline-block; min-width: 1ch; font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; font-size: 1em; font-style: normal !important; font-weight: var(--base-text-weight-normal, 400); line-height: 1; vertical-align: -0.075em; } .markdown-body g-emoji img { width: 1em; height: 1em; } .markdown-body .task-list-item { list-style-type: none; } .markdown-body .task-list-item label { font-weight: var(--base-text-weight-normal, 400); } .markdown-body .task-list-item.enabled label { cursor: pointer; } .markdown-body .task-list-item+.task-list-item { margin-top: 4px; } .markdown-body .task-list-item .handle { display: none; } .markdown-body .task-list-item-checkbox { margin: 0 .2em .25em -1.4em; vertical-align: middle; } .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { margin: 0 -1.6em .25em .2em; } .markdown-body .contains-task-list { position: relative; } .markdown-body .contains-task-list:hover .task-list-item-convert-container, .markdown-body .contains-task-list:focus-within .task-list-item-convert-container { display: block; width: auto; height: 24px; overflow: visible; clip: auto; } .markdown-body ::-webkit-calendar-picker-indicator { filter: invert(50%); } ================================================ FILE: src/styles/lib/highlight.less ================================================ html.dark { pre code.hljs { display: block; overflow-x: auto; padding: 1em } code.hljs { padding: 3px 5px } .hljs { color: #abb2bf; background: #282c34 } .hljs-keyword, .hljs-operator, .hljs-pattern-match { color: #f92672 } .hljs-function, .hljs-pattern-match .hljs-constructor { color: #61aeee } .hljs-function .hljs-params { color: #a6e22e } .hljs-function .hljs-params .hljs-typing { color: #fd971f } .hljs-module-access .hljs-module { color: #7e57c2 } .hljs-constructor { color: #e2b93d } .hljs-constructor .hljs-string { color: #9ccc65 } .hljs-comment, .hljs-quote { color: #b18eb1; font-style: italic } .hljs-doctag, .hljs-formula { color: #c678dd } .hljs-deletion, .hljs-name, .hljs-section, .hljs-selector-tag, .hljs-subst { color: #e06c75 } .hljs-literal { color: #56b6c2 } .hljs-addition, .hljs-attribute, .hljs-meta .hljs-string, .hljs-regexp, .hljs-string { color: #98c379 } .hljs-built_in, .hljs-class .hljs-title, .hljs-title.class_ { color: #e6c07b } .hljs-attr, .hljs-number, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-pseudo, .hljs-template-variable, .hljs-type, .hljs-variable { color: #d19a66 } .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-symbol, .hljs-title { color: #61aeee } .hljs-emphasis { font-style: italic } .hljs-strong { font-weight: 700 } .hljs-link { text-decoration: underline } } html { pre code.hljs { display: block; overflow-x: auto; padding: 1em } code.hljs { padding: 3px 5px; &::-webkit-scrollbar { height: 4px; } } .hljs { color: #383a42; background: #fafafa } .hljs-comment, .hljs-quote { color: #a0a1a7; font-style: italic } .hljs-doctag, .hljs-formula, .hljs-keyword { color: #a626a4 } .hljs-deletion, .hljs-name, .hljs-section, .hljs-selector-tag, .hljs-subst { color: #e45649 } .hljs-literal { color: #0184bb } .hljs-addition, .hljs-attribute, .hljs-meta .hljs-string, .hljs-regexp, .hljs-string { color: #50a14f } .hljs-attr, .hljs-number, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-pseudo, .hljs-template-variable, .hljs-type, .hljs-variable { color: #986801 } .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-symbol, .hljs-title { color: #4078f2 } .hljs-built_in, .hljs-class .hljs-title, .hljs-title.class_ { color: #c18401 } .hljs-emphasis { font-style: italic } .hljs-strong { font-weight: 700 } .hljs-link { text-decoration: underline } } ================================================ FILE: src/styles/lib/tailwind.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: src/typings/chat.d.ts ================================================ declare namespace Chat { interface Chat { dateTime: string text: string inversion?: boolean error?: boolean loading?: boolean conversationOptions?: ConversationRequest | null requestOptions: { prompt: string; options?: ConversationRequest | null } } interface History { title: string isEdit: boolean uuid: number } interface ChatState { active: number | null usingContext: boolean; history: History[] chat: { uuid: number; data: Chat[] }[] } interface ConversationRequest { conversationId?: string parentMessageId?: string } interface ConversationResponse { conversationId: string detail: { choices: { finish_reason: string; index: number; logprobs: any; text: string }[] created: number id: string model: string object: string usage: { completion_tokens: number; prompt_tokens: number; total_tokens: number } } id: string parentMessageId: string role: string text: string } } ================================================ FILE: src/typings/env.d.ts ================================================ /// interface ImportMetaEnv { readonly VITE_GLOB_API_URL: string; readonly VITE_APP_API_BASE_URL: string; readonly VITE_GLOB_OPEN_LONG_REPLY: string; readonly VITE_GLOB_APP_PWA: string; } ================================================ FILE: src/typings/global.d.ts ================================================ interface Window { $loadingBar?: import('naive-ui').LoadingBarProviderInst; $dialog?: import('naive-ui').DialogProviderInst; $message?: import('naive-ui').MessageProviderInst; $notification?: import('naive-ui').NotificationProviderInst; } ================================================ FILE: src/utils/copy.ts ================================================ export function copyToClip(text: string) { return new Promise((resolve, reject) => { try { const input: HTMLTextAreaElement = document.createElement('textarea') input.setAttribute('readonly', 'readonly') input.value = text document.body.appendChild(input) input.select() if (document.execCommand('copy')) document.execCommand('copy') document.body.removeChild(input) resolve(text) } catch (error) { reject(error) } }) } ================================================ FILE: src/utils/functions/debounce.ts ================================================ type CallbackFunc = (...args: T) => void export function debounce( func: CallbackFunc, wait: number, ): (...args: T) => void { let timeoutId: ReturnType | undefined return (...args: T) => { const later = () => { clearTimeout(timeoutId) func(...args) } clearTimeout(timeoutId) timeoutId = setTimeout(later, wait) } } ================================================ FILE: src/utils/functions/index.ts ================================================ export function getCurrentDate() { const date = new Date() const day = date.getDate() const month = date.getMonth() + 1 const year = date.getFullYear() return `${year}-${month}-${day}` } ================================================ FILE: src/utils/is/index.ts ================================================ export function isNumber(value: T | unknown): value is number { return Object.prototype.toString.call(value) === '[object Number]' } export function isString(value: T | unknown): value is string { return Object.prototype.toString.call(value) === '[object String]' } export function isBoolean(value: T | unknown): value is boolean { return Object.prototype.toString.call(value) === '[object Boolean]' } export function isNull(value: T | unknown): value is null { return Object.prototype.toString.call(value) === '[object Null]' } export function isUndefined(value: T | unknown): value is undefined { return Object.prototype.toString.call(value) === '[object Undefined]' } export function isObject(value: T | unknown): value is object { return Object.prototype.toString.call(value) === '[object Object]' } export function isArray(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Array]' } export function isFunction any | void | never>(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Function]' } export function isDate(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Date]' } export function isRegExp(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object RegExp]' } export function isPromise>(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Promise]' } export function isSet>(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Set]' } export function isMap>(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Map]' } export function isFile(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object File]' } ================================================ FILE: src/utils/request/axios.ts ================================================ import axios, { type AxiosResponse } from 'axios' import { useAuthStore } from '@/store' const service = axios.create({ baseURL: import.meta.env.VITE_GLOB_API_URL, }) service.interceptors.request.use( (config) => { const token = useAuthStore().token if (token) config.headers.Authorization = `Bearer ${token}` return config }, (error) => { return Promise.reject(error.response) }, ) service.interceptors.response.use( (response: AxiosResponse): AxiosResponse => { if (response.status === 200) return response throw new Error(response.status.toString()) }, (error) => { return Promise.reject(error) }, ) export default service ================================================ FILE: src/utils/request/index.ts ================================================ import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios' import request from './axios' import { useAuthStore } from '@/store' export interface HttpOption { url: string data?: any method?: string headers?: any onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void signal?: GenericAbortSignal beforeRequest?: () => void afterRequest?: () => void } export interface Response { data: T message: string | null status: string } function http( { url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, ) { const successHandler = (res: AxiosResponse>) => { const authStore = useAuthStore() if (res.data.status === 'Success' || typeof res.data === 'string') return res.data if (res.data.status === 'Unauthorized') { authStore.removeToken() window.location.reload() } return Promise.reject(res.data) } const failHandler = (error: Response) => { afterRequest?.() throw new Error(error?.message || 'Error') } beforeRequest?.() method = method || 'GET' const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {}) return method === 'GET' ? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler) : request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler) } export function get( { url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, ): Promise> { return http({ url, method, data, onDownloadProgress, signal, beforeRequest, afterRequest, }) } export function post( { url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, ): Promise> { return http({ url, method, data, headers, onDownloadProgress, signal, beforeRequest, afterRequest, }) } export default post ================================================ FILE: src/utils/storage/index.ts ================================================ interface StorageData { data: T expire: number | null } export function createLocalStorage(options?: { expire?: number | null }) { const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7 const { expire } = Object.assign({ expire: DEFAULT_CACHE_TIME }, options) function set(key: string, data: T) { const storageData: StorageData = { data, expire: expire !== null ? new Date().getTime() + expire * 1000 : null, } const json = JSON.stringify(storageData) window.localStorage.setItem(key, json) } function get(key: string) { const json = window.localStorage.getItem(key) if (json) { let storageData: StorageData | null = null try { storageData = JSON.parse(json) } catch { // Prevent failure } if (storageData) { const { data, expire } = storageData if (expire === null || expire >= Date.now()) return data } remove(key) return null } } function remove(key: string) { window.localStorage.removeItem(key) } function clear() { window.localStorage.clear() } return { set, get, remove, clear } } export const ls = createLocalStorage() export const ss = createLocalStorage({ expire: null }) ================================================ FILE: src/views/chat/components/Header/index.vue ================================================ ================================================ FILE: src/views/chat/components/Message/Avatar.vue ================================================ ================================================ FILE: src/views/chat/components/Message/Text.vue ================================================ ================================================ FILE: src/views/chat/components/Message/index.vue ================================================ ================================================ FILE: src/views/chat/components/Message/style.less ================================================ .markdown-body { background-color: transparent; font-size: 14px; p { white-space: pre-wrap; } ol { list-style-type: decimal; } ul { list-style-type: disc; } pre code, pre tt { line-height: 1.65; } .highlight pre, pre { background-color: #fff; } code.hljs { padding: 0; } .code-block { &-wrapper { position: relative; padding-top: 24px; } &-header { position: absolute; top: 5px; right: 0; width: 100%; padding: 0 1rem; display: flex; justify-content: flex-end; align-items: center; color: #b3b3b3; &__copy { cursor: pointer; margin-left: 0.5rem; user-select: none; &:hover { color: #65a665; } } } } // Mermaid div[id^='mermaid-container'] { padding: 4px; border-radius: 4px; overflow-x: auto !important; background-color: #fff; border: 1px solid #e5e5e5; } &.markdown-body-generate>dd:last-child:after, &.markdown-body-generate>dl:last-child:after, &.markdown-body-generate>dt:last-child:after, &.markdown-body-generate>h1:last-child:after, &.markdown-body-generate>h2:last-child:after, &.markdown-body-generate>h3:last-child:after, &.markdown-body-generate>h4:last-child:after, &.markdown-body-generate>h5:last-child:after, &.markdown-body-generate>h6:last-child:after, &.markdown-body-generate>li:last-child:after, &.markdown-body-generate>ol:last-child li:last-child:after, &.markdown-body-generate>p:last-child:after, &.markdown-body-generate>pre:last-child code:after, &.markdown-body-generate>td:last-child:after, &.markdown-body-generate>ul:last-child li:last-child:after { animation: blink 1s steps(5, start) infinite; color: #000; content: '_'; font-weight: 700; margin-left: 3px; vertical-align: baseline; } @keyframes blink { to { visibility: hidden; } } } html.dark { .markdown-body { &.markdown-body-generate>dd:last-child:after, &.markdown-body-generate>dl:last-child:after, &.markdown-body-generate>dt:last-child:after, &.markdown-body-generate>h1:last-child:after, &.markdown-body-generate>h2:last-child:after, &.markdown-body-generate>h3:last-child:after, &.markdown-body-generate>h4:last-child:after, &.markdown-body-generate>h5:last-child:after, &.markdown-body-generate>h6:last-child:after, &.markdown-body-generate>li:last-child:after, &.markdown-body-generate>ol:last-child li:last-child:after, &.markdown-body-generate>p:last-child:after, &.markdown-body-generate>pre:last-child code:after, &.markdown-body-generate>td:last-child:after, &.markdown-body-generate>ul:last-child li:last-child:after { color: #65a665; } } .message-reply { .whitespace-pre-wrap { white-space: pre-wrap; color: var(--n-text-color); } } .highlight pre, pre { background-color: #282c34; } } @media screen and (max-width: 533px) { .markdown-body .code-block-wrapper { padding: unset; code { padding: 24px 16px 16px 16px; } } } ================================================ FILE: src/views/chat/components/index.ts ================================================ import Message from './Message/index.vue' export { Message } ================================================ FILE: src/views/chat/hooks/useChat.ts ================================================ import { useChatStore } from '@/store' export function useChat() { const chatStore = useChatStore() const getChatByUuidAndIndex = (uuid: number, index: number) => { return chatStore.getChatByUuidAndIndex(uuid, index) } const addChat = (uuid: number, chat: Chat.Chat) => { chatStore.addChatByUuid(uuid, chat) } const updateChat = (uuid: number, index: number, chat: Chat.Chat) => { chatStore.updateChatByUuid(uuid, index, chat) } const updateChatSome = (uuid: number, index: number, chat: Partial) => { chatStore.updateChatSomeByUuid(uuid, index, chat) } return { addChat, updateChat, updateChatSome, getChatByUuidAndIndex, } } ================================================ FILE: src/views/chat/hooks/useScroll.ts ================================================ import type { Ref } from 'vue' import { nextTick, ref } from 'vue' type ScrollElement = HTMLDivElement | null interface ScrollReturn { scrollRef: Ref scrollToBottom: () => Promise scrollToTop: () => Promise scrollToBottomIfAtBottom: () => Promise } export function useScroll(): ScrollReturn { const scrollRef = ref(null) const scrollToBottom = async () => { await nextTick() if (scrollRef.value) scrollRef.value.scrollTop = scrollRef.value.scrollHeight } const scrollToTop = async () => { await nextTick() if (scrollRef.value) scrollRef.value.scrollTop = 0 } const scrollToBottomIfAtBottom = async () => { await nextTick() if (scrollRef.value) { const threshold = 100 // Threshold, indicating the distance threshold to the bottom of the scroll bar. const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight if (distanceToBottom <= threshold) scrollRef.value.scrollTop = scrollRef.value.scrollHeight } } return { scrollRef, scrollToBottom, scrollToTop, scrollToBottomIfAtBottom, } } ================================================ FILE: src/views/chat/hooks/useUsingContext.ts ================================================ import { computed } from 'vue' import { useMessage } from 'naive-ui' import { t } from '@/locales' import { useChatStore } from '@/store' export function useUsingContext() { const ms = useMessage() const chatStore = useChatStore() const usingContext = computed(() => chatStore.usingContext) function toggleUsingContext() { chatStore.setUsingContext(!usingContext.value) if (usingContext.value) ms.success(t('chat.turnOnContext')) else ms.warning(t('chat.turnOffContext')) } return { usingContext, toggleUsingContext, } } ================================================ FILE: src/views/chat/index.vue ================================================ ================================================ FILE: src/views/chat/layout/Layout.vue ================================================ ================================================ FILE: src/views/chat/layout/Permission.vue ================================================ ================================================ FILE: src/views/chat/layout/index.ts ================================================ import ChatLayout from './Layout.vue' export { ChatLayout } ================================================ FILE: src/views/chat/layout/sider/Footer.vue ================================================ ================================================ FILE: src/views/chat/layout/sider/List.vue ================================================ ================================================ FILE: src/views/chat/layout/sider/index.vue ================================================ ================================================ FILE: src/views/exception/404/index.vue ================================================ ================================================ FILE: src/views/exception/500/index.vue ================================================ ================================================ FILE: start.cmd ================================================ cd ./service start pnpm start > service.log & echo "Start service complete!" cd .. echo "" > front.log start pnpm dev > front.log & echo "Start front complete!" ================================================ FILE: start.sh ================================================ cd ./service nohup pnpm start > service.log & echo "Start service complete!" cd .. echo "" > front.log nohup pnpm dev > front.log & echo "Start front complete!" tail -f front.log ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ module.exports = { darkMode: 'class', content: [ './index.html', './src/**/*.{vue,js,ts,jsx,tsx}', ], theme: { extend: { animation: { blink: 'blink 1.2s infinite steps(1, start)', }, keyframes: { blink: { '0%, 100%': { 'background-color': 'currentColor' }, '50%': { 'background-color': 'transparent' }, }, }, }, }, plugins: [], } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "module": "ESNext", "target": "ESNext", "lib": ["DOM", "ESNext"], "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "jsx": "preserve", "moduleResolution": "node", "resolveJsonModule": true, "noUnusedLocals": true, "strictNullChecks": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "paths": { "@/*": ["./src/*"] }, "types": ["vite/client", "node", "naive-ui/volar"] }, "exclude": ["node_modules", "dist", "service"] } ================================================ FILE: vite.config.ts ================================================ import path from 'path' import type { PluginOption } from 'vite' import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' import { VitePWA } from 'vite-plugin-pwa' function setupPlugins(env: ImportMetaEnv): PluginOption[] { return [ vue(), env.VITE_GLOB_APP_PWA === 'true' && VitePWA({ injectRegister: 'auto', manifest: { name: 'chatGPT', short_name: 'chatGPT', icons: [ { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' }, { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' }, ], }, }), ] } export default defineConfig((env) => { const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv return { resolve: { alias: { '@': path.resolve(process.cwd(), 'src'), }, }, plugins: setupPlugins(viteEnv), server: { host: '0.0.0.0', port: 1002, open: false, proxy: { '/api': { target: viteEnv.VITE_APP_API_BASE_URL, changeOrigin: true, // 允许跨域 rewrite: path => path.replace('/api/', '/'), }, }, }, build: { reportCompressedSize: false, sourcemap: false, commonjsOptions: { ignoreTryCatch: false, }, }, } })