From 005d219a8c59678f9eae58cd15ce81d902804ec5 Mon Sep 17 00:00:00 2001 From: RockYang Date: Tue, 21 Mar 2023 18:12:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BC=9A=E8=AF=9D=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + server/config_handler.go | 23 +++++ server/server.go | 82 +++++++++++++---- types/config.go | 11 ++- types/web.go | 6 +- utils/utils.go | 9 ++ web/.env.development | 3 +- web/.env.production | 1 + web/package-lock.json | 112 +++++++++++++++++++----- web/package.json | 1 + web/src/actions/chat.js | 2 +- web/src/components/ChatReply.vue | 4 - web/src/main.js | 6 +- web/src/utils/http.js | 54 ++++++++++++ web/src/utils/storage.js | 16 ++++ web/src/views/Chat.vue | 145 ++++++++++++++++++++++++++----- 16 files changed, 403 insertions(+), 73 deletions(-) create mode 100644 web/src/utils/http.js create mode 100644 web/src/utils/storage.js diff --git a/README.md b/README.md index ca5b2840..1800621f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ * [ ] 使用 MySQL 保存用户的聊天的历史记录 * [ ] 用户聊天鉴权,设置口令模式 * [ ] 每次连接自动加载历史记录 +* [ ] OpenAI API 负载均衡,限制每个 API Key 每分钟之内调用次数不超过 15次,防止被封 * [ ] 角色设定,预设一些角色,比如程序员,产品经理,医生,作家,老师... * [ ] markdown 语法解析 * [ ] 用户配置界面 diff --git a/server/config_handler.go b/server/config_handler.go index c1c4bc44..806cda50 100644 --- a/server/config_handler.go +++ b/server/config_handler.go @@ -10,6 +10,12 @@ import ( // ConfigSetHandle set configs func (s *Server) ConfigSetHandle(c *gin.Context) { + token := c.Query("token") + if token != "RockYang" { + c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg}) + return + } + var data map[string]string err := json.NewDecoder(c.Request.Body).Decode(&data) if err != nil { @@ -71,6 +77,23 @@ func (s *Server) ConfigSetHandle(c *gin.Context) { s.Config.Chat.EnableContext = v } + // enable auth + if enableAuth, ok := data["enable_auth"]; ok { + v, err := strconv.ParseBool(enableAuth) + if err != nil { + c.JSON(http.StatusOK, types.BizVo{ + Code: types.InvalidParams, + Message: "enable_auth must be a bool parameter", + }) + return + } + s.Config.EnableAuth = v + } + + if token, ok := data["token"]; ok { + s.Config.Tokens = append(s.Config.Tokens, token) + } + // 保存配置文件 logger.Infof("Config: %+v", s.Config) err = types.SaveConfig(s.Config, s.ConfigPath) diff --git a/server/server.go b/server/server.go index cf712964..12b9cc48 100644 --- a/server/server.go +++ b/server/server.go @@ -2,6 +2,7 @@ package server import ( "embed" + "encoding/json" "fmt" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" @@ -12,6 +13,7 @@ import ( "net/url" logger2 "openai/logger" "openai/types" + "openai/utils" "os" "path/filepath" "strings" @@ -35,6 +37,8 @@ type Server struct { ConfigPath string Client *http.Client History map[string][]types.Message + + WsSession map[string]string // 关闭 Websocket 会话 } func NewServer(configPath string) (*Server, error) { @@ -55,7 +59,9 @@ func NewServer(configPath string) (*Server, error) { Config: config, Client: client, ConfigPath: configPath, - History: make(map[string][]types.Message, 16)}, nil + History: make(map[string][]types.Message, 16), + WsSession: make(map[string]string), + }, nil } func (s *Server) Run(webRoot embed.FS, path string) { @@ -63,9 +69,11 @@ func (s *Server) Run(webRoot embed.FS, path string) { engine := gin.Default() engine.Use(sessionMiddleware(s.Config)) engine.Use(corsMiddleware()) - engine.Use(AuthorizeMiddleware()) + engine.Use(AuthorizeMiddleware(s)) engine.GET("/hello", Hello) + engine.POST("/api/session/get", s.GetSessionHandle) + engine.POST("/api/login", s.LoginHandle) engine.Any("/api/chat", s.ChatHandle) engine.POST("/api/config/set", s.ConfigSetHandle) @@ -109,7 +117,7 @@ func corsMiddleware() gin.HandlerFunc { c.Writer.Header().Set("Access-Control-Allow-Origin", origin) c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") //允许跨域设置可以返回其他子段,可以自定义字段 - c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, Session-Name, Session") + c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, ChatGPT-Token, Session") // 允许浏览器(客户端)可以解析的头部 (重要) c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers") //设置缓存时间 @@ -133,27 +141,63 @@ func corsMiddleware() gin.HandlerFunc { } // AuthorizeMiddleware 用户授权验证 -func AuthorizeMiddleware() gin.HandlerFunc { +func AuthorizeMiddleware(s *Server) gin.HandlerFunc { return func(c *gin.Context) { - c.Next() - //if c.Request.URL.Path == "/login" { - // c.Next() - // return - //} + if !s.Config.EnableAuth || c.Request.URL.Path == "/api/login" || c.Request.URL.Path == "/api/config/set" { + c.Next() + return + } - //sessionName := c.GetHeader("Session-Name") - //session, err := c.Cookie(sessionName) - //if err == nil { - // c.Request.Header.Set(utils.SessionKey, session) - // c.Next() - //} else { - // logger.Fatal(err) - // c.Abort() - // c.JSON(http.StatusUnauthorized, "No session data found") - //} + tokenName := c.GetHeader("Sec-WebSocket-Protocol") + logger.Info(s.WsSession) + logger.Info(tokenName) + if addr, ok := s.WsSession[tokenName]; ok && addr == c.ClientIP() { + c.Next() + return + } + + tokenName = c.GetHeader(types.TokenName) + session := sessions.Default(c) + user := session.Get(tokenName) + if user != nil { + c.Set(types.SessionKey, user) + c.Next() + } else { + c.Abort() + c.JSON(http.StatusOK, types.BizVo{ + Code: types.NotAuthorized, + Message: "Not Authorized", + }) + } } } +func (s *Server) GetSessionHandle(c *gin.Context) { + session := sessions.Default(c) + c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: session.Get(types.TokenName)}) +} + +func (s *Server) LoginHandle(c *gin.Context) { + var data map[string]string + err := json.NewDecoder(c.Request.Body).Decode(&data) + if err != nil { + c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg}) + return + } + token := data["token"] + if !utils.ContainsItem(s.Config.Tokens, token) { + c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid token"}) + return + } + + sessionId := utils.RandString(42) + session := sessions.Default(c) + session.Set(sessionId, token) + // 记录客户端 IP 地址 + s.WsSession[sessionId] = c.ClientIP() + c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: sessionId}) +} + func Hello(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": fmt.Sprintf("HELLO, XWEBSSH !!!")}) } diff --git a/types/config.go b/types/config.go index a0dbac50..decad5ea 100644 --- a/types/config.go +++ b/types/config.go @@ -10,10 +10,12 @@ import ( ) type Config struct { - Listen string - Session Session - ProxyURL string - Chat Chat + Listen string + Session Session + ProxyURL string + Chat Chat + EnableAuth bool // 是否开启鉴权 + Tokens []string // 授权的白名单列表 TODO: 后期要存储到 LevelDB 或者 Mysql 数据库 } // Chat configs struct @@ -60,6 +62,7 @@ func NewDefaultConfig() *Config { Temperature: 1.0, EnableContext: true, }, + EnableAuth: true, } } diff --git a/types/web.go b/types/web.go index 10eaac84..0b1d4f48 100644 --- a/types/web.go +++ b/types/web.go @@ -31,5 +31,9 @@ const ( InvalidParams = BizCode(101) // 非法参数 NotAuthorized = BizCode(400) // 未授权 - OkMsg = "Success" + OkMsg = "Success" + ErrorMsg = "系统开小差了" ) + +const TokenName = "ChatGPT-Token" +const SessionKey = "WEB_SSH_SESSION" diff --git a/utils/utils.go b/utils/utils.go index 9b7c1427..cde0d879 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -30,3 +30,12 @@ func Long2IP(ipInt int64) string { func IsBlank(value string) bool { return len(strings.TrimSpace(value)) == 0 } + +func ContainsItem(slice []string, item string) bool { + for _, e := range slice { + if e == item { + return true + } + } + return false +} diff --git a/web/.env.development b/web/.env.development index ee674e92..3ea203f8 100644 --- a/web/.env.development +++ b/web/.env.development @@ -1 +1,2 @@ -VUE_APP_WS_HOST=ws://172.22.11.200:5678 \ No newline at end of file +VUE_APP_API_HOST=172.22.11.200:5678 +VUE_APP_API_SECURE=false \ No newline at end of file diff --git a/web/.env.production b/web/.env.production index 1e575296..d2ac3b7d 100644 --- a/web/.env.production +++ b/web/.env.production @@ -1 +1,2 @@ VUE_APP_WS_HOST=ws://127.0.0.1:5678 +VUE_APP_API_SECURE=false diff --git a/web/package-lock.json b/web/package-lock.json index 286480bf..7c45f7dd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,6 +14,7 @@ "element-plus": "^2.1.11", "good-storage": "^1.1.1", "json-bigint": "^1.0.0", + "qs": "^6.11.1", "vue": "^3.2.13", "vue-router": "^4.0.15" }, @@ -3624,6 +3625,18 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "dev": true, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bonjour-service": { "version": "1.0.12", "resolved": "https://registry.npmmirror.com/bonjour-service/-/bonjour-service-1.0.12.tgz", @@ -3712,7 +3725,6 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -5728,6 +5740,18 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/express/node_modules/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "dev": true, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6013,8 +6037,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -6044,7 +6067,6 @@ "version": "1.1.1", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -6157,7 +6179,6 @@ "version": "1.0.3", "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -6187,7 +6208,6 @@ "version": "1.0.3", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -7682,6 +7702,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", @@ -8747,12 +8775,17 @@ } }, "node_modules/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmmirror.com/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", - "dev": true, + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", + "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/queue-microtask": { @@ -9308,6 +9341,19 @@ "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", "dev": true }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", @@ -13832,6 +13878,12 @@ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true + }, + "qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "dev": true } } }, @@ -13911,7 +13963,6 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -15514,6 +15565,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "dev": true + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -15745,8 +15802,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -15770,7 +15826,6 @@ "version": "1.1.1", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -15865,7 +15920,6 @@ "version": "1.0.3", "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -15888,8 +15942,7 @@ "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, "hash-sum": { "version": "2.0.0", @@ -17079,6 +17132,11 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", @@ -17844,10 +17902,12 @@ "dev": true }, "qs": { - "version": "6.9.7", - "resolved": "https://registry.npmmirror.com/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", - "dev": true + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", + "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "requires": { + "side-channel": "^1.0.4" + } }, "queue-microtask": { "version": "1.2.3", @@ -18317,6 +18377,16 @@ "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", diff --git a/web/package.json b/web/package.json index 2b09a525..90d3fc83 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,7 @@ "element-plus": "^2.1.11", "good-storage": "^1.1.1", "json-bigint": "^1.0.0", + "qs": "^6.11.1", "vue": "^3.2.13", "vue-router": "^4.0.15" }, diff --git a/web/src/actions/chat.js b/web/src/actions/chat.js index 76cc5760..db214e64 100644 --- a/web/src/actions/chat.js +++ b/web/src/actions/chat.js @@ -1,3 +1,3 @@ /** * actions for chat page - */ + */ \ No newline at end of file diff --git a/web/src/components/ChatReply.vue b/web/src/components/ChatReply.vue index a71e0cbc..76bf9724 100644 --- a/web/src/components/ChatReply.vue +++ b/web/src/components/ChatReply.vue @@ -24,10 +24,6 @@ export default defineComponent({ icon: { type: String, default: 'images/gpt-icon.png', - }, - cursor: { - type: Boolean, - default: true } }, data() { diff --git a/web/src/main.js b/web/src/main.js index 60f8d8e6..2ca76f72 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -3,15 +3,17 @@ import {createApp} from 'vue' import ElementPlus from "element-plus" import "element-plus/dist/index.css" import App from './App.vue' -import Home from './views/Chat.vue' +import Chat from './views/Chat.vue' import NotFound from './views/404.vue' import './utils/prototype' import "./assets/css/bootstrap.min.css" +import {Global} from "@/utils/storage"; +Global['Chat'] = Chat const routes = [ { - name: 'home', path: '/', component: Home, meta: { + name: 'home', path: '/', component: Chat, meta: { title: 'ChatGPT-Console' } }, diff --git a/web/src/utils/http.js b/web/src/utils/http.js new file mode 100644 index 00000000..bb77c3a8 --- /dev/null +++ b/web/src/utils/http.js @@ -0,0 +1,54 @@ +import axios from 'axios' +import {getSessionId} from "@/utils/storage"; + +axios.defaults.timeout = 5000 +axios.defaults.baseURL = process.env.VUE_APP_API_SECURE === true ? 'https://' + process.env.VUE_APP_API_HOST : 'http://' + process.env.VUE_APP_API_HOST +axios.defaults.withCredentials = true +axios.defaults.headers.post['Content-Type'] = 'application/json' + +// HTTP拦截器 +axios.interceptors.request.use( + config => { + // set token + config.headers['ChatGPT-Token'] = getSessionId(); + return config + }, error => { + return Promise.reject(error) + }) +axios.interceptors.response.use( + response => { + let data = response.data; + if (data.code === 0) { + return response + } else { + return Promise.reject(response.data) + } + }, error => { + return Promise.reject(error) + }) + + +// send a http get request +export function httpGet(url, params = {}) { + return new Promise((resolve, reject) => { + axios.get(url, { + params: params + }).then(response => { + resolve(response.data) + }).catch(err => { + reject(err) + }) + }) +} + + +// send a http post request +export function httpPost(url, data = {}, options = {}) { + return new Promise((resolve, reject) => { + axios.post(url, data, options).then(response => { + resolve(response.data) + }).catch(err => { + reject(err) + }) + }) +} diff --git a/web/src/utils/storage.js b/web/src/utils/storage.js new file mode 100644 index 00000000..ac3849e4 --- /dev/null +++ b/web/src/utils/storage.js @@ -0,0 +1,16 @@ +/* eslint-disable no-constant-condition */ +/** + * storage handler + */ +import Storage from 'good-storage' + +const SessionIdKey = 'ChatGPT_SESSION_ID'; +export const Global = {} + +export function getSessionId() { + return Storage.get(SessionIdKey) +} + +export function setSessionId(value) { + Storage.set(SessionIdKey, value) +} \ No newline at end of file diff --git a/web/src/views/Chat.vue b/web/src/views/Chat.vue index f81c8327..16550542 100644 --- a/web/src/views/Chat.vue +++ b/web/src/views/Chat.vue @@ -1,6 +1,11 @@ @@ -54,34 +79,63 @@ import ChatPrompt from "@/components/ChatPrompt.vue"; import ChatReply from "@/components/ChatReply.vue"; import {randString} from "@/utils/libs"; import {ElMessage, ElMessageBox} from 'element-plus' -import {Tools} from '@element-plus/icons-vue' +import {Tools, Lock} from '@element-plus/icons-vue' import ConfigDialog from '@/components/ConfigDialog.vue' +import {httpPost} from "@/utils/http"; +import {getSessionId, setSessionId} from "@/utils/storage"; export default defineComponent({ name: "XChat", - components: {ChatPrompt, ChatReply, Tools, ConfigDialog}, + components: {ChatPrompt, ChatReply, Tools, Lock, ConfigDialog}, data() { return { - title: "ChatGPT 控制台", + title: 'ChatGPT 控制台', + logo: 'images/logo.png', chatData: [], inputValue: '', chatBoxHeight: 0, - showDialog: false, + showConnectDialog: false, + showLoginDialog: false, + token: '', connectingMessageBox: null, socket: null, - sending: false + toolBoxHeight: 61 + 42, + sending: false, + loading: false } }, - computed: {}, - mounted: function () { nextTick(() => { - this.chatBoxHeight = window.innerHeight - 61; + this.chatBoxHeight = window.innerHeight - this.toolBoxHeight; }) - this.connect(); + // 获取会话 + httpPost("/api/session/get").then(() => { + this.connect(); + }).catch(() => { + this.showLoginDialog = true; + }) + + for (let i = 0; i < 10; i++) { + this.chatData.push({ + type: "prompt", + id: randString(32), + icon: 'images/user-icon.png', + content: "孙悟空为什么可以把金棍棒放进耳朵?", + }); + this.chatData.push({ + type: "reply", + id: randString(32), + icon: 'images/gpt-icon.png', + content: "孙悟空是中国神话中的人物,传说中他可以把金箍棒放进耳朵里,这是一种超自然能力,无法用现代科学解释。这种能力可能是象征孙悟空超人力量的古代文化传说。", + }); + } + + window.addEventListener("resize", () => { + this.chatBoxHeight = window.innerHeight - this.toolBoxHeight; + }); }, methods: { @@ -91,7 +145,8 @@ export default defineComponent({ } // 初始化 WebSocket 对象 - const socket = new WebSocket(process.env.VUE_APP_WS_HOST + '/api/chat'); + const token = getSessionId(); + const socket = new WebSocket('ws://' + process.env.VUE_APP_API_HOST + '/api/chat', [token]); socket.addEventListener('open', () => { ElMessage.success('创建会话成功!'); @@ -122,6 +177,9 @@ export default defineComponent({ let content = data.content; // 替换换行符 if (content.indexOf("\n\n") >= 0) { + if (this.chatData[this.chatData.length - 1]["content"].length === 0) { + return + } content = content.replace("\n\n", "
"); } this.chatData[this.chatData.length - 1]["content"] += content; @@ -182,7 +240,6 @@ export default defineComponent({ content: this.inputValue }); - // TODO: 使用 websocket 提交数据到后端 this.sending = true; this.socket.send(this.inputValue); this.$refs["text-input"].blur(); @@ -199,6 +256,26 @@ export default defineComponent({ }, 200) }, + // 提交 Token + submitToken: function () { + this.showLoginDialog = false; + this.loading = true + + // 获取会话 + httpPost("/api/login", { + token: this.token + }).then((res) => { + setSessionId(res.data) + this.connect(); + this.loading = false; + }).catch(() => { + ElMessage.error("口令错误"); + this.token = ''; + this.showLoginDialog = true; + this.loading = false; + }) + } + }, }) @@ -211,7 +288,7 @@ export default defineComponent({ .body { background-color: rgba(247, 247, 248, 1); display flex; - justify-content center; + //justify-content center; align-items flex-start; height 100%; @@ -219,13 +296,24 @@ export default defineComponent({ overflow auto; width 100%; + .tool-box { + padding-top 10px; + display flex; + justify-content center; + align-items center; + + .el-image { + margin-right 5px; + } + } + .chat-box { // 变量定义 --content-font-size: 16px; --content-color: #374151; font-family 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; - padding: 20px 10px; + padding: 0 10px 10px 10px; .chat-line { padding 10px; @@ -304,10 +392,27 @@ export default defineComponent({ } .el-message { - width 90%; - min-width: 300px; + min-width: 100px; max-width 600px; } +.token-dialog { + .el-dialog { + --el-dialog-width 90%; + max-width 400px; + + .el-dialog__body { + padding 10px 10px 20px 10px; + } + + .el-row { + flex-wrap nowrap + + button { + margin-left 5px; + } + } + } +}