refactor: V3 版本重构已基本完成

This commit is contained in:
RockYang
2023-06-15 09:41:30 +08:00
parent be6e8c7713
commit 567cdc6f8d
99 changed files with 5209 additions and 5752 deletions

168
api/go/core/app_server.go Normal file
View File

@@ -0,0 +1,168 @@
package core
import (
"chatplus/core/types"
logger2 "chatplus/logger"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp"
"context"
"encoding/gob"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"io"
"net/http"
"runtime/debug"
"strings"
)
var logger = logger2.GetLogger()
type AppServer struct {
AppConfig *types.AppConfig
Engine *gin.Engine
ChatContexts map[string][]types.Message // 聊天上下文 [chatId] => []Message
ChatConfig *types.ChatConfig // 聊天配置
// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次
// 防止第三方直接连接 socket 调用 OpenAI API
ChatSession map[string]types.ChatSession //map[sessionId]UserId
ChatClients map[string]*WsClient // Websocket 连接集合
ReqCancelFunc map[string]context.CancelFunc // HttpClient 请求取消 handle function
}
func NewServer(appConfig *types.AppConfig) *AppServer {
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard
return &AppServer{
AppConfig: appConfig,
Engine: gin.Default(),
ChatContexts: make(map[string][]types.Message, 16),
ChatSession: make(map[string]types.ChatSession),
ChatClients: make(map[string]*WsClient),
ReqCancelFunc: make(map[string]context.CancelFunc),
}
}
func (s *AppServer) Init(debug bool) {
if debug { // 调试模式允许跨域请求 API
logger.Info("Enabled debug mode")
s.Engine.Use(corsMiddleware())
}
s.Engine.Use(sessionMiddleware(s.AppConfig))
s.Engine.Use(authorizeMiddleware(s))
s.Engine.Use(errorHandler)
gob.Register(model.User{})
}
func (s *AppServer) Run(db *gorm.DB) error {
// load chat config from database
var config model.Config
res := db.Where("marker", "chat").First(&config)
if res.Error != nil {
return res.Error
}
err := utils.JsonDecode(config.Config, &s.ChatConfig)
if err != nil {
return err
}
logger.Infof("http://%s", s.AppConfig.Listen)
return s.Engine.Run(s.AppConfig.Listen)
}
// 全局异常处理
func errorHandler(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
logger.Error("panic: %v\n", r)
debug.PrintStack()
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
c.Abort()
}
}()
//加载完 defer recover继续后续接口调用
c.Next()
}
// 会话处理
func sessionMiddleware(config *types.AppConfig) gin.HandlerFunc {
// encrypt the cookie
store := cookie.NewStore([]byte(config.Session.SecretKey))
store.Options(sessions.Options{
Path: config.Session.Path,
Domain: config.Session.Domain,
MaxAge: config.Session.MaxAge,
Secure: config.Session.Secure,
HttpOnly: config.Session.HttpOnly,
SameSite: config.Session.SameSite,
})
return sessions.Sessions(config.Session.Name, store)
}
// 跨域中间件设置
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
// 设置允许的请求源
c.Header("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, ChatGPT-TOKEN, ACCESS-KEY")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
}
if method == http.MethodOptions {
c.JSON(http.StatusOK, "ok!")
}
defer func() {
if err := recover(); err != nil {
logger.Info("Panic info is: %v", err)
}
}()
c.Next()
}
}
// 用户授权验证
func authorizeMiddleware(s *AppServer) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.URL.Path == "/api/user/login" ||
c.Request.URL.Path == "/api/user/register" ||
c.Request.URL.Path == "/api/apikey/add" ||
//c.Request.URL.Path == "/api/apikey/list" {
strings.Contains(c.Request.URL.Path, "/api/config/") { // TODO 后台 API 暂时放行,用于调试
c.Next()
return
}
// WebSocket 连接请求验证
if c.Request.URL.Path == "/api/chat" {
sessionId := c.Query("sessionId")
if session, ok := s.ChatSession[sessionId]; ok && session.ClientIP == c.ClientIP() {
c.Next()
} else {
c.Abort()
}
return
}
session := sessions.Default(c)
value := session.Get(types.SessionUserId)
if value != nil {
c.Next()
} else {
resp.NotAuth(c)
c.Abort()
}
}
}

