mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-08-31 11:17:12 +00:00
release: v4.2.8
整合开源版 v4.2.8 功能:Sora2 视频、路由重构、手机站开关、DALL-E 参考图,以及启动时自动同步数据表字段等修复与优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,7 @@ tmp
|
||||
bin
|
||||
data
|
||||
config.toml
|
||||
config.toml.*.bak
|
||||
static/upload
|
||||
static/audio
|
||||
storage.json
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Listen = "0.0.0.0:5678"
|
||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
||||
MysqlDns = "root:12345678@tcp(localhost:3306)/geekai?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
||||
StaticDir = "./static" # 静态资源的目录
|
||||
StaticUrl = "/static" # 静态资源访问 URL
|
||||
TikaHost = "http://tika:9998"
|
||||
|
||||
@@ -44,11 +44,11 @@ func NewServer(appConfig *types.AppConfig, redis *redis.Client, sysConfig *types
|
||||
}
|
||||
|
||||
func (s *AppServer) Init(client *redis.Client) {
|
||||
s.Engine.Use(middleware.ThumbMiddleware())
|
||||
s.Engine.Use(middleware.ParameterHandlerMiddleware())
|
||||
s.Engine.Use(errorHandler)
|
||||
// 添加静态资源访问
|
||||
s.Engine.Static("/static", s.Config.StaticDir)
|
||||
s.Engine.Use(middleware.StaticMiddleware())
|
||||
}
|
||||
|
||||
func (s *AppServer) Run(db *gorm.DB) error {
|
||||
|
||||
@@ -85,15 +85,6 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
|
||||
logger.Error("load system config error: ", err)
|
||||
}
|
||||
|
||||
// 加载许可证配置
|
||||
var license types.License
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyLicense).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &license)
|
||||
if err != nil {
|
||||
logger.Error("load license config error: ", err)
|
||||
}
|
||||
|
||||
// 加载验证码配置
|
||||
var captchaConfig types.CaptchaConfig
|
||||
sysConfig.Id = 0
|
||||
@@ -168,7 +159,6 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
|
||||
|
||||
return &types.SystemConfig{
|
||||
Base: baseConfig,
|
||||
License: license,
|
||||
SMS: smsConfig,
|
||||
OSS: ossConfig,
|
||||
SMTP: smtpConfig,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
@@ -13,6 +14,8 @@ import (
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
// 前端用户授权验证
|
||||
func UserAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// 静态资源中间件
|
||||
func StaticMiddleware() gin.HandlerFunc {
|
||||
// 缩略图中间件
|
||||
func ThumbMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
url := c.Request.URL.String()
|
||||
@@ -37,22 +37,6 @@ type RedisConfig struct {
|
||||
DB int
|
||||
}
|
||||
|
||||
// LicenseKey 存储许可证书的 KEY
|
||||
const LicenseKey = "Geek-AI-License"
|
||||
|
||||
type License struct {
|
||||
Key string `json:"key"` // 许可证书密钥
|
||||
MachineId string `json:"machine_id"` // 机器码
|
||||
ExpiredAt int64 `json:"expired_at"` // 过期时间
|
||||
IsActive bool `json:"is_active"` // 是否激活
|
||||
Configs LicenseConfig `json:"configs"`
|
||||
}
|
||||
|
||||
type LicenseConfig struct {
|
||||
UserNum int `json:"user_num"` // 用户数量
|
||||
DeCopy bool `json:"de_copy"` // 去版权
|
||||
}
|
||||
|
||||
func (c RedisConfig) Url() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
@@ -97,6 +81,8 @@ type BaseConfig struct {
|
||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
|
||||
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
||||
|
||||
EnableMobileSite bool `json:"enable_mobile_site"` // 是否开启手机站点
|
||||
}
|
||||
|
||||
type SystemConfig struct {
|
||||
@@ -108,7 +94,6 @@ type SystemConfig struct {
|
||||
Captcha CaptchaConfig
|
||||
WxLogin WxLoginConfig
|
||||
Jimeng JimengConfig
|
||||
License License
|
||||
Moderation ModerationConfig
|
||||
}
|
||||
|
||||
@@ -121,7 +106,6 @@ const (
|
||||
ConfigKeyMarkMap = "mark_map"
|
||||
ConfigKeyCaptcha = "captcha"
|
||||
ConfigKeyWxLogin = "wx_login"
|
||||
ConfigKeyLicense = "license"
|
||||
ConfigKeySms = "sms"
|
||||
ConfigKeySmtp = "smtp"
|
||||
ConfigKeyOss = "oss"
|
||||
|
||||
@@ -15,7 +15,8 @@ type JimengConfig struct {
|
||||
type JMTaskStatus string
|
||||
|
||||
const (
|
||||
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务已提交
|
||||
JMTaskStatusSubmited = JMTaskStatus("submited") // 任务已提交
|
||||
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务排队中
|
||||
JMTaskStatusGenerating = JMTaskStatus("generating") // 任务处理中
|
||||
JMTaskStatusDone = JMTaskStatus("done") // 处理完成
|
||||
JMTaskStatusNotFound = JMTaskStatus("not_found") // 任务未找到
|
||||
|
||||
@@ -72,14 +72,13 @@ type SdTaskParams struct {
|
||||
type DallTask struct {
|
||||
ModelId uint `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
ModelValue string `json:"model_value"`
|
||||
Image []string `json:"image,omitempty"`
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
AspectRatio string `json:"aspect_ratio"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
Power int `json:"power"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
|
||||
type ConfigHandler struct {
|
||||
handler.BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
sysConfig *types.SystemConfig
|
||||
alipayService *payment.AlipayService
|
||||
wxpayService *payment.WxPayService
|
||||
@@ -41,7 +40,6 @@ type ConfigHandler struct {
|
||||
func NewConfigHandler(
|
||||
app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
licenseService *service.LicenseService,
|
||||
sysConfig *types.SystemConfig,
|
||||
alipayService *payment.AlipayService,
|
||||
wxpayService *payment.WxPayService,
|
||||
@@ -54,7 +52,6 @@ func NewConfigHandler(
|
||||
) *ConfigHandler {
|
||||
return &ConfigHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
licenseService: licenseService,
|
||||
sysConfig: sysConfig,
|
||||
alipayService: alipayService,
|
||||
wxpayService: wxpayService,
|
||||
@@ -87,8 +84,6 @@ func (h *ConfigHandler) RegisterRoutes() {
|
||||
rg.POST("update/oss", h.UpdateOss)
|
||||
rg.POST("update/smtp", h.UpdateStmp)
|
||||
rg.GET("get", h.Get)
|
||||
rg.POST("license/active", h.Active)
|
||||
rg.GET("license/get", h.GetLicense)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,19 +96,6 @@ func (h *ConfigHandler) UpdateBase(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 未授权的话不允许修改版权
|
||||
license := h.licenseService.GetLicense()
|
||||
if !license.IsActive && data.Copyright != h.sysConfig.Base.Copyright {
|
||||
resp.ERROR(c, "未授权系统不允许修改版权信息")
|
||||
return
|
||||
}
|
||||
|
||||
// 未授权的话不允许修改 Logo
|
||||
if !license.IsActive && data.Logo != h.sysConfig.Base.Logo {
|
||||
resp.ERROR(c, "未授权系统不允许修改 Logo")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeySystem, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
@@ -407,36 +389,3 @@ func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
// Active 激活系统
|
||||
func (h *ConfigHandler) Active(c *gin.Context) {
|
||||
var data struct {
|
||||
License string `json:"license"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.licenseService.ActiveLicense(data.License)
|
||||
license := h.licenseService.GetLicense()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.Update(types.ConfigKeyLicense, license); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 更新系统配置
|
||||
h.sysConfig.License = *license
|
||||
|
||||
resp.SUCCESS(c, license.MachineId)
|
||||
|
||||
}
|
||||
|
||||
// GetLicense 获取 License 信息
|
||||
func (h *ConfigHandler) GetLicense(c *gin.Context) {
|
||||
license := h.licenseService.GetLicense()
|
||||
resp.SUCCESS(c, license)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
@@ -28,12 +27,11 @@ import (
|
||||
|
||||
type UserHandler struct {
|
||||
handler.BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
redis *redis.Client
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService, redisCli *redis.Client) *UserHandler {
|
||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService, redis: redisCli}
|
||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, redisCli *redis.Client) *UserHandler {
|
||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, redis: redisCli}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
@@ -114,13 +112,6 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 检测最大注册人数
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
var user = model.User{}
|
||||
var res *gorm.DB
|
||||
var userVo vo.User
|
||||
@@ -198,11 +189,7 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||
}
|
||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
||||
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
} else {
|
||||
u.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
|
||||
}
|
||||
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
res = h.DB.Create(&u)
|
||||
_ = utils.CopyObject(u, &userVo)
|
||||
userVo.Id = u.Id
|
||||
|
||||
+54
-24
@@ -19,6 +19,7 @@ import (
|
||||
"geekai/service"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/sora"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
@@ -65,36 +66,39 @@ type ChatHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
userLocks *types.UserLockManager
|
||||
soraService *sora.SoraService
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService, moderationManager *moderation.ServiceManager) *ChatHandler {
|
||||
func NewChatHandler(app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
redis *redis.Client,
|
||||
manager *oss.UploaderManager,
|
||||
userService *service.UserService,
|
||||
soraService *sora.SoraService,
|
||||
moderationManager *moderation.ServiceManager) *ChatHandler {
|
||||
return &ChatHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
userLocks: types.NewUserLockManager(),
|
||||
soraService: soraService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/chat/")
|
||||
|
||||
// 聊天接口不需要授权(已在authConfig中配置)
|
||||
group.Any("message", h.Chat)
|
||||
|
||||
// 其他接口需要用户授权
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.Any("message", h.Chat)
|
||||
group.GET("list", h.List)
|
||||
group.GET("detail", h.Detail)
|
||||
group.POST("update", h.Update)
|
||||
@@ -340,14 +344,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
||||
for _, file := range input.Files {
|
||||
logger.Debugf("detected file: %+v", file.URL)
|
||||
// 处理图片
|
||||
if isImageURL(file.URL) {
|
||||
if isImageURL(file.URL) || isVideoURL(file.URL) {
|
||||
imgList = append(imgList, gin.H{
|
||||
"type": "image_url",
|
||||
"image_url": gin.H{
|
||||
"url": file.URL,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
} else if isTextURL(file.URL) {
|
||||
// 处理文件,提取文件内容
|
||||
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
@@ -408,23 +412,43 @@ func isImageURL(url string) bool {
|
||||
".svg": true,
|
||||
".ico": true,
|
||||
}
|
||||
return validImageExts[ext]
|
||||
}
|
||||
|
||||
if !validImageExts[ext] {
|
||||
// 判断是个链接是否是文本链接
|
||||
func isTextURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 发送HEAD请求检查Content-Type
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validTextExts := map[string]bool{
|
||||
".txt": true,
|
||||
".doc": true,
|
||||
".docx": true,
|
||||
".pdf": true,
|
||||
}
|
||||
resp, err := client.Head(url)
|
||||
if err != nil {
|
||||
return validTextExts[ext]
|
||||
}
|
||||
|
||||
// 判断是个链接是否是视频
|
||||
func isVideoURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
return strings.HasPrefix(contentType, "image/")
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validVideoExts := map[string]bool{
|
||||
".mp4": true,
|
||||
".avi": true,
|
||||
".mov": true,
|
||||
".wmv": true,
|
||||
}
|
||||
return validVideoExts[ext]
|
||||
}
|
||||
|
||||
// Tokens 统计 token 数量
|
||||
@@ -505,11 +529,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, input
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
|
||||
// ONLY allow apiURL in blank list
|
||||
err := h.licenseService.IsValidApiURL(apiKey.ApiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Debugf("对话请求消息体:%+v", req)
|
||||
var apiURL string
|
||||
p, _ := url.Parse(apiKey.ApiURL)
|
||||
@@ -644,6 +663,16 @@ func (h *ChatHandler) saveChatHistory(
|
||||
replyTokens, _ = utils.CalcTokens(message.Content, req.Model)
|
||||
totalTokens = replyTokens + getTotalTokens(req)
|
||||
}
|
||||
|
||||
// 如果是 Sora 相关模型,则下载视频
|
||||
files := make([]vo.File, 0)
|
||||
if strings.HasPrefix(req.Model, "sora") {
|
||||
video, err := h.soraService.DownloadVideoURL(message.Content)
|
||||
if err == nil {
|
||||
files = append(files, *video)
|
||||
}
|
||||
}
|
||||
|
||||
historyReplyMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: input.ChatId,
|
||||
@@ -652,7 +681,7 @@ func (h *ChatHandler) saveChatHistory(
|
||||
Icon: input.ChatRole.Icon,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: message.Content,
|
||||
Files: input.Files,
|
||||
Files: files,
|
||||
}),
|
||||
Tokens: replyTokens,
|
||||
TotalTokens: totalTokens,
|
||||
@@ -676,6 +705,7 @@ func (h *ChatHandler) saveChatHistory(
|
||||
if err != nil {
|
||||
content.Text = historyReplyMsg.Content
|
||||
}
|
||||
content.Files = files
|
||||
messageVo.Content = content
|
||||
messageVo.CreatedAt = historyReplyMsg.CreatedAt.Unix()
|
||||
messageVo.UpdatedAt = historyReplyMsg.UpdatedAt.Unix()
|
||||
|
||||
@@ -212,6 +212,14 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
} else {
|
||||
errMsg = utils.InterfaceToString(apiRes.Data)
|
||||
contents = append(contents, errMsg)
|
||||
// 扣减算力
|
||||
if function.Power > 0 {
|
||||
h.userService.DecreasePower(userVo.Id, function.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: function.Name,
|
||||
Remark: fmt.Sprintf("函数调用,%s(%s)", function.Label, function.Name),
|
||||
})
|
||||
}
|
||||
}
|
||||
pushMessage(c, "text", errMsg)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
@@ -20,11 +19,10 @@ import (
|
||||
|
||||
type ConfigHandler struct {
|
||||
BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *ConfigHandler {
|
||||
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}, licenseService: licenseService}
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
|
||||
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
@@ -33,7 +31,6 @@ func (h *ConfigHandler) RegisterRoutes() {
|
||||
|
||||
// 无需授权的接口
|
||||
group.GET("get", h.Get)
|
||||
group.GET("license", h.License)
|
||||
}
|
||||
|
||||
// Get 获取指定的系统配置
|
||||
@@ -55,9 +52,3 @@ func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
// License 获取 License 配置
|
||||
func (h *ConfigHandler) License(c *gin.Context) {
|
||||
license := h.licenseService.GetLicense()
|
||||
resp.SUCCESS(c, license.Configs)
|
||||
}
|
||||
|
||||
@@ -117,12 +117,12 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
task := types.DallTask{
|
||||
UserId: uint(userId),
|
||||
ModelId: chatModel.Id,
|
||||
ModelName: chatModel.Value,
|
||||
ModelName: chatModel.Name,
|
||||
ModelValue: chatModel.Value,
|
||||
Image: data.Image,
|
||||
Prompt: data.Prompt,
|
||||
Quality: data.Quality,
|
||||
AspectRatio: data.AspectRatio,
|
||||
Size: data.Size,
|
||||
Style: data.Style,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: chatModel.Value,
|
||||
Model: chatModel.Name,
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -215,12 +215,9 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
UserId: user.Id,
|
||||
Prompt: prompt,
|
||||
ModelId: chatModel.Id,
|
||||
ModelName: chatModel.Value,
|
||||
ModelValue: chatModel.Value,
|
||||
ModelName: chatModel.Name,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
N: 1,
|
||||
Quality: "standard",
|
||||
Size: "1024x1024",
|
||||
Style: "vivid",
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
job := model.DallJob{
|
||||
|
||||
@@ -247,30 +247,12 @@ func (h *JimengHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
|
||||
if err := h.DB.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
|
||||
logger.Errorf("delete jimeng job failed: %v", err)
|
||||
resp.ERROR(c, "删除任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 失败任务删除后退回算力
|
||||
if job.Status == types.JMTaskStatusFailed {
|
||||
logger.Infof("delete jimeng job failed, refund power: %d", job.Power)
|
||||
err = h.userService.IncreasePower(user.Id, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: job.ReqKey,
|
||||
Remark: fmt.Sprintf("删除任务,退回%d算力", job.Power),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "退回算力失败")
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
resp.SUCCESS(c, gin.H{})
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||
func (h *MenuHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/menu/")
|
||||
group.GET("list", h.List)
|
||||
group.GET("list/all", h.ListAll)
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
@@ -54,3 +55,20 @@ func (h *MenuHandler) List(c *gin.Context) {
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
|
||||
// ListAll 获取所有的菜单列表
|
||||
func (h *MenuHandler) ListAll(c *gin.Context) {
|
||||
var items []model.Menu
|
||||
var list = make([]vo.Menu, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Menu
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
list = append(list, product)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
|
||||
@@ -138,6 +138,7 @@ func (h *PaymentHandler) SyncOrders() error {
|
||||
}
|
||||
case payment.PayChannelAL:
|
||||
res, err = h.alipayService.Query(order.OrderNo)
|
||||
logger.Debugf("支付宝支付订单状态:%+v", res)
|
||||
if err != nil {
|
||||
logger.Errorf("error with query order info: %v", err)
|
||||
continue
|
||||
|
||||
@@ -35,7 +35,6 @@ type UserHandler struct {
|
||||
searcher *xdb.Searcher
|
||||
redis *redis.Client
|
||||
levelDB *store.LevelDB
|
||||
licenseService *service.LicenseService
|
||||
captchaService *service.CaptchaService
|
||||
userService *service.UserService
|
||||
wxLoginService *service.WxLoginService
|
||||
@@ -51,15 +50,13 @@ func NewUserHandler(
|
||||
captcha *service.CaptchaService,
|
||||
userService *service.UserService,
|
||||
wxLoginService *service.WxLoginService,
|
||||
ipSearcher *xdb.Searcher,
|
||||
licenseService *service.LicenseService) *UserHandler {
|
||||
ipSearcher *xdb.Searcher) *UserHandler {
|
||||
return &UserHandler{
|
||||
BaseHandler: BaseHandler{DB: db, App: app},
|
||||
searcher: searcher,
|
||||
redis: client,
|
||||
levelDB: levelDB,
|
||||
captchaService: captcha,
|
||||
licenseService: licenseService,
|
||||
userService: userService,
|
||||
wxLoginService: wxLoginService,
|
||||
ipSearcher: ipSearcher,
|
||||
@@ -132,14 +129,6 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检测最大注册人数
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
var key string
|
||||
if data.RegWay == "email" {
|
||||
@@ -327,7 +316,7 @@ func (h *UserHandler) GetWxLoginState(c *gin.Context) {
|
||||
}
|
||||
|
||||
// createNewUser 创建新用户
|
||||
func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.User, error) {
|
||||
func (h *UserHandler) createNewUser(user model.User, code string) (model.User, error) {
|
||||
if user.OpenId != "" {
|
||||
user.Platform = "wechat"
|
||||
user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6))
|
||||
@@ -353,19 +342,22 @@ func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.U
|
||||
// 创建用户
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return user, err
|
||||
}
|
||||
|
||||
// 记录邀请关系
|
||||
if inviteCode != "" {
|
||||
inviteCode := model.InviteCode{}
|
||||
err := h.DB.Where("code = ?", inviteCode).First(&inviteCode).Error
|
||||
if code != "" {
|
||||
var inviteCode model.InviteCode
|
||||
err := h.DB.Where("code = ?", code).First(&inviteCode).Error
|
||||
if err != nil {
|
||||
logger.Error("无效的邀请码:", err.Error())
|
||||
tx.Rollback()
|
||||
return user, fmt.Errorf("无效的邀请码")
|
||||
}
|
||||
|
||||
// 增加邀请数量
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", inviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
if h.App.SysConfig.Base.InvitePower > 0 {
|
||||
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{
|
||||
Type: types.PowerInvite,
|
||||
|
||||
+6
-12
@@ -24,6 +24,7 @@ import (
|
||||
"geekai/service/payment"
|
||||
"geekai/service/sd"
|
||||
"geekai/service/sms"
|
||||
"geekai/service/sora"
|
||||
"geekai/service/suno"
|
||||
"geekai/service/video"
|
||||
"geekai/store"
|
||||
@@ -166,12 +167,6 @@ func main() {
|
||||
|
||||
// 邮件服务
|
||||
fx.Provide(service.NewSmtpService),
|
||||
// License 服务
|
||||
fx.Provide(service.NewLicenseService),
|
||||
fx.Invoke(func(licenseService *service.LicenseService) {
|
||||
licenseService.SyncLicense()
|
||||
}),
|
||||
|
||||
// Dalle 服务
|
||||
fx.Provide(dalle.NewService),
|
||||
fx.Invoke(func(s *dalle.Service) {
|
||||
@@ -189,6 +184,9 @@ func main() {
|
||||
s.DownloadImages()
|
||||
}),
|
||||
|
||||
// Sora service
|
||||
fx.Provide(sora.NewSoraService),
|
||||
|
||||
// Stable Diffusion 机器人
|
||||
fx.Provide(sd.NewService),
|
||||
fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
|
||||
@@ -222,12 +220,8 @@ func main() {
|
||||
fx.Provide(sms.NewAliYunSmsService),
|
||||
fx.Provide(sms.NewBaoSmsService),
|
||||
fx.Provide(sms.NewSmsManager),
|
||||
fx.Provide(func(config *types.SystemConfig) *service.CaptchaService {
|
||||
return service.NewCaptchaService(config.Captcha)
|
||||
}),
|
||||
fx.Provide(func(config *types.SystemConfig, client *redis.Client) *service.WxLoginService {
|
||||
return service.NewWxLoginService(config.WxLogin, client)
|
||||
}),
|
||||
fx.Provide(service.NewCaptchaService),
|
||||
fx.Provide(service.NewWxLoginService),
|
||||
|
||||
// 支付服务
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
|
||||
@@ -20,9 +20,9 @@ type CaptchaService struct {
|
||||
client *req.Client
|
||||
}
|
||||
|
||||
func NewCaptchaService(captchaConfig types.CaptchaConfig) *CaptchaService {
|
||||
func NewCaptchaService(sysConfig *types.SystemConfig) *CaptchaService {
|
||||
return &CaptchaService{
|
||||
config: captchaConfig,
|
||||
config: sysConfig.Captcha,
|
||||
client: req.C().SetTimeout(10 * time.Second),
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func (s *CaptchaService) GetConfig() types.CaptchaConfig {
|
||||
return s.config
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Get() (interface{}, error) {
|
||||
func (s *CaptchaService) Get() (any, error) {
|
||||
url := fmt.Sprintf("%s/api/captcha/get", types.GeekAPIURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
@@ -98,10 +97,8 @@ type imgReq struct {
|
||||
Model string `json:"model"`
|
||||
Image []string `json:"image,omitempty"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n,omitempty"`
|
||||
AspectRatio string `json:"aspect_ratio,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
Style string `json:"style,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
}
|
||||
|
||||
@@ -116,10 +113,10 @@ type imgRes struct {
|
||||
|
||||
type ErrRes struct {
|
||||
Error struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param interface{} `json:"param"`
|
||||
Type string `json:"type"`
|
||||
Code any `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param any `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
@@ -130,7 +127,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
if task.ModelId > 0 {
|
||||
s.db.Where("id", task.ModelId).First(&chatModel)
|
||||
} else {
|
||||
s.db.Where("value", task.ModelName).First(&chatModel)
|
||||
s.db.Where("value", task.ModelValue).First(&chatModel)
|
||||
}
|
||||
|
||||
// get image generation API KEY
|
||||
@@ -153,16 +150,16 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
|
||||
reqBody := imgReq{
|
||||
Model: chatModel.Value,
|
||||
Prompt: task.Prompt,
|
||||
N: 1,
|
||||
Size: task.Size,
|
||||
Style: task.Style,
|
||||
Quality: task.Quality,
|
||||
Model: chatModel.Value,
|
||||
Prompt: task.Prompt,
|
||||
AspectRatio: task.AspectRatio,
|
||||
Size: task.Size,
|
||||
ResponseFormat: "url",
|
||||
}
|
||||
|
||||
// 图片编辑
|
||||
if len(task.Image) > 0 {
|
||||
reqBody.Prompt = fmt.Sprintf("%s, %s", strings.Join(task.Image, " "), task.Prompt)
|
||||
reqBody.Image = task.Image
|
||||
}
|
||||
|
||||
logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
|
||||
@@ -185,7 +182,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
// update the api key last use time
|
||||
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
var imgURL string
|
||||
var data = map[string]interface{}{
|
||||
var data = map[string]any{
|
||||
"progress": 100,
|
||||
"prompt": task.Prompt,
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
@@ -23,29 +24,31 @@ var logger = logger2.GetLogger()
|
||||
|
||||
// Service 即梦服务(合并了消费者功能)
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
taskQueue *store.RedisQueue
|
||||
client *Client
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
running bool
|
||||
uploader *oss.UploaderManager
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
taskQueue *store.RedisQueue
|
||||
client *Client
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
running bool
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
// NewService 创建即梦服务
|
||||
func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager, client *Client) *Service {
|
||||
func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager, client *Client, userService *service.UserService) *Service {
|
||||
taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Service{
|
||||
db: db,
|
||||
redis: redisCli,
|
||||
taskQueue: taskQueue,
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
running: false,
|
||||
uploader: uploader,
|
||||
db: db,
|
||||
redis: redisCli,
|
||||
taskQueue: taskQueue,
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
running: false,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +118,7 @@ func (s *Service) CreateTask(userId uint, req *types.JimengTaskRequest) (*model.
|
||||
ReqKey: req.ReqKey,
|
||||
Prompt: req.Prompt,
|
||||
Params: utils.JsonEncode(req),
|
||||
Status: types.JMTaskStatusInQueue,
|
||||
Status: types.JMTaskStatusSubmited,
|
||||
Power: req.Power,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
@@ -216,6 +219,7 @@ func (s *Service) ProcessTask(jobId uint) error {
|
||||
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||
"task_id": resp.Data.TaskId,
|
||||
"raw_data": string(rawData),
|
||||
"status": types.JMTaskStatusInQueue, // 把任务状态改成排队中,以便开启轮询
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
logger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
@@ -277,12 +281,10 @@ func (s *Service) pollTaskStatus() {
|
||||
|
||||
for {
|
||||
var jobs []model.JimengJob
|
||||
s.db.Where("status IN (?)", []types.JMTaskStatus{types.JMTaskStatusGenerating, types.JMTaskStatusInQueue}).Find(&jobs)
|
||||
if len(jobs) == 0 {
|
||||
logger.Debugf("no jimeng task to poll, sleep 10s")
|
||||
time.Sleep(10 * time.Second)
|
||||
continue
|
||||
}
|
||||
// 找出排队中和处理中的任务进行轮询
|
||||
s.db.Where("status IN (?)", []types.JMTaskStatus{
|
||||
types.JMTaskStatusGenerating,
|
||||
types.JMTaskStatusInQueue}).Find(&jobs)
|
||||
|
||||
for _, job := range jobs {
|
||||
// 任务超时处理
|
||||
@@ -366,6 +368,21 @@ func (s *Service) pollTaskStatus() {
|
||||
|
||||
}
|
||||
|
||||
// 找出失败的任务,并恢复其扣减算力
|
||||
s.db.Where("status = ?", types.JMTaskStatusFailed).Where("power > ?", 0).Find(&jobs)
|
||||
for _, job := range jobs {
|
||||
err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: job.ReqKey,
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d", job.Id),
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 更新任务状态
|
||||
s.db.Model(&job).UpdateColumn("power", 0)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
package service
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LicenseService struct {
|
||||
license *types.License
|
||||
urlWhiteList []string
|
||||
machineId string
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewLicenseService(sysConfig *types.SystemConfig, db *gorm.DB) *LicenseService {
|
||||
var machineId string
|
||||
info, err := host.Info()
|
||||
if err == nil {
|
||||
machineId = info.HostID
|
||||
}
|
||||
logger.Infof("License: %+v", sysConfig.License)
|
||||
return &LicenseService{
|
||||
license: &sysConfig.License,
|
||||
machineId: machineId,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
type License struct {
|
||||
Name string `json:"name"`
|
||||
License string `json:"license"`
|
||||
MachineId string `json:"mid"`
|
||||
ActiveAt int64 `json:"active_at"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
UserNum int `json:"user_num"`
|
||||
Configs types.LicenseConfig `json:"configs"`
|
||||
}
|
||||
|
||||
// ActiveLicense 激活 License
|
||||
func (s *LicenseService) ActiveLicense(license string) error {
|
||||
var res struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data License `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/active")
|
||||
response, err := req.C().R().
|
||||
SetBody(map[string]string{"license": license, "machine_id": s.machineId}).
|
||||
SetSuccessResult(&res).Post(apiURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送激活请求失败: %v", err)
|
||||
}
|
||||
|
||||
if response.IsErrorState() {
|
||||
return fmt.Errorf("发送激活请求失败:%v", response.Status)
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
return fmt.Errorf("激活失败:%v", res.Message)
|
||||
}
|
||||
|
||||
if res.Data.ExpiredAt > 0 && res.Data.ExpiredAt < time.Now().Unix() {
|
||||
return fmt.Errorf("License 已过期")
|
||||
}
|
||||
|
||||
s.license = &types.License{
|
||||
Key: license,
|
||||
MachineId: s.machineId,
|
||||
Configs: res.Data.Configs,
|
||||
ExpiredAt: res.Data.ExpiredAt,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// 保存 License 到数据库
|
||||
err = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).UpdateColumn("value", utils.JsonEncode(s.license)).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存 License 到数据库失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncLicense 定期同步 License
|
||||
func (s *LicenseService) SyncLicense() {
|
||||
go func() {
|
||||
retryCounter := 0
|
||||
for {
|
||||
license, err := s.fetchLicense()
|
||||
if err != nil {
|
||||
retryCounter++
|
||||
if retryCounter < 5 {
|
||||
logger.Debug(err)
|
||||
}
|
||||
s.license.IsActive = false
|
||||
} else {
|
||||
s.license = license
|
||||
// 保存 License 到数据库
|
||||
err = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).UpdateColumn("value", utils.JsonEncode(s.license)).Error
|
||||
if err != nil {
|
||||
logger.Errorf("保存 License 到数据库失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
urls, err := s.fetchUrlWhiteList()
|
||||
if err == nil {
|
||||
s.urlWhiteList = urls
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *LicenseService) fetchLicense() (*types.License, error) {
|
||||
var res struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data License `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/check")
|
||||
response, err := req.C().R().
|
||||
SetBody(map[string]string{"license": s.license.Key, "machine_id": s.machineId}).
|
||||
SetSuccessResult(&res).Post(apiURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("License 同步失败: %v", err)
|
||||
}
|
||||
if response.IsErrorState() {
|
||||
return nil, fmt.Errorf("License 同步失败:%v", response.Status)
|
||||
}
|
||||
if res.Code != types.Success {
|
||||
return nil, fmt.Errorf("License 同步失败:%v", res.Message)
|
||||
}
|
||||
|
||||
return &types.License{
|
||||
Key: res.Data.License,
|
||||
MachineId: res.Data.MachineId,
|
||||
Configs: res.Data.Configs,
|
||||
ExpiredAt: res.Data.ExpiredAt,
|
||||
IsActive: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *LicenseService) fetchUrlWhiteList() ([]string, error) {
|
||||
var res struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/urls")
|
||||
response, err := req.C().R().SetSuccessResult(&res).Get(apiURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
||||
}
|
||||
if response.IsErrorState() {
|
||||
return nil, fmt.Errorf("发送请求失败:%v", response.Status)
|
||||
}
|
||||
if res.Code != types.Success {
|
||||
return nil, fmt.Errorf("获取白名单失败:%v", res.Message)
|
||||
}
|
||||
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
// GetLicense 获取许可信息
|
||||
func (s *LicenseService) GetLicense() *types.License {
|
||||
if s.license == nil {
|
||||
var config model.Config
|
||||
s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).First(&config)
|
||||
if config.Value != "" {
|
||||
utils.JsonDecode(config.Value, &s.license)
|
||||
}
|
||||
}
|
||||
return s.license
|
||||
}
|
||||
|
||||
func (s *LicenseService) SetLicense(licenseKey string) {
|
||||
s.license.Key = licenseKey
|
||||
|
||||
}
|
||||
|
||||
// IsValidApiURL 判断是否合法的中转 URL
|
||||
func (s *LicenseService) IsValidApiURL(uri string) error {
|
||||
// 获得许可授权的直接放行
|
||||
if s.license.IsActive {
|
||||
if s.license.MachineId != s.machineId {
|
||||
return errors.New("系统使用了盗版的许可证书")
|
||||
}
|
||||
|
||||
if time.Now().Unix() > s.license.ExpiredAt {
|
||||
return errors.New("系统许可证书已经过期")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(s.urlWhiteList) == 0 {
|
||||
urls, err := s.fetchUrlWhiteList()
|
||||
if err == nil {
|
||||
s.urlWhiteList = urls
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.urlWhiteList {
|
||||
if strings.HasPrefix(uri, v) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("当前 API 地址 %s 不在白名单列表当中。", uri)
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/imroc/req/v3"
|
||||
@@ -26,9 +25,8 @@ import (
|
||||
|
||||
// Client MidJourney client
|
||||
type Client struct {
|
||||
client *req.Client
|
||||
licenseService *service.LicenseService
|
||||
db *gorm.DB
|
||||
client *req.Client
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type ImageReq struct {
|
||||
@@ -77,11 +75,10 @@ type QueryRes struct {
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
func NewClient(licenseService *service.LicenseService, db *gorm.DB) *Client {
|
||||
func NewClient(db *gorm.DB) *Client {
|
||||
return &Client{
|
||||
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
|
||||
licenseService: licenseService,
|
||||
db: db,
|
||||
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,10 +195,6 @@ func (c *Client) doRequest(body interface{}, apiPath string, channel string) (Im
|
||||
return ImageRes{}, fmt.Errorf("no available MidJourney api key: %v", err)
|
||||
}
|
||||
|
||||
if err = c.licenseService.IsValidApiURL(apiKey.ApiURL); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/%s", apiKey.ApiURL, apiPath)
|
||||
logger.Info("API URL: ", apiURL)
|
||||
r, err := req.C().R().
|
||||
|
||||
@@ -203,8 +203,8 @@ func (s *Service) SyncTaskProgress() {
|
||||
go func() {
|
||||
var jobs []model.MidJourneyJob
|
||||
for {
|
||||
err := s.db.Where("progress < ?", 100).Find(&jobs).Error
|
||||
if err != nil {
|
||||
res := s.db.Where("progress < ?", 100).Where("channel_id <> ?", "").Find(&jobs)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -217,10 +217,6 @@ func (s *Service) SyncTaskProgress() {
|
||||
continue
|
||||
}
|
||||
|
||||
if job.ChannelId == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
task, err := s.client.QueryTask(job.TaskId, job.ChannelId)
|
||||
if err != nil {
|
||||
logger.Errorf("error with query task: %v", err)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package sora
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
logger2 "geekai/logger"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type SoraService struct {
|
||||
uploadManager *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewSoraService(uploadManager *oss.UploaderManager) *SoraService {
|
||||
return &SoraService{
|
||||
uploadManager: uploadManager,
|
||||
}
|
||||
}
|
||||
|
||||
// 下载视频地址
|
||||
func (s *SoraService) DownloadVideoURL(text string) (*vo.File, error) {
|
||||
videoDataURL, err := s.ExtractVideoURL(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取 JSON 数据
|
||||
resp, err := http.Get(videoDataURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析视频下载地址
|
||||
var videoData map[string]any
|
||||
err = json.Unmarshal(body, &videoData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := videoData["url"].(string); ok && v != "" {
|
||||
logger.Infof("try to download video: %s", v)
|
||||
videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v, ".mp4", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
size, _ := utils.GetFileSize(videoURL)
|
||||
name := filepath.Base(videoURL)
|
||||
return &vo.File{
|
||||
Name: name,
|
||||
ObjKey: name,
|
||||
URL: videoURL,
|
||||
Ext: ".mp4",
|
||||
Size: size,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("no video URL found: " + string(body))
|
||||
|
||||
}
|
||||
|
||||
// 从文本中提取视频URL
|
||||
func (s *SoraService) ExtractVideoURL(text string) (string, error) {
|
||||
// 提取原始 JSON 数据地址
|
||||
//[原始数据](https://asyncdata.net/source/task_01k8pye324ef7t6heq6jyaxbfe) 类似这样的链接
|
||||
re := regexp.MustCompile(`\[原始数据\]\((https?://.*?)\)`)
|
||||
matches := re.FindAllStringSubmatch(text, 1)
|
||||
if len(matches) == 0 {
|
||||
return "", errors.New("no video URL found")
|
||||
}
|
||||
return matches[0][1], nil
|
||||
}
|
||||
@@ -70,6 +70,7 @@ func (s *UserService) DecreasePower(userId uint, power int, log model.PowerLog)
|
||||
return fmt.Errorf("扣减算力失败:%v", err)
|
||||
}
|
||||
|
||||
tx.Where("id", userId).First(&user)
|
||||
err = tx.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
|
||||
@@ -39,9 +39,9 @@ const (
|
||||
LoginStatusExpired = "expired" // 登录失效,需要重新登录
|
||||
)
|
||||
|
||||
func NewWxLoginService(config types.WxLoginConfig, redisClient *redis.Client) *WxLoginService {
|
||||
func NewWxLoginService(sysConfig *types.SystemConfig, redisClient *redis.Client) *WxLoginService {
|
||||
return &WxLoginService{
|
||||
config: config,
|
||||
config: sysConfig.WxLogin,
|
||||
client: req.C().SetTimeout(10 * time.Second),
|
||||
redisClient: redisClient,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type AdminUser struct {
|
||||
Salt string `gorm:"column:salt;type:char(12);not null;comment:密码盐" json:"salt"`
|
||||
Status bool `gorm:"column:status;type:tinyint(1);not null;comment:当前状态" json:"status"`
|
||||
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(16);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(32);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ type Function struct {
|
||||
Token string `gorm:"column:token;type:varchar(255);comment:API授权token" json:"token"`
|
||||
Action string `gorm:"column:action;type:varchar(255);comment:函数处理 API" json:"action"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;default:0;comment:是否启用" json:"enabled"`
|
||||
Power int `gorm:"column:power;type:int(11);not null;default:0;comment:消费算力点数" json:"power"`
|
||||
}
|
||||
|
||||
func (m *Function) TableName() string {
|
||||
|
||||
@@ -21,7 +21,7 @@ type User struct {
|
||||
ChatModels string `gorm:"column:chat_models_json;type:text;default:null;comment:AI模型 json" json:"chat_models"`
|
||||
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
||||
Vip bool `gorm:"column:vip;type:tinyint(1);not null;default:0;comment:是否会员" json:"vip"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(16);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(32);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
OpenId string `gorm:"column:openid;type:varchar(100);comment:第三方登录账号ID" json:"openid"`
|
||||
Platform string `gorm:"column:platform;type:varchar(30);comment:登录平台" json:"platform"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
|
||||
@@ -8,7 +8,7 @@ type UserLoginLog struct {
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int(11);not null;comment:用户ID" json:"user_id"`
|
||||
Username string `gorm:"column:username;type:varchar(30);not null;comment:用户名" json:"username"`
|
||||
LoginIp string `gorm:"column:login_ip;type:char(16);not null;comment:登录IP" json:"login_ip"`
|
||||
LoginIp string `gorm:"column:login_ip;type:char(32);not null;comment:登录IP" json:"login_ip"`
|
||||
LoginAddress string `gorm:"column:login_address;type:varchar(30);not null;comment:登录地址" json:"login_address"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
|
||||
@@ -20,4 +20,5 @@ type Function struct {
|
||||
Action string `json:"action"`
|
||||
Token string `json:"token"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Power int `json:"power"`
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
fmt.Println("test")
|
||||
}
|
||||
@@ -101,3 +101,16 @@ func GetImgExt(filename string) string {
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
func GetFileSize(url string) (int64, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64(len(body)), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user