diff --git a/api/core/config.go b/api/core/config.go index a9032d9c..7f2fc971 100644 --- a/api/core/config.go +++ b/api/core/config.go @@ -157,6 +157,24 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig { logger.Error("load moderation config error: ", err) } + // 加载即梦AI配置 + var jimengConfig types.JimengConfig + sysConfig.Id = 0 + db.Where("name", types.ConfigKeyJimeng).First(&sysConfig) + err = utils.JsonDecode(sysConfig.Value, &jimengConfig) + if err != nil { + logger.Error("load jimeng config error: ", err) + } + + // 加载3D生成配置 + var ai3dConfig types.AI3DConfig + sysConfig.Id = 0 + db.Where("name", types.ConfigKeyAI3D).First(&sysConfig) + err = utils.JsonDecode(sysConfig.Value, &ai3dConfig) + if err != nil { + logger.Error("load ai3d config error: ", err) + } + return &types.SystemConfig{ Base: baseConfig, License: license, @@ -167,5 +185,7 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig { Captcha: captchaConfig, WxLogin: wxLoginConfig, Moderation: moderationConfig, + Jimeng: jimengConfig, + AI3D: ai3dConfig, } } diff --git a/api/core/types/ai3d.go b/api/core/types/ai3d.go new file mode 100644 index 00000000..a32fee1c --- /dev/null +++ b/api/core/types/ai3d.go @@ -0,0 +1,58 @@ +package types + +// AI3DConfig 3D生成配置 +type AI3DConfig struct { + Tencent Tencent3DConfig `json:"tencent,omitempty"` + Gitee Gitee3DConfig `json:"gitee,omitempty"` +} + +// Tencent3DConfig 腾讯云3D配置 +type Tencent3DConfig struct { + SecretId string `json:"secret_id,omitempty"` + SecretKey string `json:"secret_key,omitempty"` + Region string `json:"region,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Models []AI3DModel `json:"models,omitempty"` +} + +// Gitee3DConfig Gitee 3D配置 +type Gitee3DConfig struct { + APIKey string `json:"api_key,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Models []AI3DModel `json:"models,omitempty"` +} + +// AI3DJobResult 3D任务结果 +type AI3DJobResult struct { + JobId string `json:"job_id"` // 任务ID + Status string `json:"status"` // 任务状态 + Progress int `json:"progress"` // 任务进度 (0-100) + FileURL string `json:"file_url"` // 3D模型文件URL + PreviewURL string `json:"preview_url"` // 预览图片URL + ErrorMsg string `json:"error_msg"` // 错误信息 +} + +// AI3DModel 3D模型配置 +type AI3DModel struct { + Name string `json:"name"` // 模型名称 + Desc string `json:"desc"` // 模型描述 + Power int `json:"power"` // 算力消耗 + Formats []string `json:"formats"` // 支持输出的文件格式 +} + +// AI3DJobRequest 3D任务请求 +type AI3DJobRequest struct { + Type string `json:"type"` // API类型 (tencent/gitee) + Model string `json:"model"` // 3D模型类型 + Prompt string `json:"prompt"` // 文本提示词 + ImageURL string `json:"image_url"` // 输入图片URL + Power int `json:"power"` // 消耗算力 +} + +// AI3DJobStatus 3D任务状态 +const ( + AI3DJobStatusPending = "pending" // 等待中 + AI3DJobStatusProcessing = "processing" // 处理中 + AI3DJobStatusCompleted = "completed" // 已完成 + AI3DJobStatusFailed = "failed" // 失败 +) diff --git a/api/core/types/config.go b/api/core/types/config.go index 9c903b8c..70f9bbf2 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -108,6 +108,7 @@ type SystemConfig struct { Captcha CaptchaConfig WxLogin WxLoginConfig Jimeng JimengConfig + AI3D AI3DConfig License License Moderation ModerationConfig } @@ -127,4 +128,6 @@ const ( ConfigKeyOss = "oss" ConfigKeyPayment = "payment" ConfigKeyModeration = "moderation" + ConfigKeyAI3D = "ai3d" + ConfigKeyJimeng = "jimeng" ) diff --git a/api/go.mod b/api/go.mod index 37a6180d..d5ac26e0 100644 --- a/api/go.mod +++ b/api/go.mod @@ -33,6 +33,8 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible github.com/shopspring/decimal v1.3.1 github.com/syndtr/goleveldb v1.0.0 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ai3d v1.1.0 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.21 golang.org/x/image v0.15.0 ) @@ -46,8 +48,6 @@ require ( github.com/go-pay/xtime v0.0.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ai3d v1.1.0 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.20 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect diff --git a/api/go.sum b/api/go.sum index b33b4082..b1388b00 100644 --- a/api/go.sum +++ b/api/go.sum @@ -248,8 +248,8 @@ github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpP github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ai3d v1.1.0 h1:hOyYsl35o74hOhnnPVQIK/bdSIPNp3TKJlCEOXGO7ms= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ai3d v1.1.0/go.mod h1:3689peGF1zp+P9c+GnUcAzkMp+kXi0Tr44zeQ57Z+7Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.0/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.20 h1:8B80/p+WvzBVz+jM6dosTcfhRe7Jotpyqj0NoGW1wfE= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.20/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.21 h1:ikHhyiq1PiPytUMtEblKPkbf0zzTEi3CpE9z0MARlqY= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.21/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= diff --git a/api/handler/admin/ai3d_handler.go b/api/handler/admin/ai3d_handler.go new file mode 100644 index 00000000..0c335748 --- /dev/null +++ b/api/handler/admin/ai3d_handler.go @@ -0,0 +1,216 @@ +package admin + +import ( + "strconv" + + "geekai/core" + "geekai/core/types" + "geekai/service/ai3d" + "geekai/store/model" + "geekai/store/vo" + "geekai/utils" + "geekai/utils/resp" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// AI3DHandler 3D管理处理器 +type AI3DHandler struct { + app *core.AppServer + db *gorm.DB + service *ai3d.Service +} + +// NewAI3DHandler 创建3D管理处理器 +func NewAI3DHandler(app *core.AppServer, db *gorm.DB, service *ai3d.Service) *AI3DHandler { + return &AI3DHandler{ + app: app, + db: db, + service: service, + } +} + +// RegisterRoutes 注册路由 +func (h *AI3DHandler) RegisterRoutes() { + admin := h.app.Engine.Group("/api/admin/ai3d") + { + admin.GET("/jobs", h.GetJobList) + admin.GET("/jobs/:id", h.GetJobDetail) + admin.DELETE("/jobs/:id", h.DeleteJob) + admin.GET("/stats", h.GetStats) + admin.GET("/models", h.GetModels) + admin.POST("/config", h.SaveConfig) + } +} + +// GetJobList 获取任务列表 +func (h *AI3DHandler) GetJobList(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + status := c.Query("status") + jobType := c.Query("type") + userIdStr := c.Query("user_id") + + var userId uint + if userIdStr != "" { + if id, err := strconv.ParseUint(userIdStr, 10, 32); err == nil { + userId = uint(id) + } + } + + // 构建查询条件 + query := h.db.Model(&model.AI3DJob{}) + + if status != "" { + query = query.Where("status = ?", status) + } + + if jobType != "" { + query = query.Where("type = ?", jobType) + } + + if userId > 0 { + query = query.Where("user_id = ?", userId) + } + + // 获取总数 + var total int64 + query.Count(&total) + + // 获取分页数据 + var jobs []model.AI3DJob + offset := (page - 1) * pageSize + err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&jobs).Error + + if err != nil { + resp.ERROR(c, "获取任务列表失败") + return + } + + // 转换为VO + var jobList []vo.AI3DJob + for _, job := range jobs { + var jobVo vo.AI3DJob + err = utils.CopyObject(job, &jobVo) + if err != nil { + continue + } + jobList = append(jobList, jobVo) + } + + resp.SUCCESS(c, vo.NewPage(total, page, pageSize, jobList)) +} + +// GetJobDetail 获取任务详情 +func (h *AI3DHandler) GetJobDetail(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + resp.ERROR(c, "无效的任务ID") + return + } + + var job model.AI3DJob + err = h.db.First(&job, uint(id)).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + resp.ERROR(c, "任务不存在") + } else { + resp.ERROR(c, "获取任务详情失败") + } + return + } + + var jobVo vo.AI3DJob + err = utils.CopyObject(job, &jobVo) + if err != nil { + resp.ERROR(c, "获取任务详情失败") + return + } + resp.SUCCESS(c, jobVo) +} + +// DeleteJob 删除任务 +func (h *AI3DHandler) DeleteJob(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + resp.ERROR(c, "无效的任务ID") + return + } + + // 检查任务是否存在 + var job model.AI3DJob + err = h.db.First(&job, uint(id)).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + resp.ERROR(c, "任务不存在") + } else { + resp.ERROR(c, "获取任务失败") + } + return + } + + // 删除任务 + err = h.db.Delete(&job).Error + if err != nil { + resp.ERROR(c, "删除任务失败") + return + } + + resp.SUCCESS(c, "删除成功") +} + +// GetStats 获取统计数据 +func (h *AI3DHandler) GetStats(c *gin.Context) { + var stats struct { + Pending int64 `json:"pending"` + Processing int64 `json:"processing"` + Completed int64 `json:"completed"` + Failed int64 `json:"failed"` + } + + // 统计各状态的任务数量 + h.db.Model(&model.AI3DJob{}).Where("status = ?", "pending").Count(&stats.Pending) + h.db.Model(&model.AI3DJob{}).Where("status = ?", "processing").Count(&stats.Processing) + h.db.Model(&model.AI3DJob{}).Where("status = ?", "completed").Count(&stats.Completed) + h.db.Model(&model.AI3DJob{}).Where("status = ?", "failed").Count(&stats.Failed) + + resp.SUCCESS(c, stats) +} + +// GetModels 获取配置 +func (h *AI3DHandler) GetModels(c *gin.Context) { + models := h.service.GetSupportedModels() + resp.SUCCESS(c, models) +} + +// SaveGlobalSettings 保存全局配置 +func (h *AI3DHandler) SaveConfig(c *gin.Context) { + var config types.AI3DConfig + err := c.ShouldBindJSON(&config) + if err != nil { + resp.ERROR(c, "参数错误") + return + } + var exist model.Config + err = h.db.Where("name", types.ConfigKeyAI3D).First(&exist).Error + if err != nil { + exist.Name = types.ConfigKeyAI3D + exist.Value = utils.JsonEncode(config) + err = h.db.Create(&exist).Error + } else { + exist.Value = utils.JsonEncode(config) + err = h.db.Updates(&exist).Error + } + if err != nil { + resp.ERROR(c, "保存配置失败") + return + } + + h.service.UpdateConfig(config) + h.app.SysConfig.AI3D = config + + resp.SUCCESS(c, "保存成功") +} diff --git a/api/handler/admin/config_handler.go b/api/handler/admin/config_handler.go index 48cfd34c..c9210149 100644 --- a/api/handler/admin/config_handler.go +++ b/api/handler/admin/config_handler.go @@ -8,13 +8,11 @@ package admin // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( - "fmt" "geekai/core" "geekai/core/middleware" "geekai/core/types" "geekai/handler" "geekai/service" - "geekai/service/moderation" "geekai/service/oss" "geekai/service/payment" "geekai/service/sms" @@ -28,17 +26,16 @@ import ( type ConfigHandler struct { handler.BaseHandler - licenseService *service.LicenseService - sysConfig *types.SystemConfig - alipayService *payment.AlipayService - wxpayService *payment.WxPayService - epayService *payment.EPayService - smsManager *sms.SmsManager - uploaderManager *oss.UploaderManager - smtpService *service.SmtpService - captchaService *service.CaptchaService - wxLoginService *service.WxLoginService - moderationManager *moderation.ServiceManager + licenseService *service.LicenseService + sysConfig *types.SystemConfig + alipayService *payment.AlipayService + wxpayService *payment.WxPayService + epayService *payment.EPayService + smsManager *sms.SmsManager + uploaderManager *oss.UploaderManager + smtpService *service.SmtpService + captchaService *service.CaptchaService + wxLoginService *service.WxLoginService } func NewConfigHandler( @@ -54,21 +51,19 @@ func NewConfigHandler( smtpService *service.SmtpService, captchaService *service.CaptchaService, wxLoginService *service.WxLoginService, - moderationManager *moderation.ServiceManager, ) *ConfigHandler { return &ConfigHandler{ - BaseHandler: handler.BaseHandler{App: app, DB: db}, - licenseService: licenseService, - sysConfig: sysConfig, - alipayService: alipayService, - wxpayService: wxpayService, - epayService: epayService, - smsManager: smsManager, - uploaderManager: uploaderManager, - moderationManager: moderationManager, - smtpService: smtpService, - captchaService: captchaService, - wxLoginService: wxLoginService, + BaseHandler: handler.BaseHandler{App: app, DB: db}, + licenseService: licenseService, + sysConfig: sysConfig, + alipayService: alipayService, + wxpayService: wxpayService, + epayService: epayService, + smsManager: smsManager, + uploaderManager: uploaderManager, + smtpService: smtpService, + captchaService: captchaService, + wxLoginService: wxLoginService, } } @@ -91,8 +86,6 @@ func (h *ConfigHandler) RegisterRoutes() { rg.POST("update/sms", h.UpdateSms) rg.POST("update/oss", h.UpdateOss) rg.POST("update/smtp", h.UpdateStmp) - rg.POST("update/moderation", h.UpdateModeration) - rg.POST("moderation/test", h.TestModeration) rg.GET("get", h.Get) rg.POST("license/active", h.Active) rg.GET("license/get", h.GetLicense) @@ -450,90 +443,3 @@ func (h *ConfigHandler) GetLicense(c *gin.Context) { license := h.licenseService.GetLicense() resp.SUCCESS(c, license) } - -// UpdateModeration 更新文本审查配置 -func (h *ConfigHandler) UpdateModeration(c *gin.Context) { - var data types.ModerationConfig - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } - - err := h.Update(types.ConfigKeyModeration, data) - if err != nil { - resp.ERROR(c, err.Error()) - return - } - - h.moderationManager.UpdateConfig(data) - h.sysConfig.Moderation = data - - resp.SUCCESS(c, data) -} - -// 测试结果类型,用于前端显示 -type ModerationTestResult struct { - IsAbnormal bool `json:"isAbnormal"` - Details []ModerationTestDetail `json:"details"` -} - -type ModerationTestDetail struct { - Category string `json:"category"` - Description string `json:"description"` - Confidence string `json:"confidence"` - IsCategory bool `json:"isCategory"` -} - -// TestModeration 测试文本审查服务 -func (h *ConfigHandler) TestModeration(c *gin.Context) { - var data struct { - Text string `json:"text"` - Service string `json:"service"` - } - - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } - - if data.Text == "" { - resp.ERROR(c, "测试文本不能为空") - return - } - - // 检查是否启用了文本审查 - if !h.sysConfig.Moderation.Enable { - resp.ERROR(c, "文本审查服务未启用") - return - } - - // 获取当前激活的审核服务 - service := h.moderationManager.GetService() - // 执行文本审核 - result, err := service.Moderate(data.Text) - if err != nil { - resp.ERROR(c, "审核服务调用失败: "+err.Error()) - return - } - - // 转换为前端需要的格式 - testResult := ModerationTestResult{ - IsAbnormal: result.Flagged, - Details: make([]ModerationTestDetail, 0), - } - - // 构建详细信息 - for category, description := range types.ModerationCategories { - score := result.CategoryScores[category] - isCategory := result.Categories[category] - - testResult.Details = append(testResult.Details, ModerationTestDetail{ - Category: category, - Description: description, - Confidence: fmt.Sprintf("%.2f", score), - IsCategory: isCategory, - }) - } - - resp.SUCCESS(c, testResult) -} diff --git a/api/handler/admin/jimeng_handler.go b/api/handler/admin/jimeng_handler.go index 0637f69a..174e284a 100644 --- a/api/handler/admin/jimeng_handler.go +++ b/api/handler/admin/jimeng_handler.go @@ -21,18 +21,18 @@ import ( // AdminJimengHandler 管理后台即梦AI处理器 type AdminJimengHandler struct { handler.BaseHandler - jimengService *jimeng.Service - userService *service.UserService - uploader *oss.UploaderManager + jimengClient *jimeng.Client + userService *service.UserService + uploader *oss.UploaderManager } // NewAdminJimengHandler 创建管理后台即梦AI处理器 -func NewAdminJimengHandler(app *core.AppServer, db *gorm.DB, jimengService *jimeng.Service, userService *service.UserService, uploader *oss.UploaderManager) *AdminJimengHandler { +func NewAdminJimengHandler(app *core.AppServer, db *gorm.DB, jimengClient *jimeng.Client, userService *service.UserService, uploader *oss.UploaderManager) *AdminJimengHandler { return &AdminJimengHandler{ - BaseHandler: handler.BaseHandler{App: app, DB: db}, - jimengService: jimengService, - userService: userService, - uploader: uploader, + BaseHandler: handler.BaseHandler{App: app, DB: db}, + jimengClient: jimengClient, + userService: userService, + uploader: uploader, } } @@ -43,7 +43,6 @@ func (h *AdminJimengHandler) RegisterRoutes() { rg.GET("/jobs/:id", h.JobDetail) rg.POST("/jobs/remove", h.BatchRemove) rg.GET("/stats", h.Stats) - rg.GET("/config", h.GetConfig) rg.POST("/config/update", h.UpdateConfig) } @@ -213,12 +212,6 @@ func (h *AdminJimengHandler) Stats(c *gin.Context) { resp.SUCCESS(c, result) } -// GetConfig 获取即梦AI配置 -func (h *AdminJimengHandler) GetConfig(c *gin.Context) { - jimengConfig := h.jimengService.GetConfig() - resp.SUCCESS(c, jimengConfig) -} - // UpdateConfig 更新即梦AI配置 func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) { var req types.JimengConfig @@ -266,9 +259,9 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) { // 保存配置 tx := h.DB.Begin() value := utils.JsonEncode(&req) - config := model.Config{Name: "jimeng", Value: value} + config := model.Config{Name: types.ConfigKeyJimeng, Value: value} - err := tx.FirstOrCreate(&config, model.Config{Name: "jimeng"}).Error + err := tx.FirstOrCreate(&config).Error if err != nil { resp.ERROR(c, "保存配置失败: "+err.Error()) return @@ -284,13 +277,14 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) { } // 更新服务中的客户端配置 - updateErr := h.jimengService.UpdateClientConfig(req.AccessKey, req.SecretKey) - if updateErr != nil { - resp.ERROR(c, updateErr.Error()) + err = h.jimengClient.UpdateConfig(req) + if err != nil { + resp.ERROR(c, err.Error()) tx.Rollback() return } tx.Commit() + h.App.SysConfig.Jimeng = req resp.SUCCESS(c, gin.H{"message": "配置更新成功"}) } diff --git a/api/handler/admin/moderation_handler.go b/api/handler/admin/moderation_handler.go index 55cf25ce..c1d4b29c 100644 --- a/api/handler/admin/moderation_handler.go +++ b/api/handler/admin/moderation_handler.go @@ -8,10 +8,12 @@ package admin // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "fmt" "geekai/core" "geekai/core/middleware" "geekai/core/types" "geekai/handler" + "geekai/service/moderation" "geekai/store/model" "geekai/utils" "geekai/utils/resp" @@ -22,10 +24,12 @@ import ( type ModerationHandler struct { handler.BaseHandler + sysConfig *types.SystemConfig + moderationManager *moderation.ServiceManager } -func NewModerationHandler(app *core.AppServer, db *gorm.DB) *ModerationHandler { - return &ModerationHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}} +func NewModerationHandler(app *core.AppServer, db *gorm.DB, sysConfig *types.SystemConfig, moderationManager *moderation.ServiceManager) *ModerationHandler { + return &ModerationHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, sysConfig: sysConfig, moderationManager: moderationManager} } // RegisterRoutes 注册路由 @@ -39,6 +43,8 @@ func (h *ModerationHandler) RegisterRoutes() { group.GET("remove", h.Remove) group.POST("batch-remove", h.BatchRemove) group.GET("source-list", h.GetSourceList) + group.POST("config", h.UpdateModeration) + group.POST("test", h.TestModeration) } } @@ -229,3 +235,90 @@ func (h *ModerationHandler) GetSourceList(c *gin.Context) { resp.SUCCESS(c, sources) } + +// UpdateModeration 更新文本审查配置 +func (h *ModerationHandler) UpdateModeration(c *gin.Context) { + var data types.ModerationConfig + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + err := h.DB.Where("name", types.ConfigKeyModeration).FirstOrCreate(&model.Config{Name: types.ConfigKeyModeration, Value: utils.JsonEncode(data)}).Error + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + h.moderationManager.UpdateConfig(data) + h.sysConfig.Moderation = data + + resp.SUCCESS(c, data) +} + +// 测试结果类型,用于前端显示 +type ModerationTestResult struct { + IsAbnormal bool `json:"isAbnormal"` + Details []ModerationTestDetail `json:"details"` +} + +type ModerationTestDetail struct { + Category string `json:"category"` + Description string `json:"description"` + Confidence string `json:"confidence"` + IsCategory bool `json:"isCategory"` +} + +// TestModeration 测试文本审查服务 +func (h *ModerationHandler) TestModeration(c *gin.Context) { + var data struct { + Text string `json:"text"` + Service string `json:"service"` + } + + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + if data.Text == "" { + resp.ERROR(c, "测试文本不能为空") + return + } + + // 检查是否启用了文本审查 + if !h.sysConfig.Moderation.Enable { + resp.ERROR(c, "文本审查服务未启用") + return + } + + // 获取当前激活的审核服务 + service := h.moderationManager.GetService() + // 执行文本审核 + result, err := service.Moderate(data.Text) + if err != nil { + resp.ERROR(c, "审核服务调用失败: "+err.Error()) + return + } + + // 转换为前端需要的格式 + testResult := ModerationTestResult{ + IsAbnormal: result.Flagged, + Details: make([]ModerationTestDetail, 0), + } + + // 构建详细信息 + for category, description := range types.ModerationCategories { + score := result.CategoryScores[category] + isCategory := result.Categories[category] + + testResult.Details = append(testResult.Details, ModerationTestDetail{ + Category: category, + Description: description, + Confidence: fmt.Sprintf("%.2f", score), + IsCategory: isCategory, + }) + } + + resp.SUCCESS(c, testResult) +} diff --git a/api/handler/ai3d_handler.go b/api/handler/ai3d_handler.go new file mode 100644 index 00000000..28ad5ff2 --- /dev/null +++ b/api/handler/ai3d_handler.go @@ -0,0 +1,236 @@ +package handler + +import ( + "fmt" + "geekai/core" + "geekai/core/middleware" + "geekai/core/types" + "geekai/service" + "geekai/service/ai3d" + "geekai/store/vo" + "geekai/utils/resp" + "strconv" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type AI3DHandler struct { + BaseHandler + service *ai3d.Service + userService *service.UserService +} + +func NewAI3DHandler(app *core.AppServer, db *gorm.DB, service *ai3d.Service, userService *service.UserService) *AI3DHandler { + return &AI3DHandler{ + service: service, + userService: userService, + BaseHandler: BaseHandler{ + App: app, + DB: db, + }, + } +} + +// RegisterRoutes 注册路由 +func (h *AI3DHandler) RegisterRoutes() { + group := h.App.Engine.Group("/api/3d/") + + // 公开接口,不需要授权 + group.GET("models/:type", h.GetModels) + + // 需要用户授权的接口 + group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) + { + group.POST("generate", h.Generate) + group.GET("jobs", h.JobList) + group.GET("job/:id", h.JobDetail) + group.DELETE("job/:id", h.DeleteJob) + group.GET("download/:id", h.Download) + } +} + +// Generate 创建3D生成任务 +func (h *AI3DHandler) Generate(c *gin.Context) { + var request vo.AI3DJobCreate + if err := c.ShouldBindJSON(&request); err != nil { + resp.ERROR(c, "参数错误") + return + } + + // 验证必填参数 + if request.Type == "" || request.Model == "" || request.Power <= 0 { + resp.ERROR(c, "缺少必要参数") + return + } + + // 获取用户ID + userId := h.GetLoginUserId(c) + if userId == 0 { + resp.ERROR(c, "用户未登录") + return + } + + // 创建任务 + job, err := h.service.CreateJob(uint(userId), request) + if err != nil { + resp.ERROR(c, fmt.Sprintf("创建任务失败: %v", err)) + return + } + + resp.SUCCESS(c, gin.H{ + "job_id": job.Id, + "message": "任务创建成功", + }) +} + +// JobList 获取任务列表 +func (h *AI3DHandler) JobList(c *gin.Context) { + userId := h.GetLoginUserId(c) + if userId == 0 { + resp.ERROR(c, "用户未登录") + return + } + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 10 + } + + jobList, err := h.service.GetJobList(uint(userId), page, pageSize) + if err != nil { + resp.ERROR(c, fmt.Sprintf("获取任务列表失败: %v", err)) + return + } + + resp.SUCCESS(c, jobList) +} + +// JobDetail 获取任务详情 +func (h *AI3DHandler) JobDetail(c *gin.Context) { + userId := h.GetLoginUserId(c) + if userId == 0 { + resp.ERROR(c, "用户未登录") + return + } + + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + resp.ERROR(c, "任务ID格式错误") + return + } + + job, err := h.service.GetJobById(uint(id)) + if err != nil { + resp.ERROR(c, "任务不存在") + return + } + + // 检查权限 + if job.UserId != uint(userId) { + resp.ERROR(c, "无权限访问此任务") + return + } + + // 转换为VO + jobVO := vo.AI3DJob{ + Id: job.Id, + UserId: job.UserId, + Type: job.Type, + Power: job.Power, + TaskId: job.TaskId, + ImgURL: job.FileURL, + PreviewURL: job.PreviewURL, + Model: job.Model, + Status: job.Status, + ErrMsg: job.ErrMsg, + Params: job.Params, + CreatedAt: job.CreatedAt.Unix(), + UpdatedAt: job.UpdatedAt.Unix(), + } + + resp.SUCCESS(c, jobVO) +} + +// DeleteJob 删除任务 +func (h *AI3DHandler) DeleteJob(c *gin.Context) { + userId := h.GetLoginUserId(c) + if userId == 0 { + resp.ERROR(c, "用户未登录") + return + } + + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + resp.ERROR(c, "任务ID格式错误") + return + } + + err = h.service.DeleteJob(uint(id), uint(userId)) + if err != nil { + resp.ERROR(c, fmt.Sprintf("删除任务失败: %v", err)) + return + } + + resp.SUCCESS(c, gin.H{"message": "删除成功"}) +} + +// Download 下载3D模型 +func (h *AI3DHandler) Download(c *gin.Context) { + userId := h.GetLoginUserId(c) + if userId == 0 { + resp.ERROR(c, "用户未登录") + return + } + + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + resp.ERROR(c, "任务ID格式错误") + return + } + + job, err := h.service.GetJobById(uint(id)) + if err != nil { + resp.ERROR(c, "任务不存在") + return + } + + // 检查权限 + if job.UserId != uint(userId) { + resp.ERROR(c, "无权限访问此任务") + return + } + + // 检查任务状态 + if job.Status != types.AI3DJobStatusCompleted { + resp.ERROR(c, "任务尚未完成") + return + } + + if job.FileURL == "" { + resp.ERROR(c, "模型文件不存在") + return + } + + // 重定向到下载链接 + c.Redirect(302, job.FileURL) +} + +// GetModels 获取支持的模型列表 +func (h *AI3DHandler) GetModels(c *gin.Context) { + models := h.service.GetSupportedModels() + if len(models) == 0 { + resp.ERROR(c, "无可用3D模型") + return + } + + resp.SUCCESS(c, models) +} diff --git a/api/handler/jimeng_handler.go b/api/handler/jimeng_handler.go index b2b9670e..439da831 100644 --- a/api/handler/jimeng_handler.go +++ b/api/handler/jimeng_handler.go @@ -435,7 +435,7 @@ func (h *JimengHandler) Retry(c *gin.Context) { // getPowerFromConfig 从配置中获取指定类型的算力消耗 func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int { - config := h.jimengService.GetConfig() + config := h.App.SysConfig.Jimeng switch taskType { case model.JMTaskTypeTextToImage: @@ -457,7 +457,7 @@ func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int { // GetPowerConfig 获取即梦各任务类型算力消耗配置 func (h *JimengHandler) GetPowerConfig(c *gin.Context) { - config := h.jimengService.GetConfig() + config := h.App.SysConfig.Jimeng resp.SUCCESS(c, gin.H{ "text_to_image": config.Power.TextToImage, "image_to_image": config.Power.ImageToImage, diff --git a/api/main.go b/api/main.go index 33466274..d6092dd7 100644 --- a/api/main.go +++ b/api/main.go @@ -16,6 +16,7 @@ import ( "geekai/handler/admin" logger2 "geekai/logger" "geekai/service" + "geekai/service/ai3d" "geekai/service/dalle" "geekai/service/jimeng" "geekai/service/mj" @@ -210,10 +211,19 @@ func main() { }), // 即梦AI 服务 + fx.Provide(jimeng.NewClient), fx.Provide(jimeng.NewService), fx.Invoke(func(service *jimeng.Service) { service.Start() }), + + // 3D生成服务 + fx.Provide(ai3d.NewTencent3DClient), + fx.Provide(ai3d.NewGitee3DClient), + fx.Provide(ai3d.NewService), + fx.Invoke(func(s *ai3d.Service) { + s.Run() + }), fx.Provide(service.NewSnowflake), // 创建短信服务 @@ -383,6 +393,16 @@ func main() { h.RegisterRoutes() }), + // 3D生成处理器 + fx.Provide(handler.NewAI3DHandler), + fx.Invoke(func(s *core.AppServer, h *handler.AI3DHandler) { + h.RegisterRoutes() + }), + fx.Provide(admin.NewAI3DHandler), + fx.Invoke(func(s *core.AppServer, h *admin.AI3DHandler) { + h.RegisterRoutes() + }), + // 即梦AI 路由 fx.Invoke(func(s *core.AppServer, h *handler.JimengHandler) { h.RegisterRoutes() diff --git a/api/service/ai3d/gitee_client.go b/api/service/ai3d/gitee_client.go new file mode 100644 index 00000000..f83650b8 --- /dev/null +++ b/api/service/ai3d/gitee_client.go @@ -0,0 +1,150 @@ +package ai3d + +import ( + "encoding/json" + "fmt" + "geekai/core/types" + "time" + + "github.com/imroc/req/v3" +) + +type Gitee3DClient struct { + httpClient *req.Client + config types.Gitee3DConfig + apiURL string +} + +type Gitee3DParams struct { + Prompt string `json:"prompt"` // 文本提示词 + ImageURL string `json:"image_url"` // 输入图片URL + ResultFormat string `json:"result_format"` // 输出格式 +} + +type Gitee3DResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + TaskID string `json:"task_id"` + } `json:"data"` +} + +type Gitee3DQueryResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + Status string `json:"status"` + Progress int `json:"progress"` + ResultURL string `json:"result_url"` + PreviewURL string `json:"preview_url"` + ErrorMsg string `json:"error_msg"` + } `json:"data"` +} + +func NewGitee3DClient(sysConfig *types.SystemConfig) *Gitee3DClient { + return &Gitee3DClient{ + httpClient: req.C().SetTimeout(time.Minute * 3), + config: sysConfig.AI3D.Gitee, + apiURL: "https://ai.gitee.com/v1/async/image-to-3d", + } +} + +func (c *Gitee3DClient) UpdateConfig(config types.Gitee3DConfig) { + c.config = config +} + +// SubmitJob 提交3D生成任务 +func (c *Gitee3DClient) SubmitJob(params Gitee3DParams) (string, error) { + requestBody := map[string]any{ + "prompt": params.Prompt, + "image_url": params.ImageURL, + "result_format": params.ResultFormat, + } + + response, err := c.httpClient.R(). + SetHeader("Authorization", "Bearer "+c.config.APIKey). + SetHeader("Content-Type", "application/json"). + SetBody(requestBody). + Post(c.apiURL + "/async/image-to-3d") + + if err != nil { + return "", fmt.Errorf("failed to submit gitee 3D job: %v", err) + } + + var giteeResp Gitee3DResponse + if err := json.Unmarshal(response.Bytes(), &giteeResp); err != nil { + return "", fmt.Errorf("failed to parse gitee response: %v", err) + } + + if giteeResp.Code != 0 { + return "", fmt.Errorf("gitee API error: %s", giteeResp.Message) + } + + if giteeResp.Data.TaskID == "" { + return "", fmt.Errorf("no task ID returned from gitee 3D API") + } + + return giteeResp.Data.TaskID, nil +} + +// QueryJob 查询任务状态 +func (c *Gitee3DClient) QueryJob(taskId string) (*types.AI3DJobResult, error) { + response, err := c.httpClient.R(). + SetHeader("Authorization", "Bearer "+c.config.APIKey). + Get(fmt.Sprintf("%s/task/%s/get", c.apiURL, taskId)) + + if err != nil { + return nil, fmt.Errorf("failed to query gitee 3D job: %v", err) + } + + var giteeResp Gitee3DQueryResponse + if err := json.Unmarshal(response.Bytes(), &giteeResp); err != nil { + return nil, fmt.Errorf("failed to parse gitee query response: %v", err) + } + + if giteeResp.Code != 0 { + return nil, fmt.Errorf("gitee API error: %s", giteeResp.Message) + } + + result := &types.AI3DJobResult{ + JobId: taskId, + Status: c.convertStatus(giteeResp.Data.Status), + Progress: giteeResp.Data.Progress, + } + + // 根据状态设置结果 + switch giteeResp.Data.Status { + case "completed": + result.FileURL = giteeResp.Data.ResultURL + result.PreviewURL = giteeResp.Data.PreviewURL + case "failed": + result.ErrorMsg = giteeResp.Data.ErrorMsg + } + + return result, nil +} + +// convertStatus 转换Gitee状态到系统状态 +func (c *Gitee3DClient) convertStatus(giteeStatus string) string { + switch giteeStatus { + case "pending": + return types.AI3DJobStatusPending + case "processing": + return types.AI3DJobStatusProcessing + case "completed": + return types.AI3DJobStatusCompleted + case "failed": + return types.AI3DJobStatusFailed + default: + return types.AI3DJobStatusPending + } +} + +// GetSupportedModels 获取支持的模型列表 +func (c *Gitee3DClient) GetSupportedModels() []types.AI3DModel { + return []types.AI3DModel{ + {Name: "Hunyuan3D-2", Power: 100, Formats: []string{"GLB"}, Desc: "Hunyuan3D-2 是腾讯混元团队推出的高质量 3D 生成模型,具备高保真度、细节丰富和高效生成的特点,可快速将文本或图像转换为逼真的 3D 物体。"}, + {Name: "Step1X-3D", Power: 55, Formats: []string{"GLB", "STL"}, Desc: "Step1X-3D 是一款由阶跃星辰(StepFun)与光影焕像(LightIllusions)联合研发并开源的高保真 3D 生成模型,专为高质量、可控的 3D 内容创作而设计。"}, + {Name: "Hi3DGen", Power: 35, Formats: []string{"GLB", "STL"}, Desc: "Hi3DGen 是一个 AI 工具,它可以把你上传的普通图片,智能转换成有“立体感”的图片(法线图),常用于制作 3D 效果,比如游戏建模、虚拟现实、动画制作等。"}, + } +} diff --git a/api/service/ai3d/service.go b/api/service/ai3d/service.go new file mode 100644 index 00000000..f0238902 --- /dev/null +++ b/api/service/ai3d/service.go @@ -0,0 +1,327 @@ +package ai3d + +import ( + "encoding/json" + "fmt" + "geekai/core/types" + logger2 "geekai/logger" + "geekai/store" + "geekai/store/model" + "geekai/store/vo" + "time" + + "github.com/go-redis/redis/v8" + "gorm.io/gorm" +) + +var logger = logger2.GetLogger() + +// Service 3D生成服务 +type Service struct { + db *gorm.DB + taskQueue *store.RedisQueue + tencentClient *Tencent3DClient + giteeClient *Gitee3DClient +} + +// NewService 创建3D生成服务 +func NewService(db *gorm.DB, redisCli *redis.Client, tencentClient *Tencent3DClient, giteeClient *Gitee3DClient) *Service { + return &Service{ + db: db, + taskQueue: store.NewRedisQueue("3D_Task_Queue", redisCli), + tencentClient: tencentClient, + giteeClient: giteeClient, + } +} + +// CreateJob 创建3D生成任务 +func (s *Service) CreateJob(userId uint, request vo.AI3DJobCreate) (*model.AI3DJob, error) { + // 创建任务记录 + job := &model.AI3DJob{ + UserId: userId, + Type: request.Type, + Power: request.Power, + Model: request.Model, + Status: types.AI3DJobStatusPending, + } + + // 序列化参数 + params := map[string]any{ + "prompt": request.Prompt, + "image_url": request.ImageURL, + "model": request.Model, + "power": request.Power, + } + paramsJSON, _ := json.Marshal(params) + job.Params = string(paramsJSON) + + // 保存到数据库 + if err := s.db.Create(job).Error; err != nil { + return nil, fmt.Errorf("failed to create 3D job: %v", err) + } + + // 将任务添加到队列 + s.PushTask(job) + + return job, nil +} + +// PushTask 将任务添加到队列 +func (s *Service) PushTask(job *model.AI3DJob) { + logger.Infof("add a new 3D task to the queue: %+v", job) + if err := s.taskQueue.RPush(job); err != nil { + logger.Errorf("push 3D task to queue failed: %v", err) + } +} + +// Run 启动任务处理器 +func (s *Service) Run() { + // 将数据库中未完成的任务加载到队列 + var jobs []model.AI3DJob + s.db.Where("status IN ?", []string{types.AI3DJobStatusPending, types.AI3DJobStatusProcessing}).Find(&jobs) + for _, job := range jobs { + s.PushTask(&job) + } + + logger.Info("Starting 3D job consumer...") + go func() { + for { + var job model.AI3DJob + err := s.taskQueue.LPop(&job) + if err != nil { + logger.Errorf("taking 3D task with error: %v", err) + continue + } + logger.Infof("handle a new 3D task: %+v", job) + go func() { + if err := s.processJob(&job); err != nil { + logger.Errorf("error processing 3D job: %v", err) + s.updateJobStatus(&job, types.AI3DJobStatusFailed, 0, err.Error()) + } + }() + } + }() +} + +// processJob 处理3D任务 +func (s *Service) processJob(job *model.AI3DJob) error { + // 更新状态为处理中 + s.updateJobStatus(job, types.AI3DJobStatusProcessing, 10, "") + + // 解析参数 + var params map[string]any + if err := json.Unmarshal([]byte(job.Params), ¶ms); err != nil { + return fmt.Errorf("failed to parse job params: %v", err) + } + + var taskId string + var err error + + // 根据类型选择客户端 + switch job.Type { + case "tencent": + if s.tencentClient == nil { + return fmt.Errorf("tencent 3D client not initialized") + } + tencentParams := Tencent3DParams{ + Prompt: s.getString(params, "prompt"), + ImageURL: s.getString(params, "image_url"), + ResultFormat: job.Model, + EnablePBR: false, + } + taskId, err = s.tencentClient.SubmitJob(tencentParams) + case "gitee": + if s.giteeClient == nil { + return fmt.Errorf("gitee 3D client not initialized") + } + giteeParams := Gitee3DParams{ + Prompt: s.getString(params, "prompt"), + ImageURL: s.getString(params, "image_url"), + ResultFormat: job.Model, + } + taskId, err = s.giteeClient.SubmitJob(giteeParams) + default: + return fmt.Errorf("unsupported 3D API type: %s", job.Type) + } + + if err != nil { + return fmt.Errorf("failed to submit 3D job: %v", err) + } + + // 更新任务ID + job.TaskId = taskId + s.db.Model(job).Update("task_id", taskId) + + // 开始轮询任务状态 + go s.pollJobStatus(job) + + return nil +} + +// pollJobStatus 轮询任务状态 +func (s *Service) pollJobStatus(job *model.AI3DJob) { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + result, err := s.queryJobStatus(job) + if err != nil { + logger.Errorf("failed to query job status: %v", err) + continue + } + + // 更新进度 + s.updateJobStatus(job, result.Status, result.Progress, result.ErrorMsg) + + // 如果任务完成或失败,停止轮询 + if result.Status == types.AI3DJobStatusCompleted || result.Status == types.AI3DJobStatusFailed { + if result.Status == types.AI3DJobStatusCompleted { + // 更新结果文件URL + s.db.Model(job).Updates(map[string]interface{}{ + "img_url": result.FileURL, + "preview_url": result.PreviewURL, + }) + } + return + } + } + } +} + +// queryJobStatus 查询任务状态 +func (s *Service) queryJobStatus(job *model.AI3DJob) (*types.AI3DJobResult, error) { + switch job.Type { + case "tencent": + if s.tencentClient == nil { + return nil, fmt.Errorf("tencent 3D client not initialized") + } + return s.tencentClient.QueryJob(job.TaskId) + case "gitee": + if s.giteeClient == nil { + return nil, fmt.Errorf("gitee 3D client not initialized") + } + return s.giteeClient.QueryJob(job.TaskId) + default: + return nil, fmt.Errorf("unsupported 3D API type: %s", job.Type) + } +} + +// updateJobStatus 更新任务状态 +func (s *Service) updateJobStatus(job *model.AI3DJob, status string, progress int, errMsg string) { + updates := map[string]interface{}{ + "status": status, + "progress": progress, + "updated_at": time.Now(), + } + if errMsg != "" { + updates["err_msg"] = errMsg + } + + if err := s.db.Model(job).Updates(updates).Error; err != nil { + logger.Errorf("failed to update job status: %v", err) + } +} + +// GetJobList 获取任务列表 +func (s *Service) GetJobList(userId uint, page, pageSize int) (*vo.Page, error) { + var total int64 + var jobs []model.AI3DJob + + // 查询总数 + if err := s.db.Model(&model.AI3DJob{}).Where("user_id = ?", userId).Count(&total).Error; err != nil { + return nil, err + } + + // 查询任务列表 + offset := (page - 1) * pageSize + if err := s.db.Where("user_id = ?", userId).Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&jobs).Error; err != nil { + return nil, err + } + + // 转换为VO + var jobList []vo.AI3DJob + for _, job := range jobs { + jobVO := vo.AI3DJob{ + Id: job.Id, + UserId: job.UserId, + Type: job.Type, + Power: job.Power, + TaskId: job.TaskId, + ImgURL: job.FileURL, + PreviewURL: job.PreviewURL, + Model: job.Model, + Status: job.Status, + ErrMsg: job.ErrMsg, + Params: job.Params, + CreatedAt: job.CreatedAt.Unix(), + UpdatedAt: job.UpdatedAt.Unix(), + } + jobList = append(jobList, jobVO) + } + + return &vo.Page{ + Page: page, + PageSize: pageSize, + Total: total, + Items: jobList, + }, nil +} + +// GetJobById 根据ID获取任务 +func (s *Service) GetJobById(id uint) (*model.AI3DJob, error) { + var job model.AI3DJob + if err := s.db.Where("id = ?", id).First(&job).Error; err != nil { + return nil, err + } + return &job, nil +} + +// DeleteJob 删除任务 +func (s *Service) DeleteJob(id uint, userId uint) error { + var job model.AI3DJob + if err := s.db.Where("id = ? AND user_id = ?", id, userId).First(&job).Error; err != nil { + return err + } + + // 如果任务已完成,退还算力 + if job.Status == types.AI3DJobStatusCompleted { + // TODO: 实现算力退还逻辑 + logger2.GetLogger().Infof("should refund power %d for user %d", job.Power, userId) + } + + return s.db.Delete(&job).Error +} + +// GetSupportedModels 获取支持的模型列表 +func (s *Service) GetSupportedModels() map[string][]types.AI3DModel { + + models := make(map[string][]types.AI3DModel) + if s.tencentClient != nil { + models["tencent"] = s.tencentClient.GetSupportedModels() + } + if s.giteeClient != nil { + models["gitee"] = s.giteeClient.GetSupportedModels() + } + return models +} + +func (s *Service) UpdateConfig(config types.AI3DConfig) { + if s.tencentClient != nil { + s.tencentClient.UpdateConfig(config.Tencent) + } + if s.giteeClient != nil { + s.giteeClient.UpdateConfig(config.Gitee) + } +} + +// getString 从map中获取字符串值 +func (s *Service) getString(params map[string]interface{}, key string) string { + if val, ok := params[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} diff --git a/api/service/ai3d/tencent_client.go b/api/service/ai3d/tencent_client.go new file mode 100644 index 00000000..f309d69e --- /dev/null +++ b/api/service/ai3d/tencent_client.go @@ -0,0 +1,158 @@ +package ai3d + +import ( + "fmt" + "geekai/core/types" + + tencent3d "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ai3d/v20250513" + tencentcloud "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" +) + +type Tencent3DClient struct { + client *tencent3d.Client + config types.Tencent3DConfig +} + +type Tencent3DParams struct { + Prompt string `json:"prompt"` // 文本提示词 + ImageURL string `json:"image_url"` // 输入图片URL + ResultFormat string `json:"result_format"` // 输出格式 + EnablePBR bool `json:"enable_pbr"` // 是否开启PBR材质 + MultiViewImages []ViewImage `json:"multi_view_images,omitempty"` // 多视角图片 +} + +type ViewImage struct { + ViewType string `json:"view_type"` // 视角类型 (left/right/back) + ViewImageURL string `json:"view_image_url"` // 图片URL +} + +func NewTencent3DClient(sysConfig *types.SystemConfig) (*Tencent3DClient, error) { + config := sysConfig.AI3D.Tencent + credential := tencentcloud.NewCredential(config.SecretId, config.SecretKey) + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "ai3d.tencentcloudapi.com" + + client, err := tencent3d.NewClient(credential, config.Region, cpf) + if err != nil { + return nil, fmt.Errorf("failed to create tencent 3D client: %v", err) + } + + return &Tencent3DClient{ + client: client, + config: config, + }, nil +} + +func (c *Tencent3DClient) UpdateConfig(config types.Tencent3DConfig) error { + c.config = config + credential := tencentcloud.NewCredential(config.SecretId, config.SecretKey) + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "ai3d.tencentcloudapi.com" + + client, err := tencent3d.NewClient(credential, config.Region, cpf) + if err != nil { + return fmt.Errorf("failed to create tencent 3D client: %v", err) + } + c.client = client + return nil +} + +// SubmitJob 提交3D生成任务 +func (c *Tencent3DClient) SubmitJob(params Tencent3DParams) (string, error) { + request := tencent3d.NewSubmitHunyuanTo3DJobRequest() + + if params.Prompt != "" { + request.Prompt = tencentcloud.StringPtr(params.Prompt) + } + + if params.ImageURL != "" { + request.ImageUrl = tencentcloud.StringPtr(params.ImageURL) + } + + if params.ResultFormat != "" { + request.ResultFormat = tencentcloud.StringPtr(params.ResultFormat) + } + + request.EnablePBR = tencentcloud.BoolPtr(params.EnablePBR) + + if len(params.MultiViewImages) > 0 { + var viewImages []*tencent3d.ViewImage + for _, img := range params.MultiViewImages { + viewImage := &tencent3d.ViewImage{ + ViewType: tencentcloud.StringPtr(img.ViewType), + ViewImageUrl: tencentcloud.StringPtr(img.ViewImageURL), + } + viewImages = append(viewImages, viewImage) + } + request.MultiViewImages = viewImages + } + + response, err := c.client.SubmitHunyuanTo3DJob(request) + if err != nil { + return "", fmt.Errorf("failed to submit tencent 3D job: %v", err) + } + + if response.Response.JobId == nil { + return "", fmt.Errorf("no job ID returned from tencent 3D API") + } + + return *response.Response.JobId, nil +} + +// QueryJob 查询任务状态 +func (c *Tencent3DClient) QueryJob(jobId string) (*types.AI3DJobResult, error) { + request := tencent3d.NewQueryHunyuanTo3DJobRequest() + request.JobId = tencentcloud.StringPtr(jobId) + + response, err := c.client.QueryHunyuanTo3DJob(request) + if err != nil { + return nil, fmt.Errorf("failed to query tencent 3D job: %v", err) + } + + result := &types.AI3DJobResult{ + JobId: jobId, + Status: *response.Response.Status, + Progress: 0, + } + + // 根据状态设置进度 + switch *response.Response.Status { + case "WAIT": + result.Status = "pending" + result.Progress = 10 + case "RUN": + result.Status = "processing" + result.Progress = 50 + case "DONE": + result.Status = "completed" + result.Progress = 100 + // 处理结果文件 + if len(response.Response.ResultFile3Ds) > 0 { + for _, file := range response.Response.ResultFile3Ds { + if file.Url != nil { + result.FileURL = *file.Url + } + if file.PreviewImageUrl != nil { + result.PreviewURL = *file.PreviewImageUrl + } + break // 取第一个文件 + } + } + case "FAIL": + result.Status = "failed" + result.Progress = 0 + if response.Response.ErrorMessage != nil { + result.ErrorMsg = *response.Response.ErrorMessage + } + } + + return result, nil +} + +// GetSupportedModels 获取支持的模型列表 +func (c *Tencent3DClient) GetSupportedModels() []types.AI3DModel { + return []types.AI3DModel{ + {Name: "Hunyuan3D-3", Power: 500, Formats: []string{"OBJ", "GLB", "STL", "USDZ", "FBX", "MP4"}, Desc: "Hunyuan3D 是腾讯混元团队推出的高质量 3D 生成模型,具备高保真度、细节丰富和高效生成的特点,可快速将文本或图像转换为逼真的 3D 物体。"}, + } +} diff --git a/api/service/jimeng/client.go b/api/service/jimeng/client.go index 24fa0126..87864488 100644 --- a/api/service/jimeng/client.go +++ b/api/service/jimeng/client.go @@ -3,8 +3,10 @@ package jimeng import ( "encoding/json" "fmt" + "geekai/core/types" "net/http" "net/url" + "strings" "github.com/volcengine/volc-sdk-golang/base" "github.com/volcengine/volc-sdk-golang/service/visual" @@ -13,14 +15,22 @@ import ( // Client 即梦API客户端 type Client struct { visual *visual.Visual + config types.JimengConfig } // NewClient 创建即梦API客户端 -func NewClient(accessKey, secretKey string) *Client { +func NewClient(sysConfig *types.SystemConfig) *Client { + + client := &Client{} + client.UpdateConfig(sysConfig.Jimeng) + return client +} + +func (c *Client) UpdateConfig(config types.JimengConfig) error { // 使用官方SDK的visual实例 visualInstance := visual.NewInstance() - visualInstance.Client.SetAccessKey(accessKey) - visualInstance.Client.SetSecretKey(secretKey) + visualInstance.Client.SetAccessKey(config.AccessKey) + visualInstance.Client.SetSecretKey(config.SecretKey) // 添加即梦AI专有的API配置 jimengApis := map[string]*base.ApiInfo{ @@ -55,9 +65,32 @@ func NewClient(accessKey, secretKey string) *Client { visualInstance.Client.ApiInfoList[name] = info } - return &Client{ - visual: visualInstance, + c.config = config + c.visual = visualInstance + + return c.testConnection() +} + +// testConnection 测试即梦AI连接 +func (c *Client) testConnection() error { + + // 使用一个简单的查询任务来测试连接 + testReq := &QueryTaskRequest{ + ReqKey: "test_connection", + TaskId: "test_task_id_12345", } + + _, err := c.QueryTask(testReq) + // 即使任务不存在,只要不是认证错误就说明连接正常 + if err != nil { + // 检查是否是认证错误 + if strings.Contains(err.Error(), "InvalidAccessKey") { + return fmt.Errorf("认证失败,请检查AccessKey和SecretKey是否正确") + } + // 其他错误(如任务不存在)说明连接正常 + return nil + } + return nil } // SubmitTask 提交异步任务 diff --git a/api/service/jimeng/service.go b/api/service/jimeng/service.go index f857aba4..ca80c712 100644 --- a/api/service/jimeng/service.go +++ b/api/service/jimeng/service.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "strconv" - "strings" "time" "gorm.io/gorm" @@ -16,8 +15,6 @@ import ( "geekai/store/model" "geekai/utils" - "geekai/core/types" - "github.com/go-redis/redis/v8" ) @@ -36,17 +33,8 @@ type Service struct { } // NewService 创建即梦服务 -func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager) *Service { +func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager, client *Client) *Service { taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli) - // 从数据库加载配置 - var config model.Config - db.Where("name = ?", "Jimeng").First(&config) - var jimengConfig types.JimengConfig - if config.Id > 0 { - _ = utils.JsonDecode(config.Value, &jimengConfig) - } - client := NewClient(jimengConfig.AccessKey, jimengConfig.SecretKey) - ctx, cancel := context.WithCancel(context.Background()) return &Service{ db: db, @@ -522,77 +510,3 @@ func (s *Service) GetJob(jobId uint) (*model.JimengJob, error) { } return &job, nil } - -// testConnection 测试即梦AI连接 -func (s *Service) testConnection(accessKey, secretKey string) error { - testClient := NewClient(accessKey, secretKey) - - // 使用一个简单的查询任务来测试连接 - testReq := &QueryTaskRequest{ - ReqKey: "test_connection", - TaskId: "test_task_id_12345", - } - - _, err := testClient.QueryTask(testReq) - // 即使任务不存在,只要不是认证错误就说明连接正常 - if err != nil { - // 检查是否是认证错误 - if strings.Contains(err.Error(), "InvalidAccessKey") { - return fmt.Errorf("认证失败,请检查AccessKey和SecretKey是否正确") - } - // 其他错误(如任务不存在)说明连接正常 - return nil - } - return nil -} - -// UpdateClientConfig 更新客户端配置 -func (s *Service) UpdateClientConfig(accessKey, secretKey string) error { - // 创建新的客户端 - newClient := NewClient(accessKey, secretKey) - - // 测试新客户端是否可用 - err := s.testConnection(accessKey, secretKey) - if err != nil { - return err - } - - // 更新客户端 - s.client = newClient - return nil -} - -var defaultPower = types.JimengPower{ - TextToImage: 20, - ImageToImage: 20, - ImageEdit: 20, - ImageEffects: 20, - TextToVideo: 300, - ImageToVideo: 300, -} - -// GetConfig 获取即梦AI配置 -func (s *Service) GetConfig() *types.JimengConfig { - var config model.Config - err := s.db.Where("name", "jimeng").First(&config).Error - if err != nil { - // 如果配置不存在,返回默认配置 - return &types.JimengConfig{ - AccessKey: "", - SecretKey: "", - Power: defaultPower, - } - } - - var jimengConfig types.JimengConfig - err = utils.JsonDecode(config.Value, &jimengConfig) - if err != nil { - return &types.JimengConfig{ - AccessKey: "", - SecretKey: "", - Power: defaultPower, - } - } - - return &jimengConfig -} diff --git a/api/service/migration_service.go b/api/service/migration_service.go index 26292b27..47027f1c 100644 --- a/api/service/migration_service.go +++ b/api/service/migration_service.go @@ -154,6 +154,24 @@ func (s *MigrationService) MigrateConfigContent() error { return fmt.Errorf("迁移配置内容失败: %v", err) } + // 3D生成配置 + if err := s.saveConfig(types.ConfigKeyAI3D, map[string]any{ + "tencent": map[string]any{ + "access_key": "", + "secret_key": "", + "region": "", + "enabled": false, + "models": make([]types.AI3DModel, 0), + }, + "gitee": map[string]any{ + "api_key": "", + "enabled": false, + "models": make([]types.AI3DModel, 0), + }, + }); err != nil { + return fmt.Errorf("迁移配置内容失败: %v", err) + } + return nil } @@ -161,6 +179,8 @@ func (s *MigrationService) MigrateConfigContent() error { func (s *MigrationService) TableMigration() { // 新数据表 s.db.AutoMigrate(&model.Moderation{}) + s.db.AutoMigrate(&model.AI3DJob{}) + // 订单字段整理 if s.db.Migrator().HasColumn(&model.Order{}, "pay_type") { s.db.Migrator().RenameColumn(&model.Order{}, "pay_type", "channel") diff --git a/api/store/model/ai3d_job.go b/api/store/model/ai3d_job.go new file mode 100644 index 00000000..429f459d --- /dev/null +++ b/api/store/model/ai3d_job.go @@ -0,0 +1,23 @@ +package model + +import "time" + +type AI3DJob 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"` + Type string `gorm:"column:type;type:varchar(20);not null;comment:API类型 (tencent/gitee)" json:"type"` + Power int `gorm:"column:power;type:int(11);not null;comment:消耗算力" json:"power"` + TaskId string `gorm:"column:task_id;type:varchar(100);comment:第三方任务ID" json:"task_id"` + FileURL string `gorm:"column:file_url;type:varchar(1024);comment:生成的3D模型文件地址" json:"file_url"` + PreviewURL string `gorm:"column:preview_url;type:varchar(1024);comment:预览图片地址" json:"preview_url"` + Model string `gorm:"column:model;type:varchar(50);comment:使用的3D模型类型" json:"model"` + Status string `gorm:"column:status;type:varchar(20);not null;default:pending;comment:任务状态" json:"status"` + ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"` + Params string `gorm:"column:params;type:text;comment:任务参数(JSON格式)" json:"params"` + 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"` +} + +func (m *AI3DJob) TableName() string { + return "geekai_3d_jobs" +} diff --git a/api/store/vo/ai3d_job.go b/api/store/vo/ai3d_job.go new file mode 100644 index 00000000..e4ecebf1 --- /dev/null +++ b/api/store/vo/ai3d_job.go @@ -0,0 +1,32 @@ +package vo + +type AI3DJob struct { + Id uint `json:"id"` + UserId uint `json:"user_id"` + Type string `json:"type"` + Power int `json:"power"` + TaskId string `json:"task_id"` + ImgURL string `json:"img_url"` + PreviewURL string `json:"preview_url"` + Model string `json:"model"` + Status string `json:"status"` + ErrMsg string `json:"err_msg"` + Params string `json:"params"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type AI3DJobCreate struct { + Type string `json:"type" binding:"required"` // API类型 (tencent/gitee) + Model string `json:"model" binding:"required"` // 3D模型类型 + Prompt string `json:"prompt"` // 文本提示词 + ImageURL string `json:"image_url"` // 输入图片URL + Power int `json:"power" binding:"required"` // 消耗算力 +} + +type ThreeDJobList struct { + Page int `json:"page"` + PageSize int `json:"page_size"` + Total int `json:"total"` + List []AI3DJob `json:"list"` +} diff --git a/docs.md b/docs.md deleted file mode 100644 index ee474d67..00000000 --- a/docs.md +++ /dev/null @@ -1,122 +0,0 @@ -## 开发 3D 图片生成功能 - -对接 3D 图片生成接口,为当前系统添加 3D 模型生成功能,默认支持腾讯云和 Gitee 的图生 3D API 接口。 - -## 要求 - -1. 完成数据库设计,后端 API 设计,前端页面设计。 -2. 完成前端功能页面以及后台管理页面,具体设计结构可以参考即梦 AI,在对应的模块建立独立的模块 。 -3. 页面设计要精美,但是整体风格要跟整站风格一致。 -4. 支持前端 3D 模型预览,支持 3D 模型下载。 - -## 腾讯云图生 3D API 接口文档 - -1. 提交任务: https://cloud.tencent.com/document/product/1804/120826 -2. 查询任务: https://cloud.tencent.com/document/product/1804/120827 -3. Golang SDK: https://gitee.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ai3d/v20250513/client.go 依赖已经安装到本地了 - -## Gitee 图生 3D API 接口文档 - -1. 提交任务: https://ai.gitee.com/docs/openapi/v1#tag/3d-%E7%94%9F%E6%88%90/post/async/image-to-3d -2. 查询任务:https://ai.gitee.com/docs/openapi/v1#tag/%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1/get/task/{task_id}/get - -首先,你需要认真阅读上述接口文档,然后按照接口文档的示例代码实现腾讯云和 Gitee 的图生 3D API 接口,并且将接口集成到现有的系统中。 - -📋 功能概述 - - 为现有的GeekAI-Plus系统添加3D图片生成功能,集成腾讯云和Gitee的图生3D API接口,包含完整的前后端功能和管理界面。 - - 🗄️ 数据库设计 - - 新增数据表:geekai_3d_jobs - - id (uint): 主键 - - type (string): API类型 (tencent/gitee) - - user_id (uint): 用户ID - - power (int): 消耗算力 - - task_id (string): 第三方任务ID - - img_url (string): 生成的3D模型文件地址 - - model (string): 使用的3D模型类型 - - status (string): 任务状态 - - err_msg (string): 错误信息 - - params (JSON): 任务参数(包含输入图片、提示词等所有参数) - - created_at (int64): 创建时间 - - 🔧 后端API实现 - - 路由结构:/api/3d/* - - POST /api/3d/generate - 创建3D生成任务 - - GET /api/3d/jobs - 获取任务列表 - - GET /api/3d/job/{id} - 获取任务详情 - - GET /api/3d/download/{id} - 下载3D模型 - - DELETE /api/3d/job/{id} - 删除任务 - - 核心服务: - - service/3d/tencent_client.go - 腾讯云3D API客户端 - - service/3d/gitee_client.go - Gitee 3D API客户端 - - service/3d/service.go - 3D生成服务统一接口 - - handler/3d_handler.go - HTTP处理器 - - store/vo/3d_job.go - 数据模型 - - 🎨 前端界面设计 - - 用户端页面:/3d - 3D生成主页面 - - 参考JiMeng.vue的设计风格和布局 - - 使用CustomTab组件分离平台参数: - - Tab 1: "魔力方舟" (Gitee平台参数) - - Tab 2: "腾讯混元" (腾讯云平台参数) - - 每个Tab内包含: - - 图片上传区域 - - 模型选择下拉框 - - 算力消耗实时显示 - - 平台特定的参数配置 - - 生成按钮 - - 任务列表和状态显示 - - 集成3D模型预览器 (three.js) - - 模型下载功能 - - 移动端适配: - - mobile/3dCreate.vue - 移动端3D生成页面 - - 保持Tab切换功能 - - 响应式设计,触控优化 - - 🛠️ 管理后台 - - 管理功能: - - admin/3d/3dJobs.vue - 任务管理列表 - - admin/3d/3dSetting.vue - API配置页面 - - 模型配置管理: - - 分平台配置模型列表 - - 设置每个模型的算力消耗值 - - API密钥和端点配置 - - 🔌 API集成方案 - - 腾讯云集成: - - 使用官方Golang SDK - - 支持异步任务提交和状态查询 - - Gitee集成: - - HTTP客户端实现 - - 标准化响应处理 - - 🎯 核心功能特性 - - - 平台切换:通过CustomTab在魔力方舟和腾讯混元间切换 - - 模型选择:每个平台支持不同的3D模型 - - 动态算力:切换模型时实时更新算力消耗显示 - - 参数隔离:不同平台的参数配置完全分离 - - 3D预览:集成Three.js实现模型预览 - - 统一体验:保持与JiMeng.vue相似的交互风格 - - 📱 用户体验 - - - JiMeng.vue风格的简洁界面 - - Tab切换流畅的平台选择 - - 模型选择时算力消耗实时更新 - - 支持拖拽上传图片 - - 实时任务状态显示 - - 3D模型交互式预览 - - 这个设计将创建一个与现有JiMeng功能风格一致的3D生成模块,通过Tab分离实现平台参数的清晰管理。 - -整个实现严格按照现有系统的代码规范和架构模式,与 JiMeng 等模块保持一致的用户体验! diff --git a/docs/ai3d.md b/docs/ai3d.md new file mode 100644 index 00000000..ed3fb44a --- /dev/null +++ b/docs/ai3d.md @@ -0,0 +1,510 @@ +## 开发 3D 图片生成功能 + +对接 3D 图片生成接口,为当前系统添加 3D 模型生成功能,默认支持腾讯云和 Gitee 的图生 3D API 接口。 + +## 要求 + +1. 完成数据库设计,后端 API 设计,前端页面设计。 +2. 完成前端功能页面以及后台管理页面,具体设计结构可以参考即梦 AI,在对应的模块建立独立的模块 。 +3. 页面设计要精美,但是整体风格要跟整站风格一致。 +4. 支持前端 3D 模型预览,支持 3D 模型下载。 + +## 腾讯云图生 3D API 接口文档 + +1. 提交任务: https://cloud.tencent.com/document/product/1804/120826 +2. 查询任务: https://cloud.tencent.com/document/product/1804/120827 +3. Golang SDK: https://gitee.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ai3d/v20250513/client.go 依赖已经安装到本地了,如果你无法读取远程文件,下面是文件主要内容: + +client.go + +```go +// Copyright (c) 2017-2025 Tencent. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v20250513 + +import ( + "context" + "errors" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" +) + +const APIVersion = "2025-05-13" + +type Client struct { + common.Client +} + +// Deprecated +func NewClientWithSecretId(secretId, secretKey, region string) (client *Client, err error) { + cpf := profile.NewClientProfile() + client = &Client{} + client.Init(region).WithSecretId(secretId, secretKey).WithProfile(cpf) + return +} + +func NewClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *Client, err error) { + client = &Client{} + client.Init(region). + WithCredential(credential). + WithProfile(clientProfile) + return +} + + +func NewQueryHunyuanTo3DJobRequest() (request *QueryHunyuanTo3DJobRequest) { + request = &QueryHunyuanTo3DJobRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + + request.Init().WithApiInfo("ai3d", APIVersion, "QueryHunyuanTo3DJob") + + + return +} + +func NewQueryHunyuanTo3DJobResponse() (response *QueryHunyuanTo3DJobResponse) { + response = &QueryHunyuanTo3DJobResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return + +} + +// QueryHunyuanTo3DJob +// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。 +// +// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。 +func (c *Client) QueryHunyuanTo3DJob(request *QueryHunyuanTo3DJobRequest) (response *QueryHunyuanTo3DJobResponse, err error) { + return c.QueryHunyuanTo3DJobWithContext(context.Background(), request) +} + +// QueryHunyuanTo3DJob +// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。 +// +// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。 +func (c *Client) QueryHunyuanTo3DJobWithContext(ctx context.Context, request *QueryHunyuanTo3DJobRequest) (response *QueryHunyuanTo3DJobResponse, err error) { + if request == nil { + request = NewQueryHunyuanTo3DJobRequest() + } + c.InitBaseRequest(&request.BaseRequest, "ai3d", APIVersion, "QueryHunyuanTo3DJob") + + if c.GetCredential() == nil { + return nil, errors.New("QueryHunyuanTo3DJob require credential") + } + + request.SetContext(ctx) + + response = NewQueryHunyuanTo3DJobResponse() + err = c.Send(request, response) + return +} + +func NewSubmitHunyuanTo3DJobRequest() (request *SubmitHunyuanTo3DJobRequest) { + request = &SubmitHunyuanTo3DJobRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + + request.Init().WithApiInfo("ai3d", APIVersion, "SubmitHunyuanTo3DJob") + + + return +} + +func NewSubmitHunyuanTo3DJobResponse() (response *SubmitHunyuanTo3DJobResponse) { + response = &SubmitHunyuanTo3DJobResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return + +} + +// SubmitHunyuanTo3DJob +// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。 +// +// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。 +func (c *Client) SubmitHunyuanTo3DJob(request *SubmitHunyuanTo3DJobRequest) (response *SubmitHunyuanTo3DJobResponse, err error) { + return c.SubmitHunyuanTo3DJobWithContext(context.Background(), request) +} + +// SubmitHunyuanTo3DJob +// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。 +// +// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。 +func (c *Client) SubmitHunyuanTo3DJobWithContext(ctx context.Context, request *SubmitHunyuanTo3DJobRequest) (response *SubmitHunyuanTo3DJobResponse, err error) { + if request == nil { + request = NewSubmitHunyuanTo3DJobRequest() + } + c.InitBaseRequest(&request.BaseRequest, "ai3d", APIVersion, "SubmitHunyuanTo3DJob") + + if c.GetCredential() == nil { + return nil, errors.New("SubmitHunyuanTo3DJob require credential") + } + + request.SetContext(ctx) + + response = NewSubmitHunyuanTo3DJobResponse() + err = c.Send(request, response) + return +} + +``` + +model.go + +```go +// Copyright (c) 2017-2025 Tencent. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v20250513 + +import ( + tcerr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/json" +) + +type File3D struct { + // 文件格式 + Type *string `json:"Type,omitnil,omitempty" name:"Type"` + + // 文件的Url(有效期24小时) + Url *string `json:"Url,omitnil,omitempty" name:"Url"` + + // 预览图片Url + PreviewImageUrl *string `json:"PreviewImageUrl,omitnil,omitempty" name:"PreviewImageUrl"` +} + +// Predefined struct for user +type QueryHunyuanTo3DJobRequestParams struct { + // 任务ID。 + JobId *string `json:"JobId,omitnil,omitempty" name:"JobId"` +} + +type QueryHunyuanTo3DJobRequest struct { + *tchttp.BaseRequest + + // 任务ID。 + JobId *string `json:"JobId,omitnil,omitempty" name:"JobId"` +} + +func (r *QueryHunyuanTo3DJobRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +// FromJsonString It is highly **NOT** recommended to use this function +// because it has no param check, nor strict type check +func (r *QueryHunyuanTo3DJobRequest) FromJsonString(s string) error { + f := make(map[string]interface{}) + if err := json.Unmarshal([]byte(s), &f); err != nil { + return err + } + delete(f, "JobId") + if len(f) > 0 { + return tcerr.NewTencentCloudSDKError("ClientError.BuildRequestError", "QueryHunyuanTo3DJobRequest has unknown keys!", "") + } + return json.Unmarshal([]byte(s), &r) +} + +// Predefined struct for user +type QueryHunyuanTo3DJobResponseParams struct { + // 任务状态。WAIT:等待中,RUN:执行中,FAIL:任务失败,DONE:任务成功 + Status *string `json:"Status,omitnil,omitempty" name:"Status"` + + // 错误码 + ErrorCode *string `json:"ErrorCode,omitnil,omitempty" name:"ErrorCode"` + + // 错误信息 + ErrorMessage *string `json:"ErrorMessage,omitnil,omitempty" name:"ErrorMessage"` + + // 生成的3D文件数组。 + ResultFile3Ds []*File3D `json:"ResultFile3Ds,omitnil,omitempty" name:"ResultFile3Ds"` + + // 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitnil,omitempty" name:"RequestId"` +} + +type QueryHunyuanTo3DJobResponse struct { + *tchttp.BaseResponse + Response *QueryHunyuanTo3DJobResponseParams `json:"Response"` +} + +func (r *QueryHunyuanTo3DJobResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +// FromJsonString It is highly **NOT** recommended to use this function +// because it has no param check, nor strict type check +func (r *QueryHunyuanTo3DJobResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +// Predefined struct for user +type SubmitHunyuanTo3DJobRequestParams struct { + // 文生3D,3D内容的描述,中文正向提示词。 + // 最多支持1024个 utf-8 字符。 + // 文生3D, image、image_url和 prompt必填其一,且prompt和image/image_url不能同时存在。 + Prompt *string `json:"Prompt,omitnil,omitempty" name:"Prompt"` + + // 输入图 Base64 数据。 + // 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m) + // 格式:jpg,png,jpeg,webp。 + // ImageBase64、ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。 + ImageBase64 *string `json:"ImageBase64,omitnil,omitempty" name:"ImageBase64"` + + // 输入图Url。 + // 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m) + // 格式:jpg,png,jpeg,webp。 + // ImageBase64/ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。 + ImageUrl *string `json:"ImageUrl,omitnil,omitempty" name:"ImageUrl"` + + // 多视角的模型图片,视角参考值: + // left:左视图; + // right:右视图; + // back:后视图; + // + // 每个视角仅限制一张图片。 + // ●图片大小限制:编码后大小不可超过8M。 + // ●图片分辨率限制:单边分辨率小于5000且大于128。 + // ●支持图片格式:支持jpg或png + MultiViewImages []*ViewImage `json:"MultiViewImages,omitnil,omitempty" name:"MultiViewImages"` + + // 生成模型的格式,仅限制生成一种格式。 + // 生成模型文件组默认返回obj格式。 + // 可选值:OBJ,GLB,STL,USDZ,FBX,MP4。 + ResultFormat *string `json:"ResultFormat,omitnil,omitempty" name:"ResultFormat"` + + // 是否开启 PBR材质生成,默认 false。 + EnablePBR *bool `json:"EnablePBR,omitnil,omitempty" name:"EnablePBR"` +} + +type SubmitHunyuanTo3DJobRequest struct { + *tchttp.BaseRequest + + // 文生3D,3D内容的描述,中文正向提示词。 + // 最多支持1024个 utf-8 字符。 + // 文生3D, image、image_url和 prompt必填其一,且prompt和image/image_url不能同时存在。 + Prompt *string `json:"Prompt,omitnil,omitempty" name:"Prompt"` + + // 输入图 Base64 数据。 + // 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m) + // 格式:jpg,png,jpeg,webp。 + // ImageBase64、ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。 + ImageBase64 *string `json:"ImageBase64,omitnil,omitempty" name:"ImageBase64"` + + // 输入图Url。 + // 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m) + // 格式:jpg,png,jpeg,webp。 + // ImageBase64/ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。 + ImageUrl *string `json:"ImageUrl,omitnil,omitempty" name:"ImageUrl"` + + // 多视角的模型图片,视角参考值: + // left:左视图; + // right:右视图; + // back:后视图; + // + // 每个视角仅限制一张图片。 + // ●图片大小限制:编码后大小不可超过8M。 + // ●图片分辨率限制:单边分辨率小于5000且大于128。 + // ●支持图片格式:支持jpg或png + MultiViewImages []*ViewImage `json:"MultiViewImages,omitnil,omitempty" name:"MultiViewImages"` + + // 生成模型的格式,仅限制生成一种格式。 + // 生成模型文件组默认返回obj格式。 + // 可选值:OBJ,GLB,STL,USDZ,FBX,MP4。 + ResultFormat *string `json:"ResultFormat,omitnil,omitempty" name:"ResultFormat"` + + // 是否开启 PBR材质生成,默认 false。 + EnablePBR *bool `json:"EnablePBR,omitnil,omitempty" name:"EnablePBR"` +} + +func (r *SubmitHunyuanTo3DJobRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +// FromJsonString It is highly **NOT** recommended to use this function +// because it has no param check, nor strict type check +func (r *SubmitHunyuanTo3DJobRequest) FromJsonString(s string) error { + f := make(map[string]interface{}) + if err := json.Unmarshal([]byte(s), &f); err != nil { + return err + } + delete(f, "Prompt") + delete(f, "ImageBase64") + delete(f, "ImageUrl") + delete(f, "MultiViewImages") + delete(f, "ResultFormat") + delete(f, "EnablePBR") + if len(f) > 0 { + return tcerr.NewTencentCloudSDKError("ClientError.BuildRequestError", "SubmitHunyuanTo3DJobRequest has unknown keys!", "") + } + return json.Unmarshal([]byte(s), &r) +} + +// Predefined struct for user +type SubmitHunyuanTo3DJobResponseParams struct { + // 任务ID(有效期24小时) + JobId *string `json:"JobId,omitnil,omitempty" name:"JobId"` + + // 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitnil,omitempty" name:"RequestId"` +} + +type SubmitHunyuanTo3DJobResponse struct { + *tchttp.BaseResponse + Response *SubmitHunyuanTo3DJobResponseParams `json:"Response"` +} + +func (r *SubmitHunyuanTo3DJobResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +// FromJsonString It is highly **NOT** recommended to use this function +// because it has no param check, nor strict type check +func (r *SubmitHunyuanTo3DJobResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type ViewImage struct { + // 视角类型。 + // 取值:back、left、right + ViewType *string `json:"ViewType,omitnil,omitempty" name:"ViewType"` + + // 图片Url地址 + ViewImageUrl *string `json:"ViewImageUrl,omitnil,omitempty" name:"ViewImageUrl"` +} +``` + +## Gitee 图生 3D API 接口文档 + +1. 提交任务: https://ai.gitee.com/docs/openapi/v1#tag/3d-%E7%94%9F%E6%88%90/post/async/image-to-3d +2. 查询任务:https://ai.gitee.com/docs/openapi/v1#tag/%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1/get/task/{task_id}/get + +首先,你需要认真阅读上述接口文档,然后按照接口文档的示例代码实现腾讯云和 Gitee 的图生 3D API 接口,并且将接口集成到现有的系统中。 + +📋 功能概述 + + 为现有的GeekAI-Plus系统添加3D图片生成功能,集成腾讯云和Gitee的图生3D API接口,包含完整的前后端功能和管理界面。 + + 🗄️ 数据库设计 + + 新增数据表:geekai_3d_jobs + - id (uint): 主键 + - type (string): API类型 (tencent/gitee) + - user_id (uint): 用户ID + - power (int): 消耗算力 + - task_id (string): 第三方任务ID + - img_url (string): 生成的3D模型文件地址 + - model (string): 使用的3D模型类型 + - status (string): 任务状态 + - err_msg (string): 错误信息 + - params (JSON): 任务参数(包含输入图片、提示词等所有参数) + - created_at (int64): 创建时间 + + 🔧 后端API实现 + + 路由结构:/api/3d/* + - POST /api/3d/generate - 创建3D生成任务 + - GET /api/3d/jobs - 获取任务列表 + - GET /api/3d/job/{id} - 获取任务详情 + - GET /api/3d/download/{id} - 下载3D模型 + - DELETE /api/3d/job/{id} - 删除任务 + + 核心服务: + - service/3d/tencent_client.go - 腾讯云3D API客户端 + - service/3d/gitee_client.go - Gitee 3D API客户端 + - service/3d/service.go - 3D生成服务统一接口 + - handler/3d_handler.go - HTTP处理器 + - store/vo/3d_job.go - 数据模型 + + 🎨 前端界面设计 + + 用户端页面:/3d - 3D生成主页面 + - 参考JiMeng.vue的设计风格和布局 + - 使用CustomTab组件分离平台参数: + - Tab 1: "魔力方舟" (Gitee平台参数) + - Tab 2: "腾讯混元" (腾讯云平台参数) + - 每个Tab内包含: + - 图片上传区域 + - 模型选择下拉框 + - 算力消耗实时显示 + - 平台特定的参数配置 + - 生成按钮 + - 任务列表和状态显示 + - 集成3D模型预览器 (three.js) + - 模型下载功能 + + 移动端适配: + - mobile/3dCreate.vue - 移动端3D生成页面 + - 保持Tab切换功能 + - 响应式设计,触控优化 + + 🛠️ 管理后台 + + 管理功能: + - admin/3d/3dJobs.vue - 任务管理列表 + - admin/3d/3dSetting.vue - API配置页面 + - 模型配置管理: + - 分平台配置API秘钥和模型列表 + - 设置每个模型的算力消耗值 + - API密钥和端点配置 + + 🔌 API集成方案 + + 腾讯云集成: + - 使用官方Golang SDK + - 支持异步任务提交和状态查询 + + Gitee集成: + - HTTP客户端实现 + - 标准化响应处理 + + 🎯 核心功能特性 + + - 平台切换:通过CustomTab在魔力方舟和腾讯混元间切换 + - 模型选择:每个平台支持不同的3D模型 + - 动态算力:切换模型时实时更新算力消耗显示 + - 参数隔离:不同平台的参数配置完全分离 + - 3D预览:集成Three.js实现模型预览 + - 统一体验:保持与JiMeng.vue相似的交互风格 + + 📱 用户体验 + + - JiMeng.vue风格的简洁界面 + - Tab切换流畅的平台选择 + - 模型选择时算力消耗实时更新 + - 支持拖拽上传图片 + - 实时任务状态显示 + - 3D模型交互式预览 + + 这个设计将创建一个与现有JiMeng功能风格一致的3D生成模块,通过Tab分离实现平台参数的清晰管理。 + +整个实现严格按照现有系统的代码规范和架构模式,与 JiMeng 等模块保持一致的用户体验! diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 443ab01f..84f88a05 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -458,56 +458,67 @@ packages: resolution: {integrity: sha512-K4ncpWl7sQuyp6rWiGUvb6Q18ba8mzM0rjWJ5JgYKlIXAau1db7hZnR0ldJvqKWWJDxqzSLwGUhA4jp+KqgDtQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.46.1': resolution: {integrity: sha512-YykPnXsjUjmXE6j6k2QBBGAn1YsJUix7pYaPLK3RVE0bQL2jfdbfykPxfF8AgBlqtYbfEnYHmLXNa6QETjdOjQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.46.1': resolution: {integrity: sha512-kKvqBGbZ8i9pCGW3a1FH3HNIVg49dXXTsChGFsHGXQaVJPLA4f/O+XmTxfklhccxdF5FefUn2hvkoGJH0ScWOA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.46.1': resolution: {integrity: sha512-zzX5nTw1N1plmqC9RGC9vZHFuiM7ZP7oSWQGqpbmfjK7p947D518cVK1/MQudsBdcD84t6k70WNczJOct6+hdg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.46.1': resolution: {integrity: sha512-O8CwgSBo6ewPpktFfSDgB6SJN9XDcPSvuwxfejiddbIC/hn9Tg6Ai0f0eYDf3XvB/+PIWzOQL+7+TZoB8p9Yuw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.46.1': resolution: {integrity: sha512-JnCfFVEKeq6G3h3z8e60kAp8Rd7QVnWCtPm7cxx+5OtP80g/3nmPtfdCXbVl063e3KsRnGSKDHUQMydmzc/wBA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.46.1': resolution: {integrity: sha512-dVxuDqS237eQXkbYzQQfdf/njgeNw6LZuVyEdUaWwRpKHhsLI+y4H/NJV8xJGU19vnOJCVwaBFgr936FHOnJsQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.46.1': resolution: {integrity: sha512-CvvgNl2hrZrTR9jXK1ye0Go0HQRT6ohQdDfWR47/KFKiLd5oN5T14jRdUVGF4tnsN8y9oSfMOqH6RuHh+ck8+w==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.46.1': resolution: {integrity: sha512-x7ANt2VOg2565oGHJ6rIuuAon+A8sfe1IeUx25IKqi49OjSr/K3awoNqr9gCwGEJo9OuXlOn+H2p1VJKx1psxA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.46.1': resolution: {integrity: sha512-9OADZYryz/7E8/qt0vnaHQgmia2Y0wrjSSn1V/uL+zw/i7NUhxbX4cHXdEQ7dnJgzYDS81d8+tf6nbIdRFZQoQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.46.1': resolution: {integrity: sha512-NuvSCbXEKY+NGWHyivzbjSVJi68Xfq1VnIvGmsuXs6TCtveeoDRKutI5vf2ntmNnVq64Q4zInet0UDQ+yMB6tA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.46.1': resolution: {integrity: sha512-mWz+6FSRb82xuUMMV1X3NGiaPFqbLN9aIueHleTZCc46cJvwTlvIh7reQLk4p97dv0nddyewBhwzryBHH7wtPw==} diff --git a/web/src/assets/iconfont/iconfont.css b/web/src/assets/iconfont/iconfont.css index f8aff2db..494e152f 100644 --- a/web/src/assets/iconfont/iconfont.css +++ b/web/src/assets/iconfont/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 4125778 */ - src: url('iconfont.woff2?t=1756631578371') format('woff2'), - url('iconfont.woff?t=1756631578371') format('woff'), - url('iconfont.ttf?t=1756631578371') format('truetype'); + src: url('iconfont.woff2?t=1756786244728') format('woff2'), + url('iconfont.woff?t=1756786244728') format('woff'), + url('iconfont.ttf?t=1756786244728') format('truetype'); } .iconfont { @@ -13,6 +13,10 @@ -moz-osx-font-smoothing: grayscale; } +.icon-cube:before { + content: "\e876"; +} + .icon-tencent:before { content: "\e655"; } diff --git a/web/src/assets/iconfont/iconfont.js b/web/src/assets/iconfont/iconfont.js index 0f79bc47..644ac30a 100644 --- a/web/src/assets/iconfont/iconfont.js +++ b/web/src/assets/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4125778='',(a=>{var l=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var h,t,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(l&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}h=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),h()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(i=h,o=a.document,z=!1,v(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,p())})}function p(){z||(z=!0,i())}function v(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(v,50)}p()}})(window); \ No newline at end of file +window._iconfont_svg_string_4125778='',(a=>{var l=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var h,t,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(l&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}h=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),h()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(i=h,o=a.document,z=!1,v(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,p())})}function p(){z||(z=!0,i())}function v(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(v,50)}p()}})(window); \ No newline at end of file diff --git a/web/src/assets/iconfont/iconfont.json b/web/src/assets/iconfont/iconfont.json index d1f01f31..c9bc9e62 100644 --- a/web/src/assets/iconfont/iconfont.json +++ b/web/src/assets/iconfont/iconfont.json @@ -5,6 +5,13 @@ "css_prefix_text": "icon-", "description": "", "glyphs": [ + { + "icon_id": "34453337", + "name": "3D会场", + "font_class": "cube", + "unicode": "e876", + "unicode_decimal": 59510 + }, { "icon_id": "3547761", "name": "tencent", diff --git a/web/src/assets/iconfont/iconfont.ttf b/web/src/assets/iconfont/iconfont.ttf index 7834ecc8..a875fdff 100644 Binary files a/web/src/assets/iconfont/iconfont.ttf and b/web/src/assets/iconfont/iconfont.ttf differ diff --git a/web/src/assets/iconfont/iconfont.woff b/web/src/assets/iconfont/iconfont.woff index 8ed592be..6d0a267b 100644 Binary files a/web/src/assets/iconfont/iconfont.woff and b/web/src/assets/iconfont/iconfont.woff differ diff --git a/web/src/assets/iconfont/iconfont.woff2 b/web/src/assets/iconfont/iconfont.woff2 index 9d77c06d..9da21dc7 100644 Binary files a/web/src/assets/iconfont/iconfont.woff2 and b/web/src/assets/iconfont/iconfont.woff2 differ diff --git a/web/src/components/ThreeDPreview.vue b/web/src/components/ThreeDPreview.vue new file mode 100644 index 00000000..b0c985e5 --- /dev/null +++ b/web/src/components/ThreeDPreview.vue @@ -0,0 +1,547 @@ + + + + + diff --git a/web/src/components/admin/AdminSidebar.vue b/web/src/components/admin/AdminSidebar.vue index 172327f9..0b48288a 100644 --- a/web/src/components/admin/AdminSidebar.vue +++ b/web/src/components/admin/AdminSidebar.vue @@ -179,22 +179,39 @@ const items = [ }, ], }, + { + icon: 'cube', + index: '/admin/ai3d', + title: '3D生成', + subs: [ + { + icon: 'list', + index: '/admin/ai3d/jobs', + title: '任务管理', + }, + { + icon: 'config', + index: '/admin/ai3d/config', + title: '配置管理', + }, + ], + }, { icon: 'moderation', index: '/admin/config/moderation', title: '文本审查', subs: [ - { - icon: 'config', - index: '/admin/config/moderation', - title: '审查配置', - }, { icon: 'list', index: '/admin/moderation/list', title: '审核记录', }, + { + icon: 'config', + index: '/admin/moderation/config', + title: '审查配置', + }, ], }, { diff --git a/web/src/router.js b/web/src/router.js index 9d3148c3..c83c2f43 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -92,6 +92,12 @@ const routes = [ meta: { title: 'Suno音乐创作' }, component: () => import('@/views/Suno.vue'), }, + { + name: 'ai3d', + path: '/ai3d', + meta: { title: 'AI3D模型生成' }, + component: () => import('@/views/AIThreeDCreate.vue'), + }, { name: 'ExternalLink', path: '/external', @@ -148,12 +154,7 @@ const routes = [ meta: { title: '控制台登录' }, component: () => import('@/views/admin/Login.vue'), }, - { - path: '/payReturn', - name: 'pay-return', - meta: { title: '支付回调' }, - component: () => import('@/views/PayReturn.vue'), - }, + { name: 'admin', path: '/admin', @@ -210,7 +211,7 @@ const routes = [ component: () => import('@/views/admin/settings/PluginConfig.vue'), }, { - path: '/admin/config/moderation', + path: '/admin/moderation/config', name: 'admin-config-moderation', meta: { title: '文本审查配置' }, component: () => import('@/views/admin/moderation/ModerationConfig.vue'), @@ -345,7 +346,19 @@ const routes = [ path: '/admin/jimeng/config', name: 'admin-jimeng-config', meta: { title: '即梦设置' }, - component: () => import('@/views/admin/jimeng/JimengSetting.vue'), + component: () => import('@/views/admin/jimeng/JimengConfig.vue'), + }, + { + path: '/admin/ai3d/jobs', + name: 'admin-ai3d-jobs', + meta: { title: '3D任务管理' }, + component: () => import('@/views/admin/ai3d/AIThreeDJobs.vue'), + }, + { + path: '/admin/ai3d/config', + name: 'admin-ai3d-config', + meta: { title: '3D配置管理' }, + component: () => import('@/views/admin/ai3d/AIThreeDConfig.vue'), }, { path: '/admin/powerLog', @@ -459,6 +472,12 @@ const routes = [ name: 'mobile-jimeng', component: () => import('@/views/mobile/JimengCreate.vue'), }, + { + path: '/mobile/3d', + name: 'mobile-3d', + meta: { title: '3D模型生成' }, + component: () => import('@/views/mobile/ThreeDCreate.vue'), + }, ], }, diff --git a/web/src/views/AIThreeDCreate.vue b/web/src/views/AIThreeDCreate.vue new file mode 100644 index 00000000..bedf46fa --- /dev/null +++ b/web/src/views/AIThreeDCreate.vue @@ -0,0 +1,619 @@ + + + + + diff --git a/web/src/views/PayReturn.vue b/web/src/views/PayReturn.vue deleted file mode 100644 index 458d1669..00000000 --- a/web/src/views/PayReturn.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/web/src/views/admin/ai3d/AIThreeDConfig.vue b/web/src/views/admin/ai3d/AIThreeDConfig.vue new file mode 100644 index 00000000..1cda48d5 --- /dev/null +++ b/web/src/views/admin/ai3d/AIThreeDConfig.vue @@ -0,0 +1,405 @@ + + + + + diff --git a/web/src/views/admin/ai3d/AIThreeDJobs.vue b/web/src/views/admin/ai3d/AIThreeDJobs.vue new file mode 100644 index 00000000..4241fc48 --- /dev/null +++ b/web/src/views/admin/ai3d/AIThreeDJobs.vue @@ -0,0 +1,551 @@ + + + + + diff --git a/web/src/views/admin/jimeng/JimengSetting.vue b/web/src/views/admin/jimeng/JimengConfig.vue similarity index 99% rename from web/src/views/admin/jimeng/JimengSetting.vue rename to web/src/views/admin/jimeng/JimengConfig.vue index 61203a35..fe8c7e0b 100644 --- a/web/src/views/admin/jimeng/JimengSetting.vue +++ b/web/src/views/admin/jimeng/JimengConfig.vue @@ -241,7 +241,7 @@ onMounted(() => { // 加载配置 const loadConfig = async () => { try { - const res = await httpGet('/api/admin/jimeng/config') + const res = await httpGet('/api/admin/config/get?key=jimeng') jimengConfig.value = res.data } catch (e) { ElMessage.error('加载配置失败: ' + e.message) diff --git a/web/src/views/admin/moderation/ModerationConfig.vue b/web/src/views/admin/moderation/ModerationConfig.vue index a18eda50..cd954766 100644 --- a/web/src/views/admin/moderation/ModerationConfig.vue +++ b/web/src/views/admin/moderation/ModerationConfig.vue @@ -265,7 +265,7 @@ watch(activeTab, (newTab) => { const saveModerationConfig = async () => { loading.value = true try { - await httpPost('/api/admin/config/update/moderation', configs.value) + await httpPost('/api/admin/moderation/config', configs.value) ElMessage.success('保存成功') } catch (e) { ElMessage.error('保存失败:' + (e.message || '未知错误')) @@ -289,7 +289,7 @@ const testModeration = async () => { testLoading.value = true try { - const res = await httpPost('/api/admin/config/moderation/test', { + const res = await httpPost('/api/admin/moderation/test', { text: testForm.value.text.trim(), service: configs.value.active, }) diff --git a/web/src/views/mobile/Index.vue b/web/src/views/mobile/Index.vue index b020c93c..921291bb 100644 --- a/web/src/views/mobile/Index.vue +++ b/web/src/views/mobile/Index.vue @@ -167,6 +167,13 @@ const features = ref([ color: '#F97316', url: '/mobile/jimeng', }, + { + key: '3d', + name: '3D生成', + icon: 'icon-3d', + color: '#8B5CF6', + url: '/mobile/3d', + }, { key: 'agent', name: '智能体', icon: 'icon-app', color: '#3B82F6', url: '/mobile/apps' }, { key: 'imgWall', diff --git a/web/src/views/mobile/ThreeDCreate.vue b/web/src/views/mobile/ThreeDCreate.vue new file mode 100644 index 00000000..801c9dbd --- /dev/null +++ b/web/src/views/mobile/ThreeDCreate.vue @@ -0,0 +1,765 @@ + + + + +