From 3ab930a10722495c1bdcc05ff14843eef7008035 Mon Sep 17 00:00:00 2001 From: RockYang Date: Fri, 22 Dec 2023 17:25:31 +0800 Subject: [PATCH] feat: support CDN reverse proxy for MidJourney and OpenAI API --- api/core/app_server.go | 1 + api/core/types/config.go | 14 +++-- api/core/types/locked_map.go | 2 +- api/go.mod | 3 +- api/go.sum | 4 +- api/handler/chatimpl/chat_handler.go | 3 +- api/handler/mj_handler.go | 38 +++++++++++++ api/main.go | 5 +- api/service/mj/bot.go | 41 +++++++++----- api/service/mj/client.go | 43 ++++++++------- api/service/mj/pool.go | 46 ++++++++++++---- api/service/mj/service.go | 16 ++++-- api/store/model/function.go | 1 + api/store/model/mj_job.go | 1 + api/store/vo/mj_job.go | 1 + database/update-v3.2.3.sql | 5 +- web/public/images/mj/mj-v6.png | Bin 0 -> 18590 bytes web/src/views/ImageMj.vue | 78 +++++++++++++++++---------- web/src/views/ImageSd.vue | 3 +- 19 files changed, 218 insertions(+), 87 deletions(-) create mode 100644 web/public/images/mj/mj-v6.png diff --git a/api/core/app_server.go b/api/core/app_server.go index a29c32d..94837d0 100644 --- a/api/core/app_server.go +++ b/api/core/app_server.go @@ -154,6 +154,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc { c.Request.URL.Path == "/api/chat/detail" || c.Request.URL.Path == "/api/role/list" || c.Request.URL.Path == "/api/mj/jobs" || + c.Request.URL.Path == "/api/mj/client" || c.Request.URL.Path == "/api/invite/hits" || c.Request.URL.Path == "/api/sd/jobs" || c.Request.URL.Path == "/api/upload" || diff --git a/api/core/types/config.go b/api/core/types/config.go index 99dc483..584b0bc 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -34,11 +34,15 @@ type ChatPlusApiConfig struct { } type MidJourneyConfig struct { - Enabled bool - UserToken string - BotToken string - GuildId string // Server ID - ChanelId string // Chanel ID + Enabled bool + UserToken string + BotToken string + GuildId string // Server ID + ChanelId string // Chanel ID + UseCDN bool + DiscordAPI string + DiscordCDN string + DiscordGateway string } type StableDiffusionConfig struct { diff --git a/api/core/types/locked_map.go b/api/core/types/locked_map.go index 13915c4..ede72f3 100644 --- a/api/core/types/locked_map.go +++ b/api/core/types/locked_map.go @@ -6,7 +6,7 @@ import ( ) type MKey interface { - string | int + string | int | uint } type MValue interface { *WsClient | *ChatSession | context.CancelFunc | []interface{} diff --git a/api/go.mod b/api/go.mod index a95a236..98bbc10 100644 --- a/api/go.mod +++ b/api/go.mod @@ -6,7 +6,6 @@ require ( github.com/BurntSushi/toml v1.1.0 github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible - github.com/bwmarrin/discordgo v0.27.1 github.com/eatmoreapple/openwechat v1.2.1 github.com/gin-gonic/gin v1.9.1 github.com/go-redis/redis/v8 v8.11.5 @@ -26,6 +25,8 @@ require ( require github.com/xxl-job/xxl-job-executor-go v1.2.0 +require github.com/bg5t/mydiscordgo v0.28.1 + require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/bytedance/sonic v1.9.1 // indirect diff --git a/api/go.sum b/api/go.sum index b14415c..e677b54 100644 --- a/api/go.sum +++ b/api/go.sum @@ -7,8 +7,8 @@ github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9 github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= -github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/bg5t/mydiscordgo v0.28.1 h1:mVH0ZWstVdJffCi/EXJAYQDtXwIKAJYVXLmECu1hEK8= +github.com/bg5t/mydiscordgo v0.28.1/go.mod h1:n3aba73N18k1DzM0t0mGE8rwW3Z+vwTvI8pcsBgxN/8= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= diff --git a/api/handler/chatimpl/chat_handler.go b/api/handler/chatimpl/chat_handler.go index f81df2a..ce088b9 100644 --- a/api/handler/chatimpl/chat_handler.go +++ b/api/handler/chatimpl/chat_handler.go @@ -442,7 +442,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf } else { client = http.DefaultClient } - logger.Infof("Sending %s request, KEY: %s, PROXY: %s, Model: %s", platform, *apiKey, proxyURL, req.Model) + logger.Infof("Sending %s request, ApiURL:%s, PROXY: %s, Model: %s", platform, apiURL, proxyURL, req.Model) switch platform { case types.Azure: request.Header.Set("api-key", *apiKey) @@ -452,7 +452,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf if err != nil { return nil, err } - logger.Info(token) request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) break case types.Baidu: diff --git a/api/handler/mj_handler.go b/api/handler/mj_handler.go index 7b39aee..ad946e1 100644 --- a/api/handler/mj_handler.go +++ b/api/handler/mj_handler.go @@ -13,7 +13,9 @@ import ( "encoding/base64" "fmt" "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" "gorm.io/gorm" + "net/http" "strings" "time" ) @@ -58,6 +60,27 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool { } +// Client WebSocket 客户端,用于通知任务状态变更 +func (h *MidJourneyHandler) Client(c *gin.Context) { + ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil) + if err != nil { + logger.Error(err) + c.Abort() + return + } + + userId := h.GetInt(c, "user_id", 0) + if userId == 0 { + logger.Info("Invalid user ID") + c.Abort() + return + } + + client := types.NewWsClient(ws) + h.pool.Clients.Put(uint(userId), client) + logger.Infof("New websocket connected, IP: %s", c.RemoteIP()) +} + // Image 创建一个绘画任务 func (h *MidJourneyHandler) Image(c *gin.Context) { var data struct { @@ -147,6 +170,9 @@ func (h *MidJourneyHandler) Image(c *gin.Context) { UserId: userId, }) + client := h.pool.Clients.Get(uint(job.UserId)) + _ = client.Send([]byte("Task Updated")) + // update user's img calls h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) resp.SUCCESS(c) @@ -205,6 +231,10 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) { MessageId: data.MessageId, MessageHash: data.MessageHash, }) + + client := h.pool.Clients.Get(uint(job.UserId)) + _ = client.Send([]byte("Task Updated")) + resp.SUCCESS(c) } @@ -226,6 +256,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) { job := model.MidJourneyJob{ Type: types.TaskVariation.String(), + ChannelId: data.ChannelId, ReferenceId: data.MessageId, UserId: userId, TaskId: data.TaskId, @@ -250,6 +281,9 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) { MessageHash: data.MessageHash, }) + client := h.pool.Clients.Get(uint(job.UserId)) + _ = client.Send([]byte("Task Updated")) + // update user's img calls h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) resp.SUCCESS(c) @@ -320,6 +354,7 @@ func (h *MidJourneyHandler) JobList(c *gin.Context) { func (h *MidJourneyHandler) Remove(c *gin.Context) { var data struct { Id uint `json:"id"` + UserId uint `json:"user_id"` ImgURL string `json:"img_url"` } if err := c.ShouldBindJSON(&data); err != nil { @@ -340,5 +375,8 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) { logger.Error("remove image failed: ", err) } + client := h.pool.Clients.Get(data.UserId) + _ = client.Send([]byte("Task Updated")) + resp.SUCCESS(c) } diff --git a/api/main.go b/api/main.go index 9d76db9..70f1049 100644 --- a/api/main.go +++ b/api/main.go @@ -17,6 +17,7 @@ import ( "chatplus/store" "context" "embed" + "github.com/go-redis/redis/v8" "io" "log" "os" @@ -25,8 +26,6 @@ import ( "syscall" "time" - "github.com/go-redis/redis/v8" - "github.com/lionsoul2014/ip2region/binding/golang/xdb" "go.uber.org/fx" "gorm.io/gorm" @@ -168,6 +167,7 @@ func main() { fx.Invoke(func(pool *mj.ServicePool) { if pool.HasAvailableService() { pool.DownloadImages() + pool.CheckTaskNotify() } }), @@ -234,6 +234,7 @@ func main() { }), fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) { group := s.Engine.Group("/api/mj/") + group.Any("client", h.Client) group.POST("image", h.Image) group.POST("upscale", h.Upscale) group.POST("variation", h.Variation) diff --git a/api/service/mj/bot.go b/api/service/mj/bot.go index 4e2b2f5..912edf2 100644 --- a/api/service/mj/bot.go +++ b/api/service/mj/bot.go @@ -4,7 +4,7 @@ import ( "chatplus/core/types" logger2 "chatplus/logger" "chatplus/utils" - "github.com/bwmarrin/discordgo" + discordgo "github.com/bg5t/mydiscordgo" "github.com/gorilla/websocket" "net/http" "net/url" @@ -17,35 +17,48 @@ import ( var logger = logger2.GetLogger() type Bot struct { - config *types.MidJourneyConfig + config types.MidJourneyConfig bot *discordgo.Session name string service *Service } -func NewBot(name string, proxy string, config *types.MidJourneyConfig, service *Service) (*Bot, error) { - discord, err := discordgo.New("Bot " + config.BotToken) - logger.Info(config.BotToken) +func NewBot(name string, proxy string, config types.MidJourneyConfig, service *Service) (*Bot, error) { + bot, err := discordgo.New("Bot " + config.BotToken) if err != nil { logger.Error(err) return nil, err } - if proxy != "" { - proxy, _ := url.Parse(proxy) - discord.Client = &http.Client{ - Transport: &http.Transport{ + // use CDN reverse proxy + if config.UseCDN { + discordgo.SetEndpointDiscord(config.DiscordAPI) + discordgo.SetEndpointCDN(config.DiscordCDN) + discordgo.SetEndpointStatus(config.DiscordAPI + "/api/v2/") + bot.MjGateway = config.DiscordGateway + "/" + } else { // use proxy + discordgo.SetEndpointDiscord("https://discord.com") + discordgo.SetEndpointCDN("https://cdn.discordapp.com") + discordgo.SetEndpointStatus("https://discord.com/api/v2/") + bot.MjGateway = "wss://gateway.discord.gg" + + if proxy != "" { + proxy, _ := url.Parse(proxy) + bot.Client = &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxy), + }, + } + bot.Dialer = &websocket.Dialer{ Proxy: http.ProxyURL(proxy), - }, - } - discord.Dialer = &websocket.Dialer{ - Proxy: http.ProxyURL(proxy), + } } + } return &Bot{ config: config, - bot: discord, + bot: bot, name: name, service: service, }, nil diff --git a/api/service/mj/client.go b/api/service/mj/client.go index de4d7dc..eb84240 100644 --- a/api/service/mj/client.go +++ b/api/service/mj/client.go @@ -12,24 +12,32 @@ import ( type Client struct { client *req.Client - config types.MidJourneyConfig + Config types.MidJourneyConfig + apiURL string } func NewClient(config types.MidJourneyConfig, proxy string) *Client { client := req.C().SetTimeout(10 * time.Second) + var apiURL string // set proxy URL - if proxy != "" { - client.SetProxyURL(proxy) + if config.UseCDN { + apiURL = config.DiscordAPI + "/api/v9/interactions" + } else { + apiURL = "https://discord.com/api/v9/interactions" + if proxy != "" { + client.SetProxyURL(proxy) + } } - return &Client{client: client, config: config} + + return &Client{client: client, Config: config, apiURL: apiURL} } func (c *Client) Imagine(prompt string) error { interactionsReq := &InteractionsRequest{ Type: 2, ApplicationID: ApplicationID, - GuildID: c.config.GuildId, - ChannelID: c.config.ChanelId, + GuildID: c.Config.GuildId, + ChannelID: c.Config.ChanelId, SessionID: SessionID, Data: map[string]any{ "version": "1166847114203123795", @@ -67,11 +75,10 @@ func (c *Client) Imagine(prompt string) error { }, } - url := "https://discord.com/api/v9/interactions" - r, err := c.client.R().SetHeader("Authorization", c.config.UserToken). + r, err := c.client.R().SetHeader("Authorization", c.Config.UserToken). SetHeader("Content-Type", "application/json"). SetBody(interactionsReq). - Post(url) + Post(c.apiURL) if err != nil || r.IsErrorState() { return fmt.Errorf("error with http request: %w%v", err, r.Err) @@ -86,8 +93,8 @@ func (c *Client) Upscale(index int, messageId string, hash string) error { interactionsReq := &InteractionsRequest{ Type: 3, ApplicationID: ApplicationID, - GuildID: c.config.GuildId, - ChannelID: c.config.ChanelId, + GuildID: c.Config.GuildId, + ChannelID: c.Config.ChanelId, MessageFlags: &flags, MessageID: &messageId, SessionID: SessionID, @@ -98,13 +105,12 @@ func (c *Client) Upscale(index int, messageId string, hash string) error { Nonce: fmt.Sprintf("%d", time.Now().UnixNano()), } - url := "https://discord.com/api/v9/interactions" var res InteractionsResult - r, err := c.client.R().SetHeader("Authorization", c.config.UserToken). + r, err := c.client.R().SetHeader("Authorization", c.Config.UserToken). SetHeader("Content-Type", "application/json"). SetBody(interactionsReq). SetErrorResult(&res). - Post(url) + Post(c.apiURL) if err != nil || r.IsErrorState() { return fmt.Errorf("error with http request: %v%v%v", err, r.Err, res.Message) } @@ -118,8 +124,8 @@ func (c *Client) Variation(index int, messageId string, hash string) error { interactionsReq := &InteractionsRequest{ Type: 3, ApplicationID: ApplicationID, - GuildID: c.config.GuildId, - ChannelID: c.config.ChanelId, + GuildID: c.Config.GuildId, + ChannelID: c.Config.ChanelId, MessageFlags: &flags, MessageID: &messageId, SessionID: SessionID, @@ -130,13 +136,12 @@ func (c *Client) Variation(index int, messageId string, hash string) error { Nonce: fmt.Sprintf("%d", time.Now().UnixNano()), } - url := "https://discord.com/api/v9/interactions" var res InteractionsResult - r, err := c.client.R().SetHeader("Authorization", c.config.UserToken). + r, err := c.client.R().SetHeader("Authorization", c.Config.UserToken). SetHeader("Content-Type", "application/json"). SetBody(interactionsReq). SetErrorResult(&res). - Post(url) + Post(c.apiURL) if err != nil || r.IsErrorState() { return fmt.Errorf("error with http request: %v%v%v", err, r.Err, res.Message) } diff --git a/api/service/mj/pool.go b/api/service/mj/pool.go index 6fd5fd4..b446ad2 100644 --- a/api/service/mj/pool.go +++ b/api/service/mj/pool.go @@ -6,9 +6,9 @@ import ( "chatplus/store" "chatplus/store/model" "fmt" + "github.com/go-redis/redis/v8" "time" - "github.com/go-redis/redis/v8" "gorm.io/gorm" ) @@ -16,13 +16,16 @@ import ( type ServicePool struct { services []*Service taskQueue *store.RedisQueue + notifyQueue *store.RedisQueue db *gorm.DB uploaderManager *oss.UploaderManager + Clients *types.LMap[uint, *types.WsClient] // UserId => Client } func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig) *ServicePool { services := make([]*Service, 0) - queue := store.NewRedisQueue("MidJourney_Task_Queue", redisCli) + taskQueue := store.NewRedisQueue("MidJourney_Task_Queue", redisCli) + notifyQueue := store.NewRedisQueue("MidJourney_Notify_Queue", redisCli) // create mj client and service for k, config := range appConfig.MjConfigs { if config.Enabled == false { @@ -33,9 +36,9 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa name := fmt.Sprintf("MjService-%d", k) // create mj service - service := NewService(name, queue, 4, 600, db, client) + service := NewService(name, taskQueue, notifyQueue, 4, 600, db, client) botName := fmt.Sprintf("MjBot-%d", k) - bot, err := NewBot(botName, appConfig.ProxyURL, &config, service) + bot, err := NewBot(botName, appConfig.ProxyURL, config, service) if err != nil { continue } @@ -54,13 +57,32 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa } return &ServicePool{ - taskQueue: queue, + taskQueue: taskQueue, + notifyQueue: notifyQueue, services: services, uploaderManager: manager, db: db, + Clients: types.NewLMap[uint, *types.WsClient](), } } +func (p *ServicePool) CheckTaskNotify() { + go func() { + for { + var userId uint + err := p.notifyQueue.LPop(&userId) + if err != nil { + continue + } + client := p.Clients.Get(userId) + err = client.Send([]byte("Task Updated")) + if err != nil { + continue + } + } + }() +} + func (p *ServicePool) DownloadImages() { go func() { var items []model.MidJourneyJob @@ -71,15 +93,21 @@ func (p *ServicePool) DownloadImages() { } // download images - for _, item := range items { - imgURL, err := p.uploaderManager.GetUploadHandler().PutImg(item.OrgURL, true) + for _, v := range items { + imgURL, err := p.uploaderManager.GetUploadHandler().PutImg(v.OrgURL, true) if err != nil { logger.Error("error with download image: ", err) continue } - item.ImgURL = imgURL - p.db.Updates(&item) + v.ImgURL = imgURL + p.db.Updates(&v) + + client := p.Clients.Get(uint(v.UserId)) + err = client.Send([]byte("Task Updated")) + if err != nil { + continue + } } time.Sleep(time.Second * 5) diff --git a/api/service/mj/service.go b/api/service/mj/service.go index 3b3d6e7..04cd94d 100644 --- a/api/service/mj/service.go +++ b/api/service/mj/service.go @@ -15,6 +15,7 @@ type Service struct { name string // service name client *Client // MJ client taskQueue *store.RedisQueue + notifyQueue *store.RedisQueue db *gorm.DB maxHandleTaskNum int32 // max task number current service can handle handledTaskNum int32 // already handled task number @@ -22,11 +23,12 @@ type Service struct { taskTimeout int64 } -func NewService(name string, queue *store.RedisQueue, maxTaskNum int32, timeout int64, db *gorm.DB, client *Client) *Service { +func NewService(name string, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, maxTaskNum int32, timeout int64, db *gorm.DB, client *Client) *Service { return &Service{ name: name, db: db, - taskQueue: queue, + taskQueue: taskQueue, + notifyQueue: notifyQueue, client: client, taskTimeout: timeout, maxHandleTaskNum: maxTaskNum, @@ -53,9 +55,10 @@ func (s *Service) Run() { } // if it's reference message, check if it's this channel's message - if task.ChannelId != "" && task.ChannelId != s.client.config.ChanelId { + if task.ChannelId != "" && task.ChannelId != s.client.Config.ChanelId { s.taskQueue.RPush(task) s.db.Model(&model.MidJourneyJob{Id: uint(task.Id)}).UpdateColumn("progress", -1) + s.notifyQueue.RPush(task.UserId) time.Sleep(time.Second) continue } @@ -77,6 +80,7 @@ func (s *Service) Run() { logger.Error("绘画任务执行失败:", err) // update the task progress s.db.Model(&model.MidJourneyJob{Id: uint(task.Id)}).UpdateColumn("progress", -1) + s.notifyQueue.RPush(task.UserId) // restore img_call quota s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1)) continue @@ -134,6 +138,10 @@ func (s *Service) Notify(data CBReq) { job.Prompt = data.Prompt job.Hash = data.Image.Hash job.OrgURL = data.Image.URL + if s.client.Config.UseCDN { + job.UseProxy = true + job.ImgURL = strings.ReplaceAll(data.Image.URL, "https://cdn.discordapp.com", s.client.Config.DiscordCDN) + } res = s.db.Updates(&job) if res.Error != nil { @@ -146,4 +154,6 @@ func (s *Service) Notify(data CBReq) { atomic.AddInt32(&s.handledTaskNum, -1) } + s.notifyQueue.RPush(job.UserId) + } diff --git a/api/store/model/function.go b/api/store/model/function.go index b737d18..72f588d 100644 --- a/api/store/model/function.go +++ b/api/store/model/function.go @@ -3,6 +3,7 @@ package model type Function struct { Id uint `gorm:"primarykey;column:id"` Name string + Label string Description string Parameters string Required string diff --git a/api/store/model/mj_job.go b/api/store/model/mj_job.go index 488c3b2..bb94906 100644 --- a/api/store/model/mj_job.go +++ b/api/store/model/mj_job.go @@ -15,6 +15,7 @@ type MidJourneyJob struct { Hash string // message hash Progress int Prompt string + UseProxy bool // 是否使用反代加载图片 CreatedAt time.Time } diff --git a/api/store/vo/mj_job.go b/api/store/vo/mj_job.go index bfc236c..3ffcb37 100644 --- a/api/store/vo/mj_job.go +++ b/api/store/vo/mj_job.go @@ -15,5 +15,6 @@ type MidJourneyJob struct { Hash string `json:"hash"` Progress int `json:"progress"` Prompt string `json:"prompt"` + UseProxy bool `json:"use_proxy"` CreatedAt time.Time `json:"created_at"` } diff --git a/database/update-v3.2.3.sql b/database/update-v3.2.3.sql index e64c6cc..6a4f763 100644 --- a/database/update-v3.2.3.sql +++ b/database/update-v3.2.3.sql @@ -18,4 +18,7 @@ ALTER TABLE `chatgpt_functions` ADD UNIQUE(`name`); ALTER TABLE `chatgpt_functions` ADD `enabled` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '是否启用' AFTER `action`; -ALTER TABLE `chatgpt_functions` ADD `lebal` VARCHAR(30) NULL COMMENT '函数标签' AFTER `name`; \ No newline at end of file +ALTER TABLE `chatgpt_functions` ADD `label` VARCHAR(30) NULL COMMENT '函数标签' AFTER `name`; + +ALTER TABLE `chatgpt_mj_jobs` ADD `use_proxy` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '是否使用反代' AFTER `progress`; +ALTER TABLE `chatgpt_mj_jobs` CHANGE `img_url` `img_url` VARCHAR(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '图片URL'; \ No newline at end of file diff --git a/web/public/images/mj/mj-v6.png b/web/public/images/mj/mj-v6.png new file mode 100644 index 0000000000000000000000000000000000000000..2bd6f30e53ff9c43a3dbbea1661e539822341aa4 GIT binary patch literal 18590 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 literal 0 HcmV?d00001 diff --git a/web/src/views/ImageMj.vue b/web/src/views/ImageMj.vue index c5d0e96..d799378 100644 --- a/web/src/views/ImageMj.vue +++ b/web/src/views/ImageMj.vue @@ -359,7 +359,7 @@