feat(*): 生图与视频工作台 - 新增独立生成流程并整合平台配置
This commit is contained in:
1 parent
4c1d2b4012
commit
1f3f2c6020
69 files changed
+7108
-87
No files matched your search
@@ -371,11 +371,15 @@ func (r *Repository) ClaimNextTaskProviderCancellation(owner string, leaseDurati
|
||||
}
|
||||
|
||||
func (r *Repository) Tasks(userID string, limit int, projectID string, activeOnly bool) ([]model.Task, error) {
|
||||
return r.TasksWithWorkbenchKind(userID, limit, projectID, activeOnly, "")
|
||||
}
|
||||
|
||||
func (r *Repository) TasksWithWorkbenchKind(userID string, limit int, projectID string, activeOnly bool, workbenchKind string) ([]model.Task, error) {
|
||||
var tasks []model.Task
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
query := r.db.Select("id", "project_id", "type", "status", "stage", "media_stage", "media_recovery_json", "progress", "prompt", "operation", "provider", "model", "input_json", "result_json", "billing_order_id", "provider_request_id", "provider_cancel_status", "provider_cancel_error", "provider_cancel_attempts", "provider_cancel_requested_at", "provider_cancelled_at", "provider_cancel_next_check_at", "attempts", "started_at", "completed_at", "created_at", "updated_at", "agent_run_id", "generation_id", "approval_id", "authorized_charge_microcredits", "execution_diagnostic_json", "cancellation_source", "cancellation_actor_id", "cancellation_requested_at").
|
||||
query := r.db.Select("id", "project_id", "type", "workbench_kind", "status", "stage", "media_stage", "media_recovery_json", "progress", "prompt", "operation", "provider", "model", "input_json", "result_json", "billing_order_id", "provider_request_id", "provider_cancel_status", "provider_cancel_error", "provider_cancel_attempts", "provider_cancel_requested_at", "provider_cancelled_at", "provider_cancel_next_check_at", "attempts", "started_at", "completed_at", "created_at", "updated_at", "agent_run_id", "generation_id", "approval_id", "authorized_charge_microcredits", "execution_diagnostic_json", "cancellation_source", "cancellation_actor_id", "cancellation_requested_at").
|
||||
Where("user_id = ?", userID)
|
||||
if strings.TrimSpace(projectID) != "" {
|
||||
query = query.Where("project_id = ?", strings.TrimSpace(projectID))
|
||||
@@ -383,6 +387,14 @@ func (r *Repository) Tasks(userID string, limit int, projectID string, activeOnl
|
||||
if activeOnly {
|
||||
query = query.Where("status IN ?", []model.TaskStatus{model.TaskStatusQueued, model.TaskStatusRunning})
|
||||
}
|
||||
if workbenchKind != "" {
|
||||
taskType := "canvas_" + workbenchKind
|
||||
if r.db.Dialector.Name() == "postgres" {
|
||||
query = query.Where("(workbench_kind = ? AND type = ?) OR (type = ? AND input_json::jsonb -> 'metadata' ->> 'source' = ? AND input_json::jsonb -> 'metadata' ->> 'workbenchKind' = ?)", workbenchKind, taskType, taskType, "workbench", workbenchKind)
|
||||
} else {
|
||||
query = query.Where("(workbench_kind = ? AND type = ?) OR (type = ? AND json_extract(input_json, '$.metadata.source') = ? AND json_extract(input_json, '$.metadata.workbenchKind') = ?)", workbenchKind, taskType, taskType, "workbench", workbenchKind)
|
||||
}
|
||||
}
|
||||
err := query.Order("created_at desc").Limit(limit).Find(&tasks).Error
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
@@ -157,6 +157,19 @@ func (r *Repository) ResourceReferenceSnapshotExcludingAssets(userID string, exc
|
||||
}
|
||||
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "工具", ID: strconv.FormatInt(tool.ID, 10), Title: tool.Label, PrimaryJSON: string(payload)})
|
||||
}
|
||||
var workbenchTools []model.WorkbenchTool
|
||||
if err := r.db.Where("(icon_resource_id IN ? OR cover_resource_id IN ?) AND enabled = ?", resourceIDs, resourceIDs, true).Find(&workbenchTools).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, tool := range workbenchTools {
|
||||
if tool.IconResourceID != "" && slices.Contains(resourceIDs, tool.IconResourceID) {
|
||||
snapshot.Direct = append(snapshot.Direct, ResourceDirectReference{Kind: "工作台工具图标", ID: tool.ID, Title: tool.Name, ResourceID: tool.IconResourceID})
|
||||
}
|
||||
if tool.CoverResourceID != "" && slices.Contains(resourceIDs, tool.CoverResourceID) {
|
||||
snapshot.Direct = append(snapshot.Direct, ResourceDirectReference{Kind: "工作台工具封面", ID: tool.ID, Title: tool.Name, ResourceID: tool.CoverResourceID})
|
||||
}
|
||||
}
|
||||
|
||||
var assets []model.Asset
|
||||
assetQuery := r.db.Where("user_id = ?", userID)
|
||||
if len(excludingAssetIDs) > 0 {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"yingce/backend/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (r *Repository) WorkbenchTools(kind string, enabledOnly bool) ([]model.WorkbenchTool, error) {
|
||||
var items []model.WorkbenchTool
|
||||
query := r.db.Model(&model.WorkbenchTool{})
|
||||
if kind != "" {
|
||||
query = query.Where("kind = ?", kind)
|
||||
}
|
||||
if enabledOnly {
|
||||
query = query.Where("enabled = ?", true)
|
||||
}
|
||||
err := query.Order("kind asc, sort_order asc, name asc, id asc").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (r *Repository) WorkbenchTool(id string) (*model.WorkbenchTool, error) {
|
||||
var item model.WorkbenchTool
|
||||
if err := r.db.First(&item, "id = ?", id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateWorkbenchTool(item *model.WorkbenchTool) error {
|
||||
return r.db.Create(item).Error
|
||||
}
|
||||
func (r *Repository) SaveWorkbenchTool(item *model.WorkbenchTool) error { return r.db.Save(item).Error }
|
||||
|
||||
func (r *Repository) SaveWorkbenchToolWithPresets(item *model.WorkbenchTool, presets *[]model.WorkbenchPreset, create bool) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var err error
|
||||
if create {
|
||||
err = tx.Create(item).Error
|
||||
} else {
|
||||
err = tx.Save(item).Error
|
||||
}
|
||||
if err != nil || presets == nil {
|
||||
return err
|
||||
}
|
||||
ids := make([]string, 0, len(*presets))
|
||||
for index := range *presets {
|
||||
preset := &(*presets)[index]
|
||||
ids = append(ids, preset.ID)
|
||||
if err := tx.Save(preset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Omitted presets remain available to task history but leave the public catalog.
|
||||
query := tx.Model(&model.WorkbenchPreset{}).Where("tool_id = ? AND enabled = ?", item.ID, true)
|
||||
if len(ids) > 0 {
|
||||
query = query.Where("id NOT IN ?", ids)
|
||||
}
|
||||
return query.Updates(map[string]any{"enabled": false, "updated_by": item.UpdatedBy, "updated_at": item.UpdatedAt, "version": gorm.Expr("version + 1")}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) WorkbenchPresets(toolID string, enabledOnly bool) ([]model.WorkbenchPreset, error) {
|
||||
var items []model.WorkbenchPreset
|
||||
query := r.db.Model(&model.WorkbenchPreset{})
|
||||
if toolID != "" {
|
||||
query = query.Where("tool_id = ?", toolID)
|
||||
}
|
||||
if enabledOnly {
|
||||
query = query.Where("enabled = ?", true)
|
||||
}
|
||||
err := query.Order("sort_order asc, name asc, id asc").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (r *Repository) WorkbenchPreset(id string) (*model.WorkbenchPreset, error) {
|
||||
var item model.WorkbenchPreset
|
||||
if err := r.db.First(&item, "id = ?", id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateWorkbenchPreset(item *model.WorkbenchPreset) error {
|
||||
return r.db.Create(item).Error
|
||||
}
|
||||
func (r *Repository) SaveWorkbenchPreset(item *model.WorkbenchPreset) error {
|
||||
return r.db.Save(item).Error
|
||||
}
|
||||
|
||||
func (r *Repository) WorkbenchToolAndPreset(toolID, presetID string, publishedOnly bool) (*model.WorkbenchTool, *model.WorkbenchPreset, error) {
|
||||
var tool model.WorkbenchTool
|
||||
query := r.db.Where("id = ?", toolID)
|
||||
if publishedOnly {
|
||||
query = query.Where("enabled = ?", true)
|
||||
}
|
||||
if err := query.First(&tool).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if presetID == "" {
|
||||
return &tool, nil, nil
|
||||
}
|
||||
var preset model.WorkbenchPreset
|
||||
query = r.db.Where("id = ? AND tool_id = ?", presetID, toolID)
|
||||
if publishedOnly {
|
||||
query = query.Where("enabled = ?", true)
|
||||
}
|
||||
if err := query.First(&preset).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &tool, &preset, nil
|
||||
}
|
||||
|
||||
func (r *Repository) WorkbenchEnabledPresetsForTools(toolIDs []string) (map[string][]model.WorkbenchPreset, error) {
|
||||
result := make(map[string][]model.WorkbenchPreset, len(toolIDs))
|
||||
if len(toolIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
var presets []model.WorkbenchPreset
|
||||
if err := r.db.Where("tool_id IN ? AND enabled = ?", toolIDs, true).Order("sort_order asc, name asc, id asc").Find(&presets).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, preset := range presets {
|
||||
result[preset.ToolID] = append(result[preset.ToolID], preset)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var _ = gorm.ErrRecordNotFound
|
||||
@@ -0,0 +1,23 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"yingce/backend/internal/model"
|
||||
)
|
||||
|
||||
type TaskPage struct {
|
||||
Tasks []model.Task `json:"tasks"`
|
||||
NextCursor string `json:"nextCursor,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Repository) WorkbenchTasks(userID, kind string, activeOnly bool, cursor string, limit int) (TaskPage, error) {
|
||||
if limit < 1 || limit > 100 { limit = 30 }
|
||||
if kind != model.WorkbenchKindImage && kind != model.WorkbenchKindVideo { return TaskPage{}, nil }
|
||||
query := r.db.Model(&model.Task{}).Where("user_id = ? AND workbench_kind = ?", userID, kind)
|
||||
if activeOnly { query = query.Where("status IN ?", []model.TaskStatus{model.TaskStatusQueued, model.TaskStatusRunning}) }
|
||||
if cursor != "" { query = query.Where("id < ?", cursor) }
|
||||
var tasks []model.Task
|
||||
if err := query.Order("created_at desc, id desc").Limit(limit + 1).Find(&tasks).Error; err != nil { return TaskPage{}, err }
|
||||
page := TaskPage{Tasks: tasks}
|
||||
if len(tasks) > limit { page.Tasks = tasks[:limit]; page.NextCursor = page.Tasks[len(page.Tasks)-1].ID }
|
||||
return page, nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"yingce/backend/internal/model"
|
||||
)
|
||||
|
||||
func TestTasksWithWorkbenchKindRequiresMatchingTaskType(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.Task{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
legacyImage := `{"metadata":{"source":"workbench","workbenchKind":"image"}}`
|
||||
legacyVideo := `{"metadata":{"source":"workbench","workbenchKind":"video"}}`
|
||||
createdAt := time.Now()
|
||||
tasks := []model.Task{
|
||||
{ID: "image-column", UserID: "user", Type: "canvas_image", WorkbenchKind: "image", InputJSON: `{}`, CreatedAt: createdAt},
|
||||
{ID: "image-legacy", UserID: "user", Type: "canvas_image", InputJSON: legacyImage, CreatedAt: createdAt.Add(time.Second)},
|
||||
{ID: "video-column", UserID: "user", Type: "canvas_video", WorkbenchKind: "video", InputJSON: `{}`, CreatedAt: createdAt.Add(2 * time.Second)},
|
||||
{ID: "video-legacy", UserID: "user", Type: "canvas_video", InputJSON: legacyVideo, CreatedAt: createdAt.Add(3 * time.Second)},
|
||||
{ID: "wrong-column-kind", UserID: "user", Type: "canvas_video", WorkbenchKind: "image", InputJSON: `{}`, CreatedAt: createdAt.Add(4 * time.Second)},
|
||||
{ID: "wrong-legacy-type", UserID: "user", Type: "canvas_video", InputJSON: legacyImage, CreatedAt: createdAt.Add(5 * time.Second)},
|
||||
{ID: "wrong-source", UserID: "user", Type: "canvas_image", InputJSON: `{"metadata":{"source":"canvas","workbenchKind":"image"}}`, CreatedAt: createdAt.Add(6 * time.Second)},
|
||||
{ID: "other-user", UserID: "other", Type: "canvas_image", InputJSON: legacyImage, CreatedAt: createdAt.Add(7 * time.Second)},
|
||||
}
|
||||
if err := db.Create(&tasks).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
repo := New(db)
|
||||
for _, tc := range []struct {
|
||||
kind string
|
||||
want map[string]bool
|
||||
}{
|
||||
{kind: "image", want: map[string]bool{"image-column": true, "image-legacy": true}},
|
||||
{kind: "video", want: map[string]bool{"video-column": true, "video-legacy": true}},
|
||||
} {
|
||||
got, err := repo.TasksWithWorkbenchKind("user", 50, "", false, tc.kind)
|
||||
if err != nil {
|
||||
t.Fatalf("list %s tasks: %v", tc.kind, err)
|
||||
}
|
||||
if len(got) != len(tc.want) {
|
||||
t.Fatalf("%s task IDs = %#v, want %d tasks", tc.kind, got, len(tc.want))
|
||||
}
|
||||
for _, task := range got {
|
||||
if !tc.want[task.ID] {
|
||||
t.Errorf("%s list included unexpected task %q (type=%q kind=%q)", tc.kind, task.ID, task.Type, task.WorkbenchKind)
|
||||
}
|
||||
delete(tc.want, task.ID)
|
||||
}
|
||||
if len(tc.want) != 0 {
|
||||
t.Errorf("%s list omitted expected task IDs: %#v", tc.kind, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user