mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	enable to update AI Drawing configuarations in admin console page
This commit is contained in:
		@@ -6,6 +6,8 @@
 | 
			
		||||
* 功能新增:给思维导图增加 ToolBar,实现思维导图的放大缩小和定位
 | 
			
		||||
* Bug修复:修复思维导图不扣费的Bug
 | 
			
		||||
* Bug修复:修复管理后台角色删除失败的Bug
 | 
			
		||||
* Bug修复:兼容最新版秋叶SD懒人包的 SD API,新增 scheduler 参数
 | 
			
		||||
* 功能优化:支持在管理后台配置 AI 绘图相关配置,包括 SD, MJ-PLUS, MJ-PROXY
 | 
			
		||||
 | 
			
		||||
## v4.0.5
 | 
			
		||||
 | 
			
		||||
@@ -45,6 +47,7 @@
 | 
			
		||||
* 功能新增:支持管理后台 Logo 修改
 | 
			
		||||
 | 
			
		||||
## 4.0.2
 | 
			
		||||
 | 
			
		||||
* 功能新增:支持前端菜单可以配置
 | 
			
		||||
* 功能优化:在登录和注册界面标题显示软件版本号
 | 
			
		||||
* 功能优化:MJ 绘画支持 --sref 和 --cref 图片一致性参数
 | 
			
		||||
@@ -54,6 +57,7 @@
 | 
			
		||||
* 功能新增:管理后台登录页面增加行为验证码,防止爆破
 | 
			
		||||
 | 
			
		||||
## v4.0.1
 | 
			
		||||
 | 
			
		||||
* 功能重构:重构 Stable-Diffusion 绘画实现,使用 SDAPI 替换之前的 websocket 接口,SDAPI 兼容各种 stable-diffusion
 | 
			
		||||
  发行版,稳定性更强一些
 | 
			
		||||
