添加会话授权支持

This commit is contained in:
RockYang 2023-03-21 18:12:24 +08:00
parent 3bb6814493
commit 005d219a8c
16 changed files with 403 additions and 73 deletions

View File

@ -8,6 +8,7 @@
* [ ] 使用 MySQL 保存用户的聊天的历史记录 * [ ] 使用 MySQL 保存用户的聊天的历史记录
* [ ] 用户聊天鉴权,设置口令模式 * [ ] 用户聊天鉴权,设置口令模式
* [ ] 每次连接自动加载历史记录 * [ ] 每次连接自动加载历史记录
* [ ] OpenAI API 负载均衡,限制每个 API Key 每分钟之内调用次数不超过 15次防止被封
* [ ] 角色设定,预设一些角色,比如程序员,产品经理,医生,作家,老师... * [ ] 角色设定,预设一些角色,比如程序员,产品经理,医生,作家,老师...
* [ ] markdown 语法解析 * [ ] markdown 语法解析
* [ ] 用户配置界面 * [ ] 用户配置界面

View File

@ -10,6 +10,12 @@ import (
// ConfigSetHandle set configs // ConfigSetHandle set configs
func (s *Server) ConfigSetHandle(c *gin.Context) { 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 var data map[string]string
err := json.NewDecoder(c.Request.Body).Decode(&data) err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil { if err != nil {
@ -71,6 +77,23 @@ func (s *Server) ConfigSetHandle(c *gin.Context) {
s.Config.Chat.EnableContext = v 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) logger.Infof("Config: %+v", s.Config)
err = types.SaveConfig(s.Config, s.ConfigPath) err = types.SaveConfig(s.Config, s.ConfigPath)

View File

@ -2,6 +2,7 @@ package server
import ( import (
"embed" "embed"
"encoding/json"
"fmt" "fmt"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/cookie"
@ -12,6 +13,7 @@ import (
"net/url" "net/url"
logger2 "openai/logger" logger2 "openai/logger"
"openai/types" "openai/types"
"openai/utils"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -35,6 +37,8 @@ type Server struct {
ConfigPath string ConfigPath string
Client *http.Client Client *http.Client
History map[string][]types.Message History map[string][]types.Message
WsSession map[string]string // 关闭 Websocket 会话
} }
func NewServer(configPath string) (*Server, error) { func NewServer(configPath string) (*Server, error) {
@ -55,7 +59,9 @@ func NewServer(configPath string) (*Server, error) {
Config: config, Config: config,
Client: client, Client: client,
ConfigPath: configPath, 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) { 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 := gin.Default()
engine.Use(sessionMiddleware(s.Config)) engine.Use(sessionMiddleware(s.Config))
engine.Use(corsMiddleware()) engine.Use(corsMiddleware())
engine.Use(AuthorizeMiddleware()) engine.Use(AuthorizeMiddleware(s))
engine.GET("/hello", Hello) engine.GET("/hello", Hello)
engine.POST("/api/session/get", s.GetSessionHandle)
engine.POST("/api/login", s.LoginHandle)
engine.Any("/api/chat", s.ChatHandle) engine.Any("/api/chat", s.ChatHandle)
engine.POST("/api/config/set", s.ConfigSetHandle) 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.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-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") 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 用户授权验证 // AuthorizeMiddleware 用户授权验证
func AuthorizeMiddleware() gin.HandlerFunc { func AuthorizeMiddleware(s *Server) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
c.Next() if !s.Config.EnableAuth || c.Request.URL.Path == "/api/login" || c.Request.URL.Path == "/api/config/set" {
//if c.Request.URL.Path == "/login" { c.Next()
// c.Next() return
// return }
//}
//sessionName := c.GetHeader("Session-Name") tokenName := c.GetHeader("Sec-WebSocket-Protocol")
//session, err := c.Cookie(sessionName) logger.Info(s.WsSession)
//if err == nil { logger.Info(tokenName)
// c.Request.Header.Set(utils.SessionKey, session) if addr, ok := s.WsSession[tokenName]; ok && addr == c.ClientIP() {
// c.Next() c.Next()
//} else { return
// logger.Fatal(err) }
// c.Abort()
// c.JSON(http.StatusUnauthorized, "No session data found") 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) { func Hello(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": fmt.Sprintf("HELLO, XWEBSSH !!!")}) c.JSON(http.StatusOK, gin.H{"code": 0, "message": fmt.Sprintf("HELLO, XWEBSSH !!!")})
} }

View File

@ -10,10 +10,12 @@ import (
) )
type Config struct { type Config struct {
Listen string Listen string
Session Session Session Session
ProxyURL string ProxyURL string
Chat Chat Chat Chat
EnableAuth bool // 是否开启鉴权
Tokens []string // 授权的白名单列表 TODO: 后期要存储到 LevelDB 或者 Mysql 数据库
} }
// Chat configs struct // Chat configs struct
@ -60,6 +62,7 @@ func NewDefaultConfig() *Config {
Temperature: 1.0, Temperature: 1.0,
EnableContext: true, EnableContext: true,
}, },
EnableAuth: true,
} }
} }

View File

@ -31,5 +31,9 @@ const (
InvalidParams = BizCode(101) // 非法参数 InvalidParams = BizCode(101) // 非法参数
NotAuthorized = BizCode(400) // 未授权 NotAuthorized = BizCode(400) // 未授权
OkMsg = "Success" OkMsg = "Success"
ErrorMsg = "系统开小差了"
) )
const TokenName = "ChatGPT-Token"
const SessionKey = "WEB_SSH_SESSION"

View File

@ -30,3 +30,12 @@ func Long2IP(ipInt int64) string {
func IsBlank(value string) bool { func IsBlank(value string) bool {
return len(strings.TrimSpace(value)) == 0 return len(strings.TrimSpace(value)) == 0
} }
func ContainsItem(slice []string, item string) bool {
for _, e := range slice {
if e == item {
return true
}
}
return false
}

View File

@ -1 +1,2 @@
VUE_APP_WS_HOST=ws://172.22.11.200:5678 VUE_APP_API_HOST=172.22.11.200:5678
VUE_APP_API_SECURE=false

View File

@ -1 +1,2 @@
VUE_APP_WS_HOST=ws://127.0.0.1:5678 VUE_APP_WS_HOST=ws://127.0.0.1:5678
VUE_APP_API_SECURE=false

112
web/package-lock.json generated
View File

@ -14,6 +14,7 @@
"element-plus": "^2.1.11", "element-plus": "^2.1.11",
"good-storage": "^1.1.1", "good-storage": "^1.1.1",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"qs": "^6.11.1",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.15" "vue-router": "^4.0.15"
}, },
@ -3624,6 +3625,18 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true "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": { "node_modules/bonjour-service": {
"version": "1.0.12", "version": "1.0.12",
"resolved": "https://registry.npmmirror.com/bonjour-service/-/bonjour-service-1.0.12.tgz", "resolved": "https://registry.npmmirror.com/bonjour-service/-/bonjour-service-1.0.12.tgz",
@ -3712,7 +3725,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2" "get-intrinsic": "^1.0.2"
@ -5728,6 +5740,18 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true "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": { "node_modules/express/node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -6013,8 +6037,7 @@
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"dev": true
}, },
"node_modules/functional-red-black-tree": { "node_modules/functional-red-black-tree": {
"version": "1.0.1", "version": "1.0.1",
@ -6044,7 +6067,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
@ -6157,7 +6179,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
}, },
@ -6187,7 +6208,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@ -7682,6 +7702,14 @@
"node": ">=0.10.0" "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": { "node_modules/object-keys": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
@ -8747,12 +8775,17 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.9.7", "version": "6.11.1",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.9.7.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
"integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==",
"dev": true, "dependencies": {
"side-channel": "^1.0.4"
},
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
@ -9308,6 +9341,19 @@
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true "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": { "node_modules/signal-exit": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", "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", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true "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", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2" "get-intrinsic": "^1.0.2"
@ -15514,6 +15565,12 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true "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": { "safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -15745,8 +15802,7 @@
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"dev": true
}, },
"functional-red-black-tree": { "functional-red-black-tree": {
"version": "1.0.1", "version": "1.0.1",
@ -15770,7 +15826,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
@ -15865,7 +15920,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
} }
@ -15888,8 +15942,7 @@
"has-symbols": { "has-symbols": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
"dev": true
}, },
"hash-sum": { "hash-sum": {
"version": "2.0.0", "version": "2.0.0",
@ -17079,6 +17132,11 @@
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true "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": { "object-keys": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
@ -17844,10 +17902,12 @@
"dev": true "dev": true
}, },
"qs": { "qs": {
"version": "6.9.7", "version": "6.11.1",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.9.7.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
"integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==",
"dev": true "requires": {
"side-channel": "^1.0.4"
}
}, },
"queue-microtask": { "queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
@ -18317,6 +18377,16 @@
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true "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": { "signal-exit": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz",

View File

@ -14,6 +14,7 @@
"element-plus": "^2.1.11", "element-plus": "^2.1.11",
"good-storage": "^1.1.1", "good-storage": "^1.1.1",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"qs": "^6.11.1",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.15" "vue-router": "^4.0.15"
}, },

View File

@ -24,10 +24,6 @@ export default defineComponent({
icon: { icon: {
type: String, type: String,
default: 'images/gpt-icon.png', default: 'images/gpt-icon.png',
},
cursor: {
type: Boolean,
default: true
} }
}, },
data() { data() {

View File

@ -3,15 +3,17 @@ import {createApp} from 'vue'
import ElementPlus from "element-plus" import ElementPlus from "element-plus"
import "element-plus/dist/index.css" import "element-plus/dist/index.css"
import App from './App.vue' import App from './App.vue'
import Home from './views/Chat.vue' import Chat from './views/Chat.vue'
import NotFound from './views/404.vue' import NotFound from './views/404.vue'
import './utils/prototype' import './utils/prototype'
import "./assets/css/bootstrap.min.css" import "./assets/css/bootstrap.min.css"
import {Global} from "@/utils/storage";
Global['Chat'] = Chat
const routes = [ const routes = [
{ {
name: 'home', path: '/', component: Home, meta: { name: 'home', path: '/', component: Chat, meta: {
title: 'ChatGPT-Console' title: 'ChatGPT-Console'
} }
}, },

54
web/src/utils/http.js Normal file
View File

@ -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)
})
})
}

16
web/src/utils/storage.js Normal file
View File

@ -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)
}

