重构主体工作完成

This commit is contained in:
RockYang 2024-03-15 18:35:10 +08:00
commit e53db3582c
61 changed files with 1043 additions and 1097 deletions

View File

@ -30,7 +30,6 @@ type AppServer struct {
Engine *gin.Engine Engine *gin.Engine
ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message
ChatConfig *types.ChatConfig // chat config cache
SysConfig *types.SystemConfig // system config cache SysConfig *types.SystemConfig // system config cache
// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次 // 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次
@ -69,23 +68,13 @@ func (s *AppServer) Init(debug bool, client *redis.Client) {
} }
func (s *AppServer) Run(db *gorm.DB) error { func (s *AppServer) Run(db *gorm.DB) error {
// load chat config from database
var chatConfig model.Config
res := db.Where("marker", "chat").First(&chatConfig)
if res.Error != nil {
return res.Error
}
err := utils.JsonDecode(chatConfig.Config, &s.ChatConfig)
if err != nil {
return err
}
// load system configs // load system configs
var sysConfig model.Config var sysConfig model.Config
res = db.Where("marker", "system").First(&sysConfig) res := db.Where("marker", "system").First(&sysConfig)
if res.Error != nil { if res.Error != nil {
return res.Error return res.Error
} }
err = utils.JsonDecode(sysConfig.Config, &s.SysConfig) err := utils.JsonDecode(sysConfig.Config, &s.SysConfig)
if err != nil { if err != nil {
return err return err
} }

View File

@ -58,6 +58,9 @@ type ChatModel struct {
Platform Platform `json:"platform"` Platform Platform `json:"platform"`
Value string `json:"value"` Value string `json:"value"`
Power int `json:"power"` Power int `json:"power"`
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Temperature float32 `json:"temperature"` // 模型温度
} }
type ApiError struct { type ApiError struct {
@ -72,27 +75,6 @@ type ApiError struct {
const PromptMsg = "prompt" // prompt message const PromptMsg = "prompt" // prompt message
const ReplyMsg = "reply" // reply message const ReplyMsg = "reply" // reply message
var ModelToTokens = map[string]int{
"gpt-3.5-turbo": 4096,
"gpt-3.5-turbo-16k": 16384,
"gpt-4": 8192,
"gpt-4-32k": 32768,
"chatglm_pro": 32768, // 清华智普
"chatglm_std": 16384,
"chatglm_lite": 4096,
"ernie_bot_turbo": 8192, // 文心一言
"general": 8192, // 科大讯飞
"general2": 8192,
"general3": 8192,
}
func GetModelMaxToken(model string) int {
if token, ok := ModelToTokens[model]; ok {
return token
}
return 4096
}
// PowerType 算力日志类型 // PowerType 算力日志类型
type PowerType int type PowerType int

View File

@ -121,20 +121,6 @@ func (c RedisConfig) Url() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port) return fmt.Sprintf("%s:%d", c.Host, c.Port)
} }
// ChatConfig 系统默认的聊天配置
type ChatConfig struct {
OpenAI ModelAPIConfig `json:"open_ai"`
Azure ModelAPIConfig `json:"azure"`
ChatGML ModelAPIConfig `json:"chat_gml"`
Baidu ModelAPIConfig `json:"baidu"`
XunFei ModelAPIConfig `json:"xun_fei"`
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
ContextDeep int `json:"context_deep"` // 上下文深度
DallImgNum int `json:"dall_img_num"` // dall-e3 出图数量
}
type Platform string type Platform string
const OpenAI = Platform("OpenAI") const OpenAI = Platform("OpenAI")
@ -144,16 +130,6 @@ const Baidu = Platform("Baidu")
const XunFei = Platform("XunFei") const XunFei = Platform("XunFei")
const QWen = Platform("QWen") const QWen = Platform("QWen")
// UserChatConfig 用户的聊天配置
type UserChatConfig struct {
ApiKeys map[Platform]string `json:"api_keys"`
}
type ModelAPIConfig struct {
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
}
type SystemConfig struct { type SystemConfig struct {
Title string `json:"title"` Title string `json:"title"`
AdminTitle string `json:"admin_title"` AdminTitle string `json:"admin_title"`
@ -178,4 +154,7 @@ type SystemConfig struct {
DallPower int `json:"dall_power"` // DALLE3 绘图消耗算力 DallPower int `json:"dall_power"` // DALLE3 绘图消耗算力
WechatCardURL string `json:"wechat_card_url"` // 微信客服地址 WechatCardURL string `json:"wechat_card_url"` // 微信客服地址
EnableContext bool `json:"enable_context"`
ContextDeep int `json:"context_deep"`
} }

View File

@ -32,7 +32,7 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
Value string `json:"value"` Value string `json:"value"`
ApiURL string `json:"api_url"` ApiURL string `json:"api_url"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
UseProxy bool `json:"use_proxy"` ProxyURL string `json:"proxy_url"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@ -48,7 +48,7 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
apiKey.Type = data.Type apiKey.Type = data.Type
apiKey.ApiURL = data.ApiURL apiKey.ApiURL = data.ApiURL
apiKey.Enabled = data.Enabled apiKey.Enabled = data.Enabled
apiKey.UseProxy = data.UseProxy apiKey.ProxyURL = data.ProxyURL
apiKey.Name = data.Name apiKey.Name = data.Name
res := h.db.Save(&apiKey) res := h.db.Save(&apiKey)
if res.Error != nil { if res.Error != nil {

View File

@ -28,6 +28,7 @@ type chatItemVo struct {
UserId uint `json:"user_id"` UserId uint `json:"user_id"`
ChatId string `json:"chat_id"` ChatId string `json:"chat_id"`
Title string `json:"title"` Title string `json:"title"`
Role vo.ChatRole `json:"role"`
Model string `json:"model"` Model string `json:"model"`
Token int `json:"token"` Token int `json:"token"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
@ -78,18 +79,23 @@ func (h *ChatHandler) List(c *gin.Context) {
if res.Error == nil { if res.Error == nil {
userIds := make([]uint, 0) userIds := make([]uint, 0)
chatIds := make([]string, 0) chatIds := make([]string, 0)
roleIds := make([]uint, 0)
for _, item := range items { for _, item := range items {
userIds = append(userIds, item.UserId) userIds = append(userIds, item.UserId)
chatIds = append(chatIds, item.ChatId) chatIds = append(chatIds, item.ChatId)
roleIds = append(roleIds, item.RoleId)
} }
var messages []model.ChatMessage var messages []model.ChatMessage
var users []model.User var users []model.User
var roles []model.ChatRole
h.db.Where("chat_id IN ?", chatIds).Find(&messages) h.db.Where("chat_id IN ?", chatIds).Find(&messages)
h.db.Where("id IN ?", userIds).Find(&users) h.db.Where("id IN ?", userIds).Find(&users)
h.db.Where("id IN ?", roleIds).Find(&roles)
tokenMap := make(map[string]int) tokenMap := make(map[string]int)
userMap := make(map[uint]string) userMap := make(map[uint]string)
msgMap := make(map[string]int) msgMap := make(map[string]int)
roleMap := make(map[uint]vo.ChatRole)
for _, msg := range messages { for _, msg := range messages {
tokenMap[msg.ChatId] += msg.Tokens tokenMap[msg.ChatId] += msg.Tokens
msgMap[msg.ChatId] += 1 msgMap[msg.ChatId] += 1
@ -97,6 +103,14 @@ func (h *ChatHandler) List(c *gin.Context) {
for _, user := range users { for _, user := range users {
userMap[user.Id] = user.Username userMap[user.Id] = user.Username
} }
for _, r := range roles {
var roleVo vo.ChatRole
err := utils.CopyObject(r, &roleVo)
if err != nil {
continue
}
roleMap[r.Id] = roleVo
}
for _, item := range items { for _, item := range items {
list = append(list, chatItemVo{ list = append(list, chatItemVo{
UserId: item.UserId, UserId: item.UserId,
@ -106,6 +120,7 @@ func (h *ChatHandler) List(c *gin.Context) {
Model: item.Model, Model: item.Model,
Token: tokenMap[item.ChatId], Token: tokenMap[item.ChatId],
MsgNum: msgMap[item.ChatId], MsgNum: msgMap[item.ChatId],
Role: roleMap[item.RoleId],
CreatedAt: item.CreatedAt.Unix(), CreatedAt: item.CreatedAt.Unix(),
}) })
} }

View File

@ -33,7 +33,10 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
SortNum int `json:"sort_num"` SortNum int `json:"sort_num"`
Open bool `json:"open"` Open bool `json:"open"`
Platform string `json:"platform"` Platform string `json:"platform"`
Weight int `json:"weight"` Power int `json:"power"`
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Temperature string `json:"temperature"` // 模型温度
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
@ -48,7 +51,10 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
Enabled: data.Enabled, Enabled: data.Enabled,
SortNum: data.SortNum, SortNum: data.SortNum,
Open: data.Open, Open: data.Open,
Power: data.Weight} MaxTokens: data.MaxTokens,
MaxContext: data.MaxContext,
Temperature: float32(utils.Str2Float(data.Temperature)),
Power: data.Power}
item.Id = data.Id item.Id = data.Id
if item.Id > 0 { if item.Id > 0 {
item.CreatedAt = time.Unix(data.CreatedAt, 0) item.CreatedAt = time.Unix(data.CreatedAt, 0)
@ -145,19 +151,16 @@ func (h *ChatModelHandler) Sort(c *gin.Context) {
} }
func (h *ChatModelHandler) Remove(c *gin.Context) { func (h *ChatModelHandler) Remove(c *gin.Context) {
var data struct { id := h.GetInt(c, "id", 0)
Id uint if id <= 0 {
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
if data.Id > 0 {
res := h.db.Where("id = ?", data.Id).Delete(&model.ChatModel{}) res := h.db.Where("id = ?", id).Delete(&model.ChatModel{})
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, "更新数据库失败!")
return return
} }
}
resp.SUCCESS(c) resp.SUCCESS(c)
} }

View File

@ -56,8 +56,6 @@ func (h *ConfigHandler) Update(c *gin.Context) {
var err error var err error
if data.Key == "system" { if data.Key == "system" {
err = utils.JsonDecode(cfg.Config, &h.App.SysConfig) err = utils.JsonDecode(cfg.Config, &h.App.SysConfig)
} else if data.Key == "chat" {
err = utils.JsonDecode(cfg.Config, &h.App.ChatConfig)
} }
if err != nil { if err != nil {
resp.ERROR(c, "Failed to update config cache: "+err.Error()) resp.ERROR(c, "Failed to update config cache: "+err.Error())

View File

@ -107,13 +107,6 @@ func (h *UserHandler) Save(c *gin.Context) {
ChatRoles: utils.JsonEncode(data.ChatRoles), ChatRoles: utils.JsonEncode(data.ChatRoles),
ChatModels: utils.JsonEncode(data.ChatModels), ChatModels: utils.JsonEncode(data.ChatModels),
ExpiredTime: utils.Str2stamp(data.ExpiredTime), ExpiredTime: utils.Str2stamp(data.ExpiredTime),
ChatConfig: utils.JsonEncode(types.UserChatConfig{
ApiKeys: map[types.Platform]string{
types.OpenAI: "",
types.Azure: "",
types.ChatGLM: "",
},
}),
} }
res = h.db.Create(&u) res = h.db.Create(&u)
_ = utils.CopyObject(u, &userVo) _ = utils.CopyObject(u, &userVo)

View File

@ -111,14 +111,13 @@ func (h *ChatHandler) sendAzureMessage(
useMsg := types.Message{Role: "user", Content: prompt} useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息,如果是调用函数则不需要更新上下文 // 更新上下文消息,如果是调用函数则不需要更新上下文
if h.App.ChatConfig.EnableContext { if h.App.SysConfig.EnableContext {
chatCtx = append(chatCtx, useMsg) // 提问消息 chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息 chatCtx = append(chatCtx, message) // 回复消息
h.App.ChatContexts.Put(session.ChatId, chatCtx) h.App.ChatContexts.Put(session.ChatId, chatCtx)
} }
// 追加聊天记录 // 追加聊天记录
if h.App.ChatConfig.EnableHistory {
// for prompt // for prompt
promptToken, err := utils.CalcTokens(prompt, req.Model) promptToken, err := utils.CalcTokens(prompt, req.Model)
if err != nil { if err != nil {
@ -166,11 +165,10 @@ func (h *ChatHandler) sendAzureMessage(
// 更新用户算力 // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
}
// 保存当前会话 // 保存当前会话
var chatItem model.ChatItem var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) res = h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
if res.Error != nil { if res.Error != nil {
chatItem.ChatId = session.ChatId chatItem.ChatId = session.ChatId
chatItem.UserId = session.UserId chatItem.UserId = session.UserId

View File

@ -135,14 +135,13 @@ func (h *ChatHandler) sendBaiduMessage(
useMsg := types.Message{Role: "user", Content: prompt} useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息,如果是调用函数则不需要更新上下文 // 更新上下文消息,如果是调用函数则不需要更新上下文
if h.App.ChatConfig.EnableContext { if h.App.SysConfig.EnableContext {
chatCtx = append(chatCtx, useMsg) // 提问消息 chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息 chatCtx = append(chatCtx, message) // 回复消息
h.App.ChatContexts.Put(session.ChatId, chatCtx) h.App.ChatContexts.Put(session.ChatId, chatCtx)
} }
// 追加聊天记录 // 追加聊天记录
if h.App.ChatConfig.EnableHistory {
// for prompt // for prompt
promptToken, err := utils.CalcTokens(prompt, req.Model) promptToken, err := utils.CalcTokens(prompt, req.Model)
if err != nil { if err != nil {
@ -189,11 +188,10 @@ func (h *ChatHandler) sendBaiduMessage(
} }
// 更新用户算力 // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
}
// 保存当前会话 // 保存当前会话
var chatItem model.ChatItem var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) res = h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
if res.Error != nil { if res.Error != nil {
chatItem.ChatId = session.ChatId chatItem.ChatId = session.ChatId
chatItem.UserId = session.UserId chatItem.UserId = session.UserId

View File

@ -57,8 +57,6 @@ func (h *ChatHandler) Init() {
} }
} }
var chatConfig types.ChatConfig
// ChatHandle 处理聊天 WebSocket 请求 // ChatHandle 处理聊天 WebSocket 请求
func (h *ChatHandler) ChatHandle(c *gin.Context) { func (h *ChatHandler) ChatHandle(c *gin.Context) {
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil) ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
@ -112,6 +110,9 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
Id: chatModel.Id, Id: chatModel.Id,
Value: chatModel.Value, Value: chatModel.Value,
Power: chatModel.Power, Power: chatModel.Power,
MaxTokens: chatModel.MaxTokens,
MaxContext: chatModel.MaxContext,
Temperature: chatModel.Temperature,
Platform: types.Platform(chatModel.Platform)} Platform: types.Platform(chatModel.Platform)}
logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username) logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username)
var chatRole model.ChatRole var chatRole model.ChatRole
@ -122,15 +123,6 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
return return
} }
// 初始化聊天配置
var config model.Config
h.db.Where("marker", "chat").First(&config)
err = utils.JsonDecode(config.Config, &chatConfig)
if err != nil {
utils.ReplyMessage(client, "加载系统配置失败,连接已关闭!!!")
c.Abort()
return
}
h.Init() h.Init()
// 保存会话连接 // 保存会话连接
@ -213,7 +205,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
return nil return nil
} }
if userVo.Power <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { if userVo.Power <= 0 {
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!") utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!")
utils.ReplyMessage(ws, ErrImg) utils.ReplyMessage(ws, ErrImg)
return nil return nil
@ -227,7 +219,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度 // 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
promptTokens, err := utils.CalcTokens(prompt, session.Model.Value) promptTokens, err := utils.CalcTokens(prompt, session.Model.Value)
if promptTokens > types.GetModelMaxToken(session.Model.Value) { if promptTokens > session.Model.MaxContext {
utils.ReplyMessage(ws, "对话内容超出了当前模型允许的最大上下文长度!") utils.ReplyMessage(ws, "对话内容超出了当前模型允许的最大上下文长度!")
return nil return nil
} }
@ -237,21 +229,13 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
Stream: true, Stream: true,
} }
switch session.Model.Platform { switch session.Model.Platform {
case types.Azure: case types.Azure, types.ChatGLM, types.Baidu, types.XunFei:
req.Temperature = h.App.ChatConfig.Azure.Temperature req.Temperature = session.Model.Temperature
req.MaxTokens = h.App.ChatConfig.Azure.MaxTokens req.MaxTokens = session.Model.MaxTokens
break
case types.ChatGLM:
req.Temperature = h.App.ChatConfig.ChatGML.Temperature
req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens
break
case types.Baidu:
req.Temperature = h.App.ChatConfig.OpenAI.Temperature
// TODO 目前只支持 ERNIE-Bot-turbo 模型,如果是 ERNIE-Bot 模型则需要增加函数支持
break break
case types.OpenAI: case types.OpenAI:
req.Temperature = h.App.ChatConfig.OpenAI.Temperature req.Temperature = session.Model.Temperature
req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens req.MaxTokens = session.Model.MaxTokens
// OpenAI 支持函数功能 // OpenAI 支持函数功能
var items []model.Function var items []model.Function
res := h.db.Where("enabled", true).Find(&items) res := h.db.Where("enabled", true).Find(&items)
@ -283,15 +267,13 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
req.Tools = tools req.Tools = tools
req.ToolChoice = "auto" req.ToolChoice = "auto"
} }
case types.XunFei:
req.Temperature = h.App.ChatConfig.XunFei.Temperature
req.MaxTokens = h.App.ChatConfig.XunFei.MaxTokens
break
case types.QWen: case types.QWen:
req.Input = map[string]interface{}{"messages": []map[string]string{{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": prompt}}} req.Parameters = map[string]interface{}{
req.Parameters = map[string]interface{}{} "max_tokens": session.Model.MaxTokens,
"temperature": session.Model.Temperature,
}
break break
default: default:
utils.ReplyMessage(ws, "不支持的平台:"+session.Model.Platform+",请联系管理员!") utils.ReplyMessage(ws, "不支持的平台:"+session.Model.Platform+",请联系管理员!")
utils.ReplyMessage(ws, ErrImg) utils.ReplyMessage(ws, ErrImg)
@ -301,14 +283,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
// 加载聊天上下文 // 加载聊天上下文
chatCtx := make([]types.Message, 0) chatCtx := make([]types.Message, 0)
messages := make([]types.Message, 0) messages := make([]types.Message, 0)
if h.App.ChatConfig.EnableContext { if h.App.SysConfig.EnableContext {
if h.App.ChatContexts.Has(session.ChatId) { if h.App.ChatContexts.Has(session.ChatId) {
messages = h.App.ChatContexts.Get(session.ChatId) messages = h.App.ChatContexts.Get(session.ChatId)
} else { } else {
_ = utils.JsonDecode(role.Context, &messages) _ = utils.JsonDecode(role.Context, &messages)
if chatConfig.ContextDeep > 0 { if h.App.SysConfig.ContextDeep > 0 {
var historyMessages []model.ChatMessage var historyMessages []model.ChatMessage
res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("id DESC").Find(&historyMessages) res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages)
if res.Error == nil { if res.Error == nil {
for i := len(historyMessages) - 1; i >= 0; i-- { for i := len(historyMessages) - 1; i >= 0; i-- {
msg := historyMessages[i] msg := historyMessages[i]
@ -331,12 +313,12 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
for _, v := range messages { for _, v := range messages {
tks, _ := utils.CalcTokens(v.Content, req.Model) tks, _ := utils.CalcTokens(v.Content, req.Model)
// 上下文 token 超出了模型的最大上下文长度 // 上下文 token 超出了模型的最大上下文长度
if tokens+tks >= types.GetModelMaxToken(req.Model) { if tokens+tks >= session.Model.MaxContext {
break break
} }
// 上下文的深度超出了模型的最大上下文深度 // 上下文的深度超出了模型的最大上下文深度
if len(chatCtx) >= h.App.ChatConfig.ContextDeep { if len(chatCtx) >= h.App.SysConfig.ContextDeep {
break break
} }
@ -351,10 +333,17 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
reqMgs = append(reqMgs, m) reqMgs = append(reqMgs, m)
} }
if session.Model.Platform == types.QWen {
req.Input = map[string]interface{}{"prompt": prompt}
if len(reqMgs) > 0 {
req.Input["messages"] = reqMgs
}
} else {
req.Messages = append(reqMgs, map[string]interface{}{ req.Messages = append(reqMgs, map[string]interface{}{
"role": "user", "role": "user",
"content": prompt, "content": prompt,
}) })
}
switch session.Model.Platform { switch session.Model.Platform {
case types.Azure: case types.Azure:
@ -497,9 +486,8 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
request = request.WithContext(ctx) request = request.WithContext(ctx)
request.Header.Set("Content-Type", "application/json") request.Header.Set("Content-Type", "application/json")
var proxyURL string var proxyURL string
if h.App.Config.ProxyURL != "" && apiKey.UseProxy { // 使用代理 if apiKey.ProxyURL != "" { // 使用代理
proxyURL = h.App.Config.ProxyURL proxy, _ := url.Parse(apiKey.ProxyURL)
proxy, _ := url.Parse(proxyURL)
client = &http.Client{ client = &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
Proxy: http.ProxyURL(proxy), Proxy: http.ProxyURL(proxy),
@ -542,7 +530,7 @@ func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, p
res := h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("power", gorm.Expr("power - ?", power)) res := h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("power", gorm.Expr("power - ?", power))
if res.Error == nil { if res.Error == nil {
// 记录算力消费日志 // 记录算力消费日志
h.db.Debug().Create(&model.PowerLog{ h.db.Create(&model.PowerLog{
UserId: userVo.Id, UserId: userVo.Id,
Username: userVo.Username, Username: userVo.Username,
Type: types.PowerConsume, Type: types.PowerConsume,

View File

@ -126,7 +126,7 @@ func (h *ChatHandler) History(c *gin.Context) {
chatId := c.Query("chat_id") // 会话 ID chatId := c.Query("chat_id") // 会话 ID
var items []model.ChatMessage var items []model.ChatMessage
var messages = make([]vo.HistoryMessage, 0) var messages = make([]vo.HistoryMessage, 0)
res := h.db.Debug().Where("chat_id = ?", chatId).Find(&items) res := h.db.Where("chat_id = ?", chatId).Find(&items)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "No history message") resp.ERROR(c, "No history message")
return return

View File

@ -114,14 +114,13 @@ func (h *ChatHandler) sendChatGLMMessage(
useMsg := types.Message{Role: "user", Content: prompt} useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息,如果是调用函数则不需要更新上下文 // 更新上下文消息,如果是调用函数则不需要更新上下文
if h.App.ChatConfig.EnableContext { if h.App.SysConfig.EnableContext {
chatCtx = append(chatCtx, useMsg) // 提问消息 chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息 chatCtx = append(chatCtx, message) // 回复消息
h.App.ChatContexts.Put(session.ChatId, chatCtx) h.App.ChatContexts.Put(session.ChatId, chatCtx)
} }
// 追加聊天记录 // 追加聊天记录
if h.App.ChatConfig.EnableHistory {
// for prompt // for prompt
promptToken, err := utils.CalcTokens(prompt, req.Model) promptToken, err := utils.CalcTokens(prompt, req.Model)
if err != nil { if err != nil {
@ -169,11 +168,10 @@ func (h *ChatHandler) sendChatGLMMessage(
// 更新用户算力 // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
}
// 保存当前会话 // 保存当前会话
var chatItem model.ChatItem var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) res = h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
if res.Error != nil { if res.Error != nil {
chatItem.ChatId = session.ChatId chatItem.ChatId = session.ChatId
chatItem.UserId = session.UserId chatItem.UserId = session.UserId

View File

@ -180,14 +180,13 @@ func (h *ChatHandler) sendOpenAiMessage(
useMsg := types.Message{Role: "user", Content: prompt} useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息,如果是调用函数则不需要更新上下文 // 更新上下文消息,如果是调用函数则不需要更新上下文
if h.App.ChatConfig.EnableContext && toolCall == false { if h.App.SysConfig.EnableContext && toolCall == false {
chatCtx = append(chatCtx, useMsg) // 提问消息 chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息 chatCtx = append(chatCtx, message) // 回复消息
h.App.ChatContexts.Put(session.ChatId, chatCtx) h.App.ChatContexts.Put(session.ChatId, chatCtx)
} }
// 追加聊天记录 // 追加聊天记录
if h.App.ChatConfig.EnableHistory {
useContext := true useContext := true
if toolCall { if toolCall {
useContext = false useContext = false
@ -248,11 +247,10 @@ func (h *ChatHandler) sendOpenAiMessage(
// 更新用户算力 // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
}
// 保存当前会话 // 保存当前会话
var chatItem model.ChatItem var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) res = h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
if res.Error != nil { if res.Error != nil {
chatItem.ChatId = session.ChatId chatItem.ChatId = session.ChatId
chatItem.UserId = session.UserId chatItem.UserId = session.UserId

View File

@ -20,13 +20,16 @@ type qWenResp struct {
Output struct { Output struct {
FinishReason string `json:"finish_reason"` FinishReason string `json:"finish_reason"`
Text string `json:"text"` Text string `json:"text"`
} `json:"output"` } `json:"output,omitempty"`
Usage struct { Usage struct {
TotalTokens int `json:"total_tokens"` TotalTokens int `json:"total_tokens"`
InputTokens int `json:"input_tokens"` InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"` OutputTokens int `json:"output_tokens"`
} `json:"usage"` } `json:"usage,omitempty"`
RequestID string `json:"request_id"` RequestID string `json:"request_id"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
} }
// 通义千问消息发送实现 // 通义千问消息发送实现
@ -70,6 +73,7 @@ func (h *ChatHandler) sendQWenMessage(
scanner := bufio.NewScanner(response.Body) scanner := bufio.NewScanner(response.Body)
var content, lastText, newText string var content, lastText, newText string
var outPutStart = false
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
@ -77,24 +81,32 @@ func (h *ChatHandler) sendQWenMessage(
strings.HasPrefix(line, "event:") || strings.HasPrefix(line, ":HTTP_STATUS/200") { strings.HasPrefix(line, "event:") || strings.HasPrefix(line, ":HTTP_STATUS/200") {
continue continue
} }
if strings.HasPrefix(line, "data:") { if strings.HasPrefix(line, "data:") {
content = line[5:] content = line[5:]
} }
// 处理代码换行
if len(content) == 0 {
content = "\n"
}
var resp qWenResp var resp qWenResp
if len(contents) == 0 { // 发送消息头
if !outPutStart {
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
outPutStart = true
continue
} else {
// 处理代码换行
content = "\n"
}
} else {
err := utils.JsonDecode(content, &resp) err := utils.JsonDecode(content, &resp)
if err != nil { if err != nil {
logger.Error("error with parse data line: ", err) logger.Error("error with parse data line: ", content)
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err)) utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
break break
} }
if resp.Message != "" {
if len(contents) == 0 { // 发送消息头 utils.ReplyMessage(ws, fmt.Sprintf("**API 返回错误:%s**", resp.Message))
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) break
}
} }
//通过比较 lastText上一次的文本和 currentText当前的文本 //通过比较 lastText上一次的文本和 currentText当前的文本
@ -135,14 +147,13 @@ func (h *ChatHandler) sendQWenMessage(
useMsg := types.Message{Role: "user", Content: prompt} useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息,如果是调用函数则不需要更新上下文 // 更新上下文消息,如果是调用函数则不需要更新上下文
if h.App.ChatConfig.EnableContext { if h.App.SysConfig.EnableContext {
chatCtx = append(chatCtx, useMsg) // 提问消息 chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息 chatCtx = append(chatCtx, message) // 回复消息
h.App.ChatContexts.Put(session.ChatId, chatCtx) h.App.ChatContexts.Put(session.ChatId, chatCtx)
} }
// 追加聊天记录 // 追加聊天记录
if h.App.ChatConfig.EnableHistory {
// for prompt // for prompt
promptToken, err := utils.CalcTokens(prompt, req.Model) promptToken, err := utils.CalcTokens(prompt, req.Model)
if err != nil { if err != nil {
@ -190,11 +201,10 @@ func (h *ChatHandler) sendQWenMessage(
// 更新用户算力 // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
}
// 保存当前会话 // 保存当前会话
var chatItem model.ChatItem var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) res = h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
if res.Error != nil { if res.Error != nil {
chatItem.ChatId = session.ChatId chatItem.ChatId = session.ChatId
chatItem.UserId = session.UserId chatItem.UserId = session.UserId

View File

@ -53,6 +53,7 @@ var Model2URL = map[string]string{
"general": "v1.1", "general": "v1.1",
"generalv2": "v2.1", "generalv2": "v2.1",
"generalv3": "v3.1", "generalv3": "v3.1",
"generalv3.5": "v3.5",
} }
// 科大讯飞消息发送实现 // 科大讯飞消息发送实现
@ -86,6 +87,7 @@ func (h *ChatHandler) sendXunFeiMessage(
} }
apiURL := strings.Replace(apiKey.ApiURL, "{version}", Model2URL[req.Model], 1) apiURL := strings.Replace(apiKey.ApiURL, "{version}", Model2URL[req.Model], 1)
logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiURL, apiKey.Value, apiKey.ProxyURL, req.Model)
wsURL, err := assembleAuthUrl(apiURL, key[1], key[2]) wsURL, err := assembleAuthUrl(apiURL, key[1], key[2])
//握手并建立websocket 连接 //握手并建立websocket 连接
conn, resp, err := d.Dial(wsURL, nil) conn, resp, err := d.Dial(wsURL, nil)
@ -173,14 +175,13 @@ func (h *ChatHandler) sendXunFeiMessage(
useMsg := types.Message{Role: "user", Content: prompt} useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息,如果是调用函数则不需要更新上下文 // 更新上下文消息,如果是调用函数则不需要更新上下文
if h.App.ChatConfig.EnableContext { if h.App.SysConfig.EnableContext {
chatCtx = append(chatCtx, useMsg) // 提问消息 chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息 chatCtx = append(chatCtx, message) // 回复消息
h.App.ChatContexts.Put(session.ChatId, chatCtx) h.App.ChatContexts.Put(session.ChatId, chatCtx)
} }
// 追加聊天记录 // 追加聊天记录
if h.App.ChatConfig.EnableHistory {
// for prompt // for prompt
promptToken, err := utils.CalcTokens(prompt, req.Model) promptToken, err := utils.CalcTokens(prompt, req.Model)
if err != nil { if err != nil {
@ -228,11 +229,10 @@ func (h *ChatHandler) sendXunFeiMessage(
// 更新用户算力 // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
}
// 保存当前会话 // 保存当前会话
var chatItem model.ChatItem var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) res = h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
if res.Error != nil { if res.Error != nil {
chatItem.ChatId = session.ChatId chatItem.ChatId = session.ChatId
chatItem.UserId = session.UserId chatItem.UserId = session.UserId
@ -260,7 +260,7 @@ func buildRequest(appid string, req types.ApiRequest) map[string]interface{} {
"parameter": map[string]interface{}{ "parameter": map[string]interface{}{
"chat": map[string]interface{}{ "chat": map[string]interface{}{
"domain": req.Model, "domain": req.Model,
"temperature": float64(req.Temperature), "temperature": req.Temperature,
"top_k": int64(6), "top_k": int64(6),
"max_tokens": int64(req.MaxTokens), "max_tokens": int64(req.MaxTokens),
"auditing": "default", "auditing": "default",

View File

@ -22,7 +22,6 @@ type FunctionHandler struct {
db *gorm.DB db *gorm.DB
config types.ChatPlusApiConfig config types.ChatPlusApiConfig
uploadManager *oss.UploaderManager uploadManager *oss.UploaderManager
proxyURL string
} }
func NewFunctionHandler(server *core.AppServer, db *gorm.DB, config *types.AppConfig, manager *oss.UploaderManager) *FunctionHandler { func NewFunctionHandler(server *core.AppServer, db *gorm.DB, config *types.AppConfig, manager *oss.UploaderManager) *FunctionHandler {
@ -33,7 +32,6 @@ func NewFunctionHandler(server *core.AppServer, db *gorm.DB, config *types.AppCo
db: db, db: db,
config: config.ApiConfig, config: config.ApiConfig,
uploadManager: manager, uploadManager: manager,
proxyURL: config.ProxyURL,
} }
} }
@ -213,47 +211,28 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
return return
} }
// get image generation api URL
var conf model.Config
var chatConfig types.ChatConfig
tx = h.db.Where("marker", "chat").First(&conf)
if tx.Error != nil {
resp.ERROR(c, "error with get chat configs:"+tx.Error.Error())
return
}
err := utils.JsonDecode(conf.Config, &chatConfig)
if err != nil {
resp.ERROR(c, "error with decode chat config: "+err.Error())
return
}
// translate prompt // translate prompt
const translatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]" const translatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]"
pt, err := utils.OpenAIRequest(h.db, fmt.Sprintf(translatePromptTemplate, params["prompt"]), h.App.Config.ProxyURL) pt, err := utils.OpenAIRequest(h.db, fmt.Sprintf(translatePromptTemplate, params["prompt"]))
if err == nil { if err == nil {
logger.Debugf("翻译绘画提示词,原文:%s译文%s", prompt, pt) logger.Debugf("翻译绘画提示词,原文:%s译文%s", prompt, pt)
prompt = pt prompt = pt
} }
imgNum := chatConfig.DallImgNum
if imgNum <= 0 {
imgNum = 1
}
var res imgRes var res imgRes
var errRes ErrRes var errRes ErrRes
var request *req.Request var request *req.Request
if apiKey.UseProxy && h.proxyURL != "" { if apiKey.ProxyURL != "" {
request = req.C().SetProxyURL(h.proxyURL).R() request = req.C().SetProxyURL(apiKey.ProxyURL).R()
} else { } else {
request = req.C().R() request = req.C().R()
} }
logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s", apiKey.Platform, apiKey.ApiURL, apiKey.Value, h.proxyURL) logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s", apiKey.Platform, apiKey.ApiURL, apiKey.Value, apiKey.ProxyURL)
r, err := request.SetHeader("Content-Type", "application/json"). r, err := request.SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value). SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(imgReq{ SetBody(imgReq{
Model: "dall-e-3", Model: "dall-e-3",
Prompt: prompt, Prompt: prompt,
N: imgNum, N: 1,
Size: "1024x1024", Size: "1024x1024",
}). }).
SetErrorResult(&errRes). SetErrorResult(&errRes).

View File

@ -35,7 +35,7 @@ func (h *PromptHandler) Rewrite(c *gin.Context) {
return return
} }
content, err := utils.OpenAIRequest(h.db, fmt.Sprintf(rewritePromptTemplate, data.Prompt), h.App.Config.ProxyURL) content, err := utils.OpenAIRequest(h.db, fmt.Sprintf(rewritePromptTemplate, data.Prompt))
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
@ -53,7 +53,7 @@ func (h *PromptHandler) Translate(c *gin.Context) {
return return
} }
content, err := utils.OpenAIRequest(h.db, fmt.Sprintf(translatePromptTemplate, data.Prompt), h.App.Config.ProxyURL) content, err := utils.OpenAIRequest(h.db, fmt.Sprintf(translatePromptTemplate, data.Prompt))
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return

View File

@ -95,13 +95,6 @@ func (h *UserHandler) Register(c *gin.Context) {
Status: true, Status: true,
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色 ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型 ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型
ChatConfig: utils.JsonEncode(types.UserChatConfig{
ApiKeys: map[types.Platform]string{
types.OpenAI: "",
types.Azure: "",
types.ChatGLM: "",
},
}),
Power: h.App.SysConfig.InitPower, Power: h.App.SysConfig.InitPower,
} }
@ -249,7 +242,6 @@ type userProfile struct {
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
Username string `json:"username"` Username string `json:"username"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
ChatConfig types.UserChatConfig `json:"chat_config"`
Power int `json:"power"` Power int `json:"power"`
ExpiredTime int64 `json:"expired_time"` ExpiredTime int64 `json:"expired_time"`
Vip bool `json:"vip"` Vip bool `json:"vip"`

View File

@ -315,7 +315,7 @@ func main() {
group.GET("list", h.List) group.GET("list", h.List)
group.POST("set", h.Set) group.POST("set", h.Set)
group.POST("sort", h.Sort) group.POST("sort", h.Sort)
group.POST("remove", h.Remove) group.GET("remove", h.Remove)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) { fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
group := s.Engine.Group("/api/payment/") group := s.Engine.Group("/api/payment/")

View File

@ -9,6 +9,6 @@ type ApiKey struct {
Value string // API Key 的值 Value string // API Key 的值
ApiURL string // 当前 KEY 的 API 地址 ApiURL string // 当前 KEY 的 API 地址
Enabled bool // 是否启用 Enabled bool // 是否启用
UseProxy bool // 是否使用代理访问 API URL ProxyURL string // 代理地址
LastUsedAt int64 // 最后使用时间 LastUsedAt int64 // 最后使用时间
} }

View File

@ -9,4 +9,7 @@ type ChatModel struct {
Enabled bool Enabled bool
Power int // 每次对话消耗算力 Power int // 每次对话消耗算力
Open bool // 是否开放模型给所有人使用 Open bool // 是否开放模型给所有人使用
MaxTokens int // 最大响应长度
MaxContext int // 最大上下文长度
Temperature float32 // 模型温度
} }

View File

@ -9,6 +9,6 @@ type ApiKey struct {
Value string `json:"value"` // API Key 的值 Value string `json:"value"` // API Key 的值
ApiURL string `json:"api_url"` ApiURL string `json:"api_url"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
UseProxy bool `json:"use_proxy"` ProxyURL string `json:"proxy_url"`
LastUsedAt int64 `json:"last_used_at"` // 最后使用时间 LastUsedAt int64 `json:"last_used_at"` // 最后使用时间
} }

View File

@ -7,6 +7,9 @@ type ChatModel struct {
Value string `json:"value"` Value string `json:"value"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
SortNum int `json:"sort_num"` SortNum int `json:"sort_num"`
Weight int `json:"weight"` Power int `json:"power"`
Open bool `json:"open"` Open bool `json:"open"`
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Temperature float32 `json:"temperature"` // 模型温度
} }

View File

@ -5,6 +5,5 @@ import "chatplus/core/types"
type Config struct { type Config struct {
Id uint `json:"id"` Id uint `json:"id"`
Key string `json:"key"` Key string `json:"key"`
ChatConfig types.ChatConfig `json:"chat_config"`
SystemConfig types.SystemConfig `json:"system_config"` SystemConfig types.SystemConfig `json:"system_config"`
} }

View File

@ -1,7 +1,5 @@
package vo package vo
import "chatplus/core/types"
type User struct { type User struct {
BaseVo BaseVo
Username string `json:"username"` Username string `json:"username"`
@ -9,7 +7,6 @@ type User struct {
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Salt string `json:"salt"` // 密码盐 Salt string `json:"salt"` // 密码盐
Power int `json:"power"` // 剩余算力 Power int `json:"power"` // 剩余算力
ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置
ChatRoles []string `json:"chat_roles"` // 聊天角色集合 ChatRoles []string `json:"chat_roles"` // 聊天角色集合
ChatModels []string `json:"chat_models"` // AI模型集合 ChatModels []string `json:"chat_models"` // AI模型集合
ExpiredTime int64 `json:"expired_time"` // 账户到期时间 ExpiredTime int64 `json:"expired_time"` // 账户到期时间

View File

@ -88,7 +88,7 @@ type apiErrRes struct {
} `json:"error"` } `json:"error"`
} }
func OpenAIRequest(db *gorm.DB, prompt string, proxy string) (string, error) { func OpenAIRequest(db *gorm.DB, prompt string) (string, error) {
var apiKey model.ApiKey var apiKey model.ApiKey
res := db.Where("platform = ?", types.OpenAI).Where("type = ?", "chat").Where("enabled = ?", true).First(&apiKey) res := db.Where("platform = ?", types.OpenAI).Where("type = ?", "chat").Where("enabled = ?", true).First(&apiKey)
if res.Error != nil { if res.Error != nil {
@ -104,8 +104,8 @@ func OpenAIRequest(db *gorm.DB, prompt string, proxy string) (string, error) {
var response apiRes var response apiRes
var errRes apiErrRes var errRes apiErrRes
client := req.C() client := req.C()
if apiKey.UseProxy && proxy != "" { if apiKey.ProxyURL != "" {
client.SetProxyURL(proxy) client.SetProxyURL(apiKey.ApiURL)
} }
r, err := client.R().SetHeader("Content-Type", "application/json"). r, err := client.R().SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value). SetHeader("Authorization", "Bearer "+apiKey.Value).

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/rand" "math/rand"
"strconv"
"time" "time"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
@ -92,3 +93,11 @@ func InterfaceToString(value interface{}) string {
} }
return JsonEncode(value) return JsonEncode(value)
} }
func Str2Float(str string) float64 {
num, err := strconv.ParseFloat(str, 64)
if err != nil {
return 0
}
return num
}

View File

@ -130,3 +130,8 @@ INSERT INTO `chatgpt_admin_permissions` VALUES (31, '权限配置', '', 2, 28, '
INSERT INTO `chatgpt_admin_permissions` VALUES (32, '角色配置', '', 3, 28, '2024-03-14 15:29:15', '2024-03-14 15:29:15'); INSERT INTO `chatgpt_admin_permissions` VALUES (32, '角色配置', '', 3, 28, '2024-03-14 15:29:15', '2024-03-14 15:29:15');
INSERT INTO `chatgpt_admin_permissions` VALUES (33, '列表', 'api_admin_sysPermission_list', 1, 31, '2024-03-14 15:29:52', '2024-03-14 15:29:52'); INSERT INTO `chatgpt_admin_permissions` VALUES (33, '列表', 'api_admin_sysPermission_list', 1, 31, '2024-03-14 15:29:52', '2024-03-14 15:29:52');
INSERT INTO `chatgpt_admin_permissions` VALUES (34, '列表', 'api_admin_sysRole_list', 1, 32, '2024-03-14 15:30:21', '2024-03-14 15:30:21'); INSERT INTO `chatgpt_admin_permissions` VALUES (34, '列表', 'api_admin_sysRole_list', 1, 32, '2024-03-14 15:30:21', '2024-03-14 15:30:21');
ALTER TABLE `chatgpt_api_keys` CHANGE `use_proxy` `proxy_url` VARCHAR(100) NULL DEFAULT NULL COMMENT '代理地址';
-- 重置 proxy_url
UPDATE chatgpt_api_keys set proxy_url=''

View File

@ -1,4 +1,4 @@
lockfileVersion: '6.0' lockfileVersion: '6.1'
settings: settings:
autoInstallPeers: true autoInstallPeers: true
@ -127,103 +127,6 @@ importers:
specifier: ^1.8.27 specifier: ^1.8.27
version: 1.8.27(typescript@5.3.3) version: 1.8.27(typescript@5.3.3)
projects/mobile:
dependencies:
'@element-plus/icons-vue':
specifier: ^2.1.0
version: 2.3.1(vue@3.4.21)
axios:
specifier: ^0.27.2
version: 0.27.2
clipboard:
specifier: ^2.0.11
version: 2.0.11
compressorjs:
specifier: ^1.2.1
version: 1.2.1
core-js:
specifier: ^3.8.3
version: 3.36.0
element-plus:
specifier: ^2.3.0
version: 2.6.1(vue@3.4.21)
good-storage:
specifier: ^1.1.1
version: 1.1.1
highlight.js:
specifier: ^11.7.0
version: 11.9.0
json-bigint:
specifier: ^1.0.0
version: 1.0.0
lodash:
specifier: ^4.17.21
version: 4.17.21
markdown-it:
specifier: ^13.0.1
version: 13.0.2
markdown-it-latex2img:
specifier: ^0.0.6
version: 0.0.6
markdown-it-mathjax:
specifier: ^2.0.0
version: 2.0.0
md-editor-v3:
specifier: ^2.2.1
version: 2.11.3(vue@3.4.21)
pinia:
specifier: ^2.1.4
version: 2.1.7(typescript@5.3.3)(vue@3.4.21)
qrcode:
specifier: ^1.5.3
version: 1.5.3
qs:
specifier: ^6.11.1
version: 6.12.0
sortablejs:
specifier: ^1.15.0
version: 1.15.2
v3-waterfall:
specifier: ^1.2.1
version: 1.3.3
vant:
specifier: ^4.5.0
version: 4.8.5(vue@3.4.21)
vue:
specifier: ^3.2.13
version: 3.4.21(typescript@5.3.3)
vue-router:
specifier: ^4.0.15
version: 4.3.0(vue@3.4.21)
devDependencies:
'@babel/core':
specifier: 7.18.6
version: 7.18.6
'@babel/eslint-parser':
specifier: ^7.12.16
version: 7.23.10(@babel/core@7.18.6)(eslint@7.32.0)
'@vue/cli-plugin-babel':
specifier: ~5.0.0
version: 5.0.8(@vue/cli-service@5.0.8)(core-js@3.36.0)(vue@3.4.21)
'@vue/cli-plugin-eslint':
specifier: ~5.0.0
version: 5.0.8(@vue/cli-service@5.0.8)(eslint@7.32.0)
'@vue/cli-service':
specifier: ~5.0.0
version: 5.0.8(lodash@4.17.21)(prettier@3.2.5)(stylus-loader@7.1.3)(vue@3.4.21)
eslint:
specifier: ^7.32.0
version: 7.32.0
eslint-plugin-vue:
specifier: ^8.0.3
version: 8.7.1(eslint@7.32.0)
stylus:
specifier: ^0.58.1
version: 0.58.1
stylus-loader:
specifier: ^7.0.0
version: 7.1.3(stylus@0.58.1)(webpack@5.90.3)
projects/web: projects/web:
dependencies: dependencies:
'@element-plus/icons-vue': '@element-plus/icons-vue':

View File

@ -1,7 +0,0 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint"
]
}

View File

@ -0,0 +1,49 @@
<script lang="ts" setup>
import { computed } from "vue";
import { Message } from "@arco-design/web-vue";
import type { UploadInstance, FileItem } from "@arco-design/web-vue";
import { uploadUrl } from "@/http/config";
defineProps({
modelValue: String,
placeholder: String,
});
const emits = defineEmits(["update:modelValue"]);
const uploadProps = computed<UploadInstance["$props"]>(() => {
const TOKEN = JSON.parse(localStorage.getItem(__AUTH_KEY))?.token;
return {
accept: "image/*",
action: uploadUrl,
name: "file",
headers: { [__AUTH_KEY]: TOKEN },
showFileList: false,
};
});
const handleChange = (_, file: FileItem) => {
if (file?.response) {
emits("update:modelValue", file?.response?.data?.url);
Message.success("上传成功");
}
};
</script>
<template>
<a-upload v-bind="uploadProps" style="width: 100%" @change="handleChange">
<template #upload-button>
<a-input-group style="width: 100%">
<a-input
:model-value="modelValue"
:placeholder="placeholder"
readonly
allow-clear
@clear.stop="emits('update:modelValue')"
/>
<a-button type="primary" style="width: 100px">
<icon-cloud />
</a-button>
</a-input-group>
</template>
</a-upload>
</template>

View File

@ -1,18 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, onActivated } from "vue"; import { computed, onActivated } from "vue";
import useAsyncTable from "./useAsyncTable"; import useAsyncTable from "./useAsyncTable";
import { useTableScroll } from "@/components/SearchTable/utils"; import { useTableScroll } from "@/components/SearchTable/utils";
import { Message } from "@arco-design/web-vue"; import { Message, type TableColumnData } from "@arco-design/web-vue";
import type { TableRequest, TableOriginalProps } from "./useAsyncTable"; import type { TableRequest, TableOriginalProps } from "./useAsyncTable";
interface SimpleTable extends /* @vue-ignore */ TableOriginalProps { interface SimpleTable extends /* @vue-ignore */ TableOriginalProps {
request: TableRequest<Record<string, unknown>>; request: TableRequest<Record<string, unknown>>;
params?: Record<string, unknown>; params?: Record<string, unknown>;
columns?: TableOriginalProps["columns"]; columns?: TableColumnData[];
} }
const props = defineProps<SimpleTable>(); const props = defineProps<SimpleTable>();
const tableContainerRef = ref<HTMLElement>();
// //
const [tableConfig, getList] = useAsyncTable(props.request, props.params); const [tableConfig, getList] = useAsyncTable(props.request, props.params);
@ -45,7 +44,7 @@ onActivated(handleSearch);
...$attrs, ...$attrs,
...tableConfig, ...tableConfig,
...props, ...props,
scroll: useTableScroll(_columns || [], tableContainerRef as HTMLElement), scroll: useTableScroll(_columns || []),
columns: _columns, columns: _columns,
}" }"
> >

