273 lines
8.7 KiB
Go
273 lines
8.7 KiB
Go
package app
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"infinite-canvas/backend/internal/repository"
|
||
)
|
||
|
||
// Director Agent tools deliberately expose a projection rather than the raw
|
||
// DirectorScene payload. The browser owns model URLs and storage references;
|
||
// the Agent only needs semantic shot/layout data to plan a preview.
|
||
func cloudAgentDirectorSceneRead(repo *repository.Repository, userID, canvasID string, call cloudAgentCall) (any, error) {
|
||
var args struct {
|
||
SceneID string `json:"sceneId"`
|
||
ShotID string `json:"shotId"`
|
||
ObjectIDs []string `json:"objectIds"`
|
||
}
|
||
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
|
||
return nil, cloudAgentJSONArgumentError(err)
|
||
}
|
||
if err := validateCloudAgentID(args.SceneID, "导演场景 ID", 80); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := validateCloudAgentID(args.ShotID, "导演镜头 ID", 80); err != nil {
|
||
return nil, err
|
||
}
|
||
if len(args.ObjectIDs) > 16 {
|
||
return nil, BadAuthRequest("导演对象读取最多包含16个对象 ID")
|
||
}
|
||
for _, id := range args.ObjectIDs {
|
||
if err := validateCloudAgentID(id, "导演对象 ID", 80); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
canvas, err := repo.CanvasProjectForUser(userID, canvasID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
doc, err := creationDocument(canvas.PayloadJSON)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
rawScenes := creationMaps(doc["directorScenes"])
|
||
if args.SceneID == "" {
|
||
items := make([]map[string]any, 0, len(rawScenes))
|
||
for _, scene := range rawScenes {
|
||
items = append(items, map[string]any{
|
||
"id": stringValue(scene["id"]),
|
||
"title": truncateRunes(stringValue(scene["title"]), 160),
|
||
"activeShotId": stringValue(scene["activeShotId"]),
|
||
"shotCount": len(creationMaps(scene["shots"])),
|
||
"objectCount": len(creationMaps(scene["objects"])),
|
||
})
|
||
}
|
||
return map[string]any{
|
||
"canvasId": canvasID,
|
||
"scenes": items,
|
||
"count": len(items),
|
||
"guidance": "先用返回的 sceneId 读取一个场景,再使用 shotId 进行布局或预演。",
|
||
}, nil
|
||
}
|
||
|
||
scene, ok := findDirectorScene(rawScenes, args.SceneID)
|
||
if !ok {
|
||
return nil, BadAuthRequest("导演场景不存在或不属于当前画布")
|
||
}
|
||
if args.ShotID != "" {
|
||
if _, ok := findDirectorShot(scene, args.ShotID); !ok {
|
||
return nil, BadAuthRequest("导演镜头不存在或不属于指定场景")
|
||
}
|
||
}
|
||
objectIDs := make(map[string]bool, len(args.ObjectIDs))
|
||
for _, id := range args.ObjectIDs {
|
||
objectIDs[id] = true
|
||
}
|
||
return map[string]any{
|
||
"canvasId": canvasID,
|
||
"scene": projectDirectorScene(scene, args.ShotID, objectIDs),
|
||
"snapshotHash": creationHash(scene),
|
||
"revision": canvas.Revision,
|
||
"guidance": "这是安全摘要,不是可直接回写的完整 JSON;需要预演时使用 director_preview。",
|
||
}, nil
|
||
}
|
||
|
||
func cloudAgentDirectorPreview(repo *repository.Repository, userID, canvasID string, call cloudAgentCall) (any, error) {
|
||
var args struct {
|
||
SceneID string `json:"sceneId"`
|
||
ShotID string `json:"shotId"`
|
||
Duration *float64 `json:"duration"`
|
||
FPS *int `json:"fps"`
|
||
Output string `json:"output"`
|
||
}
|
||
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
|
||
return nil, cloudAgentJSONArgumentError(err)
|
||
}
|
||
if err := validateCloudAgentID(args.SceneID, "导演场景 ID", 80); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := validateCloudAgentID(args.ShotID, "导演镜头 ID", 80); err != nil {
|
||
return nil, err
|
||
}
|
||
if args.Output != "" && args.Output != "clay_video" {
|
||
return nil, BadAuthRequest("导演预演 output 目前只支持 clay_video")
|
||
}
|
||
if args.Duration != nil && (*args.Duration < 0.1 || *args.Duration > 60) {
|
||
return nil, BadAuthRequest("导演预演 duration 必须在0.1到60秒之间")
|
||
}
|
||
if args.FPS != nil && (*args.FPS < 1 || *args.FPS > 60) {
|
||
return nil, BadAuthRequest("导演预演 fps 必须在1到60之间")
|
||
}
|
||
canvas, err := repo.CanvasProjectForUser(userID, canvasID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
doc, err := creationDocument(canvas.PayloadJSON)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
scene, ok := findDirectorScene(creationMaps(doc["directorScenes"]), args.SceneID)
|
||
if !ok {
|
||
return nil, BadAuthRequest("导演场景不存在或不属于当前画布")
|
||
}
|
||
shot, ok := findDirectorShot(scene, args.ShotID)
|
||
if !ok {
|
||
return nil, BadAuthRequest("导演镜头不存在或不属于指定场景")
|
||
}
|
||
duration := numberValue(shot["duration"], 5)
|
||
if args.Duration != nil {
|
||
duration = *args.Duration
|
||
}
|
||
fps := int(numberValue(shot["fps"], 24))
|
||
if args.FPS != nil {
|
||
fps = *args.FPS
|
||
}
|
||
requestID := strings.TrimSpace(call.ID)
|
||
if requestID == "" {
|
||
requestID = fmt.Sprintf("director-preview-%s-%s", args.SceneID, args.ShotID)
|
||
}
|
||
return map[string]any{
|
||
"previewRequestId": requestID,
|
||
"canvasId": canvasID,
|
||
"sceneId": args.SceneID,
|
||
"sceneTitle": truncateRunes(stringValue(scene["title"]), 160),
|
||
"shotId": args.ShotID,
|
||
"shotName": truncateRunes(stringValue(shot["name"]), 160),
|
||
"duration": duration,
|
||
"fps": fps,
|
||
"output": "clay_video",
|
||
"snapshotHash": creationHash(scene),
|
||
"execution": "browser_director_viewport",
|
||
"text": "已请求导演台在浏览器视口录制白模预演;录制完成后会回写为画布视频节点。",
|
||
}, nil
|
||
}
|
||
|
||
func findDirectorScene(scenes []map[string]any, id string) (map[string]any, bool) {
|
||
for _, scene := range scenes {
|
||
if stringValue(scene["id"]) == id {
|
||
return scene, true
|
||
}
|
||
}
|
||
return nil, false
|
||
}
|
||
|
||
func findDirectorShot(scene map[string]any, id string) (map[string]any, bool) {
|
||
for _, shot := range creationMaps(scene["shots"]) {
|
||
if stringValue(shot["id"]) == id {
|
||
return shot, true
|
||
}
|
||
}
|
||
return nil, false
|
||
}
|
||
|
||
func projectDirectorScene(scene map[string]any, shotID string, objectIDs map[string]bool) map[string]any {
|
||
shots := make([]map[string]any, 0)
|
||
for _, shot := range creationMaps(scene["shots"]) {
|
||
if shotID != "" && stringValue(shot["id"]) != shotID {
|
||
continue
|
||
}
|
||
shots = append(shots, map[string]any{
|
||
"id": stringValue(shot["id"]),
|
||
"name": truncateRunes(stringValue(shot["name"]), 160),
|
||
"cameraId": stringValue(shot["cameraId"]),
|
||
"duration": numberValue(shot["duration"], 5),
|
||
"fps": int(numberValue(shot["fps"], 24)),
|
||
"shotSize": stringValue(shot["shotSize"]),
|
||
"cameraMove": stringValue(shot["cameraMove"]),
|
||
})
|
||
}
|
||
objects := make([]map[string]any, 0)
|
||
for _, object := range creationMaps(scene["objects"]) {
|
||
id := stringValue(object["id"])
|
||
if len(objectIDs) > 0 && !objectIDs[id] {
|
||
continue
|
||
}
|
||
objects = append(objects, map[string]any{
|
||
"id": id,
|
||
"name": truncateRunes(stringValue(object["name"]), 160),
|
||
"kind": stringValue(object["kind"]),
|
||
"primitive": stringValue(object["primitive"]),
|
||
"color": stringValue(object["color"]),
|
||
"pose": stringValue(object["pose"]),
|
||
"visible": boolValue(object["visible"], true),
|
||
"transform": projectDirectorTransform(object["transform"]),
|
||
})
|
||
}
|
||
cameras := make([]map[string]any, 0)
|
||
for _, camera := range creationMaps(scene["cameras"]) {
|
||
cameras = append(cameras, map[string]any{
|
||
"id": stringValue(camera["id"]),
|
||
"name": truncateRunes(stringValue(camera["name"]), 160),
|
||
"focalLength": numberValue(camera["focalLength"], 50),
|
||
"transform": projectDirectorTransform(camera["transform"]),
|
||
"target": projectDirectorVec3(camera["target"]),
|
||
})
|
||
}
|
||
return map[string]any{
|
||
"id": stringValue(scene["id"]),
|
||
"title": truncateRunes(stringValue(scene["title"]), 160),
|
||
"activeShotId": stringValue(scene["activeShotId"]),
|
||
"shots": shots,
|
||
"objects": objects,
|
||
"cameras": cameras,
|
||
"lightCount": len(creationMaps(scene["lights"])),
|
||
}
|
||
}
|
||
|
||
func projectDirectorTransform(value any) map[string]any {
|
||
transform, _ := value.(map[string]any)
|
||
return map[string]any{
|
||
"position": projectDirectorVec3(transform["position"]),
|
||
"rotation": projectDirectorVec3(transform["rotation"]),
|
||
"scale": projectDirectorVec3(transform["scale"]),
|
||
}
|
||
}
|
||
|
||
func projectDirectorVec3(value any) []any {
|
||
items, ok := value.([]any)
|
||
if !ok || len(items) != 3 {
|
||
return []any{0, 0, 0}
|
||
}
|
||
result := make([]any, 3)
|
||
for index, item := range items {
|
||
result[index], _ = cloudAgentSafeNumber(item)
|
||
if result[index] == nil {
|
||
result[index] = 0
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
func numberValue(value any, fallback float64) float64 {
|
||
if number, ok := cloudAgentSafeNumber(value); ok {
|
||
switch typed := number.(type) {
|
||
case float64:
|
||
return typed
|
||
case int:
|
||
return float64(typed)
|
||
case int64:
|
||
return float64(typed)
|
||
}
|
||
}
|
||
return fallback
|
||
}
|
||
|
||
func boolValue(value any, fallback bool) bool {
|
||
if result, ok := value.(bool); ok {
|
||
return result
|
||
}
|
||
return fallback
|
||
}
|