Initial open-source release (MIT): image2api AI gateway
Full Go backend + Vue 3 frontend, OpenAI-compatible API, multi-provider account pools, billing/admin, Docker one-command deploy with auto HTTPS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"backend/internal/model"
|
||||
"backend/internal/repo"
|
||||
)
|
||||
|
||||
type UserGenerationService struct {
|
||||
v1 *V1Service
|
||||
events *repo.EventRepository
|
||||
users *repo.UserRepository
|
||||
models *repo.ModelRepository
|
||||
}
|
||||
|
||||
func NewUserGenerationService(v1 *V1Service, events *repo.EventRepository, users *repo.UserRepository, models *repo.ModelRepository) *UserGenerationService {
|
||||
return &UserGenerationService{
|
||||
v1: v1,
|
||||
events: events,
|
||||
users: users,
|
||||
models: models,
|
||||
}
|
||||
}
|
||||
|
||||
type UserGenerateRequest struct {
|
||||
Model string
|
||||
Prompt string
|
||||
Ratio string
|
||||
Resolution string
|
||||
Duration string
|
||||
ReferenceImages []string
|
||||
}
|
||||
|
||||
func (s *UserGenerationService) Generate(ctx context.Context, user *model.User, in UserGenerateRequest) (map[string]any, error) {
|
||||
if user == nil || strings.TrimSpace(user.ID) == "" {
|
||||
return nil, errors.New("未登录或会话已过期")
|
||||
}
|
||||
pending, err := s.events.PendingByUser(ctx, user.ID, "user")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pending != nil {
|
||||
return nil, errors.New("已有正在生成的任务,请稍候")
|
||||
}
|
||||
|
||||
modelItem, err := s.models.Get(ctx, strings.TrimSpace(in.Model))
|
||||
if err != nil {
|
||||
return nil, ErrUnknownModel
|
||||
}
|
||||
|
||||
principal := &APIPrincipal{
|
||||
User: user,
|
||||
TokenType: "session",
|
||||
}
|
||||
|
||||
switch modelItem.Type {
|
||||
case "video":
|
||||
resp, err := s.v1.prepareSessionVideo(ctx, principal, V1VideoRequest{
|
||||
Model: in.Model,
|
||||
Prompt: in.Prompt,
|
||||
Duration: in.Duration,
|
||||
AspectRatio: in.Ratio,
|
||||
Resolution: in.Resolution,
|
||||
ReferenceImages: in.ReferenceImages,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
default:
|
||||
resp, err := s.v1.prepareSessionImage(ctx, principal, V1ImageRequest{
|
||||
Model: in.Model,
|
||||
Prompt: in.Prompt,
|
||||
AspectRatio: in.Ratio,
|
||||
Resolution: in.Resolution,
|
||||
ReferenceImages: in.ReferenceImages,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserGenerationService) AdminTest(ctx context.Context, user *model.User, in UserGenerateRequest) (map[string]any, error) {
|
||||
if user == nil || strings.TrimSpace(user.ID) == "" {
|
||||
return nil, errors.New("未登录或会话已过期")
|
||||
}
|
||||
modelItem, err := s.models.Get(ctx, strings.TrimSpace(in.Model))
|
||||
if err != nil {
|
||||
return nil, ErrUnknownModel
|
||||
}
|
||||
principal := &APIPrincipal{
|
||||
User: user,
|
||||
TokenType: "session",
|
||||
}
|
||||
switch modelItem.Type {
|
||||
case "video":
|
||||
return s.v1.prepareAdminTestVideo(ctx, principal, V1VideoRequest{
|
||||
Model: in.Model,
|
||||
Prompt: in.Prompt,
|
||||
Duration: in.Duration,
|
||||
AspectRatio: in.Ratio,
|
||||
Resolution: in.Resolution,
|
||||
ReferenceImages: in.ReferenceImages,
|
||||
})
|
||||
default:
|
||||
return s.v1.prepareAdminTestImage(ctx, principal, V1ImageRequest{
|
||||
Model: in.Model,
|
||||
Prompt: in.Prompt,
|
||||
AspectRatio: in.Ratio,
|
||||
Resolution: in.Resolution,
|
||||
ReferenceImages: in.ReferenceImages,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserGenerationService) MyJobs(ctx context.Context, user *model.User, source string) (map[string]any, error) {
|
||||
if user == nil || strings.TrimSpace(user.ID) == "" {
|
||||
return map[string]any{"pending": nil, "latest": nil}, nil
|
||||
}
|
||||
// source scopes the lookup: "user" = 画图台(默认),"admin" = 后台测试模型。
|
||||
// Both are this caller's own events; the admin-test poll uses "admin" so a
|
||||
// gateway-timed-out (524) test can still recover its result.
|
||||
if source != "admin" {
|
||||
source = "user"
|
||||
}
|
||||
pending, err := s.events.PendingByUser(ctx, user.ID, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
latest, err := s.events.LatestByUser(ctx, user.ID, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"pending": shapeJobEvent(pending),
|
||||
"latest": shapeJobEvent(latest),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func shapeJobEvent(item *model.EventLog) map[string]any {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
status := item.Status
|
||||
url := ""
|
||||
if strings.TrimSpace(item.File) != "" {
|
||||
url = "/images/" + strings.ReplaceAll(strings.TrimSpace(item.File), "\\", "/")
|
||||
}
|
||||
return map[string]any{
|
||||
"id": item.ID,
|
||||
"kind": item.Kind,
|
||||
"model": item.Model,
|
||||
"prompt": item.Prompt,
|
||||
"ratio": item.Ratio,
|
||||
"resolution": item.Resolution,
|
||||
"duration": item.Duration,
|
||||
"status": status,
|
||||
"file": emptyOrNil(item.File),
|
||||
"url": emptyOrNil(url),
|
||||
"reference_urls": referenceURLs(item.RefFiles),
|
||||
"elapsed_ms": item.ElapsedMS,
|
||||
"error": emptyOrNil(item.Error),
|
||||
"charged": item.Cost,
|
||||
"cost": item.Cost,
|
||||
"ts": item.TS.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// referenceURLs turns the stored relative reference paths into /images URLs so
|
||||
// the playground can re-display the uploaded reference image(s) after a reload.
|
||||
func referenceURLs(raw []byte) []string {
|
||||
if len(raw) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
var paths []string
|
||||
if err := json.Unmarshal(raw, &paths); err != nil {
|
||||
return []string{}
|
||||
}
|
||||
out := make([]string, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
p = strings.ReplaceAll(strings.TrimSpace(p), "\\", "/")
|
||||
if p != "" {
|
||||
out = append(out, "/images/"+p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func emptyOrNil(v string) any {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user