View File

@ -1,6 +1,11 @@
<template> <template>
<div class="body"> <div class="body" v-loading="loading">
<div id="container"> <div id="container">
<div class="tool-box">
<el-image style="width: 24px; height: 24px" :src="logo"/>
<el-button round>欢迎来到人工智能时代</el-button>
</div>
<div class="chat-box" :style="{height: chatBoxHeight+'px'}"> <div class="chat-box" :style="{height: chatBoxHeight+'px'}">
<div v-for="chat in chatData" :key="chat.id"> <div v-for="chat in chatData" :key="chat.id">
<chat-prompt <chat-prompt
@ -9,7 +14,6 @@
:content="chat.content"/> :content="chat.content"/>
<chat-reply v-else-if="chat.type==='reply'" <chat-reply v-else-if="chat.type==='reply'"
:icon="chat.icon" :icon="chat.icon"
:cursor="chat.cursor"
:content="chat.content"/> :content="chat.content"/>
</div> </div>
@ -25,14 +29,14 @@
v-on:focus="focus" v-on:focus="focus"
autofocus autofocus
type="textarea" type="textarea"
placeholder="Input any thing here..." placeholder="开始你的提问"
/> />
</div> </div>
<div class="btn-container"> <div class="btn-container">
<el-row> <el-row>
<el-button type="success" class="send" :disabled="sending" v-on:click="sendMessage">发送</el-button> <el-button type="success" class="send" :disabled="sending" v-on:click="sendMessage">发送</el-button>
<el-button type="info" class="config" circle @click="showDialog = true"> <el-button type="info" class="config" circle @click="showConnectDialog = true">
<el-icon> <el-icon>
<Tools/> <Tools/>
</el-icon> </el-icon>
@ -44,7 +48,28 @@
</div><!-- end container --> </div><!-- end container -->
<config-dialog v-model:show="showDialog"></config-dialog> <config-dialog v-model:show="showConnectDialog"></config-dialog>
<div class="token-dialog">
<el-dialog
v-model="showLoginDialog"
:show-close="false"
:close-on-click-modal="false"
title="请输入口令继续访问"
>
<el-row>
<el-input v-model="token" placeholder="在此输入口令">
<template #prefix>
<el-icon class="el-input__icon">
<Lock/>
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="submitToken">提交</el-button>
</el-row>
</el-dialog>
</div>
</div> </div>
</template> </template>
@ -54,34 +79,63 @@ import ChatPrompt from "@/components/ChatPrompt.vue";
import ChatReply from "@/components/ChatReply.vue"; import ChatReply from "@/components/ChatReply.vue";
import {randString} from "@/utils/libs"; import {randString} from "@/utils/libs";
import {ElMessage, ElMessageBox} from 'element-plus' 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 ConfigDialog from '@/components/ConfigDialog.vue'
import {httpPost} from "@/utils/http";
import {getSessionId, setSessionId} from "@/utils/storage";
export default defineComponent({ export default defineComponent({
name: "XChat", name: "XChat",
components: {ChatPrompt, ChatReply, Tools, ConfigDialog}, components: {ChatPrompt, ChatReply, Tools, Lock, ConfigDialog},
data() { data() {
return { return {
title: "ChatGPT 控制台", title: 'ChatGPT 控制台',
logo: 'images/logo.png',
chatData: [], chatData: [],
inputValue: '', inputValue: '',
chatBoxHeight: 0, chatBoxHeight: 0,
showDialog: false, showConnectDialog: false,
showLoginDialog: false,
token: '',
connectingMessageBox: null, connectingMessageBox: null,
socket: null, socket: null,
sending: false toolBoxHeight: 61 + 42,
sending: false,
loading: false
} }
}, },
computed: {},
mounted: function () { mounted: function () {
nextTick(() => { 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: { methods: {
@ -91,7 +145,8 @@ export default defineComponent({
} }
// WebSocket // 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', () => { socket.addEventListener('open', () => {
ElMessage.success('创建会话成功!'); ElMessage.success('创建会话成功!');
@ -122,6 +177,9 @@ export default defineComponent({
let content = data.content; let content = data.content;
// //
if (content.indexOf("\n\n") >= 0) { if (content.indexOf("\n\n") >= 0) {
if (this.chatData[this.chatData.length - 1]["content"].length === 0) {
return
}
content = content.replace("\n\n", "<br />"); content = content.replace("\n\n", "<br />");
} }
this.chatData[this.chatData.length - 1]["content"] += content; this.chatData[this.chatData.length - 1]["content"] += content;
@ -182,7 +240,6 @@ export default defineComponent({
content: this.inputValue content: this.inputValue
}); });
// TODO: 使 websocket
this.sending = true; this.sending = true;
this.socket.send(this.inputValue); this.socket.send(this.inputValue);
this.$refs["text-input"].blur(); this.$refs["text-input"].blur();
@ -199,6 +256,26 @@ export default defineComponent({
}, 200) }, 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 { .body {
background-color: rgba(247, 247, 248, 1); background-color: rgba(247, 247, 248, 1);
display flex; display flex;
justify-content center; //justify-content center;
align-items flex-start; align-items flex-start;
height 100%; height 100%;
@ -219,13 +296,24 @@ export default defineComponent({
overflow auto; overflow auto;
width 100%; width 100%;
.tool-box {
padding-top 10px;
display flex;
justify-content center;
align-items center;
.el-image {
margin-right 5px;
}
}
.chat-box { .chat-box {
// //
--content-font-size: 16px; --content-font-size: 16px;
--content-color: #374151; --content-color: #374151;
font-family 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; font-family 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
padding: 20px 10px; padding: 0 10px 10px 10px;
.chat-line { .chat-line {
padding 10px; padding 10px;
@ -304,10 +392,27 @@ export default defineComponent({
} }
.el-message { .el-message {
width 90%; min-width: 100px;
min-width: 300px;
max-width 600px; 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;
}
}
}
}
</style> </style>