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
+101 -6
View File
@@ -13,13 +13,22 @@ import (
"encoding/json"
"fmt"
"geekai/core/types"
"geekai/utils"
"image"
"image/png"
"io"
"mime/multipart"
"net/http"
"geekai/utils"
"strconv"
"strings"
"time"
_ "image/gif"
_ "image/jpeg"
"github.com/imroc/req/v3"
"github.com/nfnt/resize"
_ "golang.org/x/image/webp"
"gorm.io/gorm"
)
@@ -141,10 +150,10 @@ func (a *SoraAdapter) CreateTask(task types.VideoTask, videoConfig *types.VideoC
// 其他场景:保持原来的 JSON 调用,input_reference 继续传 URL 字符串
reqBody := SoraCreateRequest{
Model: model,
Prompt: task.Prompt,
Size: size,
Seconds: seconds,
Model: model,
Prompt: task.Prompt,
Size: size,
Seconds: seconds,
Watermark: watermark,
}
@@ -201,6 +210,12 @@ func (a *SoraAdapter) createOfficialSoraTask(task types.VideoTask, videoConfig *
if err != nil {
return CreateTaskResponse{}, fmt.Errorf("下载参考图片失败:%v", err)
}
if size != "" {
imgData, err = normalizeImageBytesForSize(imgData, size)
if err != nil {
return CreateTaskResponse{}, fmt.Errorf("处理参考图片尺寸失败:%v", err)
}
}
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
@@ -224,7 +239,7 @@ func (a *SoraAdapter) createOfficialSoraTask(task types.VideoTask, videoConfig *
}
// 文件字段
fileWriter, err := writer.CreateFormFile("input_reference", "image")
fileWriter, err := writer.CreateFormFile("input_reference", "image.png")
if err != nil {
return CreateTaskResponse{}, err
}
@@ -281,6 +296,86 @@ func downloadImageBytes(imageURL string) ([]byte, error) {
return body, err
}
// normalizeImageBytesForSize 将参考图等比缩放并裁切为目标尺寸,确保与 Sora size 参数一致
func normalizeImageBytesForSize(imageData []byte, size string) ([]byte, error) {
targetWidth, targetHeight, err := parseVideoSize(size)
if err != nil {
return nil, err
}
if targetWidth <= 0 || targetHeight <= 0 {
return nil, fmt.Errorf("无效的图片尺寸: %s", size)
}
srcImage, _, err := image.Decode(bytes.NewReader(imageData))
if err != nil {
return nil, fmt.Errorf("解码参考图片失败:%w", err)
}
srcBounds := srcImage.Bounds()
srcWidth := srcBounds.Dx()
srcHeight := srcBounds.Dy()
if srcWidth <= 0 || srcHeight <= 0 {
return nil, fmt.Errorf("参考图片尺寸无效")
}
if srcWidth == targetWidth && srcHeight == targetHeight {
var exactBuf bytes.Buffer
if err = png.Encode(&exactBuf, srcImage); err != nil {
return nil, fmt.Errorf("编码参考图片失败:%w", err)
}
return exactBuf.Bytes(), nil
}
scaleByWidth := float64(targetWidth) / float64(srcWidth)
scaleByHeight := float64(targetHeight) / float64(srcHeight)
scale := scaleByWidth
if scaleByHeight > scale {
scale = scaleByHeight
}
resizedWidth := int(float64(srcWidth) * scale)
resizedHeight := int(float64(srcHeight) * scale)
if resizedWidth < targetWidth {
resizedWidth = targetWidth
}
if resizedHeight < targetHeight {
resizedHeight = targetHeight
}
resizedImage := resize.Resize(uint(resizedWidth), uint(resizedHeight), srcImage, resize.Lanczos3)
offsetX := (resizedWidth - targetWidth) / 2
offsetY := (resizedHeight - targetHeight) / 2
targetRect := image.Rect(0, 0, targetWidth, targetHeight)
croppedImage := image.NewRGBA(targetRect)
for y := 0; y < targetHeight; y++ {
for x := 0; x < targetWidth; x++ {
croppedImage.Set(x, y, resizedImage.At(x+offsetX, y+offsetY))
}
}
var out bytes.Buffer
if err = png.Encode(&out, croppedImage); err != nil {
return nil, fmt.Errorf("编码裁切后的参考图片失败:%w", err)
}
return out.Bytes(), nil
}
// parseVideoSize 解析 "720x1280" 这样的尺寸字符串
func parseVideoSize(size string) (int, int, error) {
parts := strings.Split(strings.TrimSpace(size), "x")
if len(parts) != 2 {
return 0, 0, fmt.Errorf("非法尺寸格式: %s", size)
}
width, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return 0, 0, fmt.Errorf("解析宽度失败: %w", err)
}
height, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return 0, 0, fmt.Errorf("解析高度失败: %w", err)
}
return width, height, nil
}
// downloadImageAsDataURL 下载远程图片并转为 data URL,避免向官方 Sora 直接传地址
// QueryTask 查询任务状态
func (a *SoraAdapter) QueryTask(taskId string, channel string, videoConfig *types.VideoConfig) (QueryTaskResponse, error) {
+4 -4
View File
@@ -143,7 +143,7 @@ func (s *Service) PushTask(task types.VideoTask) {
func (s *Service) Run() {
// 将数据库中未提交的任务加载到队列
var jobs []model.VideoJob
s.db.Where("task_id", "").Where("progress", 0).Find(&jobs)
s.db.Where("task_id", "").Where("status", types.VideoStatusPending).Find(&jobs)
for _, v := range jobs {
var task types.VideoTask
err := utils.JsonDecode(v.Params, &task)
@@ -234,7 +234,7 @@ func (s *Service) SyncTaskProgress() {
logger.Info("[video] task status poller started")
var jobs []model.VideoJob
for {
res := s.db.Where("status IN ?", []string{types.VideoStatusInProgress, types.VideoStatusPending}).Where("task_id <> ?", "").Find(&jobs)
res := s.db.Where("status IN ?", []string{types.VideoStatusInProgress, types.VideoStatusPending, types.VideoStatusDownloading}).Find(&jobs)
if res.Error != nil {
continue
}
@@ -312,14 +312,14 @@ func (s *Service) SyncTaskProgress() {
}
// 找出失败的任务,并恢复其扣减算力
// 找出失败的任务,并恢复其扣减积分
s.db.Select("id", "user_id", "power", "task_id", "err_msg", "type").
Where("status", types.VideoStatusFailed).Where("power > ?", 0).Find(&jobs)
for _, job := range jobs {
err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: job.Type,
Remark: fmt.Sprintf("%s 任务失败,退回算力。任务ID%sErr:%s", job.Type, job.TaskId, job.ErrMsg),
Remark: fmt.Sprintf("%s 任务失败,退回积分。任务ID%sErr:%s", job.Type, job.TaskId, job.ErrMsg),
})
if err != nil {
continue