lmt commited on
Commit ·
61a67bc
1
Parent(s): 4f52deb
init
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .commitlintrc.json +3 -0
- .dockerignore +7 -0
- .editorconfig +11 -0
- .env +10 -0
- .eslintignore +2 -0
- .eslintrc.cjs +4 -0
- .gitignore +32 -0
- .npmrc +1 -0
- Dockerfile +56 -0
- index.html +83 -0
- license +21 -0
- package.json +71 -0
- pnpm-lock.yaml +0 -0
- postcss.config.js +6 -0
- public/favicon.ico +0 -0
- public/favicon.svg +1 -0
- public/pwa-192x192.png +0 -0
- public/pwa-512x512.png +0 -0
- service/.env.example +44 -0
- service/.eslintrc.json +5 -0
- service/.gitignore +31 -0
- service/.npmrc +1 -0
- service/.vscode/extensions.json +3 -0
- service/.vscode/settings.json +22 -0
- service/package.json +47 -0
- service/pnpm-lock.yaml +0 -0
- service/src/chatgpt/index.ts +218 -0
- service/src/chatgpt/types.ts +19 -0
- service/src/index.ts +89 -0
- service/src/middleware/auth.ts +21 -0
- service/src/middleware/limiter.ts +19 -0
- service/src/types.ts +34 -0
- service/src/utils/index.ts +22 -0
- service/src/utils/is.ts +19 -0
- service/tsconfig.json +27 -0
- service/tsup.config.ts +13 -0
- src/App.vue +22 -0
- src/api/index.ts +66 -0
- src/assets/avatar.jpg +0 -0
- src/assets/recommend.json +14 -0
- src/components/common/HoverButton/Button.vue +20 -0
- src/components/common/HoverButton/index.vue +46 -0
- src/components/common/NaiveProvider/index.vue +43 -0
- src/components/common/PromptStore/index.vue +480 -0
- src/components/common/Setting/About.vue +75 -0
- src/components/common/Setting/Advanced.vue +70 -0
- src/components/common/Setting/General.vue +225 -0
- src/components/common/Setting/index.vue +70 -0
- src/components/common/SvgIcon/index.vue +21 -0
- 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]"> </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>
|