feat: 迁移 geekai-plus v4.3.1 到开源版

- 同步 Plus v4.3.1 功能源并移除商业 License 闭环
- 更新开源镜像命名、Docker 部署版本和 geekai 数据库配置
- 补充前端 ESLint 检查配置并修复存量解析与模板问题
- 保留 JWT、管理员权限、API Key 和 OAuth 等正常鉴权机制
This commit is contained in:
RockYang
2026-09-13 14:37:12 +08:00
parent 3e66c5ac5b
commit d4fd38ab7e
96 changed files with 7164 additions and 3047 deletions
+188 -1
View File
@@ -58,6 +58,12 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs)
return
}
if jimeng.IsSeedanceReqKey(req.ReqKey) {
if err := h.validateSeedanceRequest(&req); err != nil {
resp.ERROR(c, err.Error())
return
}
}
// 文本审核
if h.App.SysConfig.Moderation.Enable && req.Prompt != "" {
@@ -83,7 +89,7 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
}
if req.Prompt == "" && len(req.ImageUrls) == 0 {
if !jimeng.IsSeedanceReqKey(req.ReqKey) && req.Prompt == "" && len(req.ImageUrls) == 0 {
resp.ERROR(c, "提示词和图片不能同时为空")
return
}
@@ -315,6 +321,9 @@ func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
if req.Duration == 0 {
return 0, errors.New("视频时长不能为0")
}
if req.Duration == -1 {
return basePower, nil
}
return basePower * req.Duration, nil
case types.JMTaskTypeVirtualHuman:
if req.AudioURL == "" {
@@ -354,3 +363,181 @@ func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
"powers": config.Powers,
})
}
func (h *JimengHandler) validateSeedanceRequest(req *types.JimengTaskRequest) error {
if req.Duration != 0 && req.Duration != -1 {
if req.Duration < 4 || req.Duration > 15 {
return errors.New("Seedance 视频时长必须在 4-15 秒之间,或使用 -1 智能时长")
}
}
if req.AspectRatio != "" {
allowedRatio := map[string]bool{
"16:9": true, "4:3": true, "1:1": true, "3:4": true, "9:16": true, "21:9": true, "adaptive": true,
}
if !allowedRatio[req.AspectRatio] {
return errors.New("不支持的视频比例参数")
}
}
if req.Resolution != "" {
allowedResolution := map[string]bool{"480p": true, "720p": true, "1080p": true}
if !allowedResolution[req.Resolution] {
return errors.New("不支持的视频分辨率参数")
}
}
if req.ReqKey == "doubao-seedance-2-0-fast-260128" && req.Resolution == "1080p" {
return errors.New("Seedance 2.0 Fast 不支持 1080p")
}
if !h.seedanceSupportsVideoAudio(req.ReqKey) {
if req.VideoURL != "" || req.AudioURL != "" {
return errors.New("当前 Seedance 模型不支持视频或音频输入")
}
}
content := req.Content
if len(content) == 0 {
content = h.buildCompatSeedanceContent(req)
}
if len(content) == 0 {
return errors.New("Seedance 至少需要文本、图片或视频其中之一")
}
imageRoleCount := map[string]int{}
hasImageOrVideo := false
hasAudio := false
hasText := false
for _, item := range content {
switch item.Type {
case "text":
if item.Text == "" {
return errors.New("文本输入不能为空")
}
hasText = true
case "image_url":
if item.ImageURL == nil || item.ImageURL.URL == "" {
return errors.New("图片输入缺少 url")
}
role := item.Role
if role == "" {
role = "first_frame"
}
if role != "first_frame" && role != "last_frame" && role != "reference_image" {
return errors.New("图片 role 仅支持 first_frame、last_frame、reference_image")
}
imageRoleCount[role]++
hasImageOrVideo = true
case "video_url":
if !h.seedanceSupportsVideoAudio(req.ReqKey) {
return errors.New("当前 Seedance 模型不支持视频输入")
}
if item.VideoURL == nil || item.VideoURL.URL == "" {
return errors.New("视频输入缺少 url")
}
if item.Role != "" && item.Role != "reference_video" {
return errors.New("视频 role 仅支持 reference_video")
}
hasImageOrVideo = true
case "audio_url":
if !h.seedanceSupportsVideoAudio(req.ReqKey) {
return errors.New("当前 Seedance 模型不支持音频输入")
}
if item.AudioURL == nil || item.AudioURL.URL == "" {
return errors.New("音频输入缺少 url")
}
if item.Role != "" && item.Role != "reference_audio" {
return errors.New("音频 role 仅支持 reference_audio")
}
hasAudio = true
default:
return fmt.Errorf("不支持的 Seedance content 类型: %s", item.Type)
}
}
if hasAudio && !hasImageOrVideo {
return errors.New("音频不可单独输入,必须搭配图片或视频")
}
if imageRoleCount["first_frame"] > 1 {
return errors.New("first_frame 最多只能上传 1 张")
}
if imageRoleCount["last_frame"] > 1 {
return errors.New("last_frame 最多只能上传 1 张")
}
if imageRoleCount["reference_image"] > 9 {
return errors.New("reference_image 最多支持 9 张")
}
if imageRoleCount["reference_image"] > 0 && (imageRoleCount["first_frame"] > 0 || imageRoleCount["last_frame"] > 0) {
return errors.New("reference_image 模式不能与 first_frame/last_frame 混用")
}
if imageRoleCount["last_frame"] > 0 && imageRoleCount["first_frame"] == 0 {
return errors.New("使用 last_frame 时必须同时提供 first_frame")
}
if imageRoleCount["first_frame"] > 0 && imageRoleCount["last_frame"] == 0 && imageRoleCount["reference_image"] == 0 && !hasText {
return errors.New("图生视频至少需要提示词或补充参考素材")
}
return nil
}
func (h *JimengHandler) seedanceSupportsVideoAudio(reqKey string) bool {
return reqKey == "doubao-seedance-2-0-260128" || reqKey == "doubao-seedance-2-0-fast-260128"
}
func (h *JimengHandler) buildCompatSeedanceContent(req *types.JimengTaskRequest) []types.JMContentItem {
content := make([]types.JMContentItem, 0, 4)
if req.Prompt != "" {
content = append(content, types.JMContentItem{
Type: "text",
Text: req.Prompt,
})
}
if len(req.ImageUrls) > 0 {
for index, imageURL := range req.ImageUrls {
if imageURL == "" {
continue
}
role := "reference_image"
if len(req.ImageUrls) == 1 {
role = "first_frame"
} else if len(req.ImageUrls) == 2 {
if index == 0 {
role = "first_frame"
} else {
role = "last_frame"
}
}
content = append(content, types.JMContentItem{
Type: "image_url",
ImageURL: &types.JMAssetRef{
URL: imageURL,
},
Role: role,
})
}
}
if req.VideoURL != "" {
content = append(content, types.JMContentItem{
Type: "video_url",
VideoURL: &types.JMAssetRef{
URL: req.VideoURL,
},
Role: "reference_video",
})
}
if req.AudioURL != "" {
content = append(content, types.JMContentItem{
Type: "audio_url",
AudioURL: &types.JMAssetRef{
URL: req.AudioURL,
},
Role: "reference_audio",
})
}
return content
}