View File

@ -2,11 +2,11 @@
import { getList, save, deleting, setStatus } from "./api"; import { getList, save, deleting, setStatus } from "./api";
import ApiKeyForm from "./ApiKeyForm.vue"; import ApiKeyForm from "./ApiKeyForm.vue";
import useCustomFormPopup from "@/composables/useCustomFormPopup"; import useCustomFormPopup from "@/composables/useCustomFormPopup";
import { Message } from "@arco-design/web-vue"; import { Message, type TableColumnData } from "@arco-design/web-vue";
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue"; import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
import { dateFormat } from "@chatgpt-plus/packages/utils"; import { dateFormat } from "@chatgpt-plus/packages/utils";
// table // table
const columns = [ const columns: TableColumnData[] = [
{ {
title: "所属平台", title: "所属平台",
dataIndex: "platform", dataIndex: "platform",
@ -20,6 +20,11 @@ const columns = [
dataIndex: "value", dataIndex: "value",
slotName: "value", slotName: "value",
}, },
{
title: "API URL",
dataIndex: "api_url",
slotName: "value",
},
{ {
title: "用途", title: "用途",
dataIndex: "type", dataIndex: "type",
@ -85,17 +90,26 @@ const handleStatusChange = ({ filed, value, record, reload }) => {
</script> </script>
<template> <template>
<SimpleTable :columns="columns" :request="getList"> <SimpleTable :columns="columns" :request="getList">
<template #header="{ reload }">
<a-space>
<a-button @click="popup({ reload })" size="small" type="primary"
><template #icon> <icon-plus /> </template>新增
</a-button>
<a-button type="primary" status="success" href="https://gpt.bemore.lol" target="_blank">
<template #icon>
<icon-link />
</template>
购买API-KEY
</a-button>
</a-space>
</template>
<template #action="{ record, reload }"> <template #action="{ record, reload }">
<a-link @click="popup({ record, reload })">编辑</a-link> <a-link @click="popup({ record, reload })">编辑</a-link>
<a-popconfirm content="确定删除?" @ok="handleDelete(record, reload)"> <a-popconfirm content="确定删除?" @ok="handleDelete(record, reload)">
<a-link status="danger">删除</a-link> <a-link status="danger">删除</a-link>
</a-popconfirm> </a-popconfirm>
</template> </template>
<template #header="{ reload }">
<a-button @click="popup({ reload })" size="small" type="primary"
><template #icon> <icon-plus /> </template>新增
</a-button>
</template>
<template #value="{ record, column }"> <template #value="{ record, column }">
<a-typography-text copyable ellipsis style="margin: 0"> <a-typography-text copyable ellipsis style="margin: 0">
{{ record[column.dataIndex] }} {{ record[column.dataIndex] }}

View File

@ -1,9 +1,8 @@
<template> <template>
<a-alert type="warning"> <a-alert type="warning">
<div class="warning"> <div class="warning">
{{ <div>注意如果是百度文心一言平台API-KEY APIKey|SecretKey中间用竖线|连接</div>
`注意如果是百度文心一言平台API-KEY 为 APIKey|SecretKey中间用竖线|)连接\n注意如果是讯飞星火大模型API-KEY 为 AppId|APIKey|APISecret中间用竖线|)连接` <div>注意如果是讯飞星火大模型API-KEY AppId|APIKey|APISecret中间用竖线|连接</div>
}}
</div> </div>
</a-alert> </a-alert>
<a-form <a-form
@ -18,7 +17,12 @@
:rules="[{ required: true, message: '请输入所属平台' }]" :rules="[{ required: true, message: '请输入所属平台' }]"
:validate-trigger="['change', 'input']" :validate-trigger="['change', 'input']"
> >
<a-input v-model="form.platform" placeholder="请输入所属平台" /> <a-select
v-model="form.platform"
placeholder="请输入所属平台"
:options="platformOptions"
@change="handlePlatformChange"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="name" field="name"
@ -35,7 +39,12 @@
:rules="[{ required: true, message: '请输入用途' }]" :rules="[{ required: true, message: '请输入用途' }]"
:validate-trigger="['change', 'input']" :validate-trigger="['change', 'input']"
> >
<a-select v-model="form.type" placeholder="请输入用途" :options="typeOPtions"> </a-select> <a-select
v-model="form.type"
placeholder="请输入用途"
:options="typeOptions"
@change="handlePlatformChange"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="value" field="value"
@ -55,6 +64,7 @@
</a-form-item> </a-form-item>
<a-form-item field="use_proxy" label="使用代理"> <a-form-item field="use_proxy" label="使用代理">
<a-space>
<a-switch v-model="form.use_proxy" /> <a-switch v-model="form.use_proxy" />
<a-tooltip <a-tooltip
content="是否使用代理访问 API URLOpenAI 官方API需要开启代理访问" content="是否使用代理访问 API URLOpenAI 官方API需要开启代理访问"
@ -62,6 +72,7 @@
> >
<icon-info-circle-fill /> <icon-info-circle-fill />
</a-tooltip> </a-tooltip>
</a-space>
</a-form-item> </a-form-item>
<a-form-item field="enable" label="启用状态"> <a-form-item field="enable" label="启用状态">
<a-switch v-model="form.enable" /> <a-switch v-model="form.enable" />
@ -86,7 +97,7 @@ defineExpose({
form, form,
}); });
const typeOPtions = [ const typeOptions = [
{ {
label: "聊天", label: "聊天",
value: "chart", value: "chart",
@ -96,6 +107,48 @@ const typeOPtions = [
value: "img", value: "img",
}, },
]; ];
const platformOptions = [
{
label: "【OpenAI】ChatGPT",
value: "OpenAI",
api_url: "https://gpt.bemore.lol/v1/chat/completions",
img_url: "https://gpt.bemore.lol/v1/images/generations",
},
{
label: "【讯飞】星火大模型",
value: "XunFei",
api_url: "wss://spark-api.xf-yun.com/{version}/chat",
},
{
label: "【清华智普】ChatGLM",
value: "ChatGLM",
api_url: "https://open.bigmodel.cn/api/paas/v3/model-api/{model}/sse-invoke",
},
{
label: "【百度】文心一言",
value: "Baidu",
api_url: "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}",
},
{
label: "【微软】Azure",
value: "Azure",
api_url:
"https://chat-bot-api.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-05-15",
},
{
label: "【阿里】千义通问",
value: "QWen",
api_url: "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation",
},
];
const handlePlatformChange = () => {
const obj = platformOptions.find((item) => item.value === form.value.platform);
if (obj) {
form.value.api_url = form.value.type === "img" ? obj.img_url : obj.api_url;
}
};
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.content-title { .content-title {

View File

@ -1,13 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getList, save, deleting, setStatus } from "./api"; import { getList, save, deleting, setStatus } from "./api";
import { ref } from "vue";
import ChatModelForm from "./ChatModelForm.vue"; import ChatModelForm from "./ChatModelForm.vue";
import useCustomFormPopup from "@/composables/useCustomFormPopup"; import useCustomFormPopup from "@/composables/useCustomFormPopup";
import { Message } from "@arco-design/web-vue"; import { Message, type TableColumnData } from "@arco-design/web-vue";
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue"; import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
import { dateFormat } from "@chatgpt-plus/packages/utils"; import { dateFormat } from "@chatgpt-plus/packages/utils";
// table // table
const columns = [ const columns: TableColumnData[] = [
{ {
title: "所属平台", title: "所属平台",
dataIndex: "platform", dataIndex: "platform",
@ -49,20 +48,9 @@ const columns = [
}, },
]; ];
//
const tableData = ref([]);
const getData = () => {
getList().then(({ code, data }) => {
if (code === 0) {
tableData.value = data;
}
});
};
getData();
// //
const popup = useCustomFormPopup(ChatModelForm, save, { const popup = useCustomFormPopup(ChatModelForm, save, {
popupProps: (arg) => ({ title: arg[0].record ? "编辑ApiKey" : "新增ApiKey" }), popupProps: (arg) => ({ title: arg[0].record ? "编辑模型" : "新增模型" }),
}); });
// //

View File

@ -6,7 +6,7 @@
:rules="[{ required: true, message: '请输入所属平台' }]" :rules="[{ required: true, message: '请输入所属平台' }]"
:validate-trigger="['change', 'input']" :validate-trigger="['change', 'input']"
> >
<a-input v-model="form.platform" placeholder="请输入所属平台" /> <a-select v-model="form.platform" placeholder="请输入所属平台" :options="platformOptions" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="name" field="name"
@ -33,10 +33,12 @@
:validate-trigger="['change', 'input']" :validate-trigger="['change', 'input']"
showable showable
> >
<a-space>
<a-input-number v-model="form.weight" placeholder="请输入对话权重" /> <a-input-number v-model="form.weight" placeholder="请输入对话权重" />
<a-tooltip content="对话权重,每次对话扣减多少次对话额度" position="right"> <a-tooltip content="对话权重,每次对话扣减多少次对话额度" position="right">
<icon-info-circle-fill /> <icon-info-circle-fill />
</a-tooltip> </a-tooltip>
</a-space>
</a-form-item> </a-form-item>
<a-form-item field="open" label="开放状态代理"> <a-form-item field="open" label="开放状态代理">
@ -65,15 +67,13 @@ defineExpose({
form, form,
}); });
const typeOPtions = [ const platformOptions = [
{ { label: "【OpenAI】ChatGPT", value: "OpenAI" },
label: "聊天", { label: "【讯飞】星火大模型", value: "XunFei" },
value: "chart", { label: "【清华智普】ChatGLM", value: "ChatGLM" },
}, { label: "【百度】文心一言", value: "Baidu" },
{ { label: "【微软】Azure", value: "Azure" },
label: "绘图", { label: "【阿里】通义千问", value: "QWen" },
value: "img",
},
]; ];
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -8,7 +8,7 @@ import app from "@/main";
import { getList, message, remove } from "./api"; import { getList, message, remove } from "./api";
import ChatsLogs from "./ChatsLogs.vue"; import ChatsLogs from "./ChatsLogs.vue";
const columns: SearchTableColumns[] = [ const chatColumns: SearchTableColumns[] = [
{ {
dataIndex: "user_id", dataIndex: "user_id",
title: "账户ID", title: "账户ID",
@ -42,10 +42,48 @@ const columns: SearchTableColumns[] = [
dataIndex: "token", dataIndex: "token",
title: "消耗算力", title: "消耗算力",
}, },
{
dataIndex: "created_at",
title: "创建时间",
search: {
valueType: "range",
},
render: ({ record }) => dateFormat(record.created_at),
},
{
title: "操作",
fixed: "right",
slotName: "actions",
},
];
const messageColumns: SearchTableColumns[] = [
{
dataIndex: "user_id",
title: "账户ID",
search: {
valueType: "input",
},
},
{ {
dataIndex: "username", dataIndex: "username",
title: "账户", title: "账户",
}, },
{
dataIndex: "title",
title: "标题",
search: {
valueType: "input",
},
},
{
dataIndex: "msg_num",
title: "消息数量",
},
{
dataIndex: "token",
title: "消耗算力",
},
{ {
dataIndex: "created_at", dataIndex: "created_at",
title: "创建时间", title: "创建时间",
@ -62,8 +100,8 @@ const columns: SearchTableColumns[] = [
]; ];
const tabsList = [ const tabsList = [
{ key: "1", title: "对话列表", api: getList, columns }, { key: "1", title: "对话列表", api: getList, columns: chatColumns },
{ key: "2", title: "消息记录", api: message, columns }, { key: "2", title: "消息记录", api: message, columns: messageColumns },
]; ];
const activeKey = ref(tabsList[0].key); const activeKey = ref(tabsList[0].key);
@ -101,7 +139,7 @@ const handleCheck = (record) => {
content="是否删除?" content="是否删除?"
position="left" position="left"
type="warning" type="warning"
:on-before-ok="() => handleRemove(record.id, reload)" :on-before-ok="() => handleRemove(record.chat_id, reload)"
> >
<a-link status="danger">删除</a-link> <a-link status="danger">删除</a-link>
</a-popconfirm> </a-popconfirm>

View File

@ -8,10 +8,10 @@ export const getList = (data?: Record<string, unknown>) => {
}) })
} }
export const remove = (data) => { export const remove = (params) => {
return http({ return http({
url: "/api/admin/order/remove", url: "/api/admin/order/remove",
method: "post", method: "get",
data params
}) })
} }

View File

@ -1,13 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getList, save, deleting, setStatus } from "./api"; import { getList, save, deleting, setStatus } from "./api";
import { ref } from "vue";
import ProductForm from "./ProductForm.vue"; import ProductForm from "./ProductForm.vue";
import useCustomFormPopup from "@/composables/useCustomFormPopup"; import useCustomFormPopup from "@/composables/useCustomFormPopup";
import { Message } from "@arco-design/web-vue"; import { Message, type TableColumnData } from "@arco-design/web-vue";
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue"; import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
import { dateFormat } from "@chatgpt-plus/packages/utils"; import { dateFormat } from "@chatgpt-plus/packages/utils";
// table // table
const columns = [ const columns: TableColumnData[] = [
{ {
title: "产品名称", title: "产品名称",
dataIndex: "name", dataIndex: "name",
@ -59,17 +58,6 @@ const columns = [
}, },
]; ];
//
const tableData = ref([]);
const getData = () => {
getList().then(({ code, data }) => {
if (code === 0) {
tableData.value = data;
}
});
};
getData();
// //
const popup = useCustomFormPopup(ProductForm, save, { const popup = useCustomFormPopup(ProductForm, save, {
popupProps: (arg) => ({ title: arg[0].record ? "编辑产品" : "新增产品" }), popupProps: (arg) => ({ title: arg[0].record ? "编辑产品" : "新增产品" }),

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getList, save, deleting, setStatus } from "./api"; import { getList, save, deleting, setStatus } from "./api";
import { reactive, ref } from "vue"; import { reactive } from "vue";
import RoleForm from "./RoleForm.vue"; import RoleForm from "./RoleForm.vue";
import useCustomFormPopup from "@/composables/useCustomFormPopup"; import useCustomFormPopup from "@/composables/useCustomFormPopup";
import { Message } from "@arco-design/web-vue"; import { Message } from "@arco-design/web-vue";
@ -40,17 +40,6 @@ const expandable = reactive({
width: 50, width: 50,
}); });
//
const tableData = ref([]);
const getData = () => {
getList().then(({ code, data }) => {
if (code === 0) {
tableData.value = data;
}
});
};
getData();
//table //table
const expandColumns = [ const expandColumns = [
{ {

View File

@ -23,7 +23,7 @@
:rules="[{ required: true, message: '请输入角色图标' }]" :rules="[{ required: true, message: '请输入角色图标' }]"
:validate-trigger="['change', 'input']" :validate-trigger="['change', 'input']"
> >
<a-input v-model="form.icon" placeholder="请输入角色图标" /> <CustomUploader v-model="form.icon" placeholder="请输入角色图标" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="hello_msg" field="hello_msg"
@ -67,6 +67,7 @@
<script setup> <script setup>
import { ref, defineExpose, defineProps } from "vue"; import { ref, defineExpose, defineProps } from "vue";
import CustomUploader from "@/components/CustomUploader.vue";
const props = defineProps({ const props = defineProps({
data: {}, data: {},
}); });

View File

@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from "vue"; import { onMounted } from "vue";
import { Message } from "@arco-design/web-vue"; import { Message } from "@arco-design/web-vue";
import CustomUploader from "@/components/CustomUploader.vue";
import useSubmit from "@/composables/useSubmit"; import useSubmit from "@/composables/useSubmit";
import useRequest from "@/composables/useRequest"; import useRequest from "@/composables/useRequest";
import { getConfig, modelList, save } from "./api"; import { getConfig, modelList, save } from "./api";
import SystemUploader from "./SystemUploader.vue";
const { formRef, formData: system, handleSubmit, submitting } = useSubmit({}); const { formRef, formData: system, handleSubmit, submitting } = useSubmit({});
@ -46,45 +46,32 @@ onMounted(async () => {
<a-form-item label="控制台标题" field="admin_title"> <a-form-item label="控制台标题" field="admin_title">
<a-input v-model="system['admin_title']" /> <a-input v-model="system['admin_title']" />
</a-form-item> </a-form-item>
<a-form-item label="网站Logo" field="logo"> <a-form-item label="注册赠送对话次数" field="user_init_calls">
<SystemUploader v-model="system['logo']" placeholder="推荐图片宽高比为 1:1"/> <a-input v-model.number="system['init_chat_calls']" placeholder="新用户注册赠送对话次数" />
</a-form-item> </a-form-item>
<a-form-item label="注册赠送算力" field="init_power"> <a-form-item label="注册赠送绘图次数" field="init_img_calls">
<a-input-number v-model="system['init_power']" placeholder="新用户注册赠送初始算力"/> <a-input v-model.number="system['init_img_calls']" placeholder="新用户注册赠送绘图次数" />
</a-form-item> </a-form-item>
<a-form-item label="邀请用户赠送算力" field="invite_power"> <a-form-item label="邀请赠送对话次数" field="invite_chat_calls">
<a-input-number <a-input
v-model="system['invite_power']" v-model.number="system['invite_chat_calls']"
placeholder="邀请新用户注册赠送算力" placeholder="邀请新用户注册赠送对话次数"
/> />
</a-form-item> </a-form-item>
<a-form-item label="邀请赠送绘图次数" field="invite_img_calls">
<a-form-item label="VIP每月赠送算力" field="vip_month_power"> <a-input
<a-input-number v-model="system['vip_month_power']" placeholder="VIP用户每月赠送算力"/> v-model.number="system['invite_img_calls']"
placeholder="邀请新用户注册赠送绘图次数"
/>
</a-form-item> </a-form-item>
<a-form-item label="MJ绘画价格" field="mj_power"> <a-form-item label="VIP每月对话次数" field="vip_month_calls">
<a-space> <a-input v-model.number="system['vip_month_calls']" placeholder="VIP用户每月赠送对话次数" />
<a-input-number v-model="system['mj_power']" placeholder=""/>
<a-tooltip content="MidJourney 单次绘图消耗多少单位算力" position="right">
<icon-info-circle-fill size="18"/>
</a-tooltip>
</a-space>
</a-form-item> </a-form-item>
<a-form-item label="SD绘画价格" field="sd_power"> <a-form-item label="VIP每月绘图次数" field="vip_month_img_calls">
<a-space> <a-input
<a-input-number v-model="system['sd_power']" placeholder=""/> v-model.number="system['vip_month_img_calls']"
<a-tooltip content="Stable-Diffusion 单次绘图消耗多少单位算力" position="right"> placeholder="VIP用户每月赠送绘图次数"
<icon-info-circle-fill size="18"/> />
</a-tooltip>
</a-space>
</a-form-item>
<a-form-item label="DALL绘画价格" field="dall_power">
<a-space>
<a-input-number v-model="system['dall_power']" placeholder=""/>
<a-tooltip content="DALL-E-3 单次绘图消耗多少单位算力" position="right">
<icon-info-circle-fill size="18"/>
</a-tooltip>
</a-space>
</a-form-item> </a-form-item>
<a-form-item label="开放注册" field="enabled_register"> <a-form-item label="开放注册" field="enabled_register">
<a-space> <a-space>
@ -103,26 +90,29 @@ onMounted(async () => {
<a-form-item label="启用众筹功能" field="enabled_reward"> <a-form-item label="启用众筹功能" field="enabled_reward">
<a-space> <a-space>
<a-switch v-model="system['enabled_reward']" /> <a-switch v-model="system['enabled_reward']" />
<a-tooltip content="开启众筹功能允许用户使用个人微信收款码进行收款" position="right"> <a-tooltip content="如果关闭次功能将不在用户菜单显示众筹二维码" position="right">
<icon-info-circle-fill size="18" /> <icon-info-circle-fill size="18" />
</a-tooltip> </a-tooltip>
</a-space> </a-space>
</a-form-item> </a-form-item>
<template v-if="system['enabled_reward']"> <template v-if="system['enabled_reward']">
<a-form-item label="众筹算力单价" field="power_price"> <a-form-item label="单次对话价格" field="chat_call_price">
<a-input-number v-model="system['power_price']" placeholder="单位算力价格如1块10个单位算力那便填写 0.1"/> <a-input v-model="system['chat_call_price']" placeholder="众筹金额跟对话次数的兑换比例" />
</a-form-item>
<a-form-item label="单次绘图价格" field="img_call_price">
<a-input v-model="system['img_call_price']" placeholder="众筹金额跟绘图次数的兑换比例" />
</a-form-item> </a-form-item>
<a-form-item label="收款二维码" field="reward_img"> <a-form-item label="收款二维码" field="reward_img">
<SystemUploader v-model="system['reward_img']" placeholder="众筹收款二维码地址"/> <CustomUploader v-model="system['reward_img']" placeholder="众筹收款二维码地址" />
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label="微信客服二维码" field="wechat_card_url"> <a-form-item label="微信客服二维码" field="wechat_card_url">
<SystemUploader v-model="system['wechat_card_url']" placeholder="微信客服二维码"/> <CustomUploader v-model="system['wechat_card_url']" placeholder="微信客服二维码" />
</a-form-item> </a-form-item>
<a-form-item label="订单超时时间" field="order_pay_timeout"> <a-form-item label="订单超时时间" field="order_pay_timeout">
<a-space style="width: 100%"> <a-space style="width: 100%">
<a-input-number <a-input
v-model="system['order_pay_timeout']" v-model.number="system['order_pay_timeout']"
placeholder="单位:秒" placeholder="单位:秒"
style="width: 100%" style="width: 100%"
/> />

View File

@ -8,6 +8,11 @@ import { dateFormat } from "@chatgpt-plus/packages/utils";
import UserPassword from "./UserPassword.vue"; import UserPassword from "./UserPassword.vue";
import useCustomFormPopup from "@/composables/useCustomFormPopup"; import useCustomFormPopup from "@/composables/useCustomFormPopup";
const columns: SearchTableColumns[] = [ const columns: SearchTableColumns[] = [
{
title: "用户头像",
dataIndex: "avatar",
slotName: "avatar",
},
{ {
title: "账号", title: "账号",
dataIndex: "username", dataIndex: "username",
@ -16,17 +21,9 @@ const columns: SearchTableColumns[] = [
}, },
}, },
{ {
title: "剩余对话次数", title: "剩余算力",
dataIndex: "calls", dataIndex: "calls",
}, },
{
title: "剩余绘图次数",
dataIndex: "img_calls",
},
{
title: "累计消耗tokens",
dataIndex: "total_tokens",
},
{ {
title: "状态", title: "状态",
dataIndex: "status", dataIndex: "status",
@ -38,9 +35,7 @@ const columns: SearchTableColumns[] = [
title: "过期时间", title: "过期时间",
dataIndex: "expired_time", dataIndex: "expired_time",
width: 180, width: 180,
render: ({ record }) => { slotName: "expired_time",
return dateFormat(record.expired_time);
},
}, },
{ {
title: "注册时间", title: "注册时间",
@ -76,6 +71,15 @@ const handleDelete = async ({ id }: { id: string }, reload) => {
</script> </script>
<template> <template>
<SearchTable :request="getList" :columns="columns"> <SearchTable :request="getList" :columns="columns">
<template #avatar="{ record }">
<a-avatar>
<a-image :src="record.avatar" style="border-radius: 50%" />
</a-avatar>
</template>
<template #expired_time="{ record }">
<a-tag v-if="record.expired_time === 0" color="blue">长期有效</a-tag>
<template v-else>{{ dateFormat(record.expired_time) }}</template>
</template>
<template #actions="{ record, reload }"> <template #actions="{ record, reload }">
<a-link @click="editModal({ record, reload })">编辑</a-link> <a-link @click="editModal({ record, reload })">编辑</a-link>
<a-popconfirm content="确定删除?" @ok="handleDelete(record, reload)"> <a-popconfirm content="确定删除?" @ok="handleDelete(record, reload)">

View File

@ -20,17 +20,10 @@
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="calls" field="calls"
label="对话次数" label="用户算力"
:rules="[{ required: true, message: '请输入对话次数' }]" :rules="[{ required: true, message: '请输入用户算力' }]"
> >
<a-input-number v-model="form.calls" placeholder="请输入对话次数" /> <a-input-number v-model="form.calls" placeholder="请输入用户算力" />
</a-form-item>
<a-form-item
field="img_calls"
label="绘图次数"
:rules="[{ required: true, message: '请输入绘图次数' }]"
>
<a-input-number v-model="form.img_calls" placeholder="请输入绘图次数" />
</a-form-item> </a-form-item>
<a-form-item field="expired_time" label="有效期"> <a-form-item field="expired_time" label="有效期">
<a-date-picker v-model="form.expired_time" placeholder="请选择有效期" /> <a-date-picker v-model="form.expired_time" placeholder="请选择有效期" />

View File

@ -6,107 +6,88 @@ body,
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
body { body {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
.admin-home a { .admin-home a {
text-decoration: none; text-decoration: none;
} }
.admin-home .content-box { .admin-home .content-box {
position: absolute; position: absolute;
left: 250px; left: 250px;
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
/*padding-bottom: 30px;*/ padding-bottom: 30px;
-webkit-transition: left 0.3s ease-in-out; -webkit-transition: left 0.3s ease-in-out;
transition: left 0.3s ease-in-out; transition: left 0.3s ease-in-out;
background: #f0f0f0; background: #f0f0f0;
overflow-y: scroll;
} }
.admin-home .content-box .content { .admin-home .content-box .content {
width: auto; width: auto;
height: 100%;
padding: 10px; padding: 10px;
overflow-y: scroll;
box-sizing: border-box; box-sizing: border-box;
/*BaseForm*/ /*BaseForm*/
} }
.admin-home .content-box .content .container { .admin-home .content-box .content .container {
padding: 30px; padding: 30px;
background: #fff; background: #fff;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 5px; border-radius: 5px;
} }
.admin-home .content-box .content .container .handle-box { .admin-home .content-box .content .container .handle-box {
margin-bottom: 20px; margin-bottom: 20px;
} }
.admin-home .content-box .content .crumbs { .admin-home .content-box .content .crumbs {
margin: 10px 0; margin: 10px 0;
} }
.admin-home .content-box .content .el-table th { .admin-home .content-box .content .el-table th {
background-color: #f5f7fa !important; background-color: #f5f7fa !important;
} }
.admin-home .content-box .content .pagination { .admin-home .content-box .content .pagination {
margin: 20px 0; margin: 20px 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
} }
.admin-home .content-box .content .plugins-tips { .admin-home .content-box .content .plugins-tips {
padding: 20px 10px; padding: 20px 10px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.admin-home .content-box .content .el-button + .el-tooltip { .admin-home .content-box .content .el-button + .el-tooltip {
margin-left: 10px; margin-left: 10px;
} }
.admin-home .content-box .content .el-table tr:hover { .admin-home .content-box .content .el-table tr:hover {
background: #f6faff; background: #f6faff;
} }
.admin-home .content-box .content .mgb20 { .admin-home .content-box .content .mgb20 {
margin-bottom: 20px; margin-bottom: 20px;
} }
.admin-home .content-box .content .move-enter-active, .admin-home .content-box .content .move-enter-active,
.admin-home .content-box .content .move-leave-active { .admin-home .content-box .content .move-leave-active {
transition: opacity 0.1s ease; transition: opacity 0.1s ease;
} }
.admin-home .content-box .content .move-enter-from, .admin-home .content-box .content .move-enter-from,
.admin-home .content-box .content .move-leave-to { .admin-home .content-box .content .move-leave-to {
opacity: 0; opacity: 0;
} }
.admin-home .content-box .content .form-box { .admin-home .content-box .content .form-box {
width: 600px; width: 600px;
} }
.admin-home .content-box .content .form-box .line { .admin-home .content-box .content .form-box .line {
text-align: center; text-align: center;
} }
.admin-home .content-box .content .el-time-panel__content::after, .admin-home .content-box .content .el-time-panel__content::after,
.admin-home .content-box .content .el-time-panel__content::before { .admin-home .content-box .content .el-time-panel__content::before {
margin-top: -7px; margin-top: -7px;
} }
.admin-home .content-box .content .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) { .admin-home .content-box .content .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) {
padding-bottom: 0; padding-bottom: 0;
} }
.admin-home .content-box .content [class*=" el-icon-"], .admin-home .content-box .content [class*=" el-icon-"],
.admin-home .content-box .content [class^=el-icon-] { .admin-home .content-box .content [class^=el-icon-] {
speak: none; speak: none;
@ -120,7 +101,6 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.admin-home .content-box .content .el-sub-menu [class^=el-icon-] { .admin-home .content-box .content .el-sub-menu [class^=el-icon-] {
vertical-align: middle; vertical-align: middle;
margin-right: 5px; margin-right: 5px;
@ -128,11 +108,9 @@ body {
text-align: center; text-align: center;
font-size: 18px; font-size: 18px;
} }
.admin-home .content-box .content [hidden] { .admin-home .content-box .content [hidden] {
display: none !important; display: none !important;
} }
.admin-home .content-collapse { .admin-home .content-collapse {
left: 65px; left: 65px;
} }

View File

@ -1,5 +1,5 @@
VUE_APP_API_HOST=http://172.22.11.2:5678 VUE_APP_API_HOST=http://localhost:5678
VUE_APP_WS_HOST=ws://172.22.11.2:5678 VUE_APP_WS_HOST=ws://localhost:5678
VUE_APP_USER=18575670125 VUE_APP_USER=18575670125
VUE_APP_PASS=12345678 VUE_APP_PASS=12345678
VUE_APP_ADMIN_USER=admin VUE_APP_ADMIN_USER=admin

View File

@ -2,9 +2,10 @@
display: flex; display: flex;
width: 100%; width: 100%;
} }
.el-form-item__content .tip-input .input { .el-form-item__content .tip-input .el-input {
width: 100%; width: 50%;
} }
.el-form-item__content .tip-input .info { .el-form-item__content .tip-input .info {
margin-left: 6px; margin-left: 6px;
margin-top: 2px;
} }

View File

@ -3,12 +3,13 @@
display flex display flex
width 100% width 100%
.input { .el-input {
width 100% width 50%
} }
.info { .info {
margin-left 6px margin-left 6px
margin-top 2px
} }
} }

View File

@ -342,14 +342,14 @@ onMounted(() => {
}) })
// //
httpGet("/api/admin/config/get?key=system").then(res => { httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title title.value = res.data.title
}).catch(e => { }).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message) ElMessage.error("获取系统配置失败:" + e.message)
}) })
// //
httpGet("/api/admin/config/get?key=notice").then(res => { httpGet("/api/config/get?key=notice").then(res => {
notice.value = md.render(res.data['content']) notice.value = md.render(res.data['content'])
const oldNotice = localStorage.getItem(noticeKey.value); const oldNotice = localStorage.getItem(noticeKey.value);
// //

View File

@ -129,7 +129,7 @@ onMounted(() => {
ElMessage.error("获取邀请码失败:" + e.message) ElMessage.error("获取邀请码失败:" + e.message)
}) })
httpGet("/api/admin/config/get?key=system").then(res => { httpGet("/api/config/get?key=system").then(res => {
inviteChatCalls.value = res.data["invite_chat_calls"] inviteChatCalls.value = res.data["invite_chat_calls"]
inviteImgCalls.value = res.data["invite_img_calls"] inviteImgCalls.value = res.data["invite_img_calls"]
}).catch(e => { }).catch(e => {

View File

@ -227,7 +227,7 @@ onMounted(() => {
router.push("/login") router.push("/login")
}) })
httpGet("/api/admin/config/get?key=system").then(res => { httpGet("/api/config/get?key=system").then(res => {
rewardImg.value = res.data['reward_img'] rewardImg.value = res.data['reward_img']
enableReward.value = res.data['enabled_reward'] enableReward.value = res.data['enabled_reward']
orderPayInfoText.value = res.data['order_pay_info_text'] orderPayInfoText.value = res.data['order_pay_info_text']

View File

@ -142,7 +142,7 @@ const wxImg = ref("/images/wx.png")
const ways = [] const ways = []
const placeholder = ref("用户名:") const placeholder = ref("用户名:")
httpGet("/api/admin/config/get?key=system").then(res => { httpGet("/api/config/get?key=system").then(res => {
if (res.data) { if (res.data) {
const registerWays = res.data['register_ways'] const registerWays = res.data['register_ways']
if (arrayContains(registerWays, "mobile")) { if (arrayContains(registerWays, "mobile")) {

View File

@ -3,7 +3,7 @@
<div class="handle-box"> <div class="handle-box">
<el-button type="primary" :icon="Plus" @click="add">新增</el-button> <el-button type="primary" :icon="Plus" @click="add">新增</el-button>
<a href="https://gpt.bemore.lol" target="_blank" style="margin-left: 10px"> <a href="https://api.chat-plus.net" target="_blank" style="margin-left: 10px">
<el-button type="success" :icon="ShoppingCart" @click="add" plain>购买API-KEY</el-button> <el-button type="success" :icon="ShoppingCart" @click="add" plain>购买API-KEY</el-button>
</a> </a>
</div> </div>
@ -12,13 +12,20 @@
<el-table :data="items" :row-key="row => row.id" table-layout="auto"> <el-table :data="items" :row-key="row => row.id" table-layout="auto">
<el-table-column prop="platform" label="所属平台"/> <el-table-column prop="platform" label="所属平台"/>
<el-table-column prop="name" label="名称"/> <el-table-column prop="name" label="名称"/>
<el-table-column prop="value" label="KEY"> <el-table-column prop="value" label="API KEY">
<template #default="scope"> <template #default="scope">
<el-tooltip class="box-item" <span>{{ substr(scope.row.value, 20) }}</span>
effect="dark" <el-icon class="copy-key" :data-clipboard-text="scope.row.value">
:content="scope.row.api_url" <DocumentCopy/>
placement="top">{{ scope.row.value }} </el-icon>
</el-tooltip> </template>
</el-table-column>
<el-table-column prop="api_url" label="API URL">
<template #default="scope">
<span>{{ substr(scope.row.api_url, 30) }}</span>
<el-icon class="copy-key" :data-clipboard-text="scope.row.api_url">
<DocumentCopy/>
</el-icon>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="type" label="用途"> <el-table-column prop="type" label="用途">
@ -27,11 +34,7 @@
<el-tag v-else-if="scope.row.type === 'img'" type="success">绘图</el-tag> <el-tag v-else-if="scope.row.type === 'img'" type="success">绘图</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="use_proxy" label="使用代理"> <el-table-column prop="proxy_url" label="代理地址"/>
<template #default="scope">
<el-switch v-model="scope.row['use_proxy']" @change="set('use_proxy',scope.row)"/>
</template>
</el-table-column>
<el-table-column label="最后使用时间"> <el-table-column label="最后使用时间">
<template #default="scope"> <template #default="scope">
@ -99,18 +102,8 @@
placeholder="如果你用了第三方的 API 中转,这里填写中转地址"/> placeholder="如果你用了第三方的 API 中转,这里填写中转地址"/>
</el-form-item> </el-form-item>
<el-form-item label="使用代理:" prop="use_proxy"> <el-form-item label="代理地址:" prop="proxy_url">
<el-switch v-model="item.use_proxy"/> <el-input v-model="item.proxy_url" autocomplete="off"/>
<el-tooltip
effect="dark"
content="是否使用代理访问 API URLOpenAI 官方API需要开启代理访问"
raw-content
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</el-form-item> </el-form-item>
<el-form-item label="启用状态:" prop="enable"> <el-form-item label="启用状态:" prop="enable">
@ -129,11 +122,12 @@
</template> </template>
<script setup> <script setup>
import {reactive, ref} from "vue"; import {onMounted, onUnmounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {dateFormat, disabledDate, removeArrayItem} from "@/utils/libs"; import {dateFormat, disabledDate, removeArrayItem, substr} from "@/utils/libs";
import {InfoFilled, Plus, ShoppingCart} from "@element-plus/icons-vue"; import {DocumentCopy, InfoFilled, Plus, ShoppingCart} from "@element-plus/icons-vue";
import ClipboardJS from "clipboard";
// //
const items = ref([]) const items = ref([])
@ -150,10 +144,10 @@ const formRef = ref(null)
const title = ref("") const title = ref("")
const platforms = ref([ const platforms = ref([
{ {
name: "【OpenAI】ChatGPT", name: "【OpenAI/中转】ChatGPT",
value: "OpenAI", value: "OpenAI",
api_url: "https://gpt.bemore.lol/v1/chat/completions", api_url: "https://api.chat-plus.net/v1/chat/completions",
img_url: "https://gpt.bemore.lol/v1/images/generations" img_url: "https://api.chat-plus.net/v1/images/generations"
}, },
{ {
name: "【讯飞】星火大模型", name: "【讯飞】星火大模型",
@ -186,6 +180,23 @@ const types = ref([
{name: "绘画", value: "img"}, {name: "绘画", value: "img"},
]) ])
const clipboard = ref(null)
onMounted(() => {
clipboard.value = new ClipboardJS('.copy-key');
clipboard.value.on('success', () => {
ElMessage.success('复制成功!');
})
clipboard.value.on('error', () => {
ElMessage.error('复制失败!');
})
})
onUnmounted(() => {
clipboard.value.destroy()
})
// //
httpGet('/api/admin/apikey/list').then((res) => { httpGet('/api/admin/apikey/list').then((res) => {
if (res.data) { if (res.data) {
@ -269,7 +280,6 @@ const changePlatform = () => {
} }
} }
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
@ -285,6 +295,11 @@ const changePlatform = () => {
} }
} }
.copy-key {
margin-left 5px
cursor pointer
}
.el-select { .el-select {
width: 100% width: 100%
} }

View File

@ -25,8 +25,18 @@
<el-table :data="data.chat.items" :row-key="row => row.id" table-layout="auto"> <el-table :data="data.chat.items" :row-key="row => row.id" table-layout="auto">
<el-table-column prop="user_id" label="账户ID"/> <el-table-column prop="user_id" label="账户ID"/>
<el-table-column prop="username" label="账户"/> <el-table-column prop="username" label="账户"/>
<el-table-column prop="title" label="标题"/> <el-table-column label="图标">
<template #default="scope">
<el-avatar :size="30" :src="scope.row.role.icon"/>
</template>
</el-table-column>
<el-table-column label="角色">
<template #default="scope">
<span>{{ scope.row.role.name }}</span>
</template>
</el-table-column>
<el-table-column prop="model" label="模型"/> <el-table-column prop="model" label="模型"/>
<el-table-column prop="title" label="标题"/>
<el-table-column prop="msg_num" label="消息数量"/> <el-table-column prop="msg_num" label="消息数量"/>
<el-table-column prop="token" label="消耗算力"/> <el-table-column prop="token" label="消耗算力"/>

View File

@ -14,7 +14,10 @@
</el-table-column> </el-table-column>
<el-table-column prop="name" label="模型名称"/> <el-table-column prop="name" label="模型名称"/>
<el-table-column prop="value" label="模型值"/> <el-table-column prop="value" label="模型值"/>
<el-table-column prop="weight" label="对话权重"/> <el-table-column prop="power" label="费率"/>
<el-table-column prop="max_tokens" label="最大响应长度"/>
<el-table-column prop="max_context" label="最大上下文长度"/>
<el-table-column prop="temperature" label="创意度"/>
<el-table-column prop="enabled" label="启用状态"> <el-table-column prop="enabled" label="启用状态">
<template #default="scope"> <template #default="scope">
<el-switch v-model="scope.row['enabled']" @change="modelSet('enabled',scope.row)"/> <el-switch v-model="scope.row['enabled']" @change="modelSet('enabled',scope.row)"/>
@ -69,16 +72,15 @@
<el-input v-model="item.value" autocomplete="off"/> <el-input v-model="item.value" autocomplete="off"/>
</el-form-item> </el-form-item>
<el-form-item label="对话权重:" prop="weight"> <el-form-item label="费率:" prop="weight">
<template #default> <template #default>
<div class="tip-input"> <div class="tip-input">
<el-input-number :min="1" v-model="item.weight" autocomplete="off"/> <el-input-number :min="1" v-model="item.power" autocomplete="off"/>
<div class="info"> <div class="info">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
content="对话权重,每次对话扣减多少次对话额度" content="每次对话扣减多少单位算力"
placement="right" placement="right"
> >
<el-icon> <el-icon>
@ -90,11 +92,78 @@
</template> </template>
</el-form-item> </el-form-item>
<el-form-item label="最长响应:" prop="max_tokens">
<el-input v-model.number="item.max_tokens" autocomplete="off" placeholder="模型最大响应长度"/>
</el-form-item>
<el-form-item label="最大上下文:" prop="max_context">
<div class="tip-input">
<el-input v-model.number="item.max_context" autocomplete="off" placeholder="模型最大上下文长度"/>
<div class="info">
<el-tooltip
class="box-item"
effect="dark"
raw-content
content="gpt-3.5-turbo:4096 <br/>
gpt-3.5-turbo-16k: 16384 <br/>
gpt-4: 8192 <br/>
gpt-4-32k: 32768 <br/>
chatglm_pro: 32768 <br/>
chatglm_std: 16384 <br/>
chatglm_lite: 4096 <br/>
qwen-turbo: 8192 <br/>
qwen-plus: 32768 <br/>
文心一言: 8192 <br/>
星火1.0: 4096 <br/>
星火2.0-星火3.5: 8192"
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</div>
</el-form-item>
<el-form-item label="创意度:" prop="temperature">
<div class="tip-input">
<el-input v-model="item.temperature" autocomplete="off" placeholder="模型创意度"/>
<div class="info">
<el-tooltip
class="box-item"
effect="dark"
content="OpenAI 0-2其他模型 0-1"
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</div>
</el-form-item>
<el-form-item label="启用状态:" prop="enable"> <el-form-item label="启用状态:" prop="enable">
<el-switch v-model="item.enabled"/> <el-switch v-model="item.enabled"/>
</el-form-item> </el-form-item>
<el-form-item label="开放状态:" prop="open"> <el-form-item label="开放状态:" prop="open">
<div class="tip-input">
<el-switch v-model="item.open"/> <el-switch v-model="item.open"/>
<div class="info">
<el-tooltip
class="box-item"
effect="dark"
raw-content
content="开放后,该模型将对所有用户可见。<br/> 如果模型没有启用,则当前设置无效。"
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>

View File

@ -28,9 +28,9 @@
<el-table-column prop="username" label="下单用户"/> <el-table-column prop="username" label="下单用户"/>
<el-table-column prop="subject" label="产品名称"/> <el-table-column prop="subject" label="产品名称"/>
<el-table-column prop="amount" label="订单金额"/> <el-table-column prop="amount" label="订单金额"/>
<el-table-column label="调用次数"> <el-table-column label="充值算力">
<template #default="scope"> <template #default="scope">
<span>{{ scope.row.remark?.calls }}</span> <span>{{ scope.row.remark?.power }}</span>
</template> </template>
</el-table-column> </el-table-column>

View File

@ -20,8 +20,7 @@
<span v-else>{{ scope.row.days }}</span> <span v-else>{{ scope.row.days }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="calls" label="对话次数"/> <el-table-column prop="power" label="算力"/>
<el-table-column prop="img_calls" label="绘图次数"/>
<el-table-column prop="sales" label="销量"/> <el-table-column prop="sales" label="销量"/>
<el-table-column prop="enabled" label="启用状态"> <el-table-column prop="enabled" label="启用状态">
<template #default="scope"> <template #default="scope">
@ -71,12 +70,8 @@
<el-input v-model.number="item.days" autocomplete="off" placeholder="会员有效期(天)"/> <el-input v-model.number="item.days" autocomplete="off" placeholder="会员有效期(天)"/>
</el-form-item> </el-form-item>
<el-form-item label="对话次数:" prop="calls"> <el-form-item label="算力:" prop="power">
<el-input v-model.number="item.calls" autocomplete="off" placeholder="增加对话次数"/> <el-input v-model.number="item.power" autocomplete="off" placeholder="增加算力值"/>
</el-form-item>
<el-form-item label="绘图次数:" prop="img_calls">
<el-input v-model.number="item.img_calls" autocomplete="off" placeholder="增加绘图次数"/>
</el-form-item> </el-form-item>
<el-form-item label="启用状态:" prop="enable"> <el-form-item label="启用状态:" prop="enable">

View File

@ -11,23 +11,30 @@
<el-form-item label="控制台标题" prop="admin_title"> <el-form-item label="控制台标题" prop="admin_title">
<el-input v-model="system['admin_title']"/> <el-input v-model="system['admin_title']"/>
</el-form-item> </el-form-item>
<el-form-item label="注册赠送对话次数" prop="user_init_calls"> <el-form-item label="网站 LOGO" prop="logo">
<el-input v-model.number="system['init_chat_calls']" placeholder="新用户注册赠送对话次数"/> <el-input v-model="system['logo']" placeholder="网站LOGO图片">
<template #append>
<el-upload
:auto-upload="true"
:show-file-list="false"
@click="beforeUpload('logo')"
:http-request="uploadImg"
>
<el-icon class="uploader-icon">
<UploadFilled/>
</el-icon>
</el-upload>
</template>
</el-input>
</el-form-item> </el-form-item>
<el-form-item label="注册赠送绘图次数" prop="init_img_calls"> <el-form-item label="注册赠送算力" prop="init_power">
<el-input v-model.number="system['init_img_calls']" placeholder="新用户注册赠送绘图次数"/> <el-input v-model.number="system['init_power']" placeholder="新用户注册赠送算力"/>
</el-form-item> </el-form-item>
<el-form-item label="邀请赠送对话次数" prop="invite_chat_calls"> <el-form-item label="邀请赠送算力" prop="invite_power">
<el-input v-model.number="system['invite_chat_calls']" placeholder="邀请新用户注册赠送对话次数"/> <el-input v-model.number="system['invite_power']" placeholder="邀请新用户注册赠送算力"/>
</el-form-item> </el-form-item>
<el-form-item label="邀请赠送绘图次数" prop="invite_img_calls"> <el-form-item label="VIP每月赠送算力" prop="vip_month_power">
<el-input v-model.number="system['invite_img_calls']" placeholder="邀请新用户注册赠送绘图次数"/> <el-input v-model.number="system['vip_month_power']" placeholder="VIP用户每月赠送算力"/>
</el-form-item>
<el-form-item label="VIP每月对话次数" prop="vip_month_calls">
<el-input v-model.number="system['vip_month_calls']" placeholder="VIP用户每月赠送对话次数"/>
</el-form-item>
<el-form-item label="VIP每月绘图次数" prop="vip_month_img_calls">
<el-input v-model.number="system['vip_month_img_calls']" placeholder="VIP用户每月赠送绘图次数"/>
</el-form-item> </el-form-item>
<el-form-item label="开放注册" prop="enabled_register"> <el-form-item label="开放注册" prop="enabled_register">
@ -66,11 +73,9 @@
</el-form-item> </el-form-item>
<div v-if="system['enabled_reward']"> <div v-if="system['enabled_reward']">
<el-form-item label="单次对话价格" prop="chat_call_price"> <el-form-item label="算力单价" prop="power_price">
<el-input v-model="system['chat_call_price']" placeholder="众筹金额跟对话次数的兑换比例"/> <el-input v-model="system['power_price']"
</el-form-item> placeholder="单位算力的价格,比如设置 0.1 表示捐赠1元钱可以得到10个单位算力"/>
<el-form-item label="单次绘图价格" prop="img_call_price">
<el-input v-model="system['img_call_price']" placeholder="众筹金额跟绘图次数的兑换比例"/>
</el-form-item> </el-form-item>
<el-form-item label="收款二维码" prop="reward_img"> <el-form-item label="收款二维码" prop="reward_img">
<el-input v-model="system['reward_img']" placeholder="众筹收款二维码地址"> <el-input v-model="system['reward_img']" placeholder="众筹收款二维码地址">
@ -165,82 +170,21 @@
</div> </div>
</template> </template>
</el-form-item> </el-form-item>
<el-form-item>
<el-button type="primary" @click="save('system')">保存</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<el-tab-pane label="模型配置" name="chat">
<div class="container">
<el-form :model="chat" label-position="right" label-width="150px" ref="chatFormRef" :rules="rules">
<el-form-item label="开启聊天上下文"> <el-form-item label="开启聊天上下文">
<el-switch v-model="chat['enable_context']"/> <el-switch v-model="system['enable_context']"/>
</el-form-item>
<el-form-item label="保存聊天记录">
<el-switch v-model="chat['enable_history']"/>
</el-form-item> </el-form-item>
<el-form-item label="会话上下文深度"> <el-form-item label="会话上下文深度">
<div style="width:100%"> <div style="width:100%">
<el-input-number v-model="chat['context_deep']" :min="0" :max="10"/> <el-input-number v-model="system['context_deep']" :min="0" :max="10"/>
</div> </div>
<div class="tip" style="margin-top: 10px; ">会话上下文深度在老会话中继续会话默认加载多少条聊天记录作为上下文如果设置为 <div class="tip" style="margin-top: 10px; ">会话上下文深度在老会话中继续会话默认加载多少条聊天记录作为上下文如果设置为
0 0
则不加载聊天记录仅仅使用当前角色的上下文该配置参数最好设置需要为偶数否则将无法兼容百度的 API 则不加载聊天记录仅仅使用当前角色的上下文该配置参数最好设置需要为偶数否则将无法兼容百度的 API
</div> </div>
</el-form-item> </el-form-item>
<el-form-item>
<el-divider content-position="center">OpenAI</el-divider> <el-button type="primary" @click="save('system')">保存</el-button>
<el-form-item label="模型创意度">
<el-slider v-model="chat['open_ai']['temperature']" :max="2" :step="0.1"/>
<div class="tip">值越大 AI 回答越发散值越小回答越保守建议保持默认值</div>
</el-form-item>
<el-form-item label="最大响应长度">
<el-input v-model.number="chat['open_ai']['max_tokens']" placeholder="回复的最大字数最大4096"/>
</el-form-item>
<el-divider content-position="center">Azure</el-divider>
<el-form-item label="模型创意度">
<el-slider v-model="chat['azure']['temperature']" :max="2" :step="0.1"/>
<div class="tip">值越大 AI 回答越发散值越小回答越保守建议保持默认值</div>
</el-form-item>
<el-form-item label="最大响应长度">
<el-input v-model.number="chat['azure']['max_tokens']" placeholder="回复的最大字数最大4096"/>
</el-form-item>
<el-divider content-position="center">ChatGLM</el-divider>
<el-form-item label="模型创意度">
<el-slider v-model="chat['chat_gml']['temperature']" :max="1" :step="0.01"/>
<div class="tip">值越大 AI 回答越发散值越小回答越保守建议保持默认值</div>
</el-form-item>
<el-form-item label="最大响应长度">
<el-input v-model.number="chat['chat_gml']['max_tokens']" placeholder="回复的最大字数最大4096"/>
</el-form-item>
<el-divider content-position="center">文心一言</el-divider>
<el-form-item label="模型创意度">
<el-slider v-model="chat['baidu']['temperature']" :max="1" :step="0.01"/>
<div class="tip">值越大 AI 回答越发散值越小回答越保守建议保持默认值</div>
</el-form-item>
<el-form-item label="最大响应长度">
<el-input v-model.number="chat['baidu']['max_tokens']" placeholder="回复的最大字数最大4096"/>
</el-form-item>
<el-divider content-position="center">讯飞星火</el-divider>
<el-form-item label="模型创意度">
<el-slider v-model="chat['xun_fei']['temperature']" :max="1" :step="0.1"/>
<div class="tip">值越大 AI 回答越发散值越小回答越保守建议保持默认值</div>
</el-form-item>
<el-form-item label="最大响应长度">
<el-input v-model.number="chat['xun_fei']['max_tokens']" placeholder="回复的最大字数最大4096"/>
</el-form-item>
<el-divider content-position="center">AI绘图</el-divider>
<el-form-item label="DALL-E3出图数量">
<el-input v-model.number="chat['dall_img_num']" placeholder="调用 DALL E3 API 传入的出图数量"/>
</el-form-item>
<el-form-item style="text-align: right">
<el-button type="primary" @click="save('chat')">保存</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>

View File

@ -17,9 +17,7 @@
<el-image v-if="scope.row.vip" :src="vipImg" style="height: 20px;position: relative; top:5px; left: 5px"/> <el-image v-if="scope.row.vip" :src="vipImg" style="height: 20px;position: relative; top:5px; left: 5px"/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="calls" label="剩余对话次数"/> <el-table-column prop="power" label="剩余算力"/>
<el-table-column prop="img_calls" label="剩余绘图次数"/>
<el-table-column prop="total_tokens" label="累计消耗tokens"/>
<el-table-column label="状态" width="80"> <el-table-column label="状态" width="80">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.status" type="success">正常</el-tag> <el-tag v-if="scope.row.status" type="success">正常</el-tag>
@ -76,11 +74,8 @@
<el-form-item v-if="add" label="密码:" prop="password"> <el-form-item v-if="add" label="密码:" prop="password">
<el-input v-model="user.password" autocomplete="off"/> <el-input v-model="user.password" autocomplete="off"/>
</el-form-item> </el-form-item>
<el-form-item label="对话次数:" prop="calls"> <el-form-item label="剩余算力:" prop="power">
<el-input v-model.number="user.calls" autocomplete="off" placeholder="0"/> <el-input v-model.number="user.power" autocomplete="off" placeholder="0"/>
</el-form-item>
<el-form-item label="绘图次数:" prop="img_calls">
<el-input v-model.number="user['img_calls']" autocomplete="off" placeholder="0"/>
</el-form-item> </el-form-item>
<el-form-item label="有效期:" prop="expired_time"> <el-form-item label="有效期:" prop="expired_time">

View File

@ -149,7 +149,7 @@ onMounted(() => {
showFailToast("获取产品套餐失败:" + e.message) showFailToast("获取产品套餐失败:" + e.message)
}) })
httpGet("/api/admin/config/get?key=system").then(res => { httpGet("/api/config/get?key=system").then(res => {
vipMonthCalls.value = res.data['vip_month_calls'] vipMonthCalls.value = res.data['vip_month_calls']
vipMonthImgCalls.value = res.data['vip_month_img_calls'] vipMonthImgCalls.value = res.data['vip_month_img_calls']
}).catch(e => { }).catch(e => {