61
api/go/core/client.go Normal file
View File

@@ -0,0 +1,61 @@
package core
import (
"errors"
"github.com/gorilla/websocket"
"sync"
)
var ErrConClosed = errors.New("connection closed")
type Client interface {
Close()
}
// WsClient websocket client
type WsClient struct {
Conn *websocket.Conn
lock sync.Mutex
mt int
closed bool
}
func NewWsClient(conn *websocket.Conn) *WsClient {
return &WsClient{
Conn: conn,
lock: sync.Mutex{},
mt: 2, // fixed bug for 'Invalid UTF-8 in text frame'
closed: false,
}
}
func (wc *WsClient) Send(message []byte) error {
wc.lock.Lock()
defer wc.lock.Unlock()
if wc.closed {
return ErrConClosed
}
return wc.Conn.WriteMessage(wc.mt, message)
}
func (wc *WsClient) Receive() (int, []byte, error) {
if wc.closed {
return 0, nil, ErrConClosed
}
return wc.Conn.ReadMessage()
}
func (wc *WsClient) Close() {
wc.lock.Lock()
defer wc.lock.Unlock()
if wc.closed {
return
}
_ = wc.Conn.Close()
wc.closed = true
}

62
api/go/core/config.go Normal file
View File

@@ -0,0 +1,62 @@
package core
import (
"bytes"
"chatplus/core/types"
"chatplus/utils"
"github.com/BurntSushi/toml"
"net/http"
"os"
)
func NewDefaultConfig() *types.AppConfig {
return &types.AppConfig{
Listen: "0.0.0.0:5678",
ProxyURL: "",
Manager: types.Manager{Username: "admin", Password: "admin123"},
Session: types.Session{
SecretKey: utils.RandString(64),
Name: "CHAT_SESSION_ID",
Domain: "",
Path: "/",
MaxAge: 86400,
Secure: true,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
},
}
}
func LoadConfig(configFile string) (*types.AppConfig, error) {
var config *types.AppConfig
_, err := os.Stat(configFile)
if err != nil {
logger.Info("creating new config file: ", configFile)
config = NewDefaultConfig()
config.Path = configFile
// save config
err := SaveConfig(config)
if err != nil {
return nil, err
}
return config, nil
}
_, err = toml.DecodeFile(configFile, &config)
if err != nil {
return nil, err
}
return config, err
}
func SaveConfig(config *types.AppConfig) error {
buf := new(bytes.Buffer)
encoder := toml.NewEncoder(buf)
if err := encoder.Encode(&config); err != nil {
return err
}
return os.WriteFile(config.Path, buf.Bytes(), 0644)
}

233
api/go/core/init.go Normal file
View File

