lmt commited on
Commit
61a67bc
·
1 Parent(s): 4f52deb
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .commitlintrc.json +3 -0
  2. .dockerignore +7 -0
  3. .editorconfig +11 -0
  4. .env +10 -0
  5. .eslintignore +2 -0
  6. .eslintrc.cjs +4 -0
  7. .gitignore +32 -0
  8. .npmrc +1 -0
  9. Dockerfile +56 -0
  10. index.html +83 -0
  11. license +21 -0
  12. package.json +71 -0
  13. pnpm-lock.yaml +0 -0
  14. postcss.config.js +6 -0
  15. public/favicon.ico +0 -0
  16. public/favicon.svg +1 -0
  17. public/pwa-192x192.png +0 -0
  18. public/pwa-512x512.png +0 -0
  19. service/.env.example +44 -0
  20. service/.eslintrc.json +5 -0
  21. service/.gitignore +31 -0
  22. service/.npmrc +1 -0
  23. service/.vscode/extensions.json +3 -0
  24. service/.vscode/settings.json +22 -0
  25. service/package.json +47 -0
  26. service/pnpm-lock.yaml +0 -0
  27. service/src/chatgpt/index.ts +218 -0
  28. service/src/chatgpt/types.ts +19 -0
  29. service/src/index.ts +89 -0
  30. service/src/middleware/auth.ts +21 -0
  31. service/src/middleware/limiter.ts +19 -0
  32. service/src/types.ts +34 -0
  33. service/src/utils/index.ts +22 -0
  34. service/src/utils/is.ts +19 -0
  35. service/tsconfig.json +27 -0
  36. service/tsup.config.ts +13 -0
  37. src/App.vue +22 -0
  38. src/api/index.ts +66 -0
  39. src/assets/avatar.jpg +0 -0
  40. src/assets/recommend.json +14 -0
  41. src/components/common/HoverButton/Button.vue +20 -0
  42. src/components/common/HoverButton/index.vue +46 -0
  43. src/components/common/NaiveProvider/index.vue +43 -0
  44. src/components/common/PromptStore/index.vue +480 -0
  45. src/components/common/Setting/About.vue +75 -0
  46. src/components/common/Setting/Advanced.vue +70 -0
  47. src/components/common/Setting/General.vue +225 -0
  48. src/components/common/Setting/index.vue +70 -0
  49. src/components/common/SvgIcon/index.vue +21 -0
  50. src/components/common/UserAvatar/index.vue +40 -0
.commitlintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": ["@commitlint/config-conventional"]
3
+ }
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ **/node_modules
2
+ */node_modules
3
+ node_modules
4
+ Dockerfile
5
+ .*
6
+ */.*
7
+ !.env
.editorconfig ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Editor configuration, see http://editorconfig.org
2
+
3
+ root = true
4
+
5
+ [*]
6
+ charset = utf-8
7
+ indent_style = tab
8
+ indent_size = 2
9
+ end_of_line = lf
10
+ trim_trailing_whitespace = true
11
+ insert_final_newline = true
.env ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Glob API URL
2
+ VITE_GLOB_API_URL=/api
3
+
4
+ VITE_APP_API_BASE_URL=http://127.0.0.1:3002/
5
+
6
+ # Whether long replies are supported, which may result in higher API fees
7
+ VITE_GLOB_OPEN_LONG_REPLY=false
8
+
9
+ # When you want to use PWA
10
+ VITE_GLOB_APP_PWA=false
.eslintignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ docker-compose
2
+ kubernetes
.eslintrc.cjs ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['@antfu'],
4
+ }
.gitignore ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ .DS_Store
12
+ dist
13
+ dist-ssr
14
+ coverage
15
+ *.local
16
+
17
+ /cypress/videos/
18
+ /cypress/screenshots/
19
+
20
+ # Editor directories and files
21
+ .vscode/*
22
+ !.vscode/settings.json
23
+ !.vscode/extensions.json
24
+ .idea
25
+ *.suo
26
+ *.ntvs*
27
+ *.njsproj
28
+ *.sln
29
+ *.sw?
30
+
31
+ # Environment variables files
32
+ /service/.env
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ strict-peer-dependencies=false
Dockerfile ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # build front-end
2
+ FROM node:lts-alpine AS frontend
3
+
4
+ RUN npm install pnpm -g
5
+
6
+ WORKDIR /app
7
+
8
+ COPY ./package.json /app
9
+
10
+ COPY ./pnpm-lock.yaml /app
11
+
12
+ RUN pnpm install
13
+
14
+ COPY . /app
15
+
16
+ RUN pnpm run build
17
+
18
+ # build backend
19
+ FROM node:lts-alpine as backend
20
+
21
+ RUN npm install pnpm -g
22
+
23
+ WORKDIR /app
24
+
25
+ COPY /service/package.json /app
26
+
27
+ COPY /service/pnpm-lock.yaml /app
28
+
29
+ RUN pnpm install
30
+
31
+ COPY /service /app
32
+
33
+ RUN pnpm build
34
+
35
+ # service
36
+ FROM node:lts-alpine
37
+
38
+ RUN npm install pnpm -g
39
+
40
+ WORKDIR /app
41
+
42
+ COPY /service/package.json /app
43
+
44
+ COPY /service/pnpm-lock.yaml /app
45
+
46
+ RUN pnpm install --production && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/*
47
+
48
+ COPY /service /app
49
+
50
+ COPY --from=frontend /app/dist /app/public
51
+
52
+ COPY --from=backend /app/build /app/build
53
+
54
+ EXPOSE 3002
55
+
56
+ CMD ["pnpm", "run", "prod"]
index.html ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-cmn-Hans">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
6
+ <meta content="yes" name="apple-mobile-web-app-capable"/>
7
+ <link rel="apple-touch-icon" href="/favicon.ico">
8
+ <meta name="viewport"
9
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
10
+ <title>ChatGPT Web</title>
11
+ </head>
12
+
13
+ <body class="dark:bg-black">
14
+ <div id="app">
15
+ <style>
16
+ .loading-wrap {
17
+ display: flex;
18
+ justify-content: center;
19
+ align-items: center;
20
+ height: 100vh;
21
+ }
22
+
23
+ .balls {
24
+ width: 4em;
25
+ display: flex;
26
+ flex-flow: row nowrap;
27
+ align-items: center;
28
+ justify-content: space-between;
29
+ }
30
+
31
+ .balls div {
32
+ width: 0.8em;
33
+ height: 0.8em;
34
+ border-radius: 50%;
35
+ background-color: #4b9e5f;
36
+ }
37
+
38
+ .balls div:nth-of-type(1) {
39
+ transform: translateX(-100%);
40
+ animation: left-swing 0.5s ease-in alternate infinite;
41
+ }
42
+
43
+ .balls div:nth-of-type(3) {
44
+ transform: translateX(-95%);
45
+ animation: right-swing 0.5s ease-out alternate infinite;
46
+ }
47
+
48
+ @keyframes left-swing {
49
+
50
+ 50%,
51
+ 100% {
52
+ transform: translateX(95%);
53
+ }
54
+ }
55
+
56
+ @keyframes right-swing {
57
+ 50% {
58
+ transform: translateX(-95%);
59
+ }
60
+
61
+ 100% {
62
+ transform: translateX(100%);
63
+ }
64
+ }
65
+
66
+ @media (prefers-color-scheme: dark) {
67
+ body {
68
+ background: #121212;
69
+ }
70
+ }
71
+ </style>
72
+ <div class="loading-wrap">
73
+ <div class="balls">
74
+ <div></div>
75
+ <div></div>
76
+ <div></div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <script type="module" src="/src/main.ts"></script>
81
+ </body>
82
+
83
+ </html>
license ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 ChenZhaoYu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package.json ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chatgpt-web",
3
+ "version": "2.11.0",
4
+ "private": false,
5
+ "description": "ChatGPT Web",
6
+ "author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
7
+ "keywords": [
8
+ "chatgpt-web",
9
+ "chatgpt",
10
+ "chatbot",
11
+ "vue"
12
+ ],
13
+ "scripts": {
14
+ "dev": "vite",
15
+ "build": "run-p type-check build-only",
16
+ "preview": "vite preview",
17
+ "build-only": "vite build",
18
+ "type-check": "vue-tsc --noEmit",
19
+ "lint": "eslint .",
20
+ "lint:fix": "eslint . --fix",
21
+ "bootstrap": "pnpm install && pnpm run common:prepare",
22
+ "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml",
23
+ "common:prepare": "husky install"
24
+ },
25
+ "dependencies": {
26
+ "@traptitech/markdown-it-katex": "^3.6.0",
27
+ "@vueuse/core": "^9.13.0",
28
+ "highlight.js": "^11.7.0",
29
+ "html2canvas": "^1.4.1",
30
+ "katex": "^0.16.4",
31
+ "markdown-it": "^13.0.1",
32
+ "naive-ui": "^2.34.3",
33
+ "pinia": "^2.0.33",
34
+ "vue": "^3.2.47",
35
+ "vue-i18n": "^9.2.2",
36
+ "vue-router": "^4.1.6"
37
+ },
38
+ "devDependencies": {
39
+ "@antfu/eslint-config": "^0.35.3",
40
+ "@commitlint/cli": "^17.4.4",
41
+ "@commitlint/config-conventional": "^17.4.4",
42
+ "@iconify/vue": "^4.1.0",
43
+ "@types/crypto-js": "^4.1.1",
44
+ "@types/katex": "^0.16.0",
45
+ "@types/markdown-it": "^12.2.3",
46
+ "@types/markdown-it-link-attributes": "^3.0.1",
47
+ "@types/node": "^18.14.6",
48
+ "@vitejs/plugin-vue": "^4.0.0",
49
+ "autoprefixer": "^10.4.13",
50
+ "axios": "^1.3.4",
51
+ "crypto-js": "^4.1.1",
52
+ "eslint": "^8.35.0",
53
+ "husky": "^8.0.3",
54
+ "less": "^4.1.3",
55
+ "lint-staged": "^13.1.2",
56
+ "markdown-it-link-attributes": "^4.0.1",
57
+ "npm-run-all": "^4.1.5",
58
+ "postcss": "^8.4.21",
59
+ "rimraf": "^4.2.0",
60
+ "tailwindcss": "^3.2.7",
61
+ "typescript": "~4.9.5",
62
+ "vite": "^4.2.0",
63
+ "vite-plugin-pwa": "^0.14.4",
64
+ "vue-tsc": "^1.2.0"
65
+ },
66
+ "lint-staged": {
67
+ "*.{ts,tsx,vue}": [
68
+ "pnpm lint:fix"
69
+ ]
70
+ }
71
+ }
pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
public/favicon.ico ADDED
public/favicon.svg ADDED
public/pwa-192x192.png ADDED
public/pwa-512x512.png ADDED
service/.env.example ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenAI API Key - https://platform.openai.com/overview
2
+ OPENAI_API_KEY=
3
+
4
+ # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
5
+ OPENAI_ACCESS_TOKEN=
6
+
7
+ # OpenAI API Base URL - https://api.openai.com
8
+ OPENAI_API_BASE_URL=
9
+
10
+ # OpenAI API Model - https://platform.openai.com/docs/models
11
+ OPENAI_API_MODEL=
12
+
13
+ # set `true` to disable OpenAI API debug log
14
+ OPENAI_API_DISABLE_DEBUG=
15
+
16
+ # Reverse Proxy - Available on accessToken
17
+ # Default: https://ai.fakeopen.com/api/conversation
18
+ # More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy
19
+ API_REVERSE_PROXY=
20
+
21
+ # timeout
22
+ TIMEOUT_MS=100000
23
+
24
+ # Rate Limit
25
+ MAX_REQUEST_PER_HOUR=
26
+
27
+ # Secret key
28
+ AUTH_SECRET_KEY=
29
+
30
+ # Socks Proxy Host
31
+ SOCKS_PROXY_HOST=
32
+
33
+ # Socks Proxy Port
34
+ SOCKS_PROXY_PORT=
35
+
36
+ # Socks Proxy Username
37
+ SOCKS_PROXY_USERNAME=
38
+
39
+ # Socks Proxy Password
40
+ SOCKS_PROXY_PASSWORD=
41
+
42
+ # HTTPS PROXY
43
+ HTTPS_PROXY=
44
+
service/.eslintrc.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "root": true,
3
+ "ignorePatterns": ["build"],
4
+ "extends": ["@antfu"]
5
+ }
service/.gitignore ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ .DS_Store
12
+ dist
13
+ dist-ssr
14
+ coverage
15
+ *.local
16
+
17
+ /cypress/videos/
18
+ /cypress/screenshots/
19
+
20
+ # Editor directories and files
21
+ .vscode/*
22
+ !.vscode/settings.json
23
+ !.vscode/extensions.json
24
+ .idea
25
+ *.suo
26
+ *.ntvs*
27
+ *.njsproj
28
+ *.sln
29
+ *.sw?
30
+
31
+ build
service/.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ enable-pre-post-scripts=true
service/.vscode/extensions.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "recommendations": ["dbaeumer.vscode-eslint"]
3
+ }
service/.vscode/settings.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "prettier.enable": false,
3
+ "editor.formatOnSave": false,
4
+ "editor.codeActionsOnSave": {
5
+ "source.fixAll.eslint": true
6
+ },
7
+ "eslint.validate": [
8
+ "javascript",
9
+ "typescript",
10
+ "json",
11
+ "jsonc",
12
+ "json5",
13
+ "yaml"
14
+ ],
15
+ "cSpell.words": [
16
+ "antfu",
17
+ "chatgpt",
18
+ "esno",
19
+ "GPTAPI",
20
+ "OPENAI"
21
+ ]
22
+ }
service/package.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chatgpt-web-service",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "ChatGPT Web Service",
6
+ "author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
7
+ "keywords": [
8
+ "chatgpt-web",
9
+ "chatgpt",
10
+ "chatbot",
11
+ "express"
12
+ ],
13
+ "engines": {
14
+ "node": "^16 || ^18 || ^19"
15
+ },
16
+ "scripts": {
17
+ "start": "esno ./src/index.ts",
18
+ "dev": "esno watch ./src/index.ts",
19
+ "prod": "node ./build/index.mjs",
20
+ "build": "pnpm clean && tsup",
21
+ "clean": "rimraf build",
22
+ "lint": "eslint .",
23
+ "lint:fix": "eslint . --fix",
24
+ "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml"
25
+ },
26
+ "dependencies": {
27
+ "axios": "^1.3.4",
28
+ "chatgpt": "^5.1.2",
29
+ "dotenv": "^16.0.3",
30
+ "esno": "^0.16.3",
31
+ "express": "^4.18.2",
32
+ "express-rate-limit": "^6.7.0",
33
+ "https-proxy-agent": "^5.0.1",
34
+ "isomorphic-fetch": "^3.0.0",
35
+ "node-fetch": "^3.3.0",
36
+ "socks-proxy-agent": "^7.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@antfu/eslint-config": "^0.35.3",
40
+ "@types/express": "^4.17.17",
41
+ "@types/node": "^18.14.6",
42
+ "eslint": "^8.35.0",
43
+ "rimraf": "^4.3.0",
44
+ "tsup": "^6.6.3",
45
+ "typescript": "^4.9.5"
46
+ }
47
+ }
service/pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
service/src/chatgpt/index.ts ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as dotenv from 'dotenv'
2
+ import 'isomorphic-fetch'
3
+ import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt'
4
+ import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt'
5
+ import { SocksProxyAgent } from 'socks-proxy-agent'
6
+ import httpsProxyAgent from 'https-proxy-agent'
7
+ import fetch from 'node-fetch'
8
+ import { sendResponse } from '../utils'
9
+ import { isNotEmptyString } from '../utils/is'
10
+ import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
11
+ import type { RequestOptions, SetProxyOptions, UsageResponse } from './types'
12
+
13
+ const { HttpsProxyAgent } = httpsProxyAgent
14
+
15
+ dotenv.config()
16
+
17
+ const ErrorCodeMessage: Record<string, string> = {
18
+ 401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided',
19
+ 403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later',
20
+ 502: '[OpenAI] 错误的网关 | Bad Gateway',
21
+ 503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later',
22
+ 504: '[OpenAI] 网关超时 | Gateway Time-out',
23
+ 500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error',
24
+ }
25
+
26
+ const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 100 * 1000
27
+ const disableDebug: boolean = process.env.OPENAI_API_DISABLE_DEBUG === 'true'
28
+
29
+ let apiModel: ApiModel
30
+ const model = isNotEmptyString(process.env.OPENAI_API_MODEL) ? process.env.OPENAI_API_MODEL : 'gpt-3.5-turbo'
31
+
32
+ if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN))
33
+ throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable')
34
+
35
+ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
36
+
37
+ (async () => {
38
+ // More Info: https://github.com/transitive-bullshit/chatgpt-api
39
+
40
+ if (isNotEmptyString(process.env.OPENAI_API_KEY)) {
41
+ const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
42
+
43
+ const options: ChatGPTAPIOptions = {
44
+ apiKey: process.env.OPENAI_API_KEY,
45
+ completionParams: { model },
46
+ debug: !disableDebug,
47
+ }
48
+
49
+ // increase max token limit if use gpt-4
50
+ if (model.toLowerCase().includes('gpt-4')) {
51
+ // if use 32k model
52
+ if (model.toLowerCase().includes('32k')) {
53
+ options.maxModelTokens = 32768
54
+ options.maxResponseTokens = 8192
55
+ }
56
+ else {
57
+ options.maxModelTokens = 8192
58
+ options.maxResponseTokens = 2048
59
+ }
60
+ }
61
+
62
+ if (isNotEmptyString(OPENAI_API_BASE_URL))
63
+ options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1`
64
+
65
+ setupProxy(options)
66
+
67
+ api = new ChatGPTAPI({ ...options })
68
+ apiModel = 'ChatGPTAPI'
69
+ }
70
+ else {
71
+ const options: ChatGPTUnofficialProxyAPIOptions = {
72
+ accessToken: process.env.OPENAI_ACCESS_TOKEN,
73
+ apiReverseProxyUrl: isNotEmptyString(process.env.API_REVERSE_PROXY) ? process.env.API_REVERSE_PROXY : 'https://ai.fakeopen.com/api/conversation',
74
+ model,
75
+ debug: !disableDebug,
76
+ }
77
+
78
+ setupProxy(options)
79
+
80
+ api = new ChatGPTUnofficialProxyAPI({ ...options })
81
+ apiModel = 'ChatGPTUnofficialProxyAPI'
82
+ }
83
+ })()
84
+
85
+ async function chatReplyProcess(options: RequestOptions) {
86
+ const { message, lastContext, process, systemMessage, temperature, top_p } = options
87
+ try {
88
+ let options: SendMessageOptions = { timeoutMs }
89
+
90
+ if (apiModel === 'ChatGPTAPI') {
91
+ if (isNotEmptyString(systemMessage))
92
+ options.systemMessage = systemMessage
93
+ options.completionParams = { model, temperature, top_p }
94
+ }
95
+
96
+ if (lastContext != null) {
97
+ if (apiModel === 'ChatGPTAPI')
98
+ options.parentMessageId = lastContext.parentMessageId
99
+ else
100
+ options = { ...lastContext }
101
+ }
102
+
103
+ const response = await api.sendMessage(message, {
104
+ ...options,
105
+ onProgress: (partialResponse) => {
106
+ process?.(partialResponse)
107
+ },
108
+ })
109
+
110
+ return sendResponse({ type: 'Success', data: response })
111
+ }
112
+ catch (error: any) {
113
+ const code = error.statusCode
114
+ global.console.log(error)
115
+ if (Reflect.has(ErrorCodeMessage, code))
116
+ return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] })
117
+ return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' })
118
+ }
119
+ }
120
+
121
+ async function fetchUsage() {
122
+ const OPENAI_API_KEY = process.env.OPENAI_API_KEY
123
+ const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
124
+
125
+ if (!isNotEmptyString(OPENAI_API_KEY))
126
+ return Promise.resolve('-')
127
+
128
+ const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL)
129
+ ? OPENAI_API_BASE_URL
130
+ : 'https://api.openai.com'
131
+
132
+ const [startDate, endDate] = formatDate()
133
+
134
+ // 每月使用量
135
+ const urlUsage = `${API_BASE_URL}/v1/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`
136
+
137
+ const headers = {
138
+ 'Authorization': `Bearer ${OPENAI_API_KEY}`,
139
+ 'Content-Type': 'application/json',
140
+ }
141
+
142
+ const options = {} as SetProxyOptions
143
+
144
+ setupProxy(options)
145
+
146
+ try {
147
+ // 获取已使用量
148
+ const useResponse = await options.fetch(urlUsage, { headers })
149
+ if (!useResponse.ok)
150
+ throw new Error('获取使用量失败')
151
+ const usageData = await useResponse.json() as UsageResponse
152
+ const usage = Math.round(usageData.total_usage) / 100
153
+ return Promise.resolve(usage ? `$${usage}` : '-')
154
+ }
155
+ catch (error) {
156
+ global.console.log(error)
157
+ return Promise.resolve('-')
158
+ }
159
+ }
160
+
161
+ function formatDate(): string[] {
162
+ const today = new Date()
163
+ const year = today.getFullYear()
164
+ const month = today.getMonth() + 1
165
+ const lastDay = new Date(year, month, 0)
166
+ const formattedFirstDay = `${year}-${month.toString().padStart(2, '0')}-01`
167
+ const formattedLastDay = `${year}-${month.toString().padStart(2, '0')}-${lastDay.getDate().toString().padStart(2, '0')}`
168
+ return [formattedFirstDay, formattedLastDay]
169
+ }
170
+
171
+ async function chatConfig() {
172
+ const usage = await fetchUsage()
173
+ const reverseProxy = process.env.API_REVERSE_PROXY ?? '-'
174
+ const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-'
175
+ const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT)
176
+ ? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`)
177
+ : '-'
178
+ return sendResponse<ModelConfig>({
179
+ type: 'Success',
180
+ data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, usage },
181
+ })
182
+ }
183
+
184
+ function setupProxy(options: SetProxyOptions) {
185
+ if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) {
186
+ const agent = new SocksProxyAgent({
187
+ hostname: process.env.SOCKS_PROXY_HOST,
188
+ port: process.env.SOCKS_PROXY_PORT,
189
+ userId: isNotEmptyString(process.env.SOCKS_PROXY_USERNAME) ? process.env.SOCKS_PROXY_USERNAME : undefined,
190
+ password: isNotEmptyString(process.env.SOCKS_PROXY_PASSWORD) ? process.env.SOCKS_PROXY_PASSWORD : undefined,
191
+ })
192
+ options.fetch = (url, options) => {
193
+ return fetch(url, { agent, ...options })
194
+ }
195
+ }
196
+ else if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(process.env.ALL_PROXY)) {
197
+ const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY
198
+ if (httpsProxy) {
199
+ const agent = new HttpsProxyAgent(httpsProxy)
200
+ options.fetch = (url, options) => {
201
+ return fetch(url, { agent, ...options })
202
+ }
203
+ }
204
+ }
205
+ else {
206
+ options.fetch = (url, options) => {
207
+ return fetch(url, { ...options })
208
+ }
209
+ }
210
+ }
211
+
212
+ function currentModel(): ApiModel {
213
+ return apiModel
214
+ }
215
+
216
+ export type { ChatContext, ChatMessage }
217
+
218
+ export { chatReplyProcess, chatConfig, currentModel }
service/src/chatgpt/types.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ChatMessage } from 'chatgpt'
2
+ import type fetch from 'node-fetch'
3
+
4
+ export interface RequestOptions {
5
+ message: string
6
+ lastContext?: { conversationId?: string; parentMessageId?: string }
7
+ process?: (chat: ChatMessage) => void
8
+ systemMessage?: string
9
+ temperature?: number
10
+ top_p?: number
11
+ }
12
+
13
+ export interface SetProxyOptions {
14
+ fetch?: typeof fetch
15
+ }
16
+
17
+ export interface UsageResponse {
18
+ total_usage: number
19
+ }
service/src/index.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express'
2
+ import type { RequestProps } from './types'
3
+ import type { ChatMessage } from './chatgpt'
4
+ import { chatConfig, chatReplyProcess, currentModel } from './chatgpt'
5
+ import { auth } from './middleware/auth'
6
+ import { limiter } from './middleware/limiter'
7
+ import { isNotEmptyString } from './utils/is'
8
+
9
+ const app = express()
10
+ const router = express.Router()
11
+
12
+ app.use(express.static('public'))
13
+ app.use(express.json())
14
+
15
+ app.all('*', (_, res, next) => {
16
+ res.header('Access-Control-Allow-Origin', '*')
17
+ res.header('Access-Control-Allow-Headers', 'authorization, Content-Type')
18
+ res.header('Access-Control-Allow-Methods', '*')
19
+ next()
20
+ })
21
+
22
+ router.post('/chat-process', [auth, limiter], async (req, res) => {
23
+ res.setHeader('Content-type', 'application/octet-stream')
24
+
25
+ try {
26
+ const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps
27
+ let firstChunk = true
28
+ await chatReplyProcess({
29
+ message: prompt,
30
+ lastContext: options,
31
+ process: (chat: ChatMessage) => {
32
+ res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
33
+ firstChunk = false
34
+ },
35
+ systemMessage,
36
+ temperature,
37
+ top_p,
38
+ })
39
+ }
40
+ catch (error) {
41
+ res.write(JSON.stringify(error))
42
+ }
43
+ finally {
44
+ res.end()
45
+ }
46
+ })
47
+
48
+ router.post('/config', auth, async (req, res) => {
49
+ try {
50
+ const response = await chatConfig()
51
+ res.send(response)
52
+ }
53
+ catch (error) {
54
+ res.send(error)
55
+ }
56
+ })
57
+
58
+ router.post('/session', async (req, res) => {
59
+ try {
60
+ const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
61
+ const hasAuth = isNotEmptyString(AUTH_SECRET_KEY)
62
+ res.send({ status: 'Success', message: '', data: { auth: hasAuth, model: currentModel() } })
63
+ }
64
+ catch (error) {
65
+ res.send({ status: 'Fail', message: error.message, data: null })
66
+ }
67
+ })
68
+
69
+ router.post('/verify', async (req, res) => {
70
+ try {
71
+ const { token } = req.body as { token: string }
72
+ if (!token)
73
+ throw new Error('Secret key is empty')
74
+
75
+ if (process.env.AUTH_SECRET_KEY !== token)
76
+ throw new Error('密钥无效 | Secret key is invalid')
77
+
78
+ res.send({ status: 'Success', message: 'Verify successfully', data: null })
79
+ }
80
+ catch (error) {
81
+ res.send({ status: 'Fail', message: error.message, data: null })
82
+ }
83
+ })
84
+
85
+ app.use('', router)
86
+ app.use('/api', router)
87
+ app.set('trust proxy', 1)
88
+
89
+ app.listen(3002, () => globalThis.console.log('Server is running on port 3002'))
service/src/middleware/auth.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { isNotEmptyString } from '../utils/is'
2
+
3
+ const auth = async (req, res, next) => {
4
+ const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
5
+ if (isNotEmptyString(AUTH_SECRET_KEY)) {
6
+ try {
7
+ const Authorization = req.header('Authorization')
8
+ if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim())
9
+ throw new Error('Error: 无访问权限 | No access rights')
10
+ next()
11
+ }
12
+ catch (error) {
13
+ res.send({ status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null })
14
+ }
15
+ }
16
+ else {
17
+ next()
18
+ }
19
+ }
20
+
21
+ export { auth }
service/src/middleware/limiter.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { rateLimit } from 'express-rate-limit'
2
+ import { isNotEmptyString } from '../utils/is'
3
+
4
+ const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR
5
+
6
+ const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR)))
7
+ ? parseInt(MAX_REQUEST_PER_HOUR)
8
+ : 0 // 0 means unlimited
9
+
10
+ const limiter = rateLimit({
11
+ windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour
12
+ max: maxCount,
13
+ statusCode: 200, // 200 means success,but the message is 'Too many request from this IP in 1 hour'
14
+ message: async (req, res) => {
15
+ res.send({ status: 'Fail', message: 'Too many request from this IP in 1 hour', data: null })
16
+ },
17
+ })
18
+
19
+ export { limiter }
service/src/types.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { FetchFn } from 'chatgpt'
2
+
3
+ export interface RequestProps {
4
+ prompt: string
5
+ options?: ChatContext
6
+ systemMessage: string
7
+ temperature?: number
8
+ top_p?: number
9
+ }
10
+
11
+ export interface ChatContext {
12
+ conversationId?: string
13
+ parentMessageId?: string
14
+ }
15
+
16
+ export interface ChatGPTUnofficialProxyAPIOptions {
17
+ accessToken: string
18
+ apiReverseProxyUrl?: string
19
+ model?: string
20
+ debug?: boolean
21
+ headers?: Record<string, string>
22
+ fetch?: FetchFn
23
+ }
24
+
25
+ export interface ModelConfig {
26
+ apiModel?: ApiModel
27
+ reverseProxy?: string
28
+ timeoutMs?: number
29
+ socksProxy?: string
30
+ httpsProxy?: string
31
+ usage?: string
32
+ }
33
+
34
+ export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined
service/src/utils/index.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface SendResponseOptions<T = any> {
2
+ type: 'Success' | 'Fail'
3
+ message?: string
4
+ data?: T
5
+ }
6
+
7
+ export function sendResponse<T>(options: SendResponseOptions<T>) {
8
+ if (options.type === 'Success') {
9
+ return Promise.resolve({
10
+ message: options.message ?? null,
11
+ data: options.data ?? null,
12
+ status: options.type,
13
+ })
14
+ }
15
+
16
+ // eslint-disable-next-line prefer-promise-reject-errors
17
+ return Promise.reject({
18
+ message: options.message ?? 'Failed',
19
+ data: options.data ?? null,
20
+ status: options.type,
21
+ })
22
+ }
service/src/utils/is.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function isNumber<T extends number>(value: T | unknown): value is number {
2
+ return Object.prototype.toString.call(value) === '[object Number]'
3
+ }
4
+
5
+ export function isString<T extends string>(value: T | unknown): value is string {
6
+ return Object.prototype.toString.call(value) === '[object String]'
7
+ }
8
+
9
+ export function isNotEmptyString(value: any): boolean {
10
+ return typeof value === 'string' && value.length > 0
11
+ }
12
+
13
+ export function isBoolean<T extends boolean>(value: T | unknown): value is boolean {
14
+ return Object.prototype.toString.call(value) === '[object Boolean]'
15
+ }
16
+
17
+ export function isFunction<T extends (...args: any[]) => any | void | never>(value: T | unknown): value is T {
18
+ return Object.prototype.toString.call(value) === '[object Function]'
19
+ }
service/tsconfig.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "lib": [
5
+ "esnext"
6
+ ],
7
+ "allowJs": true,
8
+ "skipLibCheck": true,
9
+ "strict": false,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "esModuleInterop": true,
12
+ "module": "esnext",
13
+ "moduleResolution": "node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "baseUrl": ".",
17
+ "outDir": "build",
18
+ "noEmit": true
19
+ },
20
+ "exclude": [
21
+ "node_modules",
22
+ "build"
23
+ ],
24
+ "include": [
25
+ "**/*.ts"
26
+ ]
27
+ }
service/tsup.config.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ outDir: 'build',
6
+ target: 'es2020',
7
+ format: ['esm'],
8
+ splitting: false,
9
+ sourcemap: true,
10
+ minify: false,
11
+ shims: true,
12
+ dts: false,
13
+ })
src/App.vue ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { NConfigProvider } from 'naive-ui'
3
+ import { NaiveProvider } from '@/components/common'
4
+ import { useTheme } from '@/hooks/useTheme'
5
+ import { useLanguage } from '@/hooks/useLanguage'
6
+
7
+ const { theme, themeOverrides } = useTheme()
8
+ const { language } = useLanguage()
9
+ </script>
10
+
11
+ <template>
12
+ <NConfigProvider
13
+ class="h-full"
14
+ :theme="theme"
15
+ :theme-overrides="themeOverrides"
16
+ :locale="language"
17
+ >
18
+ <NaiveProvider>
19
+ <RouterView />
20
+ </NaiveProvider>
21
+ </NConfigProvider>
22
+ </template>
src/api/index.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
2
+ import { post } from '@/utils/request'
3
+ import { useAuthStore, useSettingStore } from '@/store'
4
+
5
+ export function fetchChatAPI<T = any>(
6
+ prompt: string,
7
+ options?: { conversationId?: string; parentMessageId?: string },
8
+ signal?: GenericAbortSignal,
9
+ ) {
10
+ return post<T>({
11
+ url: '/chat',
12
+ data: { prompt, options },
13
+ signal,
14
+ })
15
+ }
16
+
17
+ export function fetchChatConfig<T = any>() {
18
+ return post<T>({
19
+ url: '/config',
20
+ })
21
+ }
22
+
23
+ export function fetchChatAPIProcess<T = any>(
24
+ params: {
25
+ prompt: string
26
+ options?: { conversationId?: string; parentMessageId?: string }
27
+ signal?: GenericAbortSignal
28
+ onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
29
+ ) {
30
+ const settingStore = useSettingStore()
31
+ const authStore = useAuthStore()
32
+
33
+ let data: Record<string, any> = {
34
+ prompt: params.prompt,
35
+ options: params.options,
36
+ }
37
+
38
+ if (authStore.isChatGPTAPI) {
39
+ data = {
40
+ ...data,
41
+ systemMessage: settingStore.systemMessage,
42
+ temperature: settingStore.temperature,
43
+ top_p: settingStore.top_p,
44
+ }
45
+ }
46
+
47
+ return post<T>({
48
+ url: '/chat-process',
49
+ data,
50
+ signal: params.signal,
51
+ onDownloadProgress: params.onDownloadProgress,
52
+ })
53
+ }
54
+
55
+ export function fetchSession<T>() {
56
+ return post<T>({
57
+ url: '/session',
58
+ })
59
+ }
60
+
61
+ export function fetchVerify<T>(token: string) {
62
+ return post<T>({
63
+ url: '/verify',
64
+ data: { token },
65
+ })
66
+ }
src/assets/avatar.jpg ADDED
src/assets/recommend.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "key": "awesome-chatgpt-prompts-zh",
4
+ "desc": "ChatGPT 中文调教指南",
5
+ "downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json",
6
+ "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh"
7
+ },
8
+ {
9
+ "key": "awesome-chatgpt-prompts-zh-TW",
10
+ "desc": "ChatGPT 中文調教指南 (透過 OpenAI / OpenCC 協助,從簡體中文轉換為繁體中文的版本)",
11
+ "downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh-TW.json",
12
+ "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh"
13
+ }
14
+ ]
src/components/common/HoverButton/Button.vue ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang='ts'>
2
+ interface Emit {
3
+ (e: 'click'): void
4
+ }
5
+
6
+ const emit = defineEmits<Emit>()
7
+
8
+ function handleClick() {
9
+ emit('click')
10
+ }
11
+ </script>
12
+
13
+ <template>
14
+ <button
15
+ class="flex items-center justify-center w-10 h-10 transition rounded-full hover:bg-neutral-100 dark:hover:bg-[#414755]"
16
+ @click="handleClick"
17
+ >
18
+ <slot />
19
+ </button>
20
+ </template>
src/components/common/HoverButton/index.vue ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang='ts'>
2
+ import { computed } from 'vue'
3
+ import type { PopoverPlacement } from 'naive-ui'
4
+ import { NTooltip } from 'naive-ui'
5
+ import Button from './Button.vue'
6
+
7
+ interface Props {
8
+ tooltip?: string
9
+ placement?: PopoverPlacement
10
+ }
11
+
12
+ interface Emit {
13
+ (e: 'click'): void
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ tooltip: '',
18
+ placement: 'bottom',
19
+ })
20
+
21
+ const emit = defineEmits<Emit>()
22
+
23
+ const showTooltip = computed(() => Boolean(props.tooltip))
24
+
25
+ function handleClick() {
26
+ emit('click')
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <div v-if="showTooltip">
32
+ <NTooltip :placement="placement" trigger="hover">
33
+ <template #trigger>
34
+ <Button @click="handleClick">
35
+ <slot />
36
+ </Button>
37
+ </template>
38
+ {{ tooltip }}
39
+ </NTooltip>
40
+ </div>
41
+ <div v-else>
42
+ <Button @click="handleClick">
43
+ <slot />
44
+ </Button>
45
+ </div>
46
+ </template>
src/components/common/NaiveProvider/index.vue ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { defineComponent, h } from 'vue'
3
+ import {
4
+ NDialogProvider,
5
+ NLoadingBarProvider,
6
+ NMessageProvider,
7
+ NNotificationProvider,
8
+ useDialog,
9
+ useLoadingBar,
10
+ useMessage,
11
+ useNotification,
12
+ } from 'naive-ui'
13
+
14
+ function registerNaiveTools() {
15
+ window.$loadingBar = useLoadingBar()
16
+ window.$dialog = useDialog()
17
+ window.$message = useMessage()
18
+ window.$notification = useNotification()
19
+ }
20
+
21
+ const NaiveProviderContent = defineComponent({
22
+ name: 'NaiveProviderContent',
23
+ setup() {
24
+ registerNaiveTools()
25
+ },
26
+ render() {
27
+ return h('div')
28
+ },
29
+ })
30
+ </script>
31
+
32
+ <template>
33
+ <NLoadingBarProvider>
34
+ <NDialogProvider>
35
+ <NNotificationProvider>
36
+ <NMessageProvider>
37
+ <slot />
38
+ <NaiveProviderContent />
39
+ </NMessageProvider>
40
+ </NNotificationProvider>
41
+ </NDialogProvider>
42
+ </NLoadingBarProvider>
43
+ </template>
src/components/common/PromptStore/index.vue ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang='ts'>
2
+ import type { DataTableColumns } from 'naive-ui'
3
+ import { computed, h, ref, watch } from 'vue'
4
+ import { NButton, NCard, NDataTable, NDivider, NInput, NList, NListItem, NModal, NPopconfirm, NSpace, NTabPane, NTabs, NThing, useMessage } from 'naive-ui'
5
+ import PromptRecommend from '../../../assets/recommend.json'
6
+ import { SvgIcon } from '..'
7
+ import { usePromptStore } from '@/store'
8
+ import { useBasicLayout } from '@/hooks/useBasicLayout'
9
+ import { t } from '@/locales'
10
+
11
+ interface DataProps {
12
+ renderKey: string
13
+ renderValue: string
14
+ key: string
15
+ value: string
16
+ }
17
+
18
+ interface Props {
19
+ visible: boolean
20
+ }
21
+
22
+ interface Emit {
23
+ (e: 'update:visible', visible: boolean): void
24
+ }
25
+
26
+ const props = defineProps<Props>()
27
+
28
+ const emit = defineEmits<Emit>()
29
+
30
+ const message = useMessage()
31
+
32
+ const show = computed({
33
+ get: () => props.visible,
34
+ set: (visible: boolean) => emit('update:visible', visible),
35
+ })
36
+
37
+ const showModal = ref(false)
38
+
39
+ const importLoading = ref(false)
40
+ const exportLoading = ref(false)
41
+
42
+ const searchValue = ref<string>('')
43
+
44
+ // 移动端自适应相关
45
+ const { isMobile } = useBasicLayout()
46
+
47
+ const promptStore = usePromptStore()
48
+
49
+ // Prompt在线导入推荐List,根据部署者喜好进行修改(assets/recommend.json)
50
+ const promptRecommendList = PromptRecommend
51
+ const promptList = ref<any>(promptStore.promptList)
52
+
53
+ // 用于添加修改的临时prompt参数
54
+ const tempPromptKey = ref('')
55
+ const tempPromptValue = ref('')
56
+
57
+ // Modal模式,根据不同模式渲染不同的Modal内容
58
+ const modalMode = ref('')
59
+
60
+ // 这个是为了后期的修改Prompt内容考虑,因为要针对无uuid的list进行修改,且考虑到不能出现标题和内容的冲突,所以就需要一个临时item来记录一下
61
+ const tempModifiedItem = ref<any>({})
62
+
63
+ // 添加修改导入都使用一个Modal, 临时修改内容占用tempPromptKey,切换状态前先将内容都清楚
64
+ const changeShowModal = (mode: 'add' | 'modify' | 'local_import', selected = { key: '', value: '' }) => {
65
+ if (mode === 'add') {
66
+ tempPromptKey.value = ''
67
+ tempPromptValue.value = ''
68
+ }
69
+ else if (mode === 'modify') {
70
+ tempModifiedItem.value = { ...selected }
71
+ tempPromptKey.value = selected.key
72
+ tempPromptValue.value = selected.value
73
+ }
74
+ else if (mode === 'local_import') {
75
+ tempPromptKey.value = 'local_import'
76
+ tempPromptValue.value = ''
77
+ }
78
+ showModal.value = !showModal.value
79
+ modalMode.value = mode
80
+ }
81
+
82
+ // 在线导入相关
83
+ const downloadURL = ref('')
84
+ const downloadDisabled = computed(() => downloadURL.value.trim().length < 1)
85
+ const setDownloadURL = (url: string) => {
86
+ downloadURL.value = url
87
+ }
88
+
89
+ // 控制 input 按钮
90
+ const inputStatus = computed (() => tempPromptKey.value.trim().length < 1 || tempPromptValue.value.trim().length < 1)
91
+
92
+ // Prompt模板相关操作
93
+ const addPromptTemplate = () => {
94
+ for (const i of promptList.value) {
95
+ if (i.key === tempPromptKey.value) {
96
+ message.error(t('store.addRepeatTitleTips'))
97
+ return
98
+ }
99
+ if (i.value === tempPromptValue.value) {
100
+ message.error(t('store.addRepeatContentTips', { msg: tempPromptKey.value }))
101
+ return
102
+ }
103
+ }
104
+ promptList.value.unshift({ key: tempPromptKey.value, value: tempPromptValue.value } as never)
105
+ message.success(t('common.addSuccess'))
106
+ changeShowModal('add')
107
+ }
108
+
109
+ const modifyPromptTemplate = () => {
110
+ let index = 0
111
+
112
+ // 通过临时索引把待修改项摘出来
113
+ for (const i of promptList.value) {
114
+ if (i.key === tempModifiedItem.value.key && i.value === tempModifiedItem.value.value)
115
+ break
116
+ index = index + 1
117
+ }
118
+
119
+ const tempList = promptList.value.filter((_: any, i: number) => i !== index)
120
+
121
+ // 搜索有冲突的部分
122
+ for (const i of tempList) {
123
+ if (i.key === tempPromptKey.value) {
124
+ message.error(t('store.editRepeatTitleTips'))
125
+ return
126
+ }
127
+ if (i.value === tempPromptValue.value) {
128
+ message.error(t('store.editRepeatContentTips', { msg: i.key }))
129
+ return
130
+ }
131
+ }
132
+
133
+ promptList.value = [{ key: tempPromptKey.value, value: tempPromptValue.value }, ...tempList] as never
134
+ message.success(t('common.editSuccess'))
135
+ changeShowModal('modify')
136
+ }
137
+
138
+ const deletePromptTemplate = (row: { key: string; value: string }) => {
139
+ promptList.value = [
140
+ ...promptList.value.filter((item: { key: string; value: string }) => item.key !== row.key),
141
+ ] as never
142
+ message.success(t('common.deleteSuccess'))
143
+ }
144
+
145
+ const clearPromptTemplate = () => {
146
+ promptList.value = []
147
+ message.success(t('common.clearSuccess'))
148
+ }
149
+
150
+ const importPromptTemplate = (from = 'online') => {
151
+ try {
152
+ const jsonData = JSON.parse(tempPromptValue.value)
153
+ let key = ''
154
+ let value = ''
155
+ // 可以扩展加入更多模板字典的key
156
+ if ('key' in jsonData[0]) {
157
+ key = 'key'
158
+ value = 'value'
159
+ }
160
+ else if ('act' in jsonData[0]) {
161
+ key = 'act'
162
+ value = 'prompt'
163
+ }
164
+ else {
165
+ // 不支持的字典的key防止导入 以免破坏prompt商店��开
166
+ message.warning('prompt key not supported.')
167
+ throw new Error('prompt key not supported.')
168
+ }
169
+
170
+ for (const i of jsonData) {
171
+ if (!(key in i) || !(value in i))
172
+ throw new Error(t('store.importError'))
173
+ let safe = true
174
+ for (const j of promptList.value) {
175
+ if (j.key === i[key]) {
176
+ message.warning(t('store.importRepeatTitle', { msg: i[key] }))
177
+ safe = false
178
+ break
179
+ }
180
+ if (j.value === i[value]) {
181
+ message.warning(t('store.importRepeatContent', { msg: i[key] }))
182
+ safe = false
183
+ break
184
+ }
185
+ }
186
+ if (safe)
187
+ promptList.value.unshift({ key: i[key], value: i[value] } as never)
188
+ }
189
+ message.success(t('common.importSuccess'))
190
+ }
191
+ catch {
192
+ message.error('JSON 格式错误,请检查 JSON 格式')
193
+ }
194
+ if (from === 'local')
195
+ showModal.value = !showModal.value
196
+ }
197
+
198
+ // 模板导出
199
+ const exportPromptTemplate = () => {
200
+ exportLoading.value = true
201
+ const jsonDataStr = JSON.stringify(promptList.value)
202
+ const blob = new Blob([jsonDataStr], { type: 'application/json' })
203
+ const url = URL.createObjectURL(blob)
204
+ const link = document.createElement('a')
205
+ link.href = url
206
+ link.download = 'ChatGPTPromptTemplate.json'
207
+ link.click()
208
+ URL.revokeObjectURL(url)
209
+ exportLoading.value = false
210
+ }
211
+
212
+ // 模板在线导入
213
+ const downloadPromptTemplate = async () => {
214
+ try {
215
+ importLoading.value = true
216
+ const response = await fetch(downloadURL.value)
217
+ const jsonData = await response.json()
218
+ if ('key' in jsonData[0] && 'value' in jsonData[0])
219
+ tempPromptValue.value = JSON.stringify(jsonData)
220
+ if ('act' in jsonData[0] && 'prompt' in jsonData[0]) {
221
+ const newJsonData = jsonData.map((item: { act: string; prompt: string }) => {
222
+ return {
223
+ key: item.act,
224
+ value: item.prompt,
225
+ }
226
+ })
227
+ tempPromptValue.value = JSON.stringify(newJsonData)
228
+ }
229
+ importPromptTemplate()
230
+ downloadURL.value = ''
231
+ }
232
+ catch {
233
+ message.error(t('store.downloadError'))
234
+ downloadURL.value = ''
235
+ }
236
+ finally {
237
+ importLoading.value = false
238
+ }
239
+ }
240
+
241
+ // 移动端自适应相关
242
+ const renderTemplate = () => {
243
+ const [keyLimit, valueLimit] = isMobile.value ? [10, 30] : [15, 50]
244
+
245
+ return promptList.value.map((item: { key: string; value: string }) => {
246
+ return {
247
+ renderKey: item.key.length <= keyLimit ? item.key : `${item.key.substring(0, keyLimit)}...`,
248
+ renderValue: item.value.length <= valueLimit ? item.value : `${item.value.substring(0, valueLimit)}...`,
249
+ key: item.key,
250
+ value: item.value,
251
+ }
252
+ })
253
+ }
254
+
255
+ const pagination = computed(() => {
256
+ const [pageSize, pageSlot] = isMobile.value ? [6, 5] : [7, 15]
257
+ return {
258
+ pageSize, pageSlot,
259
+ }
260
+ })
261
+
262
+ // table相关
263
+ const createColumns = (): DataTableColumns<DataProps> => {
264
+ return [
265
+ {
266
+ title: t('store.title'),
267
+ key: 'renderKey',
268
+ },
269
+ {
270
+ title: t('store.description'),
271
+ key: 'renderValue',
272
+ },
273
+ {
274
+ title: t('common.action'),
275
+ key: 'actions',
276
+ width: 100,
277
+ align: 'center',
278
+ render(row) {
279
+ return h('div', { class: 'flex items-center flex-col gap-2' }, {
280
+ default: () => [h(
281
+ NButton,
282
+ {
283
+ tertiary: true,
284
+ size: 'small',
285
+ type: 'info',
286
+ onClick: () => changeShowModal('modify', row),
287
+ },
288
+ { default: () => t('common.edit') },
289
+ ),
290
+ h(
291
+ NButton,
292
+ {
293
+ tertiary: true,
294
+ size: 'small',
295
+ type: 'error',
296
+ onClick: () => deletePromptTemplate(row),
297
+ },
298
+ { default: () => t('common.delete') },
299
+ ),
300
+ ],
301
+ })
302
+ },
303
+ },
304
+ ]
305
+ }
306
+
307
+ const columns = createColumns()
308
+
309
+ watch(
310
+ () => promptList,
311
+ () => {
312
+ promptStore.updatePromptList(promptList.value)
313
+ },
314
+ { deep: true },
315
+ )
316
+
317
+ const dataSource = computed(() => {
318
+ const data = renderTemplate()
319
+ const value = searchValue.value
320
+ if (value && value !== '') {
321
+ return data.filter((item: DataProps) => {
322
+ return item.renderKey.includes(value) || item.renderValue.includes(value)
323
+ })
324
+ }
325
+ return data
326
+ })
327
+ </script>
328
+
329
+ <template>
330
+ <NModal v-model:show="show" style="width: 90%; max-width: 900px;" preset="card">
331
+ <div class="space-y-4">
332
+ <NTabs type="segment">
333
+ <NTabPane name="local" :tab="$t('store.local')">
334
+ <div
335
+ class="flex gap-3 mb-4"
336
+ :class="[isMobile ? 'flex-col' : 'flex-row justify-between']"
337
+ >
338
+ <div class="flex items-center space-x-4">
339
+ <NButton
340
+ type="primary"
341
+ size="small"
342
+ @click="changeShowModal('add')"
343
+ >
344
+ {{ $t('common.add') }}
345
+ </NButton>
346
+ <NButton
347
+ size="small"
348
+ @click="changeShowModal('local_import')"
349
+ >
350
+ {{ $t('common.import') }}
351
+ </NButton>
352
+ <NButton
353
+ size="small"
354
+ :loading="exportLoading"
355
+ @click="exportPromptTemplate()"
356
+ >
357
+ {{ $t('common.export') }}
358
+ </NButton>
359
+ <NPopconfirm @positive-click="clearPromptTemplate">
360
+ <template #trigger>
361
+ <NButton size="small">
362
+ {{ $t('common.clear') }}
363
+ </NButton>
364
+ </template>
365
+ {{ $t('store.clearStoreConfirm') }}
366
+ </NPopconfirm>
367
+ </div>
368
+ <div class="flex items-center">
369
+ <NInput v-model:value="searchValue" style="width: 100%" />
370
+ </div>
371
+ </div>
372
+ <NDataTable
373
+ v-if="!isMobile"
374
+ :max-height="400"
375
+ :columns="columns"
376
+ :data="dataSource"
377
+ :pagination="pagination"
378
+ :bordered="false"
379
+ />
380
+ <NList v-if="isMobile" style="max-height: 400px; overflow-y: auto;">
381
+ <NListItem v-for="(item, index) of dataSource" :key="index">
382
+ <NThing :title="item.renderKey" :description="item.renderValue" />
383
+ <template #suffix>
384
+ <div class="flex flex-col items-center gap-2">
385
+ <NButton tertiary size="small" type="info" @click="changeShowModal('modify', item)">
386
+ {{ t('common.edit') }}
387
+ </NButton>
388
+ <NButton tertiary size="small" type="error" @click="deletePromptTemplate(item)">
389
+ {{ t('common.delete') }}
390
+ </NButton>
391
+ </div>
392
+ </template>
393
+ </NListItem>
394
+ </NList>
395
+ </NTabPane>
396
+ <NTabPane name="download" :tab="$t('store.online')">
397
+ <p class="mb-4">
398
+ {{ $t('store.onlineImportWarning') }}
399
+ </p>
400
+ <div class="flex items-center gap-4">
401
+ <NInput v-model:value="downloadURL" placeholder="" />
402
+ <NButton
403
+ strong
404
+ secondary
405
+ :disabled="downloadDisabled"
406
+ :loading="importLoading"
407
+ @click="downloadPromptTemplate()"
408
+ >
409
+ {{ $t('common.download') }}
410
+ </NButton>
411
+ </div>
412
+ <NDivider />
413
+ <div class="max-h-[360px] overflow-y-auto space-y-4">
414
+ <NCard
415
+ v-for="info in promptRecommendList"
416
+ :key="info.key" :title="info.key"
417
+ :bordered="true"
418
+ embedded
419
+ >
420
+ <p
421
+ class="overflow-hidden text-ellipsis whitespace-nowrap"
422
+ :title="info.desc"
423
+ >
424
+ {{ info.desc }}
425
+ </p>
426
+ <template #footer>
427
+ <div class="flex items-center justify-end space-x-4">
428
+ <NButton text>
429
+ <a
430
+ :href="info.url"
431
+ target="_blank"
432
+ >
433
+ <SvgIcon class="text-xl" icon="ri:link" />
434
+ </a>
435
+ </NButton>
436
+ <NButton text @click="setDownloadURL(info.downloadUrl) ">
437
+ <SvgIcon class="text-xl" icon="ri:add-fill" />
438
+ </NButton>
439
+ </div>
440
+ </template>
441
+ </NCard>
442
+ </div>
443
+ </NTabPane>
444
+ </NTabs>
445
+ </div>
446
+ </NModal>
447
+
448
+ <NModal v-model:show="showModal" style="width: 90%; max-width: 600px;" preset="card">
449
+ <NSpace v-if="modalMode === 'add' || modalMode === 'modify'" vertical>
450
+ {{ t('store.title') }}
451
+ <NInput v-model:value="tempPromptKey" />
452
+ {{ t('store.description') }}
453
+ <NInput v-model:value="tempPromptValue" type="textarea" />
454
+ <NButton
455
+ block
456
+ type="primary"
457
+ :disabled="inputStatus"
458
+ @click="() => { modalMode === 'add' ? addPromptTemplate() : modifyPromptTemplate() }"
459
+ >
460
+ {{ t('common.confirm') }}
461
+ </NButton>
462
+ </NSpace>
463
+ <NSpace v-if="modalMode === 'local_import'" vertical>
464
+ <NInput
465
+ v-model:value="tempPromptValue"
466
+ :placeholder="t('store.importPlaceholder')"
467
+ :autosize="{ minRows: 3, maxRows: 15 }"
468
+ type="textarea"
469
+ />
470
+ <NButton
471
+ block
472
+ type="primary"
473
+ :disabled="inputStatus"
474
+ @click="() => { importPromptTemplate('local') }"
475
+ >
476
+ {{ t('common.import') }}
477
+ </NButton>
478
+ </NSpace>
479
+ </NModal>
480
+ </template>
src/components/common/Setting/About.vue ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang='ts'>
2
+ import { computed, onMounted, ref } from 'vue'
3
+ import { NSpin } from 'naive-ui'
4
+ import { fetchChatConfig } from '@/api'
5
+ import pkg from '@/../package.json'
6
+ import { useAuthStore } from '@/store'
7
+
8
+ interface ConfigState {
9
+ timeoutMs?: number
10
+ reverseProxy?: string
11
+ apiModel?: string
12
+ socksProxy?: string
13
+ httpsProxy?: string
14
+ usage?: string
15
+ }
16
+
17
+ const authStore = useAuthStore()
18
+
19
+ const loading = ref(false)
20
+
21
+ const config = ref<ConfigState>()
22
+
23
+ const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
24
+
25
+ async function fetchConfig() {
26
+ try {
27
+ loading.value = true
28
+ const { data } = await fetchChatConfig<ConfigState>()
29
+ config.value = data
30
+ }
31
+ finally {
32
+ loading.value = false
33
+ }
34
+ }
35
+
36
+ onMounted(() => {
37
+ fetchConfig()
38
+ })
39
+ </script>
40
+
41
+ <template>
42
+ <NSpin :show="loading">
43
+ <div class="p-4 space-y-4">
44
+ <h2 class="text-xl font-bold">
45
+ Version - {{ pkg.version }}
46
+ </h2>
47
+ <div class="p-2 space-y-2 rounded-md bg-neutral-100 dark:bg-neutral-700">
48
+ <p>
49
+ 此项目开源于
50
+ <a
51
+ class="text-blue-600 dark:text-blue-500"
52
+ href="https://github.com/Chanzhaoyu/chatgpt-web"
53
+ target="_blank"
54
+ >
55
+ GitHub
56
+ </a>
57
+ ,免费且基于 MIT 协议,没有任何形式的付费行为!
58
+ </p>
59
+ <p>
60
+ 如果你觉得此项目对你有帮助,请在 GitHub 帮我点个 Star 或者给予一点赞助,谢谢!
61
+ </p>
62
+ </div>
63
+ <p>{{ $t("setting.api") }}:{{ config?.apiModel ?? '-' }}</p>
64
+ <p v-if="isChatGPTAPI">
65
+ {{ $t("setting.monthlyUsage") }}:{{ config?.usage ?? '-' }}
66
+ </p>
67
+ <p v-if="!isChatGPTAPI">
68
+ {{ $t("setting.reverseProxy") }}:{{ config?.reverseProxy ?? '-' }}
69
+ </p>
70
+ <p>{{ $t("setting.timeout") }}:{{ config?.timeoutMs ?? '-' }}</p>
71
+ <p>{{ $t("setting.socks") }}:{{ config?.socksProxy ?? '-' }}</p>
72
+ <p>{{ $t("setting.httpsProxy") }}:{{ config?.httpsProxy ?? '-' }}</p>
73
+ </div>
74
+ </NSpin>
75
+ </template>
src/components/common/Setting/Advanced.vue ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts" setup>
2
+ import { ref } from 'vue'
3
+ import { NButton, NInput, NSlider, useMessage } from 'naive-ui'
4
+ import { useSettingStore } from '@/store'
5
+ import type { SettingsState } from '@/store/modules/settings/helper'
6
+ import { t } from '@/locales'
7
+
8
+ const settingStore = useSettingStore()
9
+
10
+ const ms = useMessage()
11
+
12
+ const systemMessage = ref(settingStore.systemMessage ?? '')
13
+
14
+ const temperature = ref(settingStore.temperature ?? 0.5)
15
+
16
+ const top_p = ref(settingStore.top_p ?? 1)
17
+
18
+ function updateSettings(options: Partial<SettingsState>) {
19
+ settingStore.updateSetting(options)
20
+ ms.success(t('common.success'))
21
+ }
22
+
23
+ function handleReset() {
24
+ settingStore.resetSetting()
25
+ ms.success(t('common.success'))
26
+ window.location.reload()
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <div class="p-4 space-y-5 min-h-[200px]">
32
+ <div class="space-y-6">
33
+ <div class="flex items-center space-x-4">
34
+ <span class="flex-shrink-0 w-[120px]">{{ $t('setting.role') }}</span>
35
+ <div class="flex-1">
36
+ <NInput v-model:value="systemMessage" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" />
37
+ </div>
38
+ <NButton size="tiny" text type="primary" @click="updateSettings({ systemMessage })">
39
+ {{ $t('common.save') }}
40
+ </NButton>
41
+ </div>
42
+ <div class="flex items-center space-x-4">
43
+ <span class="flex-shrink-0 w-[120px]">{{ $t('setting.temperature') }} </span>
44
+ <div class="flex-1">
45
+ <NSlider v-model:value="temperature" :max="1" :min="0" :step="0.1" />
46
+ </div>
47
+ <span>{{ temperature }}</span>
48
+ <NButton size="tiny" text type="primary" @click="updateSettings({ temperature })">
49
+ {{ $t('common.save') }}
50
+ </NButton>
51
+ </div>
52
+ <div class="flex items-center space-x-4">
53
+ <span class="flex-shrink-0 w-[120px]">{{ $t('setting.top_p') }} </span>
54
+ <div class="flex-1">
55
+ <NSlider v-model:value="top_p" :max="1" :min="0" :step="0.1" />
56
+ </div>
57
+ <span>{{ top_p }}</span>
58
+ <NButton size="tiny" text type="primary" @click="updateSettings({ top_p })">
59
+ {{ $t('common.save') }}
60
+ </NButton>
61
+ </div>
62
+ <div class="flex items-center space-x-4">
63
+ <span class="flex-shrink-0 w-[120px]">&nbsp;</span>
64
+ <NButton size="small" @click="handleReset">
65
+ {{ $t('common.reset') }}
66
+ </NButton>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </template>
src/components/common/Setting/General.vue ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts" setup>
2
+ import { computed, ref } from 'vue'
3
+ import { NButton, NInput, NPopconfirm, NSelect, useMessage } from 'naive-ui'
4
+ import type { Language, Theme } from '@/store/modules/app/helper'
5
+ import { SvgIcon } from '@/components/common'
6
+ import { useAppStore, useUserStore } from '@/store'
7
+ import type { UserInfo } from '@/store/modules/user/helper'
8
+ import { getCurrentDate } from '@/utils/functions'
9
+ import { useBasicLayout } from '@/hooks/useBasicLayout'
10
+ import { t } from '@/locales'
11
+
12
+ const appStore = useAppStore()
13
+ const userStore = useUserStore()
14
+
15
+ const { isMobile } = useBasicLayout()
16
+
17
+ const ms = useMessage()
18
+
19
+ const theme = computed(() => appStore.theme)
20
+
21
+ const userInfo = computed(() => userStore.userInfo)
22
+
23
+ const avatar = ref(userInfo.value.avatar ?? '')
24
+
25
+ const name = ref(userInfo.value.name ?? '')
26
+
27
+ const description = ref(userInfo.value.description ?? '')
28
+
29
+ const language = computed({
30
+ get() {
31
+ return appStore.language
32
+ },
33
+ set(value: Language) {
34
+ appStore.setLanguage(value)
35
+ },
36
+ })
37
+
38
+ const themeOptions: { label: string; key: Theme; icon: string }[] = [
39
+ {
40
+ label: 'Auto',
41
+ key: 'auto',
42
+ icon: 'ri:contrast-line',
43
+ },
44
+ {
45
+ label: 'Light',
46
+ key: 'light',
47
+ icon: 'ri:sun-foggy-line',
48
+ },
49
+ {
50
+ label: 'Dark',
51
+ key: 'dark',
52
+ icon: 'ri:moon-foggy-line',
53
+ },
54
+ ]
55
+
56
+ const languageOptions: { label: string; key: Language; value: Language }[] = [
57
+ { label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
58
+ { label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
59
+ { label: 'English', key: 'en-US', value: 'en-US' },
60
+ { label: '한국어', key: 'ko-KR', value: 'ko-KR' },
61
+ { label: 'Русский язык', key: 'ru-RU', value: 'ru-RU' },
62
+ ]
63
+
64
+ function updateUserInfo(options: Partial<UserInfo>) {
65
+ userStore.updateUserInfo(options)
66
+ ms.success(t('common.success'))
67
+ }
68
+
69
+ function handleReset() {
70
+ userStore.resetUserInfo()
71
+ ms.success(t('common.success'))
72
+ window.location.reload()
73
+ }
74
+
75
+ function exportData(): void {
76
+ const date = getCurrentDate()
77
+ const data: string = localStorage.getItem('chatStorage') || '{}'
78
+ const jsonString: string = JSON.stringify(JSON.parse(data), null, 2)
79
+ const blob: Blob = new Blob([jsonString], { type: 'application/json' })
80
+ const url: string = URL.createObjectURL(blob)
81
+ const link: HTMLAnchorElement = document.createElement('a')
82
+ link.href = url
83
+ link.download = `chat-store_${date}.json`
84
+ document.body.appendChild(link)
85
+ link.click()
86
+ document.body.removeChild(link)
87
+ }
88
+
89
+ function importData(event: Event): void {
90
+ const target = event.target as HTMLInputElement
91
+ if (!target || !target.files)
92
+ return
93
+
94
+ const file: File = target.files[0]
95
+ if (!file)
96
+ return
97
+
98
+ const reader: FileReader = new FileReader()
99
+ reader.onload = () => {
100
+ try {
101
+ const data = JSON.parse(reader.result as string)
102
+ localStorage.setItem('chatStorage', JSON.stringify(data))
103
+ ms.success(t('common.success'))
104
+ location.reload()
105
+ }
106
+ catch (error) {
107
+ ms.error(t('common.invalidFileFormat'))
108
+ }
109
+ }
110
+ reader.readAsText(file)
111
+ }
112
+
113
+ function clearData(): void {
114
+ localStorage.removeItem('chatStorage')
115
+ location.reload()
116
+ }
117
+
118
+ function handleImportButtonClick(): void {
119
+ const fileInput = document.getElementById('fileInput') as HTMLElement
120
+ if (fileInput)
121
+ fileInput.click()
122
+ }
123
+ </script>
124
+
125
+ <template>
126
+ <div class="p-4 space-y-5 min-h-[200px]">
127
+ <div class="space-y-6">
128
+ <div class="flex items-center space-x-4">
129
+ <span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span>
130
+ <div class="flex-1">
131
+ <NInput v-model:value="avatar" placeholder="" />
132
+ </div>
133
+ <NButton size="tiny" text type="primary" @click="updateUserInfo({ avatar })">
134
+ {{ $t('common.save') }}
135
+ </NButton>
136
+ </div>
137
+ <div class="flex items-center space-x-4">
138
+ <span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
139
+ <div class="w-[200px]">
140
+ <NInput v-model:value="name" placeholder="" />
141
+ </div>
142
+ <NButton size="tiny" text type="primary" @click="updateUserInfo({ name })">
143
+ {{ $t('common.save') }}
144
+ </NButton>
145
+ </div>
146
+ <div class="flex items-center space-x-4">
147
+ <span class="flex-shrink-0 w-[100px]">{{ $t('setting.description') }}</span>
148
+ <div class="flex-1">
149
+ <NInput v-model:value="description" placeholder="" />
150
+ </div>
151
+ <NButton size="tiny" text type="primary" @click="updateUserInfo({ description })">
152
+ {{ $t('common.save') }}
153
+ </NButton>
154
+ </div>
155
+ <div
156
+ class="flex items-center space-x-4"
157
+ :class="isMobile && 'items-start'"
158
+ >
159
+ <span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatHistory') }}</span>
160
+
161
+ <div class="flex flex-wrap items-center gap-4">
162
+ <NButton size="small" @click="exportData">
163
+ <template #icon>
164
+ <SvgIcon icon="ri:download-2-fill" />
165
+ </template>
166
+ {{ $t('common.export') }}
167
+ </NButton>
168
+
169
+ <input id="fileInput" type="file" style="display:none" @change="importData">
170
+ <NButton size="small" @click="handleImportButtonClick">
171
+ <template #icon>
172
+ <SvgIcon icon="ri:upload-2-fill" />
173
+ </template>
174
+ {{ $t('common.import') }}
175
+ </NButton>
176
+
177
+ <NPopconfirm placement="bottom" @positive-click="clearData">
178
+ <template #trigger>
179
+ <NButton size="small">
180
+ <template #icon>
181
+ <SvgIcon icon="ri:close-circle-line" />
182
+ </template>
183
+ {{ $t('common.clear') }}
184
+ </NButton>
185
+ </template>
186
+ {{ $t('chat.clearHistoryConfirm') }}
187
+ </NPopconfirm>
188
+ </div>
189
+ </div>
190
+ <div class="flex items-center space-x-4">
191
+ <span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
192
+ <div class="flex flex-wrap items-center gap-4">
193
+ <template v-for="item of themeOptions" :key="item.key">
194
+ <NButton
195
+ size="small"
196
+ :type="item.key === theme ? 'primary' : undefined"
197
+ @click="appStore.setTheme(item.key)"
198
+ >
199
+ <template #icon>
200
+ <SvgIcon :icon="item.icon" />
201
+ </template>
202
+ </NButton>
203
+ </template>
204
+ </div>
205
+ </div>
206
+ <div class="flex items-center space-x-4">
207
+ <span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
208
+ <div class="flex flex-wrap items-center gap-4">
209
+ <NSelect
210
+ style="width: 140px"
211
+ :value="language"
212
+ :options="languageOptions"
213
+ @update-value="value => appStore.setLanguage(value)"
214
+ />
215
+ </div>
216
+ </div>
217
+ <div class="flex items-center space-x-4">
218
+ <span class="flex-shrink-0 w-[100px]">{{ $t('setting.resetUserInfo') }}</span>
219
+ <NButton size="small" @click="handleReset">
220
+ {{ $t('common.reset') }}
221
+ </NButton>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </template>
src/components/common/Setting/index.vue ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang='ts'>
2
+ import { computed, ref } from 'vue'
3
+ import { NModal, NTabPane, NTabs } from 'naive-ui'
4
+ import General from './General.vue'
5
+ import Advanced from './Advanced.vue'
6
+ import About from './About.vue'
7
+ import { useAuthStore } from '@/store'
8
+ import { SvgIcon } from '@/components/common'
9
+
10
+ interface Props {
11
+ visible: boolean
12
+ }
13
+
14
+ interface Emit {
15
+ (e: 'update:visible', visible: boolean): void
16
+ }
17
+
18
+ const props = defineProps<Props>()
19
+
20
+ const emit = defineEmits<Emit>()
21
+
22
+ const authStore = useAuthStore()
23
+
24
+ const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
25
+
26
+ const active = ref('General')
27
+
28
+ const show = computed({
29
+ get() {
30
+ return props.visible
31
+ },
32
+ set(visible: boolean) {
33
+ emit('update:visible', visible)
34
+ },
35
+ })
36
+ </script>
37
+
38
+ <template>
39
+ <NModal v-model:show="show" :auto-focus="false" preset="card" style="width: 95%; max-width: 640px">
40
+ <div>
41
+ <NTabs v-model:value="active" type="line" animated>
42
+ <NTabPane name="General" tab="General">
43
+ <template #tab>
44
+ <SvgIcon class="text-lg" icon="ri:file-user-line" />
45
+ <span class="ml-2">{{ $t('setting.general') }}</span>
46
+ </template>
47
+ <div class="min-h-[100px]">
48
+ <General />
49
+ </div>
50
+ </NTabPane>
51
+ <NTabPane v-if="isChatGPTAPI" name="Advanced" tab="Advanced">
52
+ <template #tab>
53
+ <SvgIcon class="text-lg" icon="ri:equalizer-line" />
54
+ <span class="ml-2">{{ $t('setting.advanced') }}</span>
55
+ </template>
56
+ <div class="min-h-[100px]">
57
+ <Advanced />
58
+ </div>
59
+ </NTabPane>
60
+ <NTabPane name="Config" tab="Config">
61
+ <template #tab>
62
+ <SvgIcon class="text-lg" icon="ri:list-settings-line" />
63
+ <span class="ml-2">{{ $t('setting.config') }}</span>
64
+ </template>
65
+ <About />
66
+ </NTabPane>
67
+ </NTabs>
68
+ </div>
69
+ </NModal>
70
+ </template>
src/components/common/SvgIcon/index.vue ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang='ts'>
2
+ import { computed, useAttrs } from 'vue'
3
+ import { Icon } from '@iconify/vue'
4
+
5
+ interface Props {
6
+ icon?: string
7
+ }
8
+
9
+ defineProps<Props>()
10
+
11
+ const attrs = useAttrs()
12
+
13
+ const bindAttrs = computed<{ class: string; style: string }>(() => ({
14
+ class: (attrs.class as string) || '',
15
+ style: (attrs.style as string) || '',
16
+ }))
17
+ </script>
18
+
19
+ <template>
20
+ <Icon :icon="icon" v-bind="bindAttrs" />
21
+ </template>
src/components/common/UserAvatar/index.vue ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang='ts'>
2
+ import { computed } from 'vue'
3
+ import { NAvatar } from 'naive-ui'
4
+ import { useUserStore } from '@/store'
5
+ import defaultAvatar from '@/assets/avatar.jpg'
6
+ import { isString } from '@/utils/is'
7
+
8
+ const userStore = useUserStore()
9
+
10
+ const userInfo = computed(() => userStore.userInfo)
11
+ </script>
12
+
13
+ <template>
14
+ <div class="flex items-center overflow-hidden">
15
+ <div class="w-10 h-10 overflow-hidden rounded-full shrink-0">
16
+ <template v-if="isString(userInfo.avatar) && userInfo.avatar.length > 0">
17
+ <NAvatar
18
+ size="large"
19
+ round
20
+ :src="userInfo.avatar"
21
+ :fallback-src="defaultAvatar"
22
+ />
23
+ </template>
24
+ <template v-else>
25
+ <NAvatar size="large" round :src="defaultAvatar" />
26
+ </template>
27
+ </div>
28
+ <div class="flex-1 min-w-0 ml-2">
29
+ <h2 class="overflow-hidden font-bold text-md text-ellipsis whitespace-nowrap">
30
+ {{ userInfo.name ?? 'ChenZhaoYu' }}
31
+ </h2>
32
+ <p class="overflow-hidden text-xs text-gray-500 text-ellipsis whitespace-nowrap">
33
+ <span
34
+ v-if="isString(userInfo.description) && userInfo.description !== ''"
35
+ v-html="userInfo.description"
36
+ />
37
+ </p>
38
+ </div>
39
+ </div>
40
+ </template>