* 功能优化:使用 [midjouney-proxy](https://github.com/novicezk/midjourney-proxy) 项目替换内置的原生 MidJourney API,兼容
 | 
			
		||||
@@ -63,6 +67,7 @@
 | 
			
		||||
* Bug修复:修复手机端 MidJourney 绘画页面滚动条无法滚动的Bug
 | 
			
		||||
 | 
			
		||||
## v4.0.0
 | 
			
		||||
 | 
			
		||||
非兼容版本,重大重构,引入算力概念,将系统中所有的能力(AI对话,MJ绘画,SD绘画,DALL绘画)全部使用算力来兑换。
 | 
			
		||||
只要你的算力值余额不为0,你就可以进行任何操作。比如一次 GPT3.5 对话消耗1个单位算力,一次 GPT4 对话消耗10个算力。一次 MJ
 | 
			
		||||
对话消耗15个算力...
 | 
			
		||||
 
 | 
			
		||||
@@ -55,9 +55,10 @@ type SdTaskParams struct {
 | 
			
		||||
	NegPrompt    string  `json:"neg_prompt"` // 反向提示词
 | 
			
		||||
	Steps        int     `json:"steps"`      // 迭代步数,默认20
 | 
			
		||||
	Sampler      string  `json:"sampler"`    // 采样器
 | 
			
		||||
	FaceFix      bool    `json:"face_fix"`   // 面部修复
 | 
			
		||||
	CfgScale     float32 `json:"cfg_scale"`  //引导系数,默认 7
 | 
			
		||||
	Seed         int64   `json:"seed"`       // 随机数种子
 | 
			
		||||
	Scheduler    string  `json:"scheduler"`
 | 
			
		||||
	FaceFix      bool    `json:"face_fix"`  // 面部修复
 | 
			
		||||
	CfgScale     float32 `json:"cfg_scale"` //引导系数,默认 7
 | 
			
		||||
	Seed         int64   `json:"seed"`      // 随机数种子
 | 
			
		||||
	Height       int     `json:"height"`
 | 
			
		||||
	Width        int     `json:"width"`
 | 
			
		||||
	HdFix        bool    `json:"hd_fix"`         // 启用高清修复
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,8 @@ import (
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/handler"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/service/mj"
 | 
			
		||||
	"geekai/service/sd"
 | 
			
		||||
	"geekai/store"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
@@ -26,10 +28,18 @@ type ConfigHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	levelDB        *store.LevelDB
 | 
			
		||||
	licenseService *service.LicenseService
 | 
			
		||||
	mjServicePool *mj.ServicePool
 | 
			
		||||
	sdServicePool *sd.ServicePool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService) *ConfigHandler {
 | 
			
		||||
	return &ConfigHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, levelDB: levelDB, licenseService: licenseService}
 | 
			
		||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService, mjPool *mj.ServicePool, sdPool *sd.ServicePool) *ConfigHandler {
 | 
			
		||||
	return &ConfigHandler{
 | 
			
		||||
		BaseHandler:    handler.BaseHandler{App: app, DB: db},
 | 
			
		||||
		levelDB:        levelDB,
 | 
			
		||||
		mjServicePool:  mjPool,
 | 
			
		||||
		sdServicePool:  sdPool,
 | 
			
		||||
		licenseService: licenseService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ConfigHandler) Update(c *gin.Context) {
 | 
			
		||||
@@ -138,3 +148,49 @@ func (h *ConfigHandler) GetDrawingConfig(c *gin.Context) {
 | 
			
		||||
		"sd":       h.App.Config.SdConfigs,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveDrawingConfig 保存AI绘画配置
 | 
			
		||||
func (h *ConfigHandler) SaveDrawingConfig(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Sd      []types.StableDiffusionConfig `json:"sd"`
 | 
			
		||||
		MjPlus  []types.MjPlusConfig          `json:"mj_plus"`
 | 
			
		||||
		MjProxy []types.MjProxyConfig         `json:"mj_proxy"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	changed := false
 | 
			
		||||
	if configChanged(data.Sd, h.App.Config.SdConfigs) {
 | 
			
		||||
		logger.Debugf("SD 配置变动了")
 | 
			
		||||
		h.App.Config.SdConfigs = data.Sd
 | 
			
		||||
		h.sdServicePool.InitServices(data.Sd)
 | 
			
		||||
		changed = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if configChanged(data.MjPlus, h.App.Config.MjPlusConfigs) || configChanged(data.MjProxy, h.App.Config.MjProxyConfigs) {
 | 
			
		||||
		logger.Debugf("MidJourney 配置变动了")
 | 
			
		||||
		h.App.Config.MjPlusConfigs = data.MjPlus
 | 
			
		||||
		h.App.Config.MjProxyConfigs = data.MjProxy
 | 
			
		||||
		h.mjServicePool.InitServices(data.MjPlus, data.MjProxy)
 | 
			
		||||
		changed = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if changed {
 | 
			
		||||
		err := core.SaveConfig(h.App.Config)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, "更新配置文档失败!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func configChanged(c1 interface{}, c2 interface{}) bool {
 | 
			
		||||
	encode1 := utils.JsonEncode(c1)
 | 
			
		||||
	encode2 := utils.JsonEncode(c2)
 | 
			
		||||
	return utils.Md5(encode1) != utils.Md5(encode2)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ package admin
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/handler"
 | 
			
		||||
@@ -16,7 +17,6 @@ import (
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
@@ -87,7 +87,7 @@ func (h *UserHandler) Save(c *gin.Context) {
 | 
			
		||||
	// 检测最大注册人数
 | 
			
		||||
	var totalUser int64
 | 
			
		||||
	h.DB.Model(&model.User{}).Count(&totalUser)
 | 
			
		||||
	if int(totalUser) >= h.licenseService.GetLicense().UserNum {
 | 
			
		||||
	if h.licenseService.GetLicense().UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().UserNum {
 | 
			
		||||
		resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ package handler
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
@@ -15,7 +16,6 @@ import (
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
@@ -71,7 +71,7 @@ func (h *UserHandler) Register(c *gin.Context) {
 | 
			
		||||
	// 检测最大注册人数
 | 
			
		||||
	var totalUser int64
 | 
			
		||||
	h.DB.Model(&model.User{}).Count(&totalUser)
 | 
			
		||||
	if int(totalUser) >= h.licenseService.GetLicense().UserNum {
 | 
			
		||||
	if h.licenseService.GetLicense().UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().UserNum {
 | 
			
		||||
		resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -190,7 +190,8 @@ func main() {
 | 
			
		||||
 | 
			
		||||
		// MidJourney service pool
 | 
			
		||||
		fx.Provide(mj.NewServicePool),
 | 
			
		||||
		fx.Invoke(func(pool *mj.ServicePool) {
 | 
			
		||||
		fx.Invoke(func(pool *mj.ServicePool, config *types.AppConfig) {
 | 
			
		||||
			pool.InitServices(config.MjPlusConfigs, config.MjProxyConfigs)
 | 
			
		||||
			if pool.HasAvailableService() {
 | 
			
		||||
				pool.DownloadImages()
 | 
			
		||||
				pool.CheckTaskNotify()
 | 
			
		||||
@@ -200,7 +201,8 @@ func main() {
 | 
			
		||||
 | 
			
		||||
		// Stable Diffusion 机器人
 | 
			
		||||
		fx.Provide(sd.NewServicePool),
 | 
			
		||||
		fx.Invoke(func(pool *sd.ServicePool) {
 | 
			
		||||
		fx.Invoke(func(pool *sd.ServicePool, config *types.AppConfig) {
 | 
			
		||||
			pool.InitServices(config.SdConfigs)
 | 
			
		||||
			if pool.HasAvailableService() {
 | 
			
		||||
				pool.CheckTaskNotify()
 | 
			
		||||
				pool.CheckTaskStatus()
 | 
			
		||||
@@ -303,6 +305,7 @@ func main() {
 | 
			
		||||
			group.POST("active", h.Active)
 | 
			
		||||
			group.GET("config/get/license", h.GetLicense)
 | 
			
		||||
			group.GET("config/get/draw", h.GetDrawingConfig)
 | 
			
		||||
			group.POST("config/update/draw", h.SaveDrawingConfig)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/")
 | 
			
		||||
 
 | 
			
		||||
@@ -31,48 +31,15 @@ type ServicePool struct {
 | 
			
		||||
	db              *gorm.DB
 | 
			
		||||
	uploaderManager *oss.UploaderManager
 | 
			
		||||
	Clients         *types.LMap[uint, *types.WsClient] // UserId => Client
 | 
			
		||||
	licenseService *service.LicenseService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig, licenseService *service.LicenseService) *ServicePool {
 | 
			
		||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService) *ServicePool {
 | 
			
		||||
	services := make([]*Service, 0)
 | 
			
		||||
	taskQueue := store.NewRedisQueue("MidJourney_Task_Queue", redisCli)
 | 
			
		||||
	notifyQueue := store.NewRedisQueue("MidJourney_Notify_Queue", redisCli)
 | 
			
		||||
 | 
			
		||||
	for k, config := range appConfig.MjPlusConfigs {
 | 
			
		||||
		if config.Enabled == false {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		err := licenseService.IsValidApiURL(config.ApiURL)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error(err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		cli := NewPlusClient(config)
 | 
			
		||||
		name := fmt.Sprintf("mj-plus-service-%d", k)
 | 
			
		||||
		plusService := NewService(name, taskQueue, notifyQueue, db, cli)
 | 
			
		||||
		go func() {
 | 
			
		||||
			plusService.Run()
 | 
			
		||||
		}()
 | 
			
		||||
		services = append(services, plusService)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// for mid-journey proxy
 | 
			
		||||
	for k, config := range appConfig.MjProxyConfigs {
 | 
			
		||||
		if config.Enabled == false {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		cli := NewProxyClient(config)
 | 
			
		||||
		name := fmt.Sprintf("mj-proxy-service-%d", k)
 | 
			
		||||
		proxyService := NewService(name, taskQueue, notifyQueue, db, cli)
 | 
			
		||||
		go func() {
 | 
			
		||||
			proxyService.Run()
 | 
			
		||||
		}()
 | 
			
		||||
		services = append(services, proxyService)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &ServicePool{
 | 
			
		||||
		taskQueue:       taskQueue,
 | 
			
		||||
		notifyQueue:     notifyQueue,
 | 
			
		||||
@@ -80,6 +47,47 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
 | 
			
		||||
		uploaderManager: manager,
 | 
			
		||||
		db:              db,
 | 
			
		||||
		Clients:         types.NewLMap[uint, *types.WsClient](),
 | 
			
		||||
		licenseService:  licenseService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *ServicePool) InitServices(plusConfigs []types.MjPlusConfig, proxyConfigs []types.MjProxyConfig) {
 | 
			
		||||
	// stop old service
 | 
			
		||||
	for _, s := range p.services {
 | 
			
		||||
		s.Stop()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, config := range plusConfigs {
 | 
			
		||||
		if config.Enabled == false {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		err := p.licenseService.IsValidApiURL(config.ApiURL)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorf("创建 MJ-PLUS 服务失败:%v", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cli := NewPlusClient(config)
 | 
			
		||||
		name := fmt.Sprintf("mj-plus-service-%d", k)
 | 
			
		||||
		plusService := NewService(name, p.taskQueue, p.notifyQueue, p.db, cli)
 | 
			
		||||
		go func() {
 | 
			
		||||
			plusService.Run()
 | 
			
		||||
		}()
 | 
			
		||||
		p.services = append(p.services, plusService)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// for mid-journey proxy
 | 
			
		||||
	for k, config := range proxyConfigs {
 | 
			
		||||
		if config.Enabled == false {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		cli := NewProxyClient(config)
 | 
			
		||||
		name := fmt.Sprintf("mj-proxy-service-%d", k)
 | 
			
		||||
		proxyService := NewService(name, p.taskQueue, p.notifyQueue, p.db, cli)
 | 
			
		||||
		go func() {
 | 
			
		||||
			proxyService.Run()
 | 
			
		||||
		}()
 | 
			
		||||
		p.services = append(p.services, proxyService)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ type Service struct {
 | 
			
		||||
	taskQueue   *store.RedisQueue
 | 
			
		||||
	notifyQueue *store.RedisQueue
 | 
			
		||||
	db          *gorm.DB
 | 
			
		||||
	running bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewService(name string, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, cli Client) *Service {
 | 
			
		||||
@@ -37,12 +38,13 @@ func NewService(name string, taskQueue *store.RedisQueue, notifyQueue *store.Red
 | 
			
		||||
		taskQueue:   taskQueue,
 | 
			
		||||
		notifyQueue: notifyQueue,
 | 
			
		||||
		Client:      cli,
 | 
			
		||||
		running: true,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Run() {
 | 
			
		||||
	logger.Infof("Starting MidJourney job consumer for %s", s.Name)
 | 
			
		||||
	for {
 | 
			
		||||
	for s.running {
 | 
			
		||||
		var task types.MjTask
 | 
			
		||||
		err := s.taskQueue.LPop(&task)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -125,6 +127,11 @@ func (s *Service) Run() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Stop() {
 | 
			
		||||
	s.running = false
 | 
			
		||||
	s.Client = nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CBReq struct {
 | 
			
		||||
	Id          string      `json:"id"`
 | 
			
		||||
	Action      string      `json:"action"`
 | 
			
		||||
 
 | 
			
		||||
@@ -25,28 +25,14 @@ type ServicePool struct {
 | 
			
		||||
	notifyQueue *store.RedisQueue
 | 
			
		||||
	db          *gorm.DB
 | 
			
		||||
	Clients     *types.LMap[uint, *types.WsClient] // UserId => Client
 | 
			
		||||
	uploader *oss.UploaderManager
 | 
			
		||||
	levelDB  *store.LevelDB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig, levelDB *store.LevelDB) *ServicePool {
 | 
			
		||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, levelDB *store.LevelDB) *ServicePool {
 | 
			
		||||
	services := make([]*Service, 0)
 | 
			
		||||
	taskQueue := store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli)
 | 
			
		||||
	notifyQueue := store.NewRedisQueue("StableDiffusion_Queue", redisCli)
 | 
			
		||||
	// create mj client and service
 | 
			
		||||
	for _, config := range appConfig.SdConfigs {
 | 
			
		||||
		if config.Enabled == false {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// create sd service
 | 
			
		||||
		name := fmt.Sprintf("StableDifffusion Service-%s", config.Model)
 | 
			
		||||
		service := NewService(name, config, taskQueue, notifyQueue, db, manager, levelDB)
 | 
			
		||||
		// run sd service
 | 
			
		||||
		go func() {
 | 
			
		||||
			service.Run()
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		services = append(services, service)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &ServicePool{
 | 
			
		||||
		taskQueue:   taskQueue,
 | 
			
		||||
@@ -54,6 +40,31 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
 | 
			
		||||
		services:    services,
 | 
			
		||||
		db:          db,
 | 
			
		||||
		Clients:     types.NewLMap[uint, *types.WsClient](),
 | 
			
		||||
		uploader: manager,
 | 
			
		||||
		levelDB:  levelDB,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *ServicePool) InitServices(configs []types.StableDiffusionConfig) {
 | 
			
		||||
	// stop old service
 | 
			
		||||
	for _, s := range p.services {
 | 
			
		||||
		s.Stop()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, config := range configs {
 | 
			
		||||
		if config.Enabled == false {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// create sd service
 | 
			
		||||
		name := fmt.Sprintf(" sd-service-%d", k)
 | 
			
		||||
		service := NewService(name, config, p.taskQueue, p.notifyQueue, p.db, p.uploader, p.levelDB)
 | 
			
		||||
		// run sd service
 | 
			
		||||
		go func() {
 | 
			
		||||
			service.Run()
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		p.services = append(p.services, service)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ type Service struct {
 | 
			
		||||
	uploadManager *oss.UploaderManager
 | 
			
		||||
	name          string // service name
 | 
			
		||||
	leveldb       *store.LevelDB
 | 
			
		||||
	running       bool // 运行状态
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewService(name string, config types.StableDiffusionConfig, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelDB) *Service {
 | 
			
		||||
@@ -46,18 +47,20 @@ func NewService(name string, config types.StableDiffusionConfig, taskQueue *stor
 | 
			
		||||
		db:            db,
 | 
			
		||||
		leveldb:       levelDB,
 | 
			
		||||
		uploadManager: manager,
 | 
			
		||||
		running:       true,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Run() {
 | 
			
		||||
	for {
 | 
			
		||||
	logger.Infof("Starting Stable-Diffusion job consumer for %s", s.name)
 | 
			
		||||
	for s.running {
 | 
			
		||||
		var task types.SdTask
 | 
			
		||||
		err := s.taskQueue.LPop(&task)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorf("taking task with error: %v", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		logger.Infof("%s handle a new Stable-Diffusion task: %+v", s.name, task)
 | 
			
		||||
		// translate prompt
 | 
			
		||||
		if utils.HasChinese(task.Params.Prompt) {
 | 
			
		||||
			content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt))
 | 
			
		||||
@@ -94,6 +97,10 @@ func (s *Service) Run() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Stop() {
 | 
			
		||||
	s.running = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Txt2ImgReq 文生图请求实体
 | 
			
		||||
type Txt2ImgReq struct {
 | 
			
		||||
	Prompt            string  `json:"prompt"`
 | 
			
		||||
@@ -104,6 +111,7 @@ type Txt2ImgReq struct {
 | 
			
		||||
	Width             int     `json:"width"`
 | 
			
		||||
	Height            int     `json:"height"`
 | 
			
		||||
	SamplerName       string  `json:"sampler_name"`
 | 
			
		||||
	Scheduler         string  `json:"scheduler"`
 | 
			
		||||
	EnableHr          bool    `json:"enable_hr,omitempty"`
 | 
			
		||||
	HrScale           int     `json:"hr_scale,omitempty"`
 | 
			
		||||
	HrUpscaler        string  `json:"hr_upscaler,omitempty"`
 | 
			
		||||
@@ -137,6 +145,7 @@ func (s *Service) Txt2Img(task types.SdTask) error {
 | 
			
		||||
		Width:          task.Params.Width,
 | 
			
		||||
		Height:         task.Params.Height,
 | 
			
		||||
		SamplerName:    task.Params.Sampler,
 | 
			
		||||
		Scheduler:      task.Params.Scheduler,
 | 
			
		||||
		ForceTaskId:    task.Params.TaskId,
 | 
			
		||||
	}
 | 
			
		||||
	if task.Params.Seed > 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
			
		||||
    <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
 | 
			
		||||
    <link rel="icon" href="/favicon.ico" type="image/x-icon">
 | 
			
		||||
    <title>ChatGPT-Plus</title>
 | 
			
		||||
    <title>Geek-AI 创作助手</title>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
 
 | 
			
		||||
@@ -105,7 +105,7 @@ const routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '/admin/login',
 | 
			
		||||
        name: 'admin-login',
 | 
			
		||||
        meta: {title: 'ChatPuls 控制台登录'},
 | 
			
		||||
        meta: {title: 'Geek-AI 控制台登录'},
 | 
			
		||||
        component: () => import('@/views/admin/Login.vue'),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
@@ -113,7 +113,7 @@ const routes = [
 | 
			
		||||
        path: '/admin',
 | 
			
		||||
        redirect: '/admin/dashboard',
 | 
			
		||||
        component: () => import("@/views/admin/Home.vue"),
 | 
			
		||||
        meta: {title: 'ChatPuls 管理后台'},
 | 
			
		||||
        meta: {title: 'Geek-AI 控制台'},
 | 
			
		||||
        children: [
 | 
			
		||||
            {
 | 
			
		||||
                path: '/admin/dashboard',
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,28 @@
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div class="param-line" style="padding-top: 10px">
 | 
			
		||||
                <el-form-item label="采样调度器">
 | 
			
		||||
                  <template #default>
 | 
			
		||||
                    <div class="form-item-inner">
 | 
			
		||||
                      <el-select v-model="params.scheduler" style="width:176px">
 | 
			
		||||
                        <el-option v-for="item in schedulers" :label="item" :value="item" :key="item"/>
 | 
			
		||||
                      </el-select>
 | 
			
		||||
                      <el-tooltip
 | 
			
		||||
                          effect="light"
 | 
			
		||||
                          content="推荐自动或者 Karras"
 | 
			
		||||
                          raw-content
 | 
			
		||||
                          placement="right"
 | 
			
		||||
                      >
 | 
			
		||||
                        <el-icon class="info-icon">
 | 
			
		||||
                          <InfoFilled/>
 | 
			
		||||
                        </el-icon>
 | 
			
		||||
                      </el-tooltip>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </template>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div class="param-line">
 | 
			
		||||
                <el-form-item label="图片尺寸">
 | 
			
		||||
                  <template #default>
 | 
			
		||||
@@ -509,12 +531,14 @@ window.onresize = () => {
 | 
			
		||||
  listBoxHeight.value = window.innerHeight - 40
 | 
			
		||||
  paramBoxHeight.value = window.innerHeight - 150
 | 
			
		||||
}
 | 
			
		||||
const samplers = ["Euler a", "DPM++ 2S a Karras", "DPM++ 2M Karras", "DPM++ SDE Karras", "DPM++ 2M SDE Karras"]
 | 
			
		||||
const samplers = ["Euler a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE", "UniPC", "Restart"]
 | 
			
		||||
const schedulers = ["Automatic", "Karras", "Exponential", "Uniform"]
 | 
			
		||||
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]
 | 
			
		||||
const params = ref({
 | 
			
		||||
  width: 1024,
 | 
			
		||||
  height: 1024,
 | 
			
		||||
  sampler: samplers[0],
 | 
			
		||||
  scheduler: schedulers[0],
 | 
			
		||||
  seed: -1,
 | 
			
		||||
  steps: 20,
 | 
			
		||||
  cfg_scale: 7,
 | 
			
		||||
@@ -640,7 +664,7 @@ const fetchRunningJobs = () => {
 | 
			
		||||
  if (!isLogin.value) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 获取运行中的任务
 | 
			
		||||
  httpGet(`/api/sd/jobs?status=0`).then(res => {
 | 
			
		||||
    const jobs = res.data
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,34 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-form label-width="150px" label-position="right">
 | 
			
		||||
  <el-form label-width="150px" label-position="right" class="draw-config">
 | 
			
		||||
    <el-tabs type="border-card">
 | 
			
		||||
      <el-tab-pane label="MJ-PLUS">
 | 
			
		||||
        <div v-if="mjPlusConfigs">
 | 
			
		||||
          <el-form-item label="网站标题">
 | 
			
		||||
            <el-input v-model="sdConfigs"/>
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
          <div class="config-item" v-for="(v,k) in mjPlusConfigs">
 | 
			
		||||
            <el-form-item label="是否启用">
 | 
			
		||||
              <el-switch v-model="v['Enabled']"/>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
            <el-form-item label="API 地址">
 | 
			
		||||
              <el-input v-model="v['ApiURL']" placeholder="API 地址"/>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
            <el-form-item label="API 令牌">
 | 
			
		||||
              <el-input v-model="v['ApiKey']" placeholder="API KEY"/>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
            <el-form-item label="绘画模式">
 | 
			
		||||
              <el-select v-model="v['Mode']" placeholder="请选择模式">
 | 
			
		||||
                <el-option v-for="item in mjModels" :value="item.value" :label="item.name" :key="item.value">{{
 | 
			
		||||
                    item.name
 | 
			
		||||
                  }}
 | 
			
		||||
                </el-option>
 | 
			
		||||
              </el-select>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
 | 
			
		||||
            <el-button class="remove" type="danger" :icon="Delete" circle @click="removeItem(mjPlusConfigs,k)"/>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <el-empty v-else></el-empty>
 | 
			
		||||
        <el-row style="justify-content: center">
 | 
			
		||||
          <el-button round>
 | 
			
		||||
 | 
			
		||||
        <el-row style="justify-content: center; padding: 10px">
 | 
			
		||||
          <el-button round @click="addConfig(mjPlusConfigs)">
 | 
			
		||||
            <el-icon><Plus /></el-icon>
 | 
			
		||||
            <span>新增配置</span>
 | 
			
		||||
          </el-button>
 | 
			
		||||
@@ -19,23 +38,66 @@
 | 
			
		||||
 | 
			
		||||
      <el-tab-pane label="MJ-PROXY">
 | 
			
		||||
        <div v-if="mjProxyConfigs">
 | 
			
		||||
          <el-form-item label="注册赠送算力">
 | 
			
		||||
            <el-input v-model.number="sdConfigs" placeholder="新用户注册赠送算力"/>
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
          <div class="config-item" v-for="(v,k) in mjProxyConfigs">
 | 
			
		||||
            <el-form-item label="是否启用">
 | 
			
		||||
              <el-switch v-model="v['Enabled']"/>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
            <el-form-item label="API 地址">
 | 
			
		||||
              <el-input v-model="v['ApiURL']" placeholder="API 地址"/>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
            <el-form-item label="API 令牌">
 | 
			
		||||
              <el-input v-model="v['ApiKey']" placeholder="API KEY"/>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
 | 
			
		||||
            <el-button class="remove" type="danger" :icon="Delete" circle @click="removeItem(mjProxyConfigs,k)"/>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <el-empty v-else />
 | 
			
		||||
 | 
			
		||||
        <el-row style="justify-content: center; padding: 10px">
 | 
			
		||||
          <el-button round @click="addConfig(mjProxyConfigs)">
 | 
			
		||||
            <el-icon>
 | 
			
		||||
              <Plus/>
 | 
			
		||||
            </el-icon>
 | 
			
		||||
            <span>新增配置</span>
 | 
			
		||||
          </el-button>
 | 
			
		||||
        </el-row>
 | 
			
		||||
      </el-tab-pane>
 | 
			
		||||
 | 
			
		||||
      <el-tab-pane label="Stable-Diffusion">
 | 
			
		||||
        <el-form-item label="注册赠送算力">
 | 
			
		||||
          <el-input v-model.number="sdConfigs" placeholder="新用户注册赠送算力"/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <div v-if="sdConfigs">
 | 
			
		||||
          <div class="config-item" v-for="(v,k) in sdConfigs">
 | 
			
		||||
            <el-form-item label="是否启用">
 | 
			
		||||
              <el-switch v-model="v['Enabled']"/>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
            <el-form-item label="API 地址">
 | 
			
		||||
              <el-input v-model="v['ApiURL']" placeholder="API 地址"/>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
            <el-form-item label="API 令牌">
 | 
			
		||||
              <el-input v-model="v['ApiKey']" placeholder="API KEY"/>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
            <el-form-item label="模型">
 | 
			
		||||
              <el-input v-model="v['Model']" placeholder="绘画模型"/>
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
            <el-button class="remove" type="danger" :icon="Delete" circle @click="removeItem(sdConfigs,k)"/>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <el-empty v-else/>
 | 
			
		||||
 | 
			
		||||
        <el-row style="justify-content: center; padding: 10px">
 | 
			
		||||
          <el-button round @click="addConfig(sdConfigs)">
 | 
			
		||||
            <el-icon>
 | 
			
		||||
              <Plus/>
 | 
			
		||||
            </el-icon>
 | 
			
		||||
            <span>新增配置</span>
 | 
			
		||||
          </el-button>
 | 
			
		||||
        </el-row>
 | 
			
		||||
      </el-tab-pane>
 | 
			
		||||
    </el-tabs>
 | 
			
		||||
 | 
			
		||||
    <div style="padding: 10px;">
 | 
			
		||||
      <el-form-item>
 | 
			
		||||
        <el-button type="primary" @click="save('system')">保存</el-button>
 | 
			
		||||
        <el-button type="primary" @click="saveConfig()">保存</el-button>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </div>
 | 
			
		||||
  </el-form>
 | 
			
		||||
@@ -43,14 +105,19 @@
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import {httpGet} from "@/utils/http";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {Plus} from "@element-plus/icons-vue";
 | 
			
		||||
import {Delete, Plus} from "@element-plus/icons-vue";
 | 
			
		||||
 | 
			
		||||
// 变量定义
 | 
			
		||||
const sdConfigs = ref([])
 | 
			
		||||
const mjPlusConfigs = ref([])
 | 
			
		||||
const mjProxyConfigs = ref([])
 | 
			
		||||
const mjModels = ref([
 | 
			
		||||
  {name: "慢速(Relax)", value: "relax"},
 | 
			
		||||
  {name: "快速(Fast)", value: "fast"},
 | 
			
		||||
  {name: "急速(Turbo)", value: "turbo"},
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
httpGet("/api/admin/config/get/draw").then(res => {
 | 
			
		||||
  sdConfigs.value = res.data.sd
 | 
			
		||||
@@ -59,29 +126,48 @@ httpGet("/api/admin/config/get/draw").then(res => {
 | 
			
		||||
}).catch(e =>{
 | 
			
		||||
  ElMessage.error("获取AI绘画配置失败:"+e.message)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const addConfig = (configs) => {
 | 
			
		||||
  configs.push({
 | 
			
		||||
    Enabled: true,
 | 
			
		||||
    ApiKey: '',
 | 
			
		||||
    ApiURL: '',
 | 
			
		||||
    Mode: 'fast'
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const saveConfig = () => {
 | 
			
		||||
  httpPost('/api/admin/config/update/draw', {
 | 
			
		||||
    'sd': sdConfigs.value,
 | 
			
		||||
    'mj_plus': mjPlusConfigs.value,
 | 
			
		||||
    'mj_proxy': mjProxyConfigs.value
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
    ElMessage.success("配置更新成功")
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("操作失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const removeItem = (arr, k) => {
 | 
			
		||||
  arr.splice(k, 1)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.menu {
 | 
			
		||||
.draw-config {
 | 
			
		||||
 | 
			
		||||
  .opt-box {
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
    display flex;
 | 
			
		||||
    justify-content flex-end
 | 
			
		||||
  .config-item {
 | 
			
		||||
    position relative
 | 
			
		||||
    padding 15px 10px 10px 10px
 | 
			
		||||
    border 1px solid var(--el-border-color)
 | 
			
		||||
    border-radius 10px
 | 
			
		||||
    margin-bottom 10px
 | 
			
		||||
 | 
			
		||||
    .el-icon {
 | 
			
		||||
      margin-right: 5px;
 | 
			
		||||
    .remove {
 | 
			
		||||
      position absolute
 | 
			
		||||
      right 15px
 | 
			
		||||
      top 15px
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .menu-icon {
 | 
			
		||||
    width 36px
 | 
			
		||||
    height 36px
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .el-select {
 | 
			
		||||
    width: 100%
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -2,6 +2,10 @@
 | 
			
		||||
  <div class="admin-login">
 | 
			
		||||
    <div class="main">
 | 
			
		||||
      <div class="contain">
 | 
			
		||||
        <div class="logo">
 | 
			
		||||
          <el-image :src="logo" fit="cover" @click="router.push('/')"/>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="header">{{ title }}</div>
 | 
			
		||||
        <div class="content">
 | 
			
		||||
          <div class="block">
 | 
			
		||||
@@ -42,9 +46,9 @@
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
 | 
			
		||||
import {onMounted, ref} from "vue";
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import {Lock, UserFilled} from "@element-plus/icons-vue";
 | 
			
		||||
import {httpPost} from "@/utils/http";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
import FooterBar from "@/components/FooterBar.vue";
 | 
			
		||||
@@ -52,15 +56,24 @@ import {setAdminToken} from "@/store/session";
 | 
			
		||||
import {checkAdminSession} from "@/action/session";
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const title = ref('ChatGPT Plus Admin');
 | 
			
		||||
const title = ref('Geek-AI Console');
 | 
			
		||||
const username = ref(process.env.VUE_APP_ADMIN_USER);
 | 
			
		||||
const password = ref(process.env.VUE_APP_ADMIN_PASS);
 | 
			
		||||
const logo = ref("/images/logo.png")
 | 
			
		||||
 | 
			
		||||
checkAdminSession().then(() => {
 | 
			
		||||
  router.push("/admin")
 | 
			
		||||
}).catch(() => {
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 加载系统配置
 | 
			
		||||
httpGet('/api/config/get?key=system').then(res => {
 | 
			
		||||
  title.value = res.data.admin_title
 | 
			
		||||
  logo.value = res.data.logo
 | 
			
		||||
}).catch(e => {
 | 
			
		||||
  ElMessage.error("加载系统配置失败: " + e.message)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const keyupHandle = (e) => {
 | 
			
		||||
  if (e.key === 'Enter') {
 | 
			
		||||
    login();
 | 
			
		||||
@@ -107,10 +120,20 @@ const login = function () {
 | 
			
		||||
      border-radius 10px;
 | 
			
		||||
      background rgba(255, 255, 255, 0.3)
 | 
			
		||||
 | 
			
		||||
      .logo {
 | 
			
		||||
        text-align center
 | 
			
		||||
 | 
			
		||||
        .el-image {
 | 
			
		||||
          width 120px;
 | 
			
		||||
          cursor pointer
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .header {
 | 
			
		||||
        width 100%
 | 
			
		||||
        margin-bottom 20px
 | 
			
		||||
        font-size 24px
 | 
			
		||||
        padding 10px
 | 
			
		||||
        font-size 26px
 | 
			
		||||
        text-align center
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -279,7 +279,9 @@
 | 
			
		||||
      <el-tab-pane label="公告配置" name="notice">
 | 
			
		||||
        <md-editor class="mgb20" v-model="notice" @on-upload-img="onUploadImg"/>
 | 
			
		||||
        <el-form-item>
 | 
			
		||||
          <el-button type="primary" @click="save('notice')">保存</el-button>
 | 
			
		||||
          <div style="padding-top: 10px;margin-left: 150px;">
 | 
			
		||||
            <el-button type="primary" @click="save('notice')">保存</el-button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
      </el-tab-pane>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user