diff --git a/.env b/.env index 2eca31c4..a3913ffa 100644 --- a/.env +++ b/.env @@ -8,3 +8,10 @@ VITE_GLOB_OPEN_LONG_REPLY=true # When you want to use PWA VITE_GLOB_APP_PWA=false + +# git commit hash +# GITHUB_SHA +VITE_GIT_COMMIT_HASH=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# release version +VITE_RELEASE_VERSION=v0.0.0 diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index d4b1bb52..83357628 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -4,33 +4,52 @@ on: push: branches: [main] release: - types: [created] # 表示在创建新的 Release 时触发 + types: [created] # This will only run on new releases jobs: + docker_hub_description: + name: Docker Hub description + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: | + README.en.md + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: ${{ vars.DOCKERHUB_NAMESPACE }}/chatgpt-web + # Description length should be no longer than 100 characters. + short-description: A third-party ChatGPT Web UI page, through the official OpenAI completion API. + readme-filepath: README.en.md + enable-url-completion: true + build_docker: name: Build docker runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 + # Print out all environment variables - run: | - echo "本次构建的版本为:${GITHUB_REF_NAME} (但是这个变量目前上下文中无法获取到)" - echo 本次构建的版本为:${{ github.ref_name }} env - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . push: true @@ -39,3 +58,8 @@ jobs: tags: | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:${{ github.ref_name }} ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:latest + ${{ vars.DOCKERHUB_NAMESPACE }}/chatgpt-web:${{ github.ref_name }} + ${{ vars.DOCKERHUB_NAMESPACE }}/chatgpt-web:latest + build-args: | + GIT_COMMIT_HASH=${{ github.sha }} + RELEASE_VERSION=${{ github.ref_name }} diff --git a/.vscode/launch.json b/.vscode/launch.json index cbd43f67..045d32f7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,7 @@ "type": "node", "request": "launch", "name": "Launch Service Server", - "runtimeExecutable": "${workspaceFolder}/service/node_modules/.bin/esno", + "runtimeExecutable": "${workspaceFolder}/service/node_modules/.bin/tsx", "skipFiles": ["/**"], "program": "${workspaceFolder}/service/src/index.ts", "outFiles": ["${workspaceFolder}/service/**/*.js"], diff --git a/.vscode/settings.json b/.vscode/settings.json index 005a2a68..346eb384 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "prettier.enable": false, "editor.formatOnSave": false, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "eslint.validate": [ "javascript", diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..afd50dd6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[dev@chatgpt-web.dev](mailto:dev@chatgpt-web.dev). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.en.md b/CONTRIBUTING.en.md index e0e7f27a..1336f147 100644 --- a/CONTRIBUTING.en.md +++ b/CONTRIBUTING.en.md @@ -7,7 +7,7 @@ This project follows semantic versioning. We release patch versions for importan 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. +1. Fork [this repository](https://github.com/chatgpt-web-dev/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. @@ -46,4 +46,4 @@ The following is a list of commit types: ## License -[MIT](./license) \ No newline at end of file +[MIT © github.com/chatgpt-web-dev Contributors](./LICENSE) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a7b0ca8..9c8bef00 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ 每个重大更改都将记录在 `changelog` 中。 ## 提交 Pull Request -1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。 +1. Fork [此仓库](https://github.com/chatgpt-web-dev/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。 2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。 3. `vscode` 安装了 `Eslint` 插件,其它编辑器如 `webStorm` 打开了 `eslint` 功能。 4. 根目录下执行 `pnpm bootstrap`。 @@ -46,4 +46,4 @@ Commit messages 请遵循[conventional-changelog 标准](https://www.conventiona ## License -[MIT](./license) +[MIT © github.com/chatgpt-web-dev Contributors](./LICENSE) diff --git a/Dockerfile b/Dockerfile index 508edfb4..4a2cc68b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,11 @@ # build front-end -FROM node:lts-alpine AS frontend +FROM node:20-alpine AS frontend + +ARG GIT_COMMIT_HASH=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +ARG RELEASE_VERSION=v0.0.0 + +ENV VITE_GIT_COMMIT_HASH $GIT_COMMIT_HASH +ENV VITE_RELEASE_VERSION $RELEASE_VERSION RUN npm install pnpm -g @@ -16,7 +22,7 @@ COPY . /app RUN pnpm run build # build backend -FROM node:lts-alpine as backend +FROM node:20-alpine as backend RUN npm install pnpm -g @@ -33,7 +39,7 @@ COPY /service /app RUN pnpm build # service -FROM node:lts-alpine +FROM node:20-alpine RUN npm install pnpm -g @@ -47,10 +53,6 @@ RUN pnpm install --production && rm -rf /root/.npm /root/.pnpm-store /usr/local/ COPY /service /app -COPY --from=frontend /app/replace-title.sh /app - -RUN chmod +x /app/replace-title.sh - COPY --from=frontend /app/dist /app/public COPY --from=backend /app/build /app/build @@ -59,4 +61,4 @@ COPY --from=backend /app/src/utils/templates /app/build/utils/templates EXPOSE 3002 -CMD ["sh", "-c", "./replace-title.sh && node --import tsx/esm ./build/index.js"] +CMD ["sh", "-c", "node --import tsx/esm ./build/index.js"] diff --git a/license b/LICENSE similarity index 90% rename from license rename to LICENSE index 744bc612..7a530de6 100644 --- a/license +++ b/LICENSE @@ -1,6 +1,8 @@ -MIT Kerwin1202 +MIT License Copyright (c) 2023 ChenZhaoYu +Copyright (c) 2023 Kerwin1202 +Copyright (c) 2023 - Present github.com/chatgpt-web-dev Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.en.md b/README.en.md index 8fd55558..b6c3887f 100644 --- a/README.en.md +++ b/README.en.md @@ -1,13 +1,18 @@ # ChatGPT Web -
- 中文 | - English -
-
+[中文](./README.md) | [English](./README.en.md) + ## Introduction -> **This project is forked from [Chanzhaoyu/chatgpt-web](https://github.com/Chanzhaoyu/chatgpt-web), some unique features have been added:** + +> [!IMPORTANT] +> **This project is forked from [Chanzhaoyu/chatgpt-web](https://github.com/Chanzhaoyu/chatgpt-web)** +> +> As the original project author does not agree to introduce a dependency on the database, this Hard Fork was created for independent development [discussion for details](https://github.com/Chanzhaoyu/chatgpt-web/pull/589#issuecomment-1469207694) +> +> Thank you again, the great [Chanzhaoyu](https://github.com/Chanzhaoyu), for your contributions to the open-source project 🙏 + +Some unique features have been added: [✓] Register & Login & Reset Password & 2FA @@ -23,7 +28,11 @@ [✓] Random Key -
+[✓] Conversation round limit & setting different limits by user & giftcards + + +> [!CAUTION] +> 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. ## Screenshots > Disclaimer: This project is only released on GitHub, under the MIT License, free and for open-source learning purposes. There will be no account selling, paid services, discussion groups, or forums. Beware of fraud. @@ -35,6 +44,9 @@ ![cover3](./docs/prompt_en.jpg) ![cover3](./docs/user-manager.jpg) ![cover3](./docs/key-manager-en.jpg) +![userlimit](./docs/add_redeem_and_limit.png) +![setmanuallimit](./docs/manual_set_limit.png) +![giftcarddb](./docs/giftcard_db_design.png) - [ChatGPT Web](#chatgpt-web) - [Introduction](#introduction) @@ -210,13 +222,16 @@ pnpm dev #### Docker Build & Run ```bash -docker build -t chatgpt-web . +GIT_COMMIT_HASH=`git rev-parse HEAD` +RELEASE_VERSION=`git branch --show-current` +docker build --build-arg GIT_COMMIT_HASH=${GIT_COMMIT_HASH} --build-arg RELEASE_VERSION=${RELEASE_VERSION} -t chatgpt-web . # foreground operation -docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web +# If run mongodb in host machine, please use MONGODB_URL=mongodb://host.docker.internal:27017/chatgpt +docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key --env MONGODB_URL=your_mongodb_url chatgpt-web # background operation -docker run --name chatgpt-web -d -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 --env MONGODB_URL=your_mongodb_url chatgpt-web # running address http://localhost:3002/ @@ -224,14 +239,14 @@ http://localhost:3002/ #### Docker Compose -[Hub Address](https://hub.docker.com/repository/docker/kerwin1202/chatgpt-web/general) +[Hub Address](https://hub.docker.com/r/chatgptweb/chatgpt-web) ```yml version: '3' services: app: - image: kerwin1202/chatgpt-web # always use latest, pull the tag image again when updating + image: chatgptweb/chatgpt-web # always use latest, pull the tag image again when updating container_name: chatgptweb restart: unless-stopped ports: @@ -344,13 +359,21 @@ Please read the [Contributing Guidelines](./CONTRIBUTING.en.md) before contribut Thanks to all the contributors! - - + + Contributors Image +## Star History + + + + + Star History Chart + + ## Sponsorship If you find this project helpful, please give me a star. ## License -MIT © [Kerwin1202](./license) +[MIT © github.com/chatgpt-web-dev Contributors](./LICENSE) diff --git a/README.md b/README.md index 0afc8c5f..54b467fc 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,18 @@ # ChatGPT Web -
- 中文 | - English -
-
+[中文](./README.md) | [English](./README.en.md) + ## 说明 -> **此项目 Fork 自 [Chanzhaoyu/chatgpt-web](https://github.com/Chanzhaoyu/chatgpt-web), 新增了部分特色功能:** + +> [!IMPORTANT] +> **此项目 Fork 自 [Chanzhaoyu/chatgpt-web](https://github.com/Chanzhaoyu/chatgpt-web)** +> +> 由于原项目作者不愿意引入对数据库的依赖 故制作该永久分叉独立开发 [详见讨论](https://github.com/Chanzhaoyu/chatgpt-web/pull/589#issuecomment-1469207694) +> +> 再次感谢 [Chanzhaoyu](https://github.com/Chanzhaoyu) 大佬对开源的贡献 🙏 + +新增了部分特色功能: [✓] 注册 & 登录 & 重置密码 & 2FA @@ -22,11 +27,15 @@ [✓] 用户管理 [✓] 多 Key 随机 -
-## 截图 +[✓] 对话数量限制 & 设置不同用户对话数量 & 兑换数量 + + +> [!CAUTION] > 声明:此项目只发布于 Github,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。 +## 截图 + ![cover3](./docs/login.jpg) ![cover](./docs/c1.png) ![cover2](./docs/c2.png) @@ -34,6 +43,9 @@ ![cover3](./docs/prompt.jpg) ![cover3](./docs/user-manager.jpg) ![cover3](./docs/key-manager.jpg) +![userlimit](./docs/add_redeem_and_limit.png) +![setmanuallimit](./docs/manual_set_limit.png) +![giftcarddb](./docs/giftcard_db_design.png) - [ChatGPT Web](#chatgpt-web) - [介绍](#介绍) @@ -214,13 +226,16 @@ pnpm dev #### Docker build & Run ```bash -docker build -t chatgpt-web . +GIT_COMMIT_HASH=`git rev-parse HEAD` +RELEASE_VERSION=`git branch --show-current` +docker build --build-arg GIT_COMMIT_HASH=${GIT_COMMIT_HASH} --build-arg RELEASE_VERSION=${RELEASE_VERSION} -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 +# 如果在宿主机运行 mongodb 则使用 MONGODB_URL=mongodb://host.docker.internal:27017/chatgpt +docker run --name chatgpt-web --rm -it -p 3002:3002 --env OPENAI_API_KEY=your_api_key --env MONGODB_URL=your_mongodb_url chatgpt-web # 后台运行 -docker run --name chatgpt-web -d -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 --env MONGODB_URL=your_mongodb_url chatgpt-web # 运行地址 http://localhost:3002/ @@ -228,14 +243,14 @@ http://localhost:3002/ #### Docker compose -[Hub 地址](https://hub.docker.com/repository/docker/kerwin1202/chatgpt-web/general) +[Hub 地址](https://hub.docker.com/r/chatgptweb/chatgpt-web) ```yml version: '3' services: app: - image: kerwin1202/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可 + image: chatgptweb/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可 container_name: chatgptweb restart: unless-stopped ports: @@ -357,10 +372,18 @@ A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer,则 Nginx 感谢所有做过贡献的人! - - + + Contributors Image +## Star 历史 + + + + + Star History Chart + + ## 赞助 如果你觉得这个项目对你有帮助,请给我点个Star。并且情况允许的话,可以给我一点点支持,总之非常感谢支持~ @@ -376,4 +399,4 @@ A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer,则 Nginx ## License -MIT © [Kerwin1202](./license) +[MIT © github.com/chatgpt-web-dev Contributors](./LICENSE) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 1c19dfef..400d1c72 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: app: - image: kerwin1202/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可 + image: chatgptweb/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可 container_name: chatgptweb restart: unless-stopped ports: diff --git a/docs/add_redeem_and_limit.png b/docs/add_redeem_and_limit.png new file mode 100644 index 00000000..20f83f67 Binary files /dev/null and b/docs/add_redeem_and_limit.png differ diff --git a/docs/giftcard_db_design.png b/docs/giftcard_db_design.png new file mode 100644 index 00000000..df891e39 Binary files /dev/null and b/docs/giftcard_db_design.png differ diff --git a/docs/manual_set_limit.png b/docs/manual_set_limit.png new file mode 100644 index 00000000..9a439dea Binary files /dev/null and b/docs/manual_set_limit.png differ diff --git a/index.html b/index.html index 90e4314c..8096a3b3 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,8 @@ - ${SITE_TITLE} + + Loading -ˋˏ✄┈┈┈┈ diff --git a/replace-title.sh b/replace-title.sh deleted file mode 100644 index d488f6a2..00000000 --- a/replace-title.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -SITE_TITLE=${SITE_TITLE:-ChatGPT Web} - -sed -i -E "s/([^<]*)<\/title>/<title>${SITE_TITLE}<\/title>/g" /app/public/index.html \ No newline at end of file diff --git a/service/.eslintrc.json b/service/.eslintrc.json index 9f75d07e..57f12624 100644 --- a/service/.eslintrc.json +++ b/service/.eslintrc.json @@ -3,10 +3,11 @@ "ignorePatterns": ["build", "tsconfig.json"], "extends": ["@antfu"], "rules": { - "antfu/top-level-function": "off", - "n/prefer-global/process": "off", - "no-restricted-globals": "off", - "unicorn/prefer-node-protocol": "off", - "unicorn/prefer-number-properties": "off" + "n/prefer-global/process": ["error", "always"], + "no-undef": "error" + }, + "globals": { + "__dirname": "off", + "__filename": "off" } } diff --git a/service/package.json b/service/package.json index 570bc0bd..d22368f0 100644 --- a/service/package.json +++ b/service/package.json @@ -35,11 +35,11 @@ "gpt-token": "^0.0.5", "https-proxy-agent": "^5.0.1", "isomorphic-fetch": "^3.0.0", - "jwt-decode": "^3.1.2", "jsonwebtoken": "^9.0.0", + "jwt-decode": "^3.1.2", "mongodb": "^5.9.2", "node-fetch": "^3.3.0", - "nodemailer": "^6.9.1", + "nodemailer": "^6.9.9", "request-ip": "^3.3.0", "socks-proxy-agent": "^7.0.0", "speakeasy": "^2.0.0", @@ -48,7 +48,12 @@ "devDependencies": { "@antfu/eslint-config": "^0.43.1", "@types/express": "^4.17.17", + "@types/isomorphic-fetch": "^0.0.39", + "@types/jsonwebtoken": "^9.0.5", "@types/node": "^18.14.6", + "@types/nodemailer": "^6.4.14", + "@types/request-ip": "^0.0.41", + "@types/speakeasy": "^2.0.10", "eslint": "^8.56.0", "rimraf": "^4.3.0", "typescript": "^5.3.3" diff --git a/service/pnpm-lock.yaml b/service/pnpm-lock.yaml index fe30781f..1011b105 100644 --- a/service/pnpm-lock.yaml +++ b/service/pnpm-lock.yaml @@ -45,8 +45,8 @@ dependencies: specifier: ^3.3.0 version: 3.3.0 nodemailer: - specifier: ^6.9.1 - version: 6.9.1 + specifier: ^6.9.9 + version: 6.9.9 request-ip: specifier: ^3.3.0 version: 3.3.0 @@ -67,9 +67,24 @@ devDependencies: '@types/express': specifier: ^4.17.17 version: 4.17.17 + '@types/isomorphic-fetch': + specifier: ^0.0.39 + version: 0.0.39 + '@types/jsonwebtoken': + specifier: ^9.0.5 + version: 9.0.5 '@types/node': specifier: ^18.14.6 version: 18.14.6 + '@types/nodemailer': + specifier: ^6.4.14 + version: 6.4.14 + '@types/request-ip': + specifier: ^0.0.41 + version: 0.0.41 + '@types/speakeasy': + specifier: ^2.0.10 + version: 2.0.10 eslint: specifier: ^8.56.0 version: 8.56.0 @@ -583,6 +598,10 @@ packages: '@types/serve-static': 1.15.1 dev: true + /@types/isomorphic-fetch@0.0.39: + resolution: {integrity: sha512-I0gou/ZdA1vMG7t7gMzL7VYu2xAKU78rW9U1l10MI0nn77pEHq3tQqHQ8hMmXdMpBlkxZOorjI4sO594Z3kKJw==} + dev: true + /@types/json-schema@7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true @@ -591,6 +610,12 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/jsonwebtoken@9.0.5: + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + dependencies: + '@types/node': 18.14.6 + dev: true + /@types/mdast@3.0.10: resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} dependencies: @@ -604,6 +629,12 @@ packages: /@types/node@18.14.6: resolution: {integrity: sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==} + /@types/nodemailer@6.4.14: + resolution: {integrity: sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==} + dependencies: + '@types/node': 18.14.6 + dev: true + /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -615,6 +646,12 @@ packages: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} dev: true + /@types/request-ip@0.0.41: + resolution: {integrity: sha512-Qzz0PM2nSZej4lsLzzNfADIORZhhxO7PED0fXpg4FjXiHuJ/lMyUg+YFF5q8x9HPZH3Gl6N+NOM8QZjItNgGKg==} + dependencies: + '@types/node': 18.14.6 + dev: true + /@types/semver@7.3.13: resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} dev: true @@ -630,6 +667,12 @@ packages: '@types/node': 18.14.6 dev: true + /@types/speakeasy@2.0.10: + resolution: {integrity: sha512-QVRlDW5r4yl7p7xkNIbAIC/JtyOcClDIIdKfuG7PWdDT1MmyhtXSANsildohy0K+Lmvf/9RUtLbNLMacvrVwxA==} + dependencies: + '@types/node': 18.14.6 + dev: true + /@types/unist@2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: true @@ -1063,7 +1106,7 @@ packages: /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: - function-bind: 1.1.1 + function-bind: 1.1.2 get-intrinsic: 1.2.0 dev: false @@ -1174,7 +1217,7 @@ packages: dot-prop: 7.2.0 env-paths: 3.0.0 json-schema-typed: 8.0.1 - semver: 7.3.8 + semver: 7.5.4 dev: false /content-disposition@0.5.4: @@ -2055,18 +2098,13 @@ packages: dev: false optional: true - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: false - /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true /get-intrinsic@1.2.0: resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} dependencies: - function-bind: 1.1.1 + function-bind: 1.1.2 has: 1.0.3 has-symbols: 1.0.3 dev: false @@ -2165,7 +2203,7 @@ packages: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} dependencies: - function-bind: 1.1.1 + function-bind: 1.1.2 dev: false /hasown@2.0.0: @@ -2173,7 +2211,6 @@ packages: engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 - dev: true /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -2256,8 +2293,8 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - /ip@2.0.0: - resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} + /ip@2.0.1: + resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} dev: false /ipaddr.js@1.9.1: @@ -2286,17 +2323,10 @@ packages: builtin-modules: 3.3.0 dev: true - /is-core-module@2.11.0: - resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} - dependencies: - has: 1.0.3 - dev: false - /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: hasown: 2.0.0 - dev: true /is-decimal@1.0.4: resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} @@ -2687,8 +2717,8 @@ packages: formdata-polyfill: 4.0.10 dev: false - /nodemailer@6.9.1: - resolution: {integrity: sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==} + /nodemailer@6.9.9: + resolution: {integrity: sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA==} engines: {node: '>=6.0.0'} dev: false @@ -2706,7 +2736,7 @@ packages: engines: {node: '>=10'} dependencies: hosted-git-info: 4.1.0 - is-core-module: 2.11.0 + is-core-module: 2.13.1 semver: 7.5.4 validate-npm-package-license: 3.0.4 dev: false @@ -3152,7 +3182,7 @@ packages: resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} dependencies: - ip: 2.0.0 + ip: 2.0.1 smart-buffer: 4.2.0 dev: false diff --git a/service/src/chatgpt/index.ts b/service/src/chatgpt/index.ts index bb3b12e9..99d5b350 100644 --- a/service/src/chatgpt/index.ts +++ b/service/src/chatgpt/index.ts @@ -119,7 +119,7 @@ async function chatReplyProcess(options: RequestOptions) { const maxContextCount = options.user.advanced.maxContextCount ?? 20 const messageId = options.messageId if (key == null || key === undefined) - throw new Error('没有可用的配置。请再试一次 | No available configuration. Please try again.') + throw new Error('没有对应的apikeys配置。请再试一次 | No available apikeys configuration. Please try again.') if (key.keyModel === 'ChatGPTUnofficialProxyAPI') { if (!options.room.accountId) @@ -159,7 +159,6 @@ async function chatReplyProcess(options: RequestOptions) { process?.(partialResponse) }, }) - return sendResponse({ type: 'Success', data: response }) } catch (error: any) { @@ -172,7 +171,7 @@ async function chatReplyProcess(options: RequestOptions) { return await chatReplyProcess(options) } } - global.console.error(error) + globalThis.console.error(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' }) @@ -263,7 +262,7 @@ async function fetchBalance() { if (isNotEmptyString(config.socksProxy)) { socksAgent = new SocksProxyAgent({ hostname: config.socksProxy.split(':')[0], - port: parseInt(config.socksProxy.split(':')[1]), + port: Number.parseInt(config.socksProxy.split(':')[1]), userId: isNotEmptyString(config.socksAuth) ? config.socksAuth.split(':')[0] : undefined, password: isNotEmptyString(config.socksAuth) ? config.socksAuth.split(':')[1] : undefined, }) @@ -303,7 +302,7 @@ async function fetchBalance() { return Promise.resolve(cachedBalance.toFixed(3)) } catch (error) { - global.console.error(error) + globalThis.console.error(error) return Promise.resolve('-') } } @@ -333,7 +332,7 @@ async function setupProxy(options: ChatGPTAPIOptions | ChatGPTUnofficialProxyAPI if (isNotEmptyString(config.socksProxy)) { const agent = new SocksProxyAgent({ hostname: config.socksProxy.split(':')[0], - port: parseInt(config.socksProxy.split(':')[1]), + port: Number.parseInt(config.socksProxy.split(':')[1]), userId: isNotEmptyString(config.socksAuth) ? config.socksAuth.split(':')[0] : undefined, password: isNotEmptyString(config.socksAuth) ? config.socksAuth.split(':')[1] : undefined, diff --git a/service/src/index.ts b/service/src/index.ts index 4b91d13e..d0d4f99e 100644 --- a/service/src/index.ts +++ b/service/src/index.ts @@ -4,7 +4,8 @@ import * as dotenv from 'dotenv' import { ObjectId } from 'mongodb' import { textTokens } from 'gpt-token' import speakeasy from 'speakeasy' -import { type RequestProps, TwoFAConfig } from './types' +import { TwoFAConfig } from './types' +import type { AuthJwtPayload, RequestProps } from './types' import type { ChatMessage } from './chatgpt' import { abortChatProcess, chatConfig, chatReplyProcess, containsSensitiveWords, initAuditService } from './chatgpt' import { auth, getUserId } from './middleware/auth' @@ -20,6 +21,7 @@ import { deleteChatRoom, disableUser2FA, existsChatRoom, + getAmtByCardNo, getChat, getChatRoom, getChatRooms, @@ -31,15 +33,18 @@ import { insertChat, insertChatUsage, renameChatRoom, + updateAmountMinusOne, updateApiKeyStatus, updateChat, updateConfig, + updateGiftCard, updateRoomChatModel, updateRoomPrompt, updateRoomUsingContext, updateUser, updateUser2FA, updateUserAdvancedConfig, + updateUserAmount, updateUserChatModel, updateUserInfo, updateUserPassword, @@ -198,7 +203,7 @@ router.get('/chat-history', auth, async (req, res) => { res.send({ status: 'Success', message: null, data: [] }) return } - const chats = await getChats(roomId, !isNotEmptyString(lastId) ? null : parseInt(lastId)) + const chats = await getChats(roomId, !isNotEmptyString(lastId) ? null : Number.parseInt(lastId)) const result = [] chats.forEach((c) => { @@ -367,19 +372,33 @@ router.post('/chat-process', [auth, limiter], async (req, res) => { res.setHeader('Content-type', 'application/octet-stream') let { roomId, uuid, regenerate, prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps - const userId = req.headers.userId as string + const userId = req.headers.userId.toString() + const config = await getCacheConfig() const room = await getChatRoom(userId, roomId) if (room == null) - global.console.error(`Unable to get chat room \t ${userId}\t ${roomId}`) + globalThis.console.error(`Unable to get chat room \t ${userId}\t ${roomId}`) if (room != null && isNotEmptyString(room.prompt)) systemMessage = room.prompt let lastResponse let result let message: ChatInfo + let user = await getUserById(userId) try { - const config = await getCacheConfig() - const userId = req.headers.userId.toString() - const user = await getUserById(userId) + // If use the fixed fakeuserid(some probability of duplicated with real ones), redefine user which is send to chatReplyProcess + if (userId === '6406d8c50aedd633885fa16f') { + user = { _id: userId, roles: [UserRole.User], useAmount: 999, advanced: { maxContextCount: 999 }, limit_switch: false } as UserInfo + } + else { + // If global usage count limit is enabled, check can use amount before process chat. + if (config.siteConfig?.usageCountLimit) { + const useAmount = user ? (user.useAmount ?? 0) : 0 + if (Number(useAmount) <= 0 && user.limit_switch) { + res.send({ status: 'Fail', message: '提问次数用完啦 | Question limit reached', data: null }) + return + } + } + } + if (config.auditConfig.enabled || config.auditConfig.customizeEnabled) { if (!user.roles.includes(UserRole.Admin) && await containsSensitiveWords(config.auditConfig, prompt)) { res.send({ status: 'Fail', message: '含有敏感词 | Contains sensitive words', data: null }) @@ -476,9 +495,15 @@ router.post('/chat-process', [auth, limiter], async (req, res) => { result.data.id, result.data.detail?.usage as UsageResponse) } + // update personal useAmount moved here + // if not fakeuserid, and has valid user info and valid useAmount set by admin nut null and limit is enabled + if (config.siteConfig?.usageCountLimit) { + if (userId !== '6406d8c50aedd633885fa16f' && user && user.useAmount && user.limit_switch) + await updateAmountMinusOne(userId) + } } catch (error) { - global.console.error(error) + globalThis.console.error(error) } } }) @@ -597,6 +622,23 @@ router.post('/session', async (req, res) => { let userInfo: { name: string; description: string; avatar: string; userId: string; root: boolean; roles: UserRole[]; config: UserConfig; advanced: AdvancedConfig } if (userId != null) { const user = await getUserById(userId) + if (user === null) { + globalThis.console.error(`session userId ${userId} but query user is null.`) + res.send({ + status: 'Success', + message: '', + data: { + auth: hasAuth, + allowRegister, + model: config.apiModel, + title: config.siteConfig.siteTitle, + chatModels, + allChatModels: chatModelOptions, + }, + }) + return + } + userInfo = { name: user.name, description: user.description, @@ -633,6 +675,22 @@ router.post('/session', async (req, res) => { value: c.key, }) }) + + res.send({ + status: 'Success', + message: '', + data: { + auth: hasAuth, + allowRegister, + model: config.apiModel, + title: config.siteConfig.siteTitle, + chatModels, + allChatModels: chatModelOptions, + usageCountLimit: config.siteConfig?.usageCountLimit, + userInfo, + }, + }) + return } res.send({ @@ -643,7 +701,7 @@ router.post('/session', async (req, res) => { allowRegister, model: config.apiModel, title: config.siteConfig.siteTitle, - chatModels, + chatModels: chatModelOptions, // if userId is null which means in nologin mode, open all model options, otherwise user can only choose gpt-3.5-turbo allChatModels: chatModelOptions, userInfo, }, @@ -690,10 +748,10 @@ router.post('/user-login', authLimiter, async (req, res) => { name: user.name ? user.name : user.email, avatar: user.avatar, description: user.description, - userId: user._id, + userId: user._id.toString(), root: user.roles.includes(UserRole.Admin), config: user.config, - }, config.siteConfig.loginSalt.trim()) + } as AuthJwtPayload, config.siteConfig.loginSalt.trim()) res.send({ status: 'Success', message: '登录成功 | Login successfully', data: { token: jwtToken } }) } catch (error) { @@ -754,6 +812,66 @@ router.post('/user-info', auth, async (req, res) => { } }) +// 使用兑换码后更新用户用量 +router.post('/user-updateamtinfo', auth, async (req, res) => { + try { + const { useAmount } = req.body as { useAmount: number } + const userId = req.headers.userId.toString() + + const user = await getUserById(userId) + if (user == null || user.status !== Status.Normal) + throw new Error('用户不存在 | User does not exist.') + await updateUserAmount(userId, useAmount) + res.send({ status: 'Success', message: '更新用量成功 | Update Amount successfully' }) + } + catch (error) { + res.send({ status: 'Fail', message: error.message, data: null }) + } +}) + +// 获取用户对话额度 +router.get('/user-getamtinfo', auth, async (req, res) => { + try { + const userId = req.headers.userId as string + const user = await getUserById(userId) + const data = { + amount: user.useAmount, + limit: user.limit_switch, + } + res.send({ status: 'Success', message: null, data }) + } + catch (error) { + console.error(error) + res.send({ status: 'Fail', message: 'Read Amount Error', data: 0 }) + } +}) +// 兑换对话额度 +router.post('/redeem-card', auth, async (req, res) => { + try { + const { redeemCardNo } = req.body as { redeemCardNo: string } + const userId = req.headers.userId.toString() + const user = await getUserById(userId) + + if (user == null || user.status !== Status.Normal) + throw new Error('用户不存在 | User does not exist.') + + const amt_isused = await getAmtByCardNo(redeemCardNo) + if (amt_isused) { + if (amt_isused.redeemed === 1) + throw new Error('该兑换码已被使用过 | RedeemCode been redeemed.') + await updateGiftCard(redeemCardNo, userId) + const data = amt_isused.amount + res.send({ status: 'Success', message: '兑换成功 | Redeem successfully', data }) + } + else { + throw new Error('该兑换码无效,请检查是否输错 | RedeemCode not exist or Misspelled.') + } + } + catch (error) { + res.send({ status: 'Fail', message: error.message, data: null }) + } +}) + router.post('/user-chat-model', auth, async (req, res) => { try { const { chatModel } = req.body as { chatModel: string } @@ -796,15 +914,16 @@ router.post('/user-status', rootAuth, async (req, res) => { } }) +// 函数中加入useAmount limit_switch router.post('/user-edit', rootAuth, async (req, res) => { try { - const { userId, email, password, roles, remark } = req.body as { userId?: string; email: string; password: string; roles: UserRole[]; remark?: string } + const { userId, email, password, roles, remark, useAmount, limit_switch } = req.body as { userId?: string; email: string; password: string; roles: UserRole[]; remark?: string; useAmount?: number; limit_switch?: boolean } if (userId) { - await updateUser(userId, roles, password, remark) + await updateUser(userId, roles, password, remark, Number(useAmount), limit_switch) } else { const newPassword = md5(password) - const user = await createUser(email, newPassword, roles, remark) + const user = await createUser(email, newPassword, roles, remark, Number(useAmount), limit_switch) await updateUserStatus(user._id.toString(), Status.Normal) } res.send({ status: 'Success', message: '更新成功 | Update successfully' }) diff --git a/service/src/middleware/auth.ts b/service/src/middleware/auth.ts index 47e72c7c..14fa13d4 100644 --- a/service/src/middleware/auth.ts +++ b/service/src/middleware/auth.ts @@ -3,13 +3,14 @@ import type { Request } from 'express' import { getCacheConfig } from '../storage/config' import { getUserById } from '../storage/mongo' import { Status } from '../storage/model' +import type { AuthJwtPayload } from '../types' -const auth = async (req, res, next) => { +async function auth(req, res, next) { const config = await getCacheConfig() if (config.siteConfig.loginEnabled) { try { const token = req.header('Authorization').replace('Bearer ', '') - const info = jwt.verify(token, config.siteConfig.loginSalt.trim()) + const info = jwt.verify(token, config.siteConfig.loginSalt.trim()) as AuthJwtPayload req.headers.userId = info.userId const user = await getUserById(info.userId) if (user == null || user.status !== Status.Normal) @@ -29,14 +30,18 @@ const auth = async (req, res, next) => { } async function getUserId(req: Request): Promise<string | undefined> { + let token: string try { - const token = req.header('Authorization').replace('Bearer ', '') + // no Authorization info is received withput login + if (!(req.header('Authorization') as string)) + return null // '6406d8c50aedd633885fa16f' + token = req.header('Authorization').replace('Bearer ', '') const config = await getCacheConfig() - const info = jwt.verify(token, config.siteConfig.loginSalt.trim()) + const info = jwt.verify(token, config.siteConfig.loginSalt.trim()) as AuthJwtPayload return info.userId } catch (error) { - + globalThis.console.error(`auth middleware getUserId err from token ${token} `, error.message) } return null } diff --git a/service/src/middleware/limiter.ts b/service/src/middleware/limiter.ts index ea0cf133..0d454384 100644 --- a/service/src/middleware/limiter.ts +++ b/service/src/middleware/limiter.ts @@ -9,11 +9,11 @@ dotenv.config() const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR const AUTH_MAX_REQUEST_PER_MINUTE = process.env.AUTH_MAX_REQUEST_PER_MINUTE -const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR))) - ? parseInt(MAX_REQUEST_PER_HOUR) +const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !Number.isNaN(Number(MAX_REQUEST_PER_HOUR))) + ? Number.parseInt(MAX_REQUEST_PER_HOUR) : 0 // 0 means unlimited -const authMaxCount = (isNotEmptyString(AUTH_MAX_REQUEST_PER_MINUTE) && !isNaN(Number(AUTH_MAX_REQUEST_PER_MINUTE))) - ? parseInt(AUTH_MAX_REQUEST_PER_MINUTE) +const authMaxCount = (isNotEmptyString(AUTH_MAX_REQUEST_PER_MINUTE) && !Number.isNaN(Number(AUTH_MAX_REQUEST_PER_MINUTE))) + ? Number.parseInt(AUTH_MAX_REQUEST_PER_MINUTE) : 0 // 0 means unlimited const limiter = rateLimit({ windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour diff --git a/service/src/middleware/rootAuth.ts b/service/src/middleware/rootAuth.ts index 7bd8432e..3ce346e2 100644 --- a/service/src/middleware/rootAuth.ts +++ b/service/src/middleware/rootAuth.ts @@ -3,15 +3,16 @@ import * as dotenv from 'dotenv' import { Status, UserRole } from '../storage/model' import { getUserById } from '../storage/mongo' import { getCacheConfig } from '../storage/config' +import type { AuthJwtPayload } from '../types' dotenv.config() -const rootAuth = async (req, res, next) => { +async function rootAuth(req, res, next) { const config = await getCacheConfig() if (config.siteConfig.loginEnabled) { try { const token = req.header('Authorization').replace('Bearer ', '') - const info = jwt.verify(token, config.siteConfig.loginSalt.trim()) + const info = jwt.verify(token, config.siteConfig.loginSalt.trim()) as AuthJwtPayload req.headers.userId = info.userId const user = await getUserById(info.userId) if (user == null || user.status !== Status.Normal || !user.roles.includes(UserRole.Admin)) @@ -28,7 +29,7 @@ const rootAuth = async (req, res, next) => { } } -const isAdmin = async (userId: string) => { +async function isAdmin(userId: string) { const user = await getUserById(userId) return user != null && user.status === Status.Normal && user.roles.includes(UserRole.Admin) } diff --git a/service/src/storage/config.ts b/service/src/storage/config.ts index a41774b0..67011987 100644 --- a/service/src/storage/config.ts +++ b/service/src/storage/config.ts @@ -27,7 +27,7 @@ export async function getOriginConfig() { let config = await getConfig() if (config == null) { config = new Config(new ObjectId(), - !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 600 * 1000, + !Number.isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 600 * 1000, process.env.OPENAI_API_KEY, process.env.OPENAI_API_DISABLE_DEBUG === 'true', process.env.OPENAI_ACCESS_TOKEN, @@ -42,7 +42,7 @@ export async function getOriginConfig() { : '', process.env.HTTPS_PROXY, new SiteConfig( - process.env.SITE_TITLE || 'ChatGpt Web', + process.env.SITE_TITLE || 'ChatGPT Web', isNotEmptyString(process.env.AUTH_SECRET_KEY), process.env.AUTH_SECRET_KEY, process.env.REGISTER_ENABLED === 'true', @@ -50,7 +50,7 @@ export async function getOriginConfig() { process.env.REGISTER_MAILS, process.env.SITE_DOMAIN), new MailConfig(process.env.SMTP_HOST, - !isNaN(+process.env.SMTP_PORT) ? +process.env.SMTP_PORT : 465, + !Number.isNaN(+process.env.SMTP_PORT) ? +process.env.SMTP_PORT : 465, process.env.SMTP_TSL === 'true', process.env.SMTP_USERNAME, process.env.SMTP_PASSWORD)) diff --git a/service/src/storage/model.ts b/service/src/storage/model.ts index f320eb23..f1359abb 100644 --- a/service/src/storage/model.ts +++ b/service/src/storage/model.ts @@ -22,6 +22,19 @@ export enum UserRole { Tester = 7, Partner = 8, } +// 新增一个兑换码的类 +export class GiftCard { + _id: ObjectId + cardno: string + amount: number + redeemed: number // boolean + redeemed_by: string + redeemed_date: string + constructor(amount: number, redeemed: number) { + this.amount = amount + this.redeemed = redeemed + } +} export class UserInfo { _id: ObjectId @@ -39,6 +52,8 @@ export class UserInfo { remark?: string secretKey?: string // 2fa advanced?: AdvancedConfig + useAmount?: number // chat usage amount + limit_switch?: boolean // chat amount limit switch constructor(email: string, password: string) { this.name = email this.email = email @@ -49,6 +64,8 @@ export class UserInfo { this.updateTime = new Date().toLocaleString() this.roles = [UserRole.User] this.remark = null + this.useAmount = null + this.limit_switch = true } } @@ -180,6 +197,7 @@ export class SiteConfig { public registerMails?: string, public siteDomain?: string, public chatModels?: string, + public usageCountLimit?: boolean, ) { } } diff --git a/service/src/storage/mongo.ts b/service/src/storage/mongo.ts index e0b83bef..fcf50b7d 100644 --- a/service/src/storage/mongo.ts +++ b/service/src/storage/mongo.ts @@ -3,16 +3,25 @@ import { MongoClient, ObjectId } from 'mongodb' import * as dotenv from 'dotenv' import dayjs from 'dayjs' import { md5 } from '../utils/security' -import type { AdvancedConfig, ChatOptions, Config, KeyConfig, UsageResponse } from './model' +import type { AdvancedConfig, ChatOptions, Config, GiftCard, KeyConfig, UsageResponse } from './model' import { ChatInfo, ChatRoom, ChatUsage, Status, UserConfig, UserInfo, UserRole } from './model' import { getCacheConfig } from './config' dotenv.config() const url = process.env.MONGODB_URL -const parsedUrl = new URL(url) -const dbName = (parsedUrl.pathname && parsedUrl.pathname !== '/') ? parsedUrl.pathname.substring(1) : 'chatgpt' -const client = new MongoClient(url) + +let client: MongoClient +let dbName: string +try { + client = new MongoClient(url) + const parsedUrl = new URL(url) + dbName = (parsedUrl.pathname && parsedUrl.pathname !== '/') ? parsedUrl.pathname.substring(1) : 'chatgpt' +} +catch (e) { + globalThis.console.error('MongoDB url invalid. please ensure set valid env MONGODB_URL.', e.message) + process.exit(1) +} const chatCol = client.db(dbName).collection<ChatInfo>('chat') const roomCol = client.db(dbName).collection<ChatRoom>('chat_room') @@ -20,6 +29,16 @@ const userCol = client.db(dbName).collection<UserInfo>('user') const configCol = client.db(dbName).collection<Config>('config') const usageCol = client.db(dbName).collection<ChatUsage>('chat_usage') const keyCol = client.db(dbName).collection<KeyConfig>('key_config') +// 新增兑换券的数据库 +// { +// "_id": { "$comment": "Mongodb系统自动" , "$type": "ObjectId" }, +// "cardno": { "$comment": "卡号(可以用csv导入)", "$type": "String" }, +// "amount": { "$comment": "卡号对应的额度", "$type": "Int32" }, +// "redeemed": { "$comment": "标记是否已被兑换,0|1表示false|true,目前类型为Int是为图方便和测试考虑以后识别泄漏啥的(多次被兑换)", "$type": "Int32" }, +// "redeemed_by": { "$comment": "执行成功兑换的用户", "$type": "String" }, +// "redeemed_date": { "$comment": "执行成功兑换的日期,考虑通用性选择了String类型,由new Date().toLocaleString()产生", "$type": "String" } +// } +const redeemCol = client.db(dbName).collection<GiftCard>('giftcards') /** * 插入聊天信息 @@ -29,6 +48,25 @@ const keyCol = client.db(dbName).collection<KeyConfig>('key_config') * @param options * @returns model */ + +// 获取、比对兑换券号码 +export async function getAmtByCardNo(redeemCardNo: string) { + // const chatInfo = new ChatInfo(roomId, uuid, text, options) + const amt_isused = await redeemCol.findOne({ cardno: redeemCardNo.trim() }) as GiftCard + return amt_isused +} +// 兑换后更新兑换券信息 +export async function updateGiftCard(redeemCardNo: string, userId: string) { + return await redeemCol.updateOne({ cardno: redeemCardNo.trim() } + , { $set: { redeemed: 1, redeemed_date: new Date().toLocaleString(), redeemed_by: userId } }) +} +// 使用对话后更新用户额度 +export async function updateAmountMinusOne(userId: string) { + const result = await userCol.updateOne({ _id: new ObjectId(userId) } + , { $inc: { useAmount: -1 } }) + return result.modifiedCount > 0 +} + export async function insertChat(uuid: number, text: string, roomId: number, options?: ChatOptions) { const chatInfo = new ChatInfo(roomId, uuid, text, options) await chatCol.insertOne(chatInfo) @@ -209,20 +247,29 @@ export async function deleteChat(roomId: number, uuid: number, inversion: boolea await chatCol.updateOne(query, update) } -export async function createUser(email: string, password: string, roles?: UserRole[], remark?: string): Promise<UserInfo> { +// createUser、updateUserInfo中加入useAmount limit_switch +export async function createUser(email: string, password: string, roles?: UserRole[], remark?: string, useAmount?: number, limit_switch?: boolean): Promise<UserInfo> { email = email.toLowerCase() const userInfo = new UserInfo(email, password) if (roles && roles.includes(UserRole.Admin)) userInfo.status = Status.Normal userInfo.roles = roles userInfo.remark = remark + userInfo.useAmount = useAmount + userInfo.limit_switch = limit_switch await userCol.insertOne(userInfo) return userInfo } export async function updateUserInfo(userId: string, user: UserInfo) { await userCol.updateOne({ _id: new ObjectId(userId) } - , { $set: { name: user.name, description: user.description, avatar: user.avatar } }) + , { $set: { name: user.name, description: user.description, avatar: user.avatar, useAmount: user.useAmount } }) +} + +// 兑换后更新用户对话额度(兑换计算目前在前端完成,将总数报给后端) +export async function updateUserAmount(userId: string, amt: number) { + return userCol.updateOne({ _id: new ObjectId(userId) } + , { $set: { useAmount: amt } }) } export async function updateUserChatModel(userId: string, chatModel: string) { @@ -310,15 +357,16 @@ export async function updateUserStatus(userId: string, status: Status) { await userCol.updateOne({ _id: new ObjectId(userId) }, { $set: { status, verifyTime: new Date().toLocaleString() } }) } -export async function updateUser(userId: string, roles: UserRole[], password: string, remark?: string) { +// 增加了useAmount信息 and limit_switch +export async function updateUser(userId: string, roles: UserRole[], password: string, remark?: string, useAmount?: number, limit_switch?: boolean) { const user = await getUserById(userId) const query = { _id: new ObjectId(userId) } if (user.password !== password && user.password) { const newPassword = md5(password) - await userCol.updateOne(query, { $set: { roles, verifyTime: new Date().toLocaleString(), password: newPassword, remark } }) + await userCol.updateOne(query, { $set: { roles, verifyTime: new Date().toLocaleString(), password: newPassword, remark, useAmount, limit_switch } }) } else { - await userCol.updateOne(query, { $set: { roles, verifyTime: new Date().toLocaleString(), remark } }) + await userCol.updateOne(query, { $set: { roles, verifyTime: new Date().toLocaleString(), remark, useAmount, limit_switch } }) } } diff --git a/service/src/types.ts b/service/src/types.ts index 1181db99..36ecb04b 100644 --- a/service/src/types.ts +++ b/service/src/types.ts @@ -1,4 +1,5 @@ import type { FetchFn } from 'chatgpt' +import type { JwtPayload } from 'jsonwebtoken' export interface RequestProps { roomId: number @@ -56,6 +57,15 @@ export interface JWT { 'scope': string } +export interface AuthJwtPayload extends JwtPayload { + name: string + avatar: string + description: string + userId: string + root: boolean + config: any +} + export class TwoFAConfig { enaled: boolean userName: string diff --git a/service/src/utils/mail.ts b/service/src/utils/mail.ts index 9a8b4ac6..80138312 100644 --- a/service/src/utils/mail.ts +++ b/service/src/utils/mail.ts @@ -1,12 +1,13 @@ -import * as fs from 'fs' -import * as path from 'path' -import { fileURLToPath } from 'node:url' -import { dirname } from 'node:path' +import * as fs from 'node:fs' +import * as path from 'node:path' +import * as url from 'node:url' + import nodemailer from 'nodemailer' + import type { MailConfig } from '../storage/model' import { getCacheConfig } from '../storage/config' -const __dirname = dirname(fileURLToPath(import.meta.url)) +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) export async function sendVerifyMail(toMail: string, verifyUrl: string) { const config = (await getCacheConfig()) diff --git a/service/src/utils/security.ts b/service/src/utils/security.ts index 29906a5b..61b7cc77 100644 --- a/service/src/utils/security.ts +++ b/service/src/utils/security.ts @@ -1,4 +1,4 @@ -import { createHash } from 'crypto' +import { createHash } from 'node:crypto' import * as dotenv from 'dotenv' import { getCacheConfig } from '../storage/config' diff --git a/service/src/utils/textAudit.ts b/service/src/utils/textAudit.ts index 88eb5048..b392a857 100644 --- a/service/src/utils/textAudit.ts +++ b/service/src/utils/textAudit.ts @@ -72,7 +72,7 @@ export class BaiduTextAuditService implements TextAuditService { return true } catch (error) { - global.console.error(`百度审核${error}`) + globalThis.console.error(`百度审核${error}`) } return false } diff --git a/src/api/index.ts b/src/api/index.ts index 949039c3..3246e432 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -117,6 +117,27 @@ export function fetchUpdateUserInfo<T = any>(name: string, avatar: string, descr }) } +// 提交用户兑换后额度 +export function fetchUpdateUserAmt<T = any>(useAmount: number) { + return post<T>({ + url: '/user-updateamtinfo', + data: { useAmount }, + }) +} +// 获取用户目前额度(因为兑换加总在前端完成,因此先查询一次实际额度) +export function fetchUserAmt<T = any>() { + return get<T>({ + url: '/user-getamtinfo', + }) +} +// 获取兑换码对应的额度 +export function decode_redeemcard<T = any>(redeemCardNo: string) { + return post<T>({ + url: '/redeem-card', + data: { redeemCardNo }, + }) +} + export function fetchUpdateUserChatModel<T = any>(chatModel: string) { return post<T>({ url: '/user-chat-model', @@ -165,10 +186,11 @@ export function fetchUpdateUserStatus<T = any>(userId: string, status: Status) { }) } +// 增加useAmount信息 limit_switch export function fetchUpdateUser<T = any>(userInfo: UserInfo) { return post<T>({ url: '/user-edit', - data: { userId: userInfo._id, roles: userInfo.roles, email: userInfo.email, password: userInfo.password, remark: userInfo.remark }, + data: { userId: userInfo._id, roles: userInfo.roles, email: userInfo.email, password: userInfo.password, remark: userInfo.remark, useAmount: userInfo.useAmount, limit_switch: userInfo.limit_switch }, }) } diff --git a/src/components/common/GitHubSite/index.vue b/src/components/common/GitHubSite/index.vue new file mode 100644 index 00000000..607cf90c --- /dev/null +++ b/src/components/common/GitHubSite/index.vue @@ -0,0 +1,18 @@ +<script setup lang="ts"> +const gitCommitSha = (import.meta.env.VITE_GIT_COMMIT_HASH || 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx').slice(0, 7) +const releaseVersion = import.meta.env.VITE_RELEASE_VERSION || 'v0.0.0' +</script> + +<template> + <div class="text-center text-xs"> + <span class="text-neutral-400"> + Powered by + </span> + <a href="https://chatgpt-web.dev" target="_blank"> + <span class="text-blue-500"> + chatgpt-web + </span> + </a> + <span v-text="`${releaseVersion}-${gitCommitSha}`" /> + </div> +</template> diff --git a/src/components/common/Setting/General.vue b/src/components/common/Setting/General.vue index 1c74fd1a..66549e4b 100644 --- a/src/components/common/Setting/General.vue +++ b/src/components/common/Setting/General.vue @@ -1,5 +1,5 @@ <script lang="ts" setup> -import { computed, ref } from 'vue' +import { computed, onMounted, ref } from 'vue' import { NButton, NDivider, NInput, NPopconfirm, NSelect, useMessage } from 'naive-ui' import { UserConfig } from '@/components/common/Setting/model' import type { Language, Theme } from '@/store/modules/app/helper' @@ -9,12 +9,17 @@ import type { UserInfo } from '@/store/modules/user/helper' import { getCurrentDate } from '@/utils/functions' import { useBasicLayout } from '@/hooks/useBasicLayout' import { t } from '@/locales' -import { fetchClearAllChat, fetchUpdateUserChatModel } from '@/api' +import { decode_redeemcard, fetchClearAllChat, fetchUpdateUserChatModel } from '@/api' const appStore = useAppStore() const userStore = useUserStore() const authStore = useAuthStore() +// 页面加载时读取后端额度(因为扣减计算在后端完成,前端信息滞后) +onMounted(() => { + userStore.readUserAmt() +}) + const { isMobile } = useBasicLayout() const ms = useMessage() @@ -29,6 +34,10 @@ const name = ref(userInfo.value.name ?? '') const description = ref(userInfo.value.description ?? '') +// 新创建额度和兑换相关响应式变量,为null的话默认送10次 +const useAmount = computed(() => userStore.userInfo.useAmount ?? 10) +const redeemCardNo = ref('') + const language = computed({ get() { return appStore.language @@ -65,7 +74,28 @@ const languageOptions: { label: string; key: Language; value: Language }[] = [ async function updateUserInfo(options: Partial<UserInfo>) { await userStore.updateUserInfo(true, options) - ms.success(t('common.success')) + ms.success(`更新个人信息 ${t('common.success')}`) +} +// 更新并兑换,这里图页面设计方便暂时先放一起了,下方页面新增了两个输入框 +async function redeemandupdateUserInfo(options: { avatar: string; name: string; description: string; useAmount: number; redeemCardNo: string }) { + const { avatar, name, description, useAmount, redeemCardNo } = options + let add_amt = 0 + let message = '' + try { + const res = await decode_redeemcard(redeemCardNo) + add_amt = Number(res.data) + message = res.message ?? '' + } + catch (error: any) { + add_amt = 0 + message = error.message ?? '' + } + const new_useAmount = useAmount + add_amt + const new_options = { avatar, name, description, useAmount: new_useAmount } + + await updateUserInfo(new_options) + userStore.readUserAmt() + ms.success(`兑换码:${message},本次充值${add_amt.toString()}次,总计${new_useAmount.toString()}次`) } async function updateUserChatModel(chatModel: string) { @@ -142,6 +172,18 @@ function handleImportButtonClick(): void { <NInput v-model:value="description" placeholder="" /> </div> </div> + <div v-if="authStore.session?.usageCountLimit && userStore.userInfo.limit" class="flex items-center space-x-4"> + <span class="flex-shrink-0 w-[100px]">{{ $t('setting.useAmount') }}</span> + <div class="flex-1"> + <div v-text="useAmount" /> + </div> + </div> + <div v-if="authStore.session?.usageCountLimit && userStore.userInfo.limit" class="flex items-center space-x-4"> + <span class="flex-shrink-0 w-[100px]">{{ $t('setting.redeemCardNo') }}</span> + <div class="flex-1"> + <NInput v-model:value="redeemCardNo" placeholder="" /> + </div> + </div> <div class="flex items-center space-x-4"> <span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span> <div class="flex-1"> @@ -150,7 +192,7 @@ function handleImportButtonClick(): void { </div> <div class="flex items-center space-x-4"> <span class="flex-shrink-0 w-[100px]">{{ $t('setting.saveUserInfo') }}</span> - <NButton type="primary" @click="updateUserInfo({ avatar, name, description })"> + <NButton type="primary" @click="redeemandupdateUserInfo({ avatar, name, description, useAmount, redeemCardNo })"> {{ $t('common.save') }} </NButton> </div> diff --git a/src/components/common/Setting/Keys.vue b/src/components/common/Setting/Keys.vue index f5bde273..601ba6a1 100644 --- a/src/components/common/Setting/Keys.vue +++ b/src/components/common/Setting/Keys.vue @@ -1,4 +1,5 @@ <script lang="ts" setup> +import type { DataTableColumns } from 'naive-ui' import { h, onMounted, reactive, ref } from 'vue' import { NButton, NDataTable, NInput, NModal, NSelect, NSpace, NSwitch, NTag, useDialog, useMessage } from 'naive-ui' import { KeyConfig, Status, UserRole, apiModelOptions, userRoleOptions } from './model' @@ -18,96 +19,85 @@ const handleSaving = ref(false) const keyConfig = ref(new KeyConfig('', 'ChatGPTAPI', [], [], '')) const keys = ref([]) -const columns = [ - { - title: 'Key', - key: 'key', - resizable: true, - width: 120, - minWidth: 50, - maxWidth: 120, - ellipsis: true, - }, - { - title: 'Api Model', - key: 'keyModel', - width: 150, - }, - { - title: 'Base url', - key: 'baseUrl', - width: 150, - }, - { - title: 'Chat Model', - key: 'chatModels', - width: 300, - render(row: any) { - const tags = row.chatModels.map((chatModel: string) => { - return h( - NTag, - { - style: { - marginRight: '6px', +const createColumns = (): DataTableColumns => { + return [ + { + title: 'Key', + key: 'key', + resizable: true, + width: 120, + minWidth: 50, + maxWidth: 120, + ellipsis: true, + }, + { + title: 'Api Model', + key: 'keyModel', + width: 150, + }, + { + title: 'Base url', + key: 'baseUrl', + width: 150, + }, + { + title: 'Chat Model', + key: 'chatModels', + width: 300, + render(row: any) { + const tags = row.chatModels.map((chatModel: string) => { + return h( + NTag, + { + style: { + marginRight: '6px', + }, + type: 'info', + bordered: false, }, - type: 'info', - bordered: false, - }, - { - default: () => chatModel, - }, - ) - }) - return tags + { + default: () => chatModel, + }, + ) + }) + return tags + }, }, - }, - { - title: 'User Roles', - key: 'userRoles', - width: 180, - render(row: any) { - const tags = row.userRoles.map((userRole: UserRole) => { - return h( - NTag, - { - style: { - marginRight: '6px', + { + title: 'User Roles', + key: 'userRoles', + width: 180, + render(row: any) { + const tags = row.userRoles.map((userRole: UserRole) => { + return h( + NTag, + { + style: { + marginRight: '6px', + }, + type: 'info', + bordered: false, }, - type: 'info', - bordered: false, - }, - { - default: () => UserRole[userRole], - }, - ) - }) - return tags + { + default: () => UserRole[userRole], + }, + ) + }) + return tags + }, }, - }, - { - title: 'Remark', - key: 'remark', - width: 150, - }, - { - title: 'Action', - key: '_id', - width: 220, - render(row: KeyConfig) { - const actions: any[] = [] - actions.push(h( - NButton, - { - size: 'small', - style: { - marginRight: '6px', - }, - type: 'error', - onClick: () => handleUpdateApiKeyStatus(row._id as string, Status.Disabled), - }, - { default: () => t('common.delete') }, - )) - if (row.status === Status.Normal) { + { + title: 'Remark', + key: 'remark', + width: 150, + }, + { + title: 'Action', + key: '_id', + width: 220, + fixed: 'right', + render(row: any) { + const actions: any[] = [] actions.push(h( NButton, { @@ -115,16 +105,33 @@ const columns = [ style: { marginRight: '6px', }, - type: 'info', - onClick: () => handleEditKey(row), + type: 'error', + onClick: () => handleUpdateApiKeyStatus(row._id as string, Status.Disabled), }, - { default: () => t('common.edit') }, + { default: () => t('common.delete') }, )) - } - return actions + if (row.status === Status.Normal) { + actions.push(h( + NButton, + { + size: 'small', + style: { + marginRight: '6px', + }, + type: 'info', + onClick: () => handleEditKey(row as KeyConfig), + }, + { default: () => t('common.edit') }, + )) + } + return actions + }, }, - }, -] + ] +} + +const columns = createColumns() + const pagination = reactive({ page: 1, pageSize: 100, diff --git a/src/components/common/Setting/Site.vue b/src/components/common/Setting/Site.vue index 1f07f716..5c74925e 100644 --- a/src/components/common/Setting/Site.vue +++ b/src/components/common/Setting/Site.vue @@ -129,6 +129,16 @@ onMounted(() => { /> </div> </div> + <div class="flex items-center space-x-4"> + <span class="flex-shrink-0 w-[100px]">{{ $t('setting.usageCountLimit') }}</span> + <div class="flex-1"> + <NSwitch + :round="false" + :value="config && config.usageCountLimit" + @update:value="(val) => { if (config) config.usageCountLimit = val }" + /> + </div> + </div> <div class="flex items-center space-x-4"> <span class="flex-shrink-0 w-[100px]" /> <NButton :loading="saving" type="primary" @click="updateSiteInfo(config)"> diff --git a/src/components/common/Setting/User.vue b/src/components/common/Setting/User.vue index ebce04ca..0a2236de 100644 --- a/src/components/common/Setting/User.vue +++ b/src/components/common/Setting/User.vue @@ -1,6 +1,7 @@ <script lang="ts" setup> +import type { DataTableColumns } from 'naive-ui' import { h, onMounted, reactive, ref } from 'vue' -import { NButton, NDataTable, NInput, NModal, NSelect, NSpace, NTag, useDialog, useMessage } from 'naive-ui' +import { NButton, NDataTable, NInput, NInputNumber, NModal, NSelect, NSpace, NSwitch, NTag, useDialog, useMessage } from 'naive-ui' import { Status, UserInfo, UserRole, userRoleOptions } from './model' import { fetchDisableUser2FAByAdmin, fetchGetUsers, fetchUpdateUser, fetchUpdateUserStatus } from '@/api' import { t } from '@/locales' @@ -15,119 +16,159 @@ const handleSaving = ref(false) const userRef = ref(new UserInfo([UserRole.User])) const users = ref([]) -const columns = [ - { - title: 'Email', - key: 'email', - resizable: true, - width: 200, - minWidth: 100, - maxWidth: 200, - }, - { - title: 'Register Time', - key: 'createTime', - width: 200, - }, - { - title: 'Verify Time', - key: 'verifyTime', - width: 200, - }, - { - title: 'Roles', - key: 'status', - width: 200, - render(row: any) { - const roles = row.roles.map((role: UserRole) => { - return h( - NTag, - { - style: { - marginRight: '6px', + +const createColumns = (): DataTableColumns => { + return [ + { + title: 'Email', + key: 'email', + resizable: true, + width: 200, + minWidth: 80, + maxWidth: 200, + }, + { + title: 'Register Time', + key: 'createTime', + resizable: true, + width: 200, + minWidth: 80, + maxWidth: 200, + }, + { + title: 'Verify Time', + key: 'verifyTime', + resizable: true, + width: 200, + minWidth: 80, + maxWidth: 200, + }, + { + title: 'Roles', + key: 'status', + resizable: true, + width: 200, + minWidth: 80, + maxWidth: 200, + render(row: any) { + const roles = row.roles.map((role: UserRole) => { + return h( + NTag, + { + style: { + marginRight: '6px', + }, + type: 'info', + bordered: false, }, - type: 'info', - bordered: false, - }, - { - default: () => UserRole[role], - }, - ) - }) - return roles + { + default: () => UserRole[role], + }, + ) + }) + return roles + }, }, - }, - { - title: 'Status', - key: 'status', - width: 80, - render(row: any) { - return Status[row.status] + { + title: 'Status', + key: 'status', + width: 80, + render(row: any) { + return Status[row.status] + }, }, - }, - { - title: 'Remark', - key: 'remark', - width: 220, - }, - { - title: 'Action', - key: '_id', - width: 220, - render(row: any) { - const actions: any[] = [] - actions.push(h( - NButton, - { - size: 'small', - type: 'error', - style: { - marginRight: '6px', - }, - onClick: () => handleUpdateUserStatus(row._id, Status.Deleted), - }, - { default: () => t('common.delete') }, - )) - if (row.status === Status.Normal) { + { + title: 'Remark', + key: 'remark', + resizable: true, + width: 200, + minWidth: 80, + maxWidth: 200, + }, + // switch off amt limit + { + title: 'Limit Enabled', + key: 'limit_switch', + resizable: true, + width: 100, + minWidth: 30, + maxWidth: 100, + render(row: any) { + return h('div', row.limit_switch ? 'True' : 'False') + }, + }, + // 新增额度信息 + { + title: 'Amounts', + key: 'useAmount', + resizable: true, + width: 80, + minWidth: 30, + maxWidth: 100, + }, + { + title: 'Action', + key: '_id', + width: 220, + fixed: 'right', + render(row: any) { + const actions: any[] = [] actions.push(h( NButton, { size: 'small', - type: 'primary', + type: 'error', style: { - marginRight: '8px', + marginRight: '6px', }, - onClick: () => handleEditUser(row), - }, - { default: () => t('chat.editUser') }, - )) - } - if (row.status === Status.PreVerify || row.status === Status.AdminVerify) { - actions.push(h( - NButton, - { - size: 'small', - type: 'info', - onClick: () => handleUpdateUserStatus(row._id, Status.Normal), - }, - { default: () => t('chat.verifiedUser') }, - )) - } - if (row.secretKey) { - actions.push(h( - NButton, - { - size: 'small', - type: 'warning', - onClick: () => handleDisable2FA(row._id), + onClick: () => handleUpdateUserStatus(row._id, Status.Deleted), }, - { default: () => t('chat.disable2FA') }, + { default: () => t('common.delete') }, )) - } - return actions + if (row.status === Status.Normal) { + actions.push(h( + NButton, + { + size: 'small', + type: 'primary', + style: { + marginRight: '8px', + }, + onClick: () => handleEditUser(row), + }, + { default: () => t('chat.editUser') }, + )) + } + if (row.status === Status.PreVerify || row.status === Status.AdminVerify) { + actions.push(h( + NButton, + { + size: 'small', + type: 'info', + onClick: () => handleUpdateUserStatus(row._id, Status.Normal), + }, + { default: () => t('chat.verifiedUser') }, + )) + } + if (row.secretKey) { + actions.push(h( + NButton, + { + size: 'small', + type: 'warning', + onClick: () => handleDisable2FA(row._id), + }, + { default: () => t('chat.disable2FA') }, + )) + } + return actions + }, }, - }, -] + ] +} + +const columns = createColumns() + const pagination = reactive ({ page: 1, pageSize: 25, @@ -247,7 +288,7 @@ onMounted(async () => { :pagination="pagination" :max-height="444" striped - :scroll-x="1260" + :scroll-x="1800" @update:page="handleGetUsers" /> </NSpace> @@ -298,6 +339,25 @@ onMounted(async () => { /> </div> </div> + <div class="flex items-center space-x-4"> + <span class="flex-shrink-0 w-[100px]">{{ $t('setting.useAmount') }}</span> + <div class="flex-1"> + <NInputNumber + v-model:value="userRef.useAmount" + :autosize="{ minRows: 1, maxRows: 2 }" placeholder="" + /> + </div> + </div> + <div class="flex items-center space-x-4"> + <span class="flex-shrink-0 w-[100px]">{{ $t('setting.limit_switch') }}</span> + <div class="flex-1"> + <NSwitch + v-model:value="userRef.limit_switch" + :round="false" + @update:value="(val) => { if (userRef) userRef.limit_switch = val }" + /> + </div> + </div> <div class="flex items-center space-x-4"> <span class="flex-shrink-0 w-[100px]" /> <NButton type="primary" :loading="handleSaving" @click="handleUpdateUser()"> diff --git a/src/components/common/Setting/model.ts b/src/components/common/Setting/model.ts index e97e1ba3..ffd2642f 100644 --- a/src/components/common/Setting/model.ts +++ b/src/components/common/Setting/model.ts @@ -29,6 +29,7 @@ export class SiteConfig { registerMails?: string siteDomain?: string chatModels?: string + usageCountLimit?: boolean } export class MailConfig { @@ -126,6 +127,9 @@ export class UserInfo { password?: string roles: UserRole[] remark?: string + useAmount?: number + // 配合改造,增加额度信息 and it's switch + limit_switch?: boolean constructor(roles: UserRole[]) { this.roles = roles } diff --git a/src/components/common/index.ts b/src/components/common/index.ts index d8f03ec6..7f62199a 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -1,3 +1,4 @@ +import GithubSite from './GitHubSite/index.vue' import HoverButton from './HoverButton/index.vue' import NaiveProvider from './NaiveProvider/index.vue' import SvgIcon from './SvgIcon/index.vue' @@ -5,4 +6,4 @@ 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 } +export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore, GithubSite } diff --git a/src/components/custom/GithubSite.vue b/src/components/custom/GithubSite.vue deleted file mode 100644 index 5f0419da..00000000 --- a/src/components/custom/GithubSite.vue +++ /dev/null @@ -1,8 +0,0 @@ -<template> - <div class="text-neutral-400"> - <span>Star on</span> - <a href="https://github.com/Chanzhaoyu/chatgpt-bot" target="_blank" class="text-blue-500"> - GitHub - </a> - </div> -</template> diff --git a/src/components/custom/index.ts b/src/components/custom/index.ts deleted file mode 100644 index 6e036989..00000000 --- a/src/components/custom/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import GithubSite from './GithubSite.vue' - -export { GithubSite } diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts index dbda3893..77b4e599 100644 --- a/src/hooks/useTheme.ts +++ b/src/hooks/useTheme.ts @@ -31,10 +31,14 @@ export function useTheme() { watch( () => isDark.value, (dark) => { - if (dark) + if (dark) { document.documentElement.classList.add('dark') - else + document.querySelector('head meta[name="theme-color"]')?.setAttribute('content', '#121212') + } + else { document.documentElement.classList.remove('dark') + document.querySelector('head meta[name="theme-color"]')?.setAttribute('content', '#eee') + } }, { immediate: true }, ) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 7f62b365..4c9c4c68 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -55,6 +55,10 @@ export default { 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.', + clickTurnOnContext: 'Click to enable sending messages will carry previous chat records.', + clickTurnOffContext: 'Click to disable sending messages will carry previous chat records.', + showOnContext: 'Include context', + showOffContext: 'Not include context', deleteMessage: 'Delete Message', deleteMessageConfirm: 'Are you sure to delete this message?', deleteHistoryConfirm: 'Are you sure to clear this history?', @@ -76,6 +80,10 @@ export default { disable2FAConfirm: 'Are you sure to disable 2FA for this user?', }, setting: { + limit_switch: 'Open Usage Limitation', + usageCountLimit: 'Enable Usage Count Limit', + redeemCardNo: 'Redeem CardNo', + useAmount: 'No. of questions', setting: 'Setting', general: 'General', advanced: 'Advanced', diff --git a/src/locales/ko-KR.ts b/src/locales/ko-KR.ts index 2404f8ed..09ed60ae 100644 --- a/src/locales/ko-KR.ts +++ b/src/locales/ko-KR.ts @@ -55,6 +55,10 @@ export default { usingContext: '컨텍스트 모드', turnOnContext: '현재 모드에서는 메시지를 보내면 이전 채팅 기록이 포함됩니다.', turnOffContext: '현재 모드에서는 메시지를 보낼 때 이전 채팅 기록이 포함되지 않습니다.', + clickTurnOnContext: '클릭하여 컨텍스트 포함 켜기', + clickTurnOffContext: '클릭하여 컨텍스트 포함 끄기', + showOnContext: '컨텍스트 포함됨', + showOffContext: '컨텍스트 미포함', deleteMessage: '메시지 삭제', deleteMessageConfirm: '이 메시지를 정말 삭제하시겠습니까?', deleteHistoryConfirm: '이 기록을 지우시겠습니까?', @@ -76,6 +80,9 @@ export default { disable2FAConfirm: 'Are you sure to disable 2FA for this user?', }, setting: { + limit_switch: '오픈 횟수 제한', + redeemCardNo: '질문 허용 횟수', + useAmount: '질문 허용 횟수', setting: '설정', general: '일반', advanced: '고급', @@ -167,7 +174,7 @@ export default { info2FAStep3Tip1: 'Note: How to turn off two-step verification?', info2FAStep3Tip2: '1. After logging in, use the two-step verification on the Two-Step Verification page to disable it.', info2FAStep3Tip3: '2. Contact the administrator to disable two-step verification.', - maxContextCount: 'Max Context Count', + maxContextCount: '최대 컨텍스트 수량', }, store: { siderButton: '프롬프트 스토어', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 68b496e9..6d1c71e1 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -55,6 +55,10 @@ export default { usingContext: '上下文模式', turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录', turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录', + clickTurnOnContext: '点击开启包含上下文', + clickTurnOffContext: '点击停止包含上下文', + showOnContext: '包含上下文', + showOffContext: '不含上下文', deleteMessage: '删除消息', deleteMessageConfirm: '是否删除此消息?', deleteHistoryConfirm: '确定删除此记录?', @@ -76,6 +80,10 @@ export default { disable2FAConfirm: '您确定要为此用户禁用两步验证吗??', }, setting: { + limit_switch: '打开次数限制', + usageCountLimit: '使用次数限制', + redeemCardNo: '兑换码卡号', + useAmount: '可提问次数', setting: '设置', general: '总览', advanced: '高级', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 1dcf9212..0875eeb6 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -55,6 +55,10 @@ export default { usingContext: '上下文模式', turnOnContext: '啟用上下文模式,在此模式下,發送訊息會包含之前的聊天記錄。', turnOffContext: '關閉上下文模式,在此模式下,發送訊息不會包含之前的聊天記錄。', + clickTurnOnContext: '點擊開啟包含上下文', + clickTurnOffContext: '點擊停止包含上下文', + showOnContext: '包含上下文', + showOffContext: '不含上下文', deleteMessage: '刪除訊息', deleteMessageConfirm: '是否刪除此訊息?', deleteHistoryConfirm: '確定刪除此紀錄?', @@ -76,6 +80,9 @@ export default { disable2FAConfirm: '您确定要为此用户禁用两步验证吗??', }, setting: { + limit_switch: '開啟次數限制', + redeemCardNo: '兌換碼卡號', + useAmount: '可提問次數', setting: '設定', general: '總覽', advanced: '高級', @@ -169,7 +176,7 @@ export default { info2FAStep3Tip1: '注意:如何关闭两步验证?', info2FAStep3Tip2: '1. 登录后,在 两步验证 页面使用两步验证码关闭。', info2FAStep3Tip3: '2. 联系管理员来关闭两步验证。', - maxContextCount: '最大上下文数量', + maxContextCount: '最大上下文數量', }, store: { siderButton: '提示詞商店', diff --git a/src/router/permission.ts b/src/router/permission.ts index 5cfc2bdf..8e23d3f8 100644 --- a/src/router/permission.ts +++ b/src/router/permission.ts @@ -8,6 +8,7 @@ export function setupPageGuard(router: Router) { if (!authStore.session) { try { const data = await authStore.getSession() + document.title = data.title if (String(data.auth) === 'false' && authStore.token) await authStore.removeToken() else diff --git a/src/store/modules/app/helper.ts b/src/store/modules/app/helper.ts index 66e6f139..d522f27e 100644 --- a/src/store/modules/app/helper.ts +++ b/src/store/modules/app/helper.ts @@ -13,7 +13,7 @@ export interface AppState { } export function defaultSetting(): AppState { - return { siderCollapsed: false, theme: 'light', language: 'zh-CN' } + return { siderCollapsed: false, theme: 'auto', language: 'zh-CN' } } export function getLocalSetting(): AppState { diff --git a/src/store/modules/auth/index.ts b/src/store/modules/auth/index.ts index 66f6b8ab..419c67dc 100644 --- a/src/store/modules/auth/index.ts +++ b/src/store/modules/auth/index.ts @@ -21,6 +21,7 @@ interface SessionResponse { key: string value: string }[] + usageCountLimit: boolean userInfo: { name: string; description: string; avatar: string; userId: string; root: boolean; config: UserConfig } } diff --git a/src/store/modules/user/helper.ts b/src/store/modules/user/helper.ts index e4d2e777..a44aa175 100644 --- a/src/store/modules/user/helper.ts +++ b/src/store/modules/user/helper.ts @@ -12,6 +12,9 @@ export interface UserInfo { config: UserConfig roles: UserRole[] advanced: SettingsState + limit?: boolean + useAmount?: number // chat usage amount + redeemCardNo?: string // add giftcard info } export interface UserState { @@ -40,6 +43,7 @@ export function defaultSetting(): UserState { top_p: 1, maxContextCount: 20, }, + useAmount: 1, // chat usage amount }, } } diff --git a/src/store/modules/user/index.ts b/src/store/modules/user/index.ts index 3a15166c..33a07c1e 100644 --- a/src/store/modules/user/index.ts +++ b/src/store/modules/user/index.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import { fetchResetAdvanced, fetchUpdateAdvanced, fetchUpdateUserInfo } from '../../../api/' +import { fetchResetAdvanced, fetchUpdateAdvanced, fetchUpdateUserAmt, fetchUpdateUserInfo, fetchUserAmt } from '../../../api/' import type { UserInfo, UserState } from './helper' import { defaultSetting, getLocalState, setLocalState } from './helper' @@ -9,13 +9,24 @@ export const useUserStore = defineStore('user-store', { async updateUserInfo(update: boolean, userInfo: Partial<UserInfo>) { this.userInfo = { ...this.userInfo, ...userInfo } this.recordState() - if (update) + if (update) { await fetchUpdateUserInfo(userInfo.name ?? '', userInfo.avatar ?? '', userInfo.description ?? '') + // 更新用户信息和额度写一起了,如果传了额度则更新 + if (userInfo.useAmount) + await fetchUpdateUserAmt(userInfo.useAmount) + } }, async updateSetting(sync: boolean) { await fetchUpdateAdvanced(sync, this.userInfo.advanced) this.recordState() }, + // 对应页面加载时的读取,为空送10个 + async readUserAmt() { + const data = (await fetchUserAmt()).data + this.userInfo.limit = data?.limit + this.userInfo.useAmount = data?.amount ?? 10 + }, + async resetSetting() { await fetchResetAdvanced() this.userInfo.advanced = { ...defaultSetting().userInfo.advanced } diff --git a/src/views/chat/components/Header/index.vue b/src/views/chat/components/Header/index.vue index ceb463e8..f0efabb3 100644 --- a/src/views/chat/components/Header/index.vue +++ b/src/views/chat/components/Header/index.vue @@ -74,11 +74,11 @@ function handleShowPrompt() { <IconPrompt class="w-[20px] m-auto" /> </span> </HoverButton> - <HoverButton :tooltip="usingContext ? '点击停止包含上下文' : '点击开启包含上下文'" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }" @click="toggleUsingContext"> + <HoverButton :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }" @click="toggleUsingContext"> <span class="text-xl"> <SvgIcon icon="ri:chat-history-line" /> </span> - <span style="margin-left:.25em">{{ usingContext ? '包含上下文' : '不含上下文' }}</span> + <span style="margin-left:.25em">{{ usingContext ? $t('chat.showOnContext') : $t('chat.showOffContext') }}</span> </HoverButton> <HoverButton @click="handleExport"> <span class="text-xl text-[#4f555e] dark:text-white"> diff --git a/src/views/chat/index.vue b/src/views/chat/index.vue index 968706bc..82058d5d 100644 --- a/src/views/chat/index.vue +++ b/src/views/chat/index.vue @@ -588,6 +588,10 @@ async function handleSyncChatModel(chatModel: string) { await chatStore.setChatModel(chatModel, +uuid) } +function formatTooltip(value: number) { + return `${t('setting.maxContextCount')}: ${value}` +} + onMounted(() => { firstLoading.value = true handleSyncChat() @@ -681,7 +685,7 @@ onUnmounted(() => { <IconPrompt class="w-[20px] m-auto" /> </span> </HoverButton> - <HoverButton v-if="!isMobile" :tooltip="usingContext ? '点击停止包含上下文' : '点击开启包含上下文'" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }" @click="handleToggleUsingContext"> + <HoverButton v-if="!isMobile" :tooltip="usingContext ? $t('chat.clickTurnOffContext') : $t('chat.clickTurnOnContext')" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }" @click="handleToggleUsingContext"> <span class="text-xl"> <SvgIcon icon="ri:chat-history-line" /> </span> @@ -694,7 +698,7 @@ onUnmounted(() => { :disabled="!!authStore.session?.auth && !authStore.token" @update-value="(val) => handleSyncChatModel(val)" /> - <NSlider v-model:value="userStore.userInfo.advanced.maxContextCount" :max="100" :min="0" :step="1" style="width: 88px" @update:value="() => { userStore.updateSetting(false) }" /> + <NSlider v-model:value="userStore.userInfo.advanced.maxContextCount" :max="100" :min="0" :step="1" style="width: 88px" :format-tooltip="formatTooltip" @update:value="() => { userStore.updateSetting(false) }" /> </div> <div class="flex items-center justify-between space-x-2"> <NAutoComplete v-model:value="prompt" :options="searchOptions" :render-label="renderOption"> diff --git a/src/views/chat/layout/sider/Footer.vue b/src/views/chat/layout/sider/Footer.vue index 4de6bbd7..b11abd60 100644 --- a/src/views/chat/layout/sider/Footer.vue +++ b/src/views/chat/layout/sider/Footer.vue @@ -14,7 +14,7 @@ async function handleLogout() { </script> <template> - <footer class="flex items-center justify-between min-w-0 p-4 overflow-hidden border-t dark:border-neutral-800"> + <footer class="flex items-center justify-between min-w-0 p-2 pl-4 overflow-hidden border-t dark:border-neutral-800"> <div class="flex-1 flex-shrink-0 overflow-hidden"> <UserAvatar /> </div> diff --git a/src/views/chat/layout/sider/index.vue b/src/views/chat/layout/sider/index.vue index 813ecb5e..809a50f7 100644 --- a/src/views/chat/layout/sider/index.vue +++ b/src/views/chat/layout/sider/index.vue @@ -6,7 +6,7 @@ import List from './List.vue' import Footer from './Footer.vue' import { useAppStore, useAuthStore, useChatStore } from '@/store' import { useBasicLayout } from '@/hooks/useBasicLayout' -import { PromptStore } from '@/components/common' +import { GithubSite, PromptStore } from '@/components/common' const appStore = useAppStore() const authStore = useAuthStore() @@ -87,6 +87,7 @@ watch( </div> </main> <Footer /> + <GithubSite class="flex-col-2 text-center m-0" /> </div> </NLayoutSider> <template v-if="isMobile"> diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 index 8440c6ce..8e7b0d3d --- a/start.sh +++ b/start.sh @@ -1,3 +1,5 @@ +export VITE_GIT_COMMIT_HASH=$(git rev-parse HEAD 2>/dev/null) +export VITE_RELEASE_VERSION=$(git describe --tags --exact-match 2>/dev/null) cd ./service nohup pnpm start > service.log &