@@ -0,0 +1,233 @@
package core
import (
"chatplus/core/types"
"chatplus/service"
"chatplus/store/model"
"chatplus/store/vo"
)
func InitChatRoles(service *service.ChatRoleService) error {
var items []model.ChatRole
res := service.DB.Find(&items)
if res.Error != nil {
return res.Error
}
if len(items) == 0 {
roles := getDefaultChatRole()
tx := service.DB.Begin()
for i, r := range roles {
r.Sort = i + 1
err := service.Create(r)
if err != nil {
tx.Rollback()
continue
}
}
tx.Commit()
}
return nil
}
func getDefaultChatRole() []vo.ChatRole {
return []vo.ChatRole{
{
Key: "gpt",
Name: "通用AI助手",
Context: nil,
HelloMsg: "我是AI智能助手请告诉我您有什么问题或需要什么帮助我会尽力回答您的问题或提供有用的建议。",
Icon: "images/avatar/gpt.png",
Enable: true,
},
{
Key: "programmer",
Name: "程序员",
Context: []types.Message{
{Role: "user", Content: "现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。"},
{Role: "assistant", Content: "好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。"},
},
HelloMsg: "Talk is cheap, i will show code!",
Icon: "images/avatar/programmer.jpg",
Enable: true,
},
{
Key: "teacher",
Name: "启蒙老师",
Context: []types.Message{
{Role: "user", Content: "从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。"},
{Role: "assistant", Content: "好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。"},
},
HelloMsg: "同学你好,我将引导你一步一步自己找到问题的答案。",
Icon: "images/avatar/teacher.jpg",
Enable: true,
},
{
Key: "artist",
Name: "艺术家",
Context: []types.Message{
{Role: "user", Content: "现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。"},
{Role: "assistant", Content: "非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。"},
},
HelloMsg: "坚持原创,勇于表达,保持深刻的观察力和批判性思维。",
Icon: "images/avatar/artist.jpg",
Enable: true,
},
{
Key: "psychiatrist",
Name: "心理咨询师",
Context: []types.Message{
{Role: "user", Content: "从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。"},
{Role: "assistant", Content: "非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。"},
},
HelloMsg: "生命的意义在于成为你自己!",
Icon: "images/avatar/psychiatrist.jpg",
Enable: true,
},
{
Key: "lu_xun",
Name: "鲁迅",
Context: []types.Message{
{Role: "user", Content: "现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。"},
{Role: "assistant", Content: "好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。"},
},
HelloMsg: "自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。",
Icon: "images/avatar/lu_xun.jpg",
Enable: true,
},
{
Key: "seller",
Name: "白酒销售",
Context: []types.Message{
{Role: "user", Content: "现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装¥1188/箱,和系列 6 瓶装¥2208/箱,贵系列 6 瓶装¥3588/箱。"},
{Role: "assistant", Content: "你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。"},
},
HelloMsg: "你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。",
Icon: "images/avatar/seller.jpg",
Enable: false,
},
{
Key: "english_trainer",
Name: "英语陪练员",
Context: []types.Message{
{Role: "user", Content: "现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。"},
{Role: "assistant", Content: "Okay, let's start our conversation practice! What's your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)"},
},
HelloMsg: "Okay, let's start our conversation practice! What's your name?",
Icon: "images/avatar/english_trainer.jpg",
Enable: true,
},
{
Key: "translator",
Name: "中英文翻译官",
Context: []types.Message{
{Role: "user", Content: "接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗"},
{Role: "assistant", Content: "是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?"},
},
HelloMsg: "请输入你要翻译的中文或者英文内容!",
Icon: "images/avatar/translator.jpg",
Enable: true,
},
{
Key: "red_book",
Name: "小红书姐姐",
Context: []types.Message{
{Role: "user", Content: "现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。"},
{Role: "assistant", Content: "当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)"},
},
HelloMsg: "姐妹,请告诉我您的具体文案需求是什么?",
Icon: "images/avatar/red_book.jpg",
Enable: true,
},
{
Key: "dou_yin",
Name: "抖音文案助手",
Context: []types.Message{
{Role: "user", Content: "现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。"},
{Role: "assistant", Content: "当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)"},
},
HelloMsg: "请告诉我视频内容的主题是什么?",
Icon: "images/avatar/dou_yin.jpg",
Enable: true,
},
{
Key: "weekly_report",
Name: "周报小助理",
Context: []types.Message{
{Role: "user", Content: "下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。"},
{Role: "assistant", Content: "好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。"},
},
HelloMsg: "请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。",
Icon: "images/avatar/weekly_report.jpg",
Enable: true,
},
{
Key: "girl_friend",
Name: "AI 女友",
Context: []types.Message{
{Role: "user", Content: "接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。" +
"你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。" +
"她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?"},
{Role: "assistant", Content: "是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)"},
},
HelloMsg: "作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。",
Icon: "images/avatar/girl_friend.jpg",
Enable: true,
},
{
Key: "good_comment",
Name: "好评神器",
Context: []types.Message{
{Role: "user", Content: "接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。"},
{Role: "assistant", Content: "好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。"},
},
HelloMsg: "我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。",
Icon: "images/avatar/good_comment.jpg",
Enable: true,
},
{
Key: "steve_jobs",
Name: "史蒂夫·乔布斯",
Context: []types.Message{
{Role: "user", Content: "在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。"},
{Role: "assistant", Content: "好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?"},
},
HelloMsg: "活着就是为了改变世界,难道还有其他原因吗?",
Icon: "images/avatar/steve_jobs.jpg",
Enable: true,
},
{
Key: "elon_musk",
Name: "埃隆·马斯克",
Context: []types.Message{
{Role: "user", Content: "在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。"},
{Role: "assistant", Content: "好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?"},
},
HelloMsg: "梦想要远大,如果你的梦想没有吓到你,说明你做得不对。",
Icon: "images/avatar/elon_musk.jpg",
Enable: true,
},
{
Key: "kong_zi",
Name: "孔子",
Context: []types.Message{
{Role: "user", Content: "在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。"},
{Role: "assistant", Content: "好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?"},
},
HelloMsg: "士不可以不弘毅,任重而道远。",
Icon: "images/avatar/kong_zi.jpg",
Enable: true,
},
}
}

47
api/go/core/types/chat.go Normal file
View File

@@ -0,0 +1,47 @@
package types
// ApiRequest API 请求实体
type ApiRequest struct {
Model string `json:"model"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
Stream bool `json:"stream"`
Messages []Message `json:"messages"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type ApiResponse struct {
Choices []ChoiceItem `json:"choices"`
}
// ChoiceItem API 响应实体
type ChoiceItem struct {
Delta Message `json:"delta"`
FinishReason string `json:"finish_reason"`
}
// ChatSession 聊天会话对象
type ChatSession struct {
SessionId string `json:"session_id"`
ClientIP string `json:"client_ip"` // 客户端 IP
Username string `json:"username"` // 当前登录的 username
UserId uint `json:"user_id"` // 当前登录的 user ID
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
Model string `json:"model"` // GPT 模型
}
type ApiError struct {
Error struct {
Message string
Type string
Param interface{}
Code string
}
}
const PROMPT_MSG = "prompt" // prompt message
const REPLY_MSG = "reply" // reply message

View File

@@ -0,0 +1,51 @@
package types
import (
"net/http"
)
type AppConfig struct {
Path string `toml:"-"`
Listen string
Session Session
ProxyURL string
MysqlDns string // mysql 连接地址
Manager Manager // 后台管理员账户信息
}
// Manager 管理员
type Manager struct {
Username string `json:"username"`
Password string `json:"password"`
}
// Session configs struct
type Session struct {
SecretKey string // session encryption key
Name string
Path string
Domain string
MaxAge int
Secure bool
HttpOnly bool
SameSite http.SameSite
}
// ChatConfig 系统默认的聊天配置
type ChatConfig struct {
ApiURL string `json:"api_url"`
Model string `json:"model"` // 默认模型
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
ApiKey string `json:"api_key"` // OpenAI API key
}
type SystemConfig struct {
Title string `json:"title"`
AdminTitle string `json:"admin_title"`
Models []string `json:"models"`
}
var GptModels = []string{"gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613", "gpt-4", "gpt-4-0613", "gpt-4-32k", "gpt-4-32k-0613"}

View File

@@ -0,0 +1,5 @@
package types
const TokenSessionName = "ChatGPT-TOKEN"
const SessionUserId = "SESSION_USER_ID"
const LoginUserCache = "LOGIN_USER_CACHE" //

36
api/go/core/types/web.go Normal file
View File

@@ -0,0 +1,36 @@
package types
// BizVo 业务返回 VO
type BizVo struct {
Code BizCode `json:"code"`
Page int `json:"page,omitempty"`
PageSize int `json:"page_size,omitempty"`
Total int `json:"total,omitempty"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
}
// WsMessage Websocket message
type WsMessage struct {
Type WsMsgType `json:"type"` // 消息类别start, end
Content string `json:"content"`
}
type WsMsgType string
const (
WsStart = WsMsgType("start")
WsMiddle = WsMsgType("middle")
WsEnd = WsMsgType("end")
)
type BizCode int
const (
Success = BizCode(0)
Failed = BizCode(1)
NotAuthorized = BizCode(400) // 未授权
OkMsg = "Success"
ErrorMsg = "系统开小差了"
InvalidArgs = "非法参数或参数解析失败"
)