fix(agent): 视频生成 - 支持 Agent 设置首尾帧 (#696)

* fix(agent): 视频生成 - 支持 Agent 设置首尾帧

* fix(agent): 工具参数 - 精简首尾帧说明并保持体积预算

---------

Co-authored-by: tanzui <305375675@.com>
This commit is contained in:
tanzui
2026-10-06 09:28:22 +08:00
committed by GitHub
parent fb101b76d0
commit 3750fada7a
10 changed files with 342 additions and 8 deletions

View File

@@ -0,0 +1,130 @@
package app
import (
"encoding/json"
"fmt"
"net/url"
"os"
"testing"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"infinite-canvas/backend/internal/database"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
)
// TestCloudAgentVideoFramesPostgres 验证 Agent 的帧选择进入任务输入、协议角色和持久化草稿。
// 测试使用独立 schema,结束后删除测试表;不调用外部模型、不提交收费任务。
func TestCloudAgentVideoFramesPostgres(t *testing.T) {
dsn := os.Getenv("CANVAS_TEST_POSTGRES_DSN")
if dsn == "" {
t.Skip("CANVAS_TEST_POSTGRES_DSN is not configured")
}
options := &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}
admin, err := gorm.Open(postgres.Open(dsn), options)
if err != nil {
t.Fatal(err)
}
adminSQL, err := admin.DB()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = adminSQL.Close() })
schema := fmt.Sprintf("agent_video_frames_%d", time.Now().UnixNano())
if err := admin.Exec("CREATE SCHEMA " + schema).Error; err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := admin.Exec("DROP SCHEMA " + schema + " CASCADE").Error; err != nil {
t.Error(err)
}
})
parsed, err := url.Parse(dsn)
if err != nil {
t.Fatal(err)
}
query := parsed.Query()
query.Set("search_path", schema)
parsed.RawQuery = query.Encode()
db, err := gorm.Open(postgres.Open(parsed.String()), options)
if err != nil {
t.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sqlDB.Close() })
if err := db.AutoMigrate(database.Models()...); err != nil {
t.Fatal(err)
}
s := &Service{repo: repository.New(db), dataDir: t.TempDir()}
doc := map[string]any{"nodes": []map[string]any{
{"id": "first", "type": "image", "metadata": map[string]any{"storageKey": "resource:first", "status": "success"}},
{"id": "last", "type": "image", "metadata": map[string]any{"storageKey": "resource:last", "status": "success"}},
}, "connections": []any{}}
raw, err := json.Marshal(doc)
if err != nil {
t.Fatal(err)
}
for _, row := range []any{
&model.CanvasProject{ID: "canvas", UserID: "user", PayloadJSON: string(raw)},
&model.Resource{ID: "first", UserID: "user", Kind: "image", Status: "ready", MimeType: "image/png"},
&model.Resource{ID: "last", UserID: "user", Kind: "image", Status: "ready", MimeType: "image/png"},
} {
if err := db.Create(row).Error; err != nil {
t.Fatal(err)
}
}
// 故意把尾帧放在参考数组前面,帧角色必须来自明确字段,而不是数组顺序。
a := cloudAgentMediaArgs{Mode: "video", Prompt: "猫咪做饭", ChannelID: "channel", ChannelModelKey: "video", NodeID: "target", Title: "镜头视频", ReferenceNodeIDs: []string{"last", "first"}, VideoStartFrameNodeID: "first", VideoEndFrameNodeID: "last"}
state := &cloudAgentRuntime{Request: CloudAgentRequest{CanvasID: "canvas"}}
req, plan, err := s.prepareCloudAgentMedia(&model.CloudAgentExecution{ID: "run", UserID: "user"}, state, agentMediaCall(a))
if err != nil {
t.Fatal(err)
}
encoded, err := json.Marshal(req.Input)
if err != nil {
t.Fatal(err)
}
var input canvasGenerationInput
if err := json.Unmarshal(encoded, &input); err != nil {
t.Fatal(err)
}
if input.Metadata["videoStartFrameNodeId"] != "first" || input.Metadata["videoEndFrameNodeId"] != "last" {
t.Fatalf("任务输入丢失帧选择:%v", input.Metadata)
}
for i := range input.ReferenceImages {
input.ReferenceImages[i].DataURL = "data:image/png;base64,aGVsbG8="
}
references := protocolVideoImageReferences(input)
if len(references) != 2 || references[0].ID != "last" || references[0].Role != "last_frame" || references[1].ID != "first" || references[1].Role != "first_frame" {
t.Fatalf("协议收到错误的首尾帧:%+v", references)
}
policy, err := s.RuntimePolicy()
if err != nil {
t.Fatal(err)
}
if err := createCloudAgentMediaNode(s.repo, "user", "canvas", plan, nil, policy); err != nil {
t.Fatal(err)
}
canvas, err := s.repo.CanvasProjectForUser("user", "canvas")
if err != nil {
t.Fatal(err)
}
saved, err := creationDocument(canvas.PayloadJSON)
if err != nil {
t.Fatal(err)
}
node := cloudAgentNodeByID(creationMaps(saved["nodes"]), "target")
if node == nil {
t.Fatal("草稿节点未保存")
}
metadata := node["metadata"].(map[string]any)
if metadata["videoStartFrameNodeId"] != "first" || metadata["videoEndFrameNodeId"] != "last" || len(creationMaps(saved["connections"])) != 2 {
t.Fatalf("草稿未保存帧选择和引用连线:%v", saved)
}
}