package app import ( "encoding/json" "fmt" "math" "strconv" "strings" "time" "unicode" "unicode/utf8" "infinite-canvas/backend/internal/model" "infinite-canvas/backend/internal/repository" ) const ( cloudAgentPrevisMaxOperations = 32 cloudAgentPrevisMaxScenes = 32 cloudAgentPrevisMaxShots = 64 cloudAgentPrevisMaxObjects = 128 cloudAgentPrevisMaxCameras = 32 cloudAgentPrevisMaxLights = 64 cloudAgentPrevisDocumentLimit = 1 << 20 cloudAgentPrevisTextLimit = 160 ) type cloudAgentPrevisSceneCreateArgs struct { CanvasSnapshotHash string `json:"canvasSnapshotHash"` SceneID string `json:"sceneId"` Title string `json:"title"` TemplateID string `json:"templateId"` } type cloudAgentPrevisApplyPatchArgs struct { SnapshotHash string `json:"snapshotHash"` SceneID string `json:"sceneId"` Operations []cloudAgentPrevisPatchOperation `json:"operations"` } // cloudAgentPrevisPatchOperation is deliberately a closed semantic contract. // It has no generic path, metadata, URL, storage key or raw scene field. type cloudAgentPrevisPatchOperation struct { Type string `json:"type"` ID string `json:"id"` Name *string `json:"name"` Title *string `json:"title"` Kind string `json:"kind"` Primitive string `json:"primitive"` Archetype string `json:"archetype"` // adult/child/elderly/man/woman/monster Color *string `json:"color"` ParentID *string `json:"parentId"` Visible *bool `json:"visible"` Pose string `json:"pose"` CharacterAssetID *string `json:"characterAssetId"` CharacterVersionID *string `json:"characterVersionId"` ReferenceNodeID *string `json:"referenceNodeId"` CharacterName *string `json:"characterName"` Position *[]float64 `json:"position"` Rotation *[]float64 `json:"rotation"` Scale *[]float64 `json:"scale"` Target *[]float64 `json:"target"` KeyframeID *string `json:"keyframeId"` Time *float64 `json:"time"` Easing string `json:"easing"` PathPoints [][]float64 `json:"pathPoints"` PathSpeed string `json:"pathSpeed"` StartDelay *float64 `json:"startDelay"` OrientToPath *bool `json:"orientToPath"` CameraID *string `json:"cameraId"` Duration *float64 `json:"duration"` FPS *int `json:"fps"` ShotSize string `json:"shotSize"` CameraMove string `json:"cameraMove"` Prompt *string `json:"prompt"` FocalLength *float64 `json:"focalLength"` LightType *string `json:"lightType"` Intensity *float64 `json:"intensity"` Angle *float64 `json:"angle"` Penumbra *float64 `json:"penumbra"` CastShadow *bool `json:"castShadow"` Background *string `json:"background"` EnvironmentIntensity *float64 `json:"environmentIntensity"` GridVisible *bool `json:"gridVisible"` ActiveShotID *string `json:"activeShotId"` } type cloudAgentPrevisMutationPlan struct { Canvas *model.CanvasProject Document map[string]any BeforeJSON string BeforeSnapshotHash string AfterSnapshotHash string ResultSnapshotHash string Operation string Preview cloudAgentApprovalPreview SceneID string } func cloudAgentPrevisSceneCreateSchema() map[string]any { return map[string]any{ "type": "object", "properties": map[string]any{ "canvasSnapshotHash": map[string]any{"type": "string", "description": "previs_scene_read 场景目录返回的 canvasSnapshotHash;创建前画布必须未变化"}, "sceneId": map[string]any{"type": "string", "maxLength": 80, "description": "新的稳定场景 ID"}, "title": map[string]any{"type": "string", "maxLength": cloudAgentPrevisTextLimit, "description": "场景标题"}, "templateId": map[string]any{"type": "string", "enum": []string{"empty", "monologue", "dialogue", "blocking", "product", "action_chase", "interior", "crowd"}, "description": "模板:empty/monologue/dialogue/blocking/product/action_chase/interior/crowd"}, }, "required": []string{"canvasSnapshotHash", "sceneId", "title", "templateId"}, "additionalProperties": false, } } func cloudAgentPrevisPatchOperationSchema() map[string]any { vec3 := map[string]any{"type": "array", "minItems": 3, "maxItems": 3, "items": map[string]any{"type": "number"}} properties := map[string]any{ "type": map[string]any{"type": "string", "enum": []string{"scene_update", "shot_add", "shot_update", "shot_remove", "object_add", "object_update", "object_remove", "object_keyframe_upsert", "object_keyframe_remove", "object_motion_path_set", "object_motion_path_clear", "camera_add", "camera_update", "camera_remove", "camera_keyframe_upsert", "camera_keyframe_remove", "light_add", "light_update", "light_remove"}}, "id": map[string]any{"type": "string", "maxLength": 80, "description": "目标或新建资源 ID"}, "name": map[string]any{"type": "string", "maxLength": cloudAgentPrevisTextLimit}, "title": map[string]any{"type": "string", "maxLength": cloudAgentPrevisTextLimit}, "kind": map[string]any{"type": "string", "enum": []string{"primitive", "actor"}}, "primitive": map[string]any{"type": "string", "enum": []string{"box", "sphere", "cylinder", "plane", "character"}}, "archetype": map[string]any{"type": "string", "enum": []string{"adult", "child", "elderly", "man", "woman", "monster"}, "description": "演员体型原型"}, "characterAssetId": map[string]any{"type": "string", "maxLength": 120}, "characterVersionId": map[string]any{"type": "string", "maxLength": 120}, "referenceNodeId": map[string]any{"type": "string", "maxLength": 120}, "characterName": map[string]any{"type": "string", "maxLength": cloudAgentPrevisTextLimit}, "pose": map[string]any{"type": "string", "enum": []string{"stand", "sit", "walk", "run", "wave", "think", "fight", "kick", "throw", "push", "reach", "arms_crossed", "phone", "bow", "kneel_single", "kneel_double", "lean", "squat", "hands_hips", "neutral", "t_pose"}, "description": "演员姿态"}, "color": map[string]any{"type": "string", "maxLength": 9, "pattern": `^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$`, "description": "十六进制颜色"}, "parentId": map[string]any{"type": "string", "maxLength": 80}, "visible": map[string]any{"type": "boolean"}, "position": vec3, "rotation": vec3, "scale": vec3, "target": vec3, "keyframeId": map[string]any{"type": "string", "maxLength": 120}, "time": map[string]any{"type": "number", "minimum": 0, "maximum": 60}, "easing": map[string]any{"type": "string", "enum": []string{"step", "linear", "smooth"}}, "pathPoints": map[string]any{"type": "array", "minItems": 2, "maxItems": 64, "items": vec3}, "pathSpeed": map[string]any{"type": "string", "enum": []string{"slow", "walk", "run"}}, "startDelay": map[string]any{"type": "number", "minimum": 0, "maximum": 60}, "orientToPath": map[string]any{"type": "boolean"}, "cameraId": map[string]any{"type": "string", "maxLength": 80}, "duration": map[string]any{"type": "number", "minimum": 0.1, "maximum": 60}, "fps": map[string]any{"type": "integer", "minimum": 1, "maximum": 60}, "shotSize": map[string]any{"type": "string", "enum": []string{"extreme_wide", "wide", "full", "medium", "close_up", "extreme_close_up"}}, "cameraMove": map[string]any{"type": "string", "enum": []string{"static", "push_in", "pull_out", "pan_left", "pan_right", "tilt_up", "tilt_down", "orbit_left", "orbit_right", "handheld"}}, "prompt": map[string]any{"type": "string", "maxLength": 2000}, "focalLength": map[string]any{"type": "number", "minimum": 1, "maximum": 300}, "lightType": map[string]any{"type": "string", "enum": []string{"directional", "point", "spot", "ambient"}}, "intensity": map[string]any{"type": "number", "minimum": 0, "maximum": 100}, "angle": map[string]any{"type": "number", "minimum": 0, "maximum": 6.29}, "penumbra": map[string]any{"type": "number", "minimum": 0, "maximum": 1}, "castShadow": map[string]any{"type": "boolean"}, "background": map[string]any{"type": "string", "maxLength": 32}, "environmentIntensity": map[string]any{"type": "number", "minimum": 0, "maximum": 100}, "gridVisible": map[string]any{"type": "boolean"}, "activeShotId": map[string]any{"type": "string", "maxLength": 80}, } return map[string]any{"type": "object", "properties": properties, "required": []string{"type", "id"}, "additionalProperties": false} } func cloudAgentPrevisApplyPatchSchema() map[string]any { return map[string]any{ "type": "object", "properties": map[string]any{ "snapshotHash": map[string]any{"type": "string", "description": "previs_scene_read 聚焦读取返回的场景 snapshotHash;只代表该场景"}, "sceneId": map[string]any{"type": "string", "maxLength": 80, "description": "要修改的场景 ID"}, "operations": map[string]any{"type": "array", "minItems": 1, "maxItems": cloudAgentPrevisMaxOperations, "items": cloudAgentPrevisPatchOperationSchema(), "description": "受限语义操作;不能替换整个场景或写任意 JSON"}, }, "required": []string{"snapshotHash", "sceneId", "operations"}, "additionalProperties": false, } } func cloudAgentPrevisValidateText(value, label string, maxRunes int, required bool) error { if required && strings.TrimSpace(value) == "" { return BadAuthRequest(label + "不能为空") } if !utf8.ValidString(value) || strings.TrimSpace(value) != value { return BadAuthRequest(label + "不能包含首尾空白或无效字符") } if utf8.RuneCountInString(value) > maxRunes { return fmt.Errorf("%s不能超过 %d 个字符", label, maxRunes) } for _, r := range value { if unicode.IsControl(r) { return BadAuthRequest(label + "不能包含控制字符") } } return nil } // cloudAgentPrevisValidatePose validates pose against the canonical enum. func cloudAgentPrevisValidatePose(pose string) error { valid := map[string]bool{ "stand": true, "sit": true, "walk": true, "run": true, "wave": true, "think": true, "fight": true, "kick": true, "throw": true, "push": true, "reach": true, "arms_crossed": true, "phone": true, "bow": true, "kneel_single": true, "kneel_double": true, "lean": true, "squat": true, "hands_hips": true, "neutral": true, "t_pose": true, } if !valid[pose] { return BadAuthRequest(fmt.Sprintf("pose 值 %q 不受支持;请使用 stand/sit/walk/run/wave/think/fight 等", pose)) } return nil } func cloudAgentPrevisValidateEnum(value, label string, allowed ...string) error { for _, candidate := range allowed { if value == candidate { return nil } } return BadAuthRequest(label + "不受支持") } func cloudAgentPrevisIsHexColor(value string) bool { if len(value) != 4 && len(value) != 7 && len(value) != 9 || value[0] != '#' { return false } for _, r := range value[1:] { if !strings.ContainsRune("0123456789abcdefABCDEF", r) { return false } } return true } func cloudAgentPrevisNormalizeLegacyColor(value string) string { trimmed := strings.TrimSpace(value) if cloudAgentPrevisIsHexColor(trimmed) { return strings.ToLower(trimmed) } lower := strings.ToLower(trimmed) var raw string var rgba bool switch { case strings.HasPrefix(lower, "rgb(") && strings.HasSuffix(lower, ")"): raw = trimmed[4 : len(trimmed)-1] case strings.HasPrefix(lower, "rgba(") && strings.HasSuffix(lower, ")"): raw = trimmed[5 : len(trimmed)-1] rgba = true default: return value } parts := strings.Split(raw, ",") if len(parts) != 3 && len(parts) != 4 { return value } channels := make([]int, 3) for index := range channels { channel, err := strconv.Atoi(strings.TrimSpace(parts[index])) if err != nil || channel < 0 || channel > 255 { return value } channels[index] = channel } if !rgba && len(parts) == 4 { return value } result := fmt.Sprintf("#%02x%02x%02x", channels[0], channels[1], channels[2]) if rgba && len(parts) == 4 { alpha, err := strconv.ParseFloat(strings.TrimSpace(parts[3]), 64) if err != nil { return value } if alpha <= 1 { alpha = math.Round(alpha * 255) } if alpha < 0 || alpha > 255 { return value } result += fmt.Sprintf("%02x", int(alpha)) } return result } // Legacy scenes can contain CSS rgb/rgba colors from the browser-era editor. // Normalize those values before strict validation so a harmless scene read or // patch is not blocked by historical presentation data. func cloudAgentPrevisNormalizeSceneColor(value, fallback string) string { normalized := cloudAgentPrevisNormalizeLegacyColor(value) if cloudAgentPrevisIsHexColor(normalized) { return normalized } if strings.TrimSpace(value) == "" { return fallback } return value } func cloudAgentPrevisValidateColor(value, label string) error { if err := cloudAgentPrevisValidateText(value, label, 32, true); err != nil { return err } if !cloudAgentPrevisIsHexColor(value) { return BadAuthRequest(label + "必须是十六进制颜色") } return nil } func cloudAgentPrevisFiniteInRange(value float64, label string, minimum, maximum float64) error { if math.IsNaN(value) || math.IsInf(value, 0) || value < minimum || value > maximum { return BadAuthRequest(label + "超出限制") } return nil } func cloudAgentPrevisValidateTemplateID(value string) error { valid := map[string]bool{"empty": true, "monologue": true, "dialogue": true, "blocking": true, "product": true, "action_chase": true, "interior": true, "crowd": true} if !valid[value] { return BadAuthRequest("templateId 不受支持;有效值:empty/monologue/dialogue/blocking/product/action_chase/interior/crowd") } return nil } func cloudAgentPrevisValidateVec3(value []float64, label string) error { if len(value) != 3 { return BadAuthRequest(label + "必须是包含3个数字的数组") } for _, number := range value { if math.IsNaN(number) || math.IsInf(number, 0) || math.Abs(number) > 100000 { return BadAuthRequest(label + "包含无效或超出范围的数字") } } return nil } func cloudAgentPrevisVec3(value []float64) []any { return []any{value[0], value[1], value[2]} } func cloudAgentPrevisNumberVec3(value any) ([]float64, bool) { result := make([]float64, 3) switch items := value.(type) { case []float64: if len(items) != 3 { return nil, false } copy(result, items) case []any: if len(items) != 3 { return nil, false } for index, item := range items { result[index] = numberValue(item, math.NaN()) } default: return nil, false } for _, number := range result { if math.IsNaN(number) || math.IsInf(number, 0) { return nil, false } } return result, true } func cloudAgentPrevisOptionalVec3(value *[]float64, label string) error { if value == nil { return nil } return cloudAgentPrevisValidateVec3(*value, label) } func cloudAgentPrevisTextPointer(value *string, label string, maxRunes int) error { if value == nil { return nil } return cloudAgentPrevisValidateText(*value, label, maxRunes, false) } func cloudAgentPrevisCanvas(repo *repository.Repository, userID, canvasID string) (*model.CanvasProject, map[string]any, error) { canvas, err := repo.CanvasProjectForUser(userID, canvasID) if err != nil { return nil, nil, err } doc, err := creationDocument(canvas.PayloadJSON) if err != nil { return nil, nil, err } return canvas, doc, nil } func cloudAgentPrevisScenesSize(scenes []map[string]any) error { encoded, err := json.Marshal(scenes) if err != nil || len(encoded) > cloudAgentPrevisDocumentLimit { return BadAuthRequest("预演台场景数据超过限制") } return nil } func cloudAgentPrevisIDSet(items []map[string]any, label string) (map[string]map[string]any, error) { result := make(map[string]map[string]any, len(items)) for _, item := range items { id := stringValue(item["id"]) if err := validateCloudAgentID(id, label, 80); err != nil { return nil, err } if _, exists := result[id]; exists { return nil, BadAuthRequest(label + "重复") } result[id] = item } return result, nil } func cloudAgentPrevisFinite(value any) bool { number := numberValue(value, math.NaN()) return !math.IsNaN(number) && !math.IsInf(number, 0) } func cloudAgentPrevisValidateTransform(value any, label string) error { transform, ok := value.(map[string]any) if !ok { return BadAuthRequest(label + "缺少 transform") } for _, key := range []string{"position", "rotation", "scale"} { items, ok := transform[key].([]any) if !ok || len(items) != 3 { return BadAuthRequest(label + "的 transform." + key + " 必须包含3个数字") } for _, item := range items { if !cloudAgentPrevisFinite(item) { return BadAuthRequest(label + "的 transform 包含无效数字") } } } return nil } func cloudAgentPrevisValidateScene(scene map[string]any) error { if err := validateCloudAgentID(stringValue(scene["id"]), "导演场景 ID", 80); err != nil { return err } if numberValue(scene["version"], 0) != 1 { return BadAuthRequest("预演台场景版本不受支持") } if err := cloudAgentPrevisValidateText(stringValue(scene["title"]), "场景标题", cloudAgentPrevisTextLimit, true); err != nil { return err } background := cloudAgentPrevisNormalizeSceneColor(stringValue(scene["background"]), "#111827") scene["background"] = background if err := cloudAgentPrevisValidateColor(stringValue(scene["background"]), "场景背景色"); err != nil { return err } if err := cloudAgentPrevisFiniteInRange(numberValue(scene["environmentIntensity"], math.NaN()), "环境光强度", 0, 100); err != nil { return err } objects := creationMaps(scene["objects"]) cameras := creationMaps(scene["cameras"]) lights := creationMaps(scene["lights"]) shots := creationMaps(scene["shots"]) if len(objects) > cloudAgentPrevisMaxObjects || len(cameras) > cloudAgentPrevisMaxCameras || len(lights) > cloudAgentPrevisMaxLights || len(shots) > cloudAgentPrevisMaxShots { return BadAuthRequest("预演台场景对象、摄影机、灯光或镜头数量超过限制") } objectByID, err := cloudAgentPrevisIDSet(objects, "导演对象 ID") if err != nil { return err } cameraByID, err := cloudAgentPrevisIDSet(cameras, "导演摄影机 ID") if err != nil { return err } _, err = cloudAgentPrevisIDSet(lights, "导演灯光 ID") if err != nil { return err } shotByID, err := cloudAgentPrevisIDSet(shots, "导演镜头 ID") if err != nil { return err } if len(shots) == 0 || stringValue(scene["activeShotId"]) == "" || shotByID[stringValue(scene["activeShotId"])] == nil { return BadAuthRequest("预演台场景必须包含有效的 activeShotId") } for id, object := range objectByID { if err := cloudAgentPrevisValidateText(stringValue(object["name"]), "导演对象名称", cloudAgentPrevisTextLimit, true); err != nil { return err } if err := cloudAgentPrevisValidateEnum(stringValue(object["kind"]), "导演对象类型", "primitive", "model", "actor", "billboard"); err != nil { return err } if primitive := stringValue(object["primitive"]); primitive != "" { if err := cloudAgentPrevisValidateEnum(primitive, "导演对象 primitive", "box", "sphere", "cylinder", "plane", "character"); err != nil { return err } } objectColor := cloudAgentPrevisNormalizeSceneColor(stringValue(object["color"]), "#8795a5") object["color"] = objectColor if err := cloudAgentPrevisValidateColor(stringValue(object["color"]), "导演对象颜色"); err != nil { return err } if err := cloudAgentPrevisValidateTransform(object["transform"], "导演对象 "+id); err != nil { return err } if binding, ok := object["characterBinding"].(map[string]any); ok { for _, key := range []string{"characterAssetId", "characterVersionId", "referenceNodeId"} { if err := validateCloudAgentID(stringValue(binding[key]), "角色绑定 "+key, 120); err != nil { return err } } if name := stringValue(binding["characterName"]); name != "" { if err := cloudAgentPrevisValidateText(name, "角色绑定 characterName", cloudAgentPrevisTextLimit, true); err != nil { return err } } } if keyframes, ok := object["keyframes"]; ok { for _, keyframe := range creationMaps(keyframes) { if err := validateCloudAgentID(stringValue(keyframe["id"]), "对象关键帧 ID", 120); err != nil { return err } if err := cloudAgentPrevisFiniteInRange(numberValue(keyframe["time"], math.NaN()), "对象关键帧 time", 0, 60); err != nil { return err } if err := cloudAgentPrevisValidateTransform(keyframe["transform"], "对象关键帧 "+stringValue(keyframe["id"])); err != nil { return err } } } if motionPath, ok := object["motionPath"].(map[string]any); ok { points, pointsOK := motionPath["points"].([]any) if !pointsOK || len(points) < 2 || len(points) > 64 { return BadAuthRequest("预演台 motionPath 点数无效") } for index, point := range points { value, ok := cloudAgentPrevisNumberVec3(point) if !ok { return BadAuthRequest(fmt.Sprintf("预演台 motionPath 点 %d 无效", index)) } if err := cloudAgentPrevisValidateVec3(value, "motionPath point"); err != nil { return err } } if err := cloudAgentPrevisValidateEnum(stringValue(motionPath["speed"]), "motionPath speed", "slow", "walk", "run"); err != nil { return err } if err := cloudAgentPrevisFiniteInRange(numberValue(motionPath["startDelay"], math.NaN()), "motionPath startDelay", 0, 60); err != nil { return err } } if parentID := stringValue(object["parentId"]); parentID != "" { if err := validateCloudAgentID(parentID, "导演对象 parentId", 80); err != nil { return err } if objectByID[parentID] == nil || parentID == id { return BadAuthRequest("预演台对象 parentId 无效") } } } for id, camera := range cameraByID { if err := cloudAgentPrevisValidateText(stringValue(camera["name"]), "导演摄影机名称", cloudAgentPrevisTextLimit, true); err != nil { return err } if err := cloudAgentPrevisFiniteInRange(numberValue(camera["focalLength"], math.NaN()), "摄影机焦段", 1, 300); err != nil { return err } target, targetOK := cloudAgentPrevisNumberVec3(camera["target"]) if !targetOK { return BadAuthRequest("导演摄影机 " + id + " target 必须包含3个数字") } if err := cloudAgentPrevisValidateVec3(target, "导演摄影机 "+id+" target"); err != nil { return err } if err := cloudAgentPrevisValidateTransform(camera["transform"], "导演摄影机 "+id); err != nil { return err } if keyframes, ok := camera["keyframes"]; ok { for _, keyframe := range creationMaps(keyframes) { if err := validateCloudAgentID(stringValue(keyframe["id"]), "摄影机关键帧 ID", 120); err != nil { return err } if err := cloudAgentPrevisFiniteInRange(numberValue(keyframe["time"], math.NaN()), "摄影机关键帧 time", 0, 60); err != nil { return err } if err := cloudAgentPrevisValidateTransform(keyframe["transform"], "摄影机关键帧 "+stringValue(keyframe["id"])); err != nil { return err } } } } for id, shot := range shotByID { cameraID := stringValue(shot["cameraId"]) if cameraByID[cameraID] == nil { return BadAuthRequest("导演镜头 " + id + " 引用了不存在的摄影机") } duration := numberValue(shot["duration"], math.NaN()) fps := numberValue(shot["fps"], math.NaN()) if err := cloudAgentPrevisFiniteInRange(duration, "导演镜头时长", 0.1, 60); err != nil { return err } if err := cloudAgentPrevisFiniteInRange(fps, "导演镜头帧率", 1, 60); err != nil { return err } if err := cloudAgentPrevisValidateText(stringValue(shot["name"]), "导演镜头名称", cloudAgentPrevisTextLimit, true); err != nil { return err } if err := cloudAgentPrevisValidateEnum(stringValue(shot["shotSize"]), "导演景别", "extreme_wide", "wide", "full", "medium", "close_up", "extreme_close_up"); err != nil { return err } if err := cloudAgentPrevisValidateEnum(stringValue(shot["cameraMove"]), "导演运镜", "static", "push_in", "pull_out", "pan_left", "pan_right", "tilt_up", "tilt_down", "orbit_left", "orbit_right", "handheld"); err != nil { return err } if err := cloudAgentPrevisValidateText(stringValue(shot["prompt"]), "导演镜头提示词", 2000, false); err != nil { return err } } for _, light := range creationMaps(scene["lights"]) { lightID := stringValue(light["id"]) if err := cloudAgentPrevisValidateText(stringValue(light["name"]), "导演灯光名称", cloudAgentPrevisTextLimit, true); err != nil { return err } if err := cloudAgentPrevisValidateEnum(stringValue(light["type"]), "导演灯光类型", "directional", "point", "spot", "ambient"); err != nil { return err } lightColor := cloudAgentPrevisNormalizeSceneColor(stringValue(light["color"]), "#ffffff") light["color"] = lightColor if err := cloudAgentPrevisValidateColor(stringValue(light["color"]), "导演灯光颜色"); err != nil { return err } if err := cloudAgentPrevisFiniteInRange(numberValue(light["intensity"], math.NaN()), "导演灯光强度", 0, 100); err != nil { return err } if angle, ok := light["angle"]; ok { if err := cloudAgentPrevisFiniteInRange(numberValue(angle, math.NaN()), "导演灯光 "+lightID+" angle", 0, 6.29); err != nil { return err } } if penumbra, ok := light["penumbra"]; ok { if err := cloudAgentPrevisFiniteInRange(numberValue(penumbra, math.NaN()), "导演灯光 "+lightID+" penumbra", 0, 1); err != nil { return err } } if err := cloudAgentPrevisValidateTransform(light["transform"], "导演灯光 "+lightID); err != nil { return err } } // Parent links are a small directed graph. Walk every chain to reject cycles. for id := range objectByID { seen := map[string]bool{} for current := id; current != ""; { if seen[current] { return BadAuthRequest("预演台对象 parentId 不能形成环") } seen[current] = true current = stringValue(objectByID[current]["parentId"]) if current != "" && objectByID[current] == nil { return BadAuthRequest("预演台对象 parentId 引用不存在对象") } } } return nil } func cloudAgentPrevisSceneCreateTemplate(args cloudAgentPrevisSceneCreateArgs) (map[string]any, error) { if err := validateCloudAgentID(args.SceneID, "导演场景 ID", 80); err != nil { return nil, err } if err := cloudAgentPrevisValidateText(args.Title, "场景标题", cloudAgentPrevisTextLimit, true); err != nil { return nil, err } if err := cloudAgentPrevisValidateTemplateID(args.TemplateID); err != nil { return nil, err } now := time.Now().UTC().Format(time.RFC3339Nano) cameraID := args.SceneID + "-camera-1" shotID := args.SceneID + "-shot-1" camera := map[string]any{ "id": cameraID, "name": "主摄影机", "transform": map[string]any{"position": []any{4.8, 2.7, 6.8}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "target": []any{0, 1, 0}, "focalLength": 35, "fov": 50, "aperture": 2.8, "focusDistance": 5, "near": 0.05, "far": 500, "keyframes": []any{}, } objects := []any{} lights := []any{ map[string]any{"id": args.SceneID + "-light-key", "name": "主光", "type": "directional", "transform": map[string]any{"position": []any{4, 6, 4}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#ffffff", "intensity": 2.4, "angle": 0.7853981634, "penumbra": 0.35, "castShadow": true}, map[string]any{"id": args.SceneID + "-light-fill", "name": "轮廓光", "type": "directional", "transform": map[string]any{"position": []any{-4, 3, -2}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#ffffff", "intensity": 1.1, "angle": 0.7853981634, "penumbra": 0.35, "castShadow": true}, map[string]any{"id": args.SceneID + "-light-ambient", "name": "环境光", "type": "ambient", "transform": map[string]any{"position": []any{0, 0, 0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#ffffff", "intensity": 0.65, "angle": 0.7853981634, "penumbra": 0.35, "castShadow": false}, } if args.TemplateID == "action_chase" { // Two actors: pursuer behind, target ahead, side-angle camera for kinetic energy objects = append(objects, map[string]any{"id": args.SceneID + "-actor-1", "name": "追逐者", "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{0, 0, -1.2}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#2f7de1", "visible": true, "castShadow": true, "receiveShadow": true, "pose": "run", "archetype": "man", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}}, map[string]any{"id": args.SceneID + "-actor-2", "name": "逃跑者", "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{0, 0, 2.0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#d84949", "visible": true, "castShadow": true, "receiveShadow": true, "pose": "run", "archetype": "adult", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}}, ) camera["transform"] = map[string]any{"position": []any{3.8, 1.4, 0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}} camera["target"] = []any{0, 1, 0.5} camera["focalLength"] = 50 camera["fov"] = 39.6 } if args.TemplateID == "interior" { // Room sketch: floor plane, 3 wall planes, sofa-scale box, table-scale box objects = append(objects, map[string]any{"id": args.SceneID + "-wall-back", "name": "后墙", "kind": "primitive", "primitive": "plane", "transform": map[string]any{"position": []any{0, 2.0, -3.0}, "rotation": []any{0, 0, 0}, "scale": []any{6, 4, 1}}, "color": "#d8dde3", "visible": true, "castShadow": false, "receiveShadow": true, "keyframes": []any{}}, map[string]any{"id": args.SceneID + "-wall-left", "name": "左墙", "kind": "primitive", "primitive": "plane", "transform": map[string]any{"position": []any{-3.0, 2.0, 0}, "rotation": []any{0, 1.5708, 0}, "scale": []any{6, 4, 1}}, "color": "#d8dde3", "visible": true, "castShadow": false, "receiveShadow": true, "keyframes": []any{}}, map[string]any{"id": args.SceneID + "-sofa", "name": "沙发", "kind": "primitive", "primitive": "box", "transform": map[string]any{"position": []any{0, 0.45, -1.8}, "rotation": []any{0, 0, 0}, "scale": []any{2.2, 0.9, 0.9}}, "color": "#8795a5", "visible": true, "castShadow": true, "receiveShadow": true, "keyframes": []any{}}, map[string]any{"id": args.SceneID + "-table", "name": "茶几", "kind": "primitive", "primitive": "box", "transform": map[string]any{"position": []any{0, 0.23, 0.2}, "rotation": []any{0, 0, 0}, "scale": []any{1.1, 0.46, 0.65}}, "color": "#6b7580", "visible": true, "castShadow": true, "receiveShadow": true, "keyframes": []any{}}, map[string]any{"id": args.SceneID + "-actor-1", "name": "演员 1", "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{-0.6, 0, 0.6}, "rotation": []any{0, -0.4, 0}, "scale": []any{1, 1, 1}}, "color": "#2f7de1", "visible": true, "castShadow": true, "receiveShadow": true, "pose": "sit", "archetype": "adult", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}}, ) camera["transform"] = map[string]any{"position": []any{3.2, 1.65, 3.0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}} camera["target"] = []any{-0.2, 1.0, 0} camera["focalLength"] = 35 } if args.TemplateID == "crowd" { // 3x3 crowd grid with distinct colors crowdColors := []string{"#2f7de1", "#d84949", "#dfae3f", "#34a276", "#8b5cf6", "#e879f9", "#2f7de1", "#d84949", "#dfae3f"} for row := 0; row < 3; row++ { for col := 0; col < 3; col++ { idx := row*3 + col id := fmt.Sprintf("%s-actor-%d", args.SceneID, idx+1) x := float64(col-1) * 1.2 z := float64(row-1) * 1.2 objects = append(objects, map[string]any{ "id": id, "name": fmt.Sprintf("群众 %d", idx+1), "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{x, 0, z}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": crowdColors[idx], "visible": true, "castShadow": true, "receiveShadow": true, "pose": "stand", "archetype": "adult", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}, }) } } camera["transform"] = map[string]any{"position": []any{0.5, 4.5, 6.5}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}} camera["target"] = []any{0, 0.8, 0} camera["focalLength"] = 28 camera["fov"] = 65.0 } if args.TemplateID == "monologue" || args.TemplateID == "dialogue" || args.TemplateID == "blocking" { objects = append(objects, map[string]any{"id": args.SceneID + "-actor-1", "name": "演员 1", "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{0, 0, 0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#f1f3f5", "visible": true, "castShadow": true, "receiveShadow": true, "pose": "stand", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}}) } if args.TemplateID == "dialogue" || args.TemplateID == "blocking" { objects = append(objects, map[string]any{"id": args.SceneID + "-actor-2", "name": "演员 2", "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{1.8, 0, -0.8}, "rotation": []any{0, 0.2, 0}, "scale": []any{1, 1, 1}}, "color": "#2f7de1", "visible": true, "castShadow": true, "receiveShadow": true, "pose": "stand", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}}) } if args.TemplateID == "product" { objects = append(objects, map[string]any{"id": args.SceneID + "-product-1", "name": "产品", "kind": "primitive", "primitive": "box", "transform": map[string]any{"position": []any{0, 0.6, 0}, "rotation": []any{0, 0, 0}, "scale": []any{1.2, 1.2, 1.2}}, "color": "#8795a5", "visible": true, "castShadow": true, "receiveShadow": true, "keyframes": []any{}}) } scene := map[string]any{ "id": args.SceneID, "version": 1, "title": args.Title, "background": "#d8dde3", "environmentIntensity": 0.7, "gridVisible": true, "objects": objects, "cameras": []any{camera}, "lights": lights, "shots": []any{map[string]any{"id": shotID, "name": "镜头 1", "cameraId": cameraID, "duration": 5, "fps": 24, "shotSize": "medium", "cameraMove": "static", "prompt": "", "cuts": []any{}}}, "activeShotId": shotID, "createdAt": now, "updatedAt": now, } return scene, cloudAgentPrevisValidateScene(scene) } func prepareCloudAgentPrevisSceneCreate(repo *repository.Repository, userID, canvasID string, call cloudAgentCall) (*cloudAgentPrevisMutationPlan, error) { var args cloudAgentPrevisSceneCreateArgs if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil { return nil, cloudAgentJSONArgumentError(err) } if strings.TrimSpace(args.CanvasSnapshotHash) == "" { return nil, cloudAgentFieldError("canvasSnapshotHash", "required", "创建预演场景前必须先读取场景目录") } canvas, doc, err := cloudAgentPrevisCanvas(repo, userID, canvasID) if err != nil { return nil, err } beforeHash := cloudAgentCanvasHash(doc) if beforeHash != args.CanvasSnapshotHash { return nil, &cloudAgentFieldArgumentError{error: &cloudAgentArgumentError{creationConflict("画布已变化,本次未创建场景;请重新读取场景目录")}, Field: "canvasSnapshotHash", Issue: "stale_snapshot"} } scenes := creationMaps(doc["previsScenes"]) if len(scenes) >= cloudAgentPrevisMaxScenes { return nil, BadAuthRequest("当前画布的预演场景数量已达到限制") } if _, exists := findPrevisScene(scenes, args.SceneID); exists { return nil, BadAuthRequest("预演场景 ID 已存在") } scene, err := cloudAgentPrevisSceneCreateTemplate(args) if err != nil { return nil, err } scenes = append(scenes, scene) if err := cloudAgentPrevisScenesSize(scenes); err != nil { return nil, err } doc["previsScenes"] = scenes return &cloudAgentPrevisMutationPlan{Canvas: canvas, Document: doc, BeforeJSON: canvas.PayloadJSON, BeforeSnapshotHash: beforeHash, AfterSnapshotHash: cloudAgentCanvasHash(doc), ResultSnapshotHash: creationHash(scene), Operation: "previs_scene_create", SceneID: args.SceneID, Preview: cloudAgentPrevisMutationApprovalPreview("previs_scene_create", args.SceneID, args.Title, []string{"创建受控白模场景", "模板:" + args.TemplateID})}, nil } func prepareCloudAgentPrevisApplyPatch(repo *repository.Repository, userID, canvasID string, call cloudAgentCall) (*cloudAgentPrevisMutationPlan, error) { var args cloudAgentPrevisApplyPatchArgs if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil { return nil, cloudAgentJSONArgumentError(err) } if strings.TrimSpace(args.SnapshotHash) == "" { return nil, cloudAgentFieldError("snapshotHash", "required", "应用预演补丁前必须先读取指定场景") } if len(args.Operations) < 1 || len(args.Operations) > cloudAgentPrevisMaxOperations { return nil, cloudAgentFieldError("operations", "item_count", fmt.Sprintf("预演补丁 operations 必须包含1到%d项操作", cloudAgentPrevisMaxOperations)) } if err := validateCloudAgentID(args.SceneID, "导演场景 ID", 80); err != nil { return nil, err } canvas, doc, err := cloudAgentPrevisCanvas(repo, userID, canvasID) if err != nil { return nil, err } scenes := creationMaps(doc["previsScenes"]) scene, ok := findPrevisScene(scenes, args.SceneID) if !ok { return nil, BadAuthRequest("导演场景不存在或不属于当前画布") } canvasSnapshotHash := cloudAgentCanvasHash(doc) sceneSnapshotHash := creationHash(scene) if sceneSnapshotHash != args.SnapshotHash { return nil, &cloudAgentFieldArgumentError{error: &cloudAgentArgumentError{creationConflict("预演场景已变化,本次未写入;请重新读取并重新申请审批")}, Field: "snapshotHash", Issue: "stale_snapshot"} } items := make([]cloudAgentApprovalPreviewItem, 0, len(args.Operations)) for index, operation := range args.Operations { if err := cloudAgentPrevisValidateCharacterBindingInCanvas(doc, operation); err != nil { return nil, err } item, err := cloudAgentPrevisApplyPatchOperation(scene, operation, index) if err != nil { return nil, err } items = append(items, item) } scene["updatedAt"] = time.Now().UTC().Format(time.RFC3339Nano) if err := cloudAgentPrevisValidateScene(scene); err != nil { return nil, err } if err := cloudAgentPrevisScenesSize(scenes); err != nil { return nil, err } doc["previsScenes"] = scenes return &cloudAgentPrevisMutationPlan{Canvas: canvas, Document: doc, BeforeJSON: canvas.PayloadJSON, BeforeSnapshotHash: canvasSnapshotHash, AfterSnapshotHash: cloudAgentCanvasHash(doc), ResultSnapshotHash: creationHash(scene), Operation: "previs_apply_patch", SceneID: args.SceneID, Preview: cloudAgentPrevisMutationApprovalPreview("previs_apply_patch", args.SceneID, stringValue(scene["title"]), itemsToDetails(items))}, nil } func itemsToDetails(items []cloudAgentApprovalPreviewItem) []string { result := make([]string, 0, len(items)) for _, item := range items { if item.Summary != "" { result = append(result, item.Summary) } } return result } func cloudAgentPrevisMutationApprovalPreview(operation, sceneID, title string, details []string) cloudAgentApprovalPreview { if len(details) > 12 { details = details[:12] } return cloudAgentApprovalPreview{Kind: "previs_mutation", Title: "确认预演台修改", Description: fmt.Sprintf("Agent 准备修改预演场景《%s》。批准后才会写入当前画布。", truncateRunes(title, cloudAgentPrevisTextLimit)), Items: []cloudAgentApprovalPreviewItem{{Operation: operation, NodeID: sceneID, NodeTitle: truncateRunes(title, cloudAgentPrevisTextLimit), Details: details, Summary: fmt.Sprintf("修改预演场景《%s》", truncateRunes(title, cloudAgentPrevisTextLimit))}}} } func cloudAgentPrevisMapTransform(item map[string]any) map[string]any { if transform, ok := item["transform"].(map[string]any); ok { return transform } transform := map[string]any{"position": []any{0, 0, 0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}} item["transform"] = transform return transform } func cloudAgentPrevisApplyTransform(item map[string]any, operation cloudAgentPrevisPatchOperation) error { if err := cloudAgentPrevisOptionalVec3(operation.Position, "position"); err != nil { return err } if err := cloudAgentPrevisOptionalVec3(operation.Rotation, "rotation"); err != nil { return err } if err := cloudAgentPrevisOptionalVec3(operation.Scale, "scale"); err != nil { return err } transform := cloudAgentPrevisMapTransform(item) if operation.Position != nil { transform["position"] = cloudAgentPrevisVec3(*operation.Position) } if operation.Rotation != nil { transform["rotation"] = cloudAgentPrevisVec3(*operation.Rotation) } if operation.Scale != nil { transform["scale"] = cloudAgentPrevisVec3(*operation.Scale) } return nil } func cloudAgentPrevisApplyName(item map[string]any, value *string, label string) error { if value == nil { return nil } if err := cloudAgentPrevisValidateText(*value, label, cloudAgentPrevisTextLimit, true); err != nil { return err } item["name"] = *value return nil } func cloudAgentPrevisApplyCharacterBinding(item map[string]any, operation cloudAgentPrevisPatchOperation) error { provided := operation.CharacterAssetID != nil || operation.CharacterVersionID != nil || operation.ReferenceNodeID != nil || operation.CharacterName != nil if !provided { return nil } if operation.CharacterAssetID == nil || operation.CharacterVersionID == nil || operation.ReferenceNodeID == nil { return BadAuthRequest("角色绑定需要 characterAssetId、characterVersionId 和 referenceNodeId") } for value, label := range map[*string]string{ operation.CharacterAssetID: "characterAssetId", operation.CharacterVersionID: "characterVersionId", operation.ReferenceNodeID: "referenceNodeId", } { if value == nil || strings.TrimSpace(*value) == "" { return BadAuthRequest(label + " 不能为空") } if err := validateCloudAgentID(*value, label, 120); err != nil { return err } } binding := map[string]any{ "characterAssetId": *operation.CharacterAssetID, "characterVersionId": *operation.CharacterVersionID, "referenceNodeId": *operation.ReferenceNodeID, } if operation.CharacterName != nil { if err := cloudAgentPrevisValidateText(*operation.CharacterName, "characterName", cloudAgentPrevisTextLimit, true); err != nil { return err } binding["characterName"] = *operation.CharacterName } item["characterBinding"] = binding item["sourceNodeId"] = *operation.ReferenceNodeID return nil } func cloudAgentPrevisValidateCharacterBindingInCanvas(doc map[string]any, operation cloudAgentPrevisPatchOperation) error { if operation.CharacterAssetID == nil && operation.CharacterVersionID == nil && operation.ReferenceNodeID == nil && operation.CharacterName == nil { return nil } if operation.CharacterAssetID == nil || operation.CharacterVersionID == nil || operation.ReferenceNodeID == nil { return BadAuthRequest("角色绑定需要 characterAssetId、characterVersionId 和 referenceNodeId") } for _, node := range creationMaps(doc["nodes"]) { if stringValue(node["id"]) != *operation.ReferenceNodeID { continue } metadata, _ := node["metadata"].(map[string]any) if stringValue(metadata["workflowKind"]) != "character" || stringValue(metadata["characterAssetId"]) != *operation.CharacterAssetID || stringValue(metadata["characterVersionId"]) != *operation.CharacterVersionID { return BadAuthRequest("referenceNodeId 必须是匹配 characterAssetId 和 characterVersionId 的角色卡节点") } return nil } return BadAuthRequest("referenceNodeId 不属于当前画布") } func cloudAgentPrevisShotDuration(scene map[string]any) float64 { for _, shot := range creationMaps(scene["shots"]) { if stringValue(shot["id"]) == stringValue(scene["activeShotId"]) { return numberValue(shot["duration"], 5) } } return 5 } func cloudAgentPrevisKeyframeTransform(item map[string]any, operation cloudAgentPrevisPatchOperation) (map[string]any, error) { transform := cloudAgentPrevisMapTransform(item) result := map[string]any{} for _, key := range []string{"position", "rotation", "scale"} { value, ok := cloudAgentPrevisNumberVec3(transform[key]) if !ok { return nil, BadAuthRequest("关键帧目标对象 transform 无效") } result[key] = cloudAgentPrevisVec3(value) } for value, key := range map[*[]float64]string{operation.Position: "position", operation.Rotation: "rotation", operation.Scale: "scale"} { if value == nil { continue } if err := cloudAgentPrevisValidateVec3(*value, key); err != nil { return nil, err } result[key] = cloudAgentPrevisVec3(*value) } return result, nil } func cloudAgentPrevisValidateEasing(value string) error { if value == "" { return nil } return cloudAgentPrevisValidateEnum(value, "关键帧 easing", "step", "linear", "smooth") } func cloudAgentPrevisUpsertKeyframe(item map[string]any, operation cloudAgentPrevisPatchOperation, duration float64) error { if operation.Time == nil { return BadAuthRequest("关键帧操作需要 time") } if err := cloudAgentPrevisFiniteInRange(*operation.Time, "关键帧 time", 0, duration); err != nil { return err } if err := cloudAgentPrevisValidateEasing(operation.Easing); err != nil { return err } transform, err := cloudAgentPrevisKeyframeTransform(item, operation) if err != nil { return err } keyframes := creationMaps(item["keyframes"]) keyframeID := "" if operation.KeyframeID != nil { if err := validateCloudAgentID(*operation.KeyframeID, "keyframeId", 120); err != nil { return err } keyframeID = *operation.KeyframeID } match := -1 for index, keyframe := range keyframes { if (keyframeID != "" && stringValue(keyframe["id"]) == keyframeID) || math.Abs(numberValue(keyframe["time"], math.NaN())-*operation.Time) < 0.001 { match = index break } } if match < 0 { if keyframeID == "" { keyframeID = fmt.Sprintf("%s-kf-%d", stringValue(item["id"]), len(keyframes)+1) } keyframe := map[string]any{"id": keyframeID, "time": *operation.Time, "transform": transform} if operation.Easing != "" { keyframe["easing"] = operation.Easing } keyframes = append(keyframes, keyframe) } else { keyframes[match]["id"] = keyframeIDOrExisting(keyframeID, keyframes[match]) keyframes[match]["time"] = *operation.Time keyframes[match]["transform"] = transform if operation.Easing == "" { delete(keyframes[match], "easing") } else { keyframes[match]["easing"] = operation.Easing } } for i := range keyframes { for j := i + 1; j < len(keyframes); j++ { if numberValue(keyframes[j]["time"], 0) < numberValue(keyframes[i]["time"], 0) { keyframes[i], keyframes[j] = keyframes[j], keyframes[i] } } } item["keyframes"] = keyframes return nil } func keyframeIDOrExisting(requested string, existing map[string]any) string { if requested != "" { return requested } return stringValue(existing["id"]) } func cloudAgentPrevisRemoveKeyframe(item map[string]any, operation cloudAgentPrevisPatchOperation) error { if operation.KeyframeID == nil || strings.TrimSpace(*operation.KeyframeID) == "" { return BadAuthRequest("删除关键帧需要 keyframeId") } if err := validateCloudAgentID(*operation.KeyframeID, "keyframeId", 120); err != nil { return err } keyframes := creationMaps(item["keyframes"]) filtered := make([]map[string]any, 0, len(keyframes)) for _, keyframe := range keyframes { if stringValue(keyframe["id"]) != *operation.KeyframeID { filtered = append(filtered, keyframe) } } if len(filtered) == len(keyframes) { return BadAuthRequest("目标关键帧不存在") } item["keyframes"] = filtered return nil } func cloudAgentPrevisSetMotionPath(item map[string]any, operation cloudAgentPrevisPatchOperation, duration float64) error { if len(operation.PathPoints) < 2 || len(operation.PathPoints) > 64 { return BadAuthRequest("motion path 至少需要2个、最多64个点") } for index, point := range operation.PathPoints { if err := cloudAgentPrevisValidateVec3(point, fmt.Sprintf("pathPoints[%d]", index)); err != nil { return err } } speed := operation.PathSpeed if speed == "" { speed = "walk" } if err := cloudAgentPrevisValidateEnum(speed, "pathSpeed", "slow", "walk", "run"); err != nil { return err } startDelay := 0.0 if operation.StartDelay != nil { startDelay = *operation.StartDelay } if err := cloudAgentPrevisFiniteInRange(startDelay, "startDelay", 0, duration); err != nil { return err } orient := false if operation.OrientToPath != nil { orient = *operation.OrientToPath } points := make([]any, 0, len(operation.PathPoints)) for _, point := range operation.PathPoints { points = append(points, cloudAgentPrevisVec3(point)) } item["motionPath"] = map[string]any{"points": points, "speed": speed, "startDelay": startDelay, "orientToPath": orient} return nil } func cloudAgentPrevisApplyObjectPatch(objects *[]map[string]any, operation cloudAgentPrevisPatchOperation, index int) (cloudAgentApprovalPreviewItem, error) { items := *objects byID, err := cloudAgentPrevisIDSet(items, "导演对象 ID") if err != nil { return cloudAgentApprovalPreviewItem{}, err } if err := validateCloudAgentID(operation.ID, fmt.Sprintf("operations[%d].id", index), 80); err != nil { return cloudAgentApprovalPreviewItem{}, err } existing := byID[operation.ID] summary := "" switch operation.Type { case "object_add": if existing != nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增对象 ID 已存在") } if operation.Kind != "primitive" && operation.Kind != "actor" { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增对象 kind 只支持 primitive 或 actor") } primitive := operation.Primitive if primitive == "" { primitive = "box" } if primitive != "box" && primitive != "sphere" && primitive != "cylinder" && primitive != "plane" && primitive != "character" { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增对象 primitive 不受支持") } name := "新对象" if operation.Name != nil { name = *operation.Name } if err := cloudAgentPrevisValidateText(name, "对象名称", cloudAgentPrevisTextLimit, true); err != nil { return cloudAgentApprovalPreviewItem{}, err } position, rotation, scale := []float64{0, 0.5, 0}, []float64{0, 0, 0}, []float64{1, 1, 1} if operation.Position != nil { position = *operation.Position } if operation.Rotation != nil { rotation = *operation.Rotation } if operation.Scale != nil { scale = *operation.Scale } if err := cloudAgentPrevisValidateVec3(position, "position"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if err := cloudAgentPrevisValidateVec3(rotation, "rotation"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if err := cloudAgentPrevisValidateVec3(scale, "scale"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if operation.Color != nil { if err := cloudAgentPrevisValidateColor(*operation.Color, "对象颜色"); err != nil { return cloudAgentApprovalPreviewItem{}, err } } if operation.Pose != "" { if err := cloudAgentPrevisValidatePose(operation.Pose); err != nil { return cloudAgentApprovalPreviewItem{}, err } } if operation.ParentID != nil && *operation.ParentID != "" { if err := validateCloudAgentID(*operation.ParentID, "对象 parentId", 80); err != nil { return cloudAgentApprovalPreviewItem{}, err } } defaultColor := "#8795a5" if operation.Kind == "actor" { // Assign a distinct colour based on current actor count so each new actor stands out actorCount := 0 for _, obj := range items { if stringValue(obj["kind"]) == "actor" { actorCount++ } } actorColors := []string{"#2f7de1", "#d84949", "#dfae3f", "#34a276", "#8b5cf6", "#e879f9"} defaultColor = actorColors[actorCount%len(actorColors)] } item := map[string]any{"id": operation.ID, "name": name, "kind": operation.Kind, "primitive": primitive, "transform": map[string]any{"position": cloudAgentPrevisVec3(position), "rotation": cloudAgentPrevisVec3(rotation), "scale": cloudAgentPrevisVec3(scale)}, "color": defaultColor, "visible": true, "castShadow": true, "receiveShadow": true, "keyframes": []any{}} if operation.Color != nil { item["color"] = *operation.Color } if operation.Visible != nil { item["visible"] = *operation.Visible } if operation.Pose != "" { item["pose"] = operation.Pose } if operation.Kind == "actor" && operation.Pose == "" { item["pose"] = "stand" } if operation.Kind == "actor" { item["archetype"] = "adult" item["boneTracks"] = []any{} item["boneOverrides"] = map[string]any{} item["motionClips"] = []any{} } if operation.Archetype != "" { validArchetypes := map[string]bool{"adult": true, "child": true, "elderly": true, "man": true, "woman": true, "monster": true} if !validArchetypes[operation.Archetype] { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("archetype 值不受支持") } item["archetype"] = operation.Archetype } if operation.ParentID != nil && *operation.ParentID != "" { item["parentId"] = *operation.ParentID } if err := cloudAgentPrevisApplyCharacterBinding(item, operation); err != nil { return cloudAgentApprovalPreviewItem{}, err } items = append(items, item) summary = fmt.Sprintf("新增对象《%s》", name) case "object_update": if existing == nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("要更新的对象不存在") } if err := cloudAgentPrevisApplyName(existing, operation.Name, "对象名称"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if operation.Color != nil { if err := cloudAgentPrevisValidateColor(*operation.Color, "对象颜色"); err != nil { return cloudAgentApprovalPreviewItem{}, err } existing["color"] = *operation.Color } if operation.Visible != nil { existing["visible"] = *operation.Visible } if operation.Pose != "" { if err := cloudAgentPrevisValidatePose(operation.Pose); err != nil { return cloudAgentApprovalPreviewItem{}, err } existing["pose"] = operation.Pose } if operation.ParentID != nil { if *operation.ParentID == "" { delete(existing, "parentId") } else { if err := validateCloudAgentID(*operation.ParentID, "对象 parentId", 80); err != nil { return cloudAgentApprovalPreviewItem{}, err } existing["parentId"] = *operation.ParentID } } if err := cloudAgentPrevisApplyCharacterBinding(existing, operation); err != nil { return cloudAgentApprovalPreviewItem{}, err } if err := cloudAgentPrevisApplyTransform(existing, operation); err != nil { return cloudAgentApprovalPreviewItem{}, err } summary = fmt.Sprintf("修改对象《%s》", stringValue(existing["name"])) case "object_remove": if existing == nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("要删除的对象不存在") } for _, candidate := range items { if stringValue(candidate["parentId"]) == operation.ID { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("存在子对象时不能删除父对象") } } for i, candidate := range items { if stringValue(candidate["id"]) == operation.ID { items = append(items[:i], items[i+1:]...) break } } summary = fmt.Sprintf("删除对象《%s》", stringValue(existing["name"])) default: return cloudAgentApprovalPreviewItem{}, nil } *objects = items nodeTitle := stringValue(existing["name"]) if nodeTitle == "" && operation.Type == "object_add" { nodeTitle = "新对象" if operation.Name != nil { nodeTitle = *operation.Name } } return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: truncateRunes(nodeTitle, cloudAgentPrevisTextLimit), Summary: summary}, nil } func cloudAgentPrevisFindByID(items []map[string]any, id string) map[string]any { for _, item := range items { if stringValue(item["id"]) == id { return item } } return nil } func cloudAgentPrevisApplyPatchOperation(scene map[string]any, operation cloudAgentPrevisPatchOperation, index int) (cloudAgentApprovalPreviewItem, error) { if err := cloudAgentPrevisValidateText(operation.Type, fmt.Sprintf("operations[%d].type", index), 40, true); err != nil { return cloudAgentApprovalPreviewItem{}, err } if err := validateCloudAgentID(operation.ID, fmt.Sprintf("operations[%d].id", index), 80); err != nil { return cloudAgentApprovalPreviewItem{}, err } if operation.Type == "scene_update" && operation.ID != stringValue(scene["id"]) { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("scene_update 的 id 必须是当前场景 ID") } objects, cameras, lights, shots := creationMaps(scene["objects"]), creationMaps(scene["cameras"]), creationMaps(scene["lights"]), creationMaps(scene["shots"]) switch operation.Type { case "scene_update": if operation.Title != nil { if err := cloudAgentPrevisValidateText(*operation.Title, "场景标题", cloudAgentPrevisTextLimit, true); err != nil { return cloudAgentApprovalPreviewItem{}, err } scene["title"] = *operation.Title } if operation.Background != nil { if err := cloudAgentPrevisValidateColor(*operation.Background, "场景背景色"); err != nil { return cloudAgentApprovalPreviewItem{}, err } scene["background"] = *operation.Background } if operation.EnvironmentIntensity != nil { if err := cloudAgentPrevisFiniteInRange(*operation.EnvironmentIntensity, "environmentIntensity", 0, 100); err != nil { return cloudAgentApprovalPreviewItem{}, err } scene["environmentIntensity"] = *operation.EnvironmentIntensity } if operation.GridVisible != nil { scene["gridVisible"] = *operation.GridVisible } if operation.ActiveShotID != nil { if *operation.ActiveShotID == "" { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("activeShotId 不能为空") } if err := validateCloudAgentID(*operation.ActiveShotID, "activeShotId", 80); err != nil { return cloudAgentApprovalPreviewItem{}, err } scene["activeShotId"] = *operation.ActiveShotID } return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: stringValue(scene["id"]), NodeTitle: stringValue(scene["title"]), Summary: "修改场景级设置"}, nil case "object_add", "object_update", "object_remove": item, err := cloudAgentPrevisApplyObjectPatch(&objects, operation, index) if err == nil { scene["objects"] = objects } return item, err case "object_keyframe_upsert", "object_keyframe_remove", "object_motion_path_set", "object_motion_path_clear": object := cloudAgentPrevisFindByID(objects, operation.ID) if object == nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("目标对象不存在") } duration := cloudAgentPrevisShotDuration(scene) var err error switch operation.Type { case "object_keyframe_upsert": err = cloudAgentPrevisUpsertKeyframe(object, operation, duration) case "object_keyframe_remove": err = cloudAgentPrevisRemoveKeyframe(object, operation) case "object_motion_path_set": err = cloudAgentPrevisSetMotionPath(object, operation, duration) case "object_motion_path_clear": delete(object, "motionPath") } if err != nil { return cloudAgentApprovalPreviewItem{}, err } scene["objects"] = objects return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(object["name"]), Summary: fmt.Sprintf("修改对象《%s》的动画", stringValue(object["name"]))}, nil case "shot_add": if cloudAgentPrevisFindByID(shots, operation.ID) != nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增镜头 ID 已存在") } cameraID := "" if operation.CameraID != nil { if err := validateCloudAgentID(*operation.CameraID, "摄影机 ID", 80); err != nil { return cloudAgentApprovalPreviewItem{}, err } cameraID = *operation.CameraID } else if len(cameras) > 0 { cameraID = stringValue(cameras[0]["id"]) } if cloudAgentPrevisFindByID(cameras, cameraID) == nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增镜头必须引用现有摄影机") } name := "新镜头" if operation.Name != nil { name = *operation.Name } if err := cloudAgentPrevisValidateText(name, "镜头名称", cloudAgentPrevisTextLimit, true); err != nil { return cloudAgentApprovalPreviewItem{}, err } duration, fps := 5.0, 24 if operation.Duration != nil { duration = *operation.Duration } if operation.FPS != nil { fps = *operation.FPS } if err := cloudAgentPrevisFiniteInRange(duration, "镜头时长", 0.1, 60); err != nil { return cloudAgentApprovalPreviewItem{}, err } if fps < 1 || fps > 60 { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("镜头帧率超出限制") } shotSize, cameraMove := operation.ShotSize, operation.CameraMove if shotSize == "" { shotSize = "medium" } if cameraMove == "" { cameraMove = "static" } if err := cloudAgentPrevisValidateEnum(shotSize, "镜头景别", "extreme_wide", "wide", "full", "medium", "close_up", "extreme_close_up"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if err := cloudAgentPrevisValidateEnum(cameraMove, "镜头运镜", "static", "push_in", "pull_out", "pan_left", "pan_right", "tilt_up", "tilt_down", "orbit_left", "orbit_right", "handheld"); err != nil { return cloudAgentApprovalPreviewItem{}, err } shot := map[string]any{"id": operation.ID, "name": name, "cameraId": cameraID, "duration": duration, "fps": fps, "shotSize": shotSize, "cameraMove": cameraMove, "prompt": "", "cuts": []any{}} if operation.Prompt != nil { if err := cloudAgentPrevisValidateText(*operation.Prompt, "镜头提示词", 2000, false); err != nil { return cloudAgentApprovalPreviewItem{}, err } shot["prompt"] = *operation.Prompt } scene["shots"] = append(shots, shot) if stringValue(scene["activeShotId"]) == "" { scene["activeShotId"] = operation.ID } return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: name, Summary: fmt.Sprintf("新增镜头《%s》", name)}, nil case "shot_update", "shot_remove": shot := cloudAgentPrevisFindByID(shots, operation.ID) if shot == nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("目标镜头不存在") } if operation.Type == "shot_remove" { if len(shots) <= 1 { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("预演场景至少保留一个镜头") } next := make([]map[string]any, 0, len(shots)-1) for _, item := range shots { if stringValue(item["id"]) != operation.ID { next = append(next, item) } } scene["shots"] = next if stringValue(scene["activeShotId"]) == operation.ID { scene["activeShotId"] = stringValue(next[0]["id"]) } return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(shot["name"]), Summary: fmt.Sprintf("删除镜头《%s》", stringValue(shot["name"]))}, nil } if err := cloudAgentPrevisApplyName(shot, operation.Name, "镜头名称"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if operation.CameraID != nil { if err := validateCloudAgentID(*operation.CameraID, "摄影机 ID", 80); err != nil { return cloudAgentApprovalPreviewItem{}, err } if cloudAgentPrevisFindByID(cameras, *operation.CameraID) == nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("镜头引用的摄影机不存在") } shot["cameraId"] = *operation.CameraID } if operation.Duration != nil { if err := cloudAgentPrevisFiniteInRange(*operation.Duration, "镜头时长", 0.1, 60); err != nil { return cloudAgentApprovalPreviewItem{}, err } shot["duration"] = *operation.Duration } if operation.FPS != nil { if *operation.FPS < 1 || *operation.FPS > 60 { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("镜头帧率超出限制") } shot["fps"] = *operation.FPS } if operation.ShotSize != "" { if err := cloudAgentPrevisValidateEnum(operation.ShotSize, "镜头景别", "extreme_wide", "wide", "full", "medium", "close_up", "extreme_close_up"); err != nil { return cloudAgentApprovalPreviewItem{}, err } shot["shotSize"] = operation.ShotSize } if operation.CameraMove != "" { if err := cloudAgentPrevisValidateEnum(operation.CameraMove, "镜头运镜", "static", "push_in", "pull_out", "pan_left", "pan_right", "tilt_up", "tilt_down", "orbit_left", "orbit_right", "handheld"); err != nil { return cloudAgentApprovalPreviewItem{}, err } shot["cameraMove"] = operation.CameraMove } if operation.Prompt != nil { if err := cloudAgentPrevisValidateText(*operation.Prompt, "镜头提示词", 2000, false); err != nil { return cloudAgentApprovalPreviewItem{}, err } shot["prompt"] = *operation.Prompt } return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(shot["name"]), Summary: fmt.Sprintf("修改镜头《%s》", stringValue(shot["name"]))}, nil case "camera_keyframe_upsert", "camera_keyframe_remove": camera := cloudAgentPrevisFindByID(cameras, operation.ID) if camera == nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("目标摄影机不存在") } duration := cloudAgentPrevisShotDuration(scene) var err error if operation.Type == "camera_keyframe_upsert" { err = cloudAgentPrevisUpsertKeyframe(camera, operation, duration) } else { err = cloudAgentPrevisRemoveKeyframe(camera, operation) } if err != nil { return cloudAgentApprovalPreviewItem{}, err } return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(camera["name"]), Summary: fmt.Sprintf("修改摄影机《%s》的动画", stringValue(camera["name"]))}, nil case "camera_add": if cloudAgentPrevisFindByID(cameras, operation.ID) != nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增摄影机 ID 已存在") } name := "摄影机" if operation.Name != nil { name = *operation.Name } if err := cloudAgentPrevisValidateText(name, "摄影机名称", cloudAgentPrevisTextLimit, true); err != nil { return cloudAgentApprovalPreviewItem{}, err } camPos := []float64{4.8, 2.7, 6.8} camRot := []float64{0, 0, 0} camScale := []float64{1, 1, 1} camTarget := []float64{0, 1, 0} if operation.Position != nil { camPos = *operation.Position } if operation.Rotation != nil { camRot = *operation.Rotation } if operation.Scale != nil { camScale = *operation.Scale } if operation.Target != nil { camTarget = *operation.Target } if err := cloudAgentPrevisValidateVec3(camPos, "摄影机 position"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if err := cloudAgentPrevisValidateVec3(camTarget, "摄影机 target"); err != nil { return cloudAgentApprovalPreviewItem{}, err } focalLength := 35.0 if operation.FocalLength != nil { focalLength = *operation.FocalLength } if err := cloudAgentPrevisFiniteInRange(focalLength, "摄影机焦段", 1, 300); err != nil { return cloudAgentApprovalPreviewItem{}, err } fov := 2 * math.Atan(36.0/(2*math.Max(1, focalLength))) * 180 / math.Pi newCamera := map[string]any{ "id": operation.ID, "name": name, "transform": map[string]any{"position": cloudAgentPrevisVec3(camPos), "rotation": cloudAgentPrevisVec3(camRot), "scale": cloudAgentPrevisVec3(camScale)}, "target": cloudAgentPrevisVec3(camTarget), "focalLength": focalLength, "fov": fov, "aperture": 2.8, "focusDistance": 5.0, "near": 0.05, "far": 500.0, "keyframes": []any{}, } scene["cameras"] = append(cameras, newCamera) return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: name, Summary: fmt.Sprintf("新增摄影机《%s》", name)}, nil case "camera_update", "camera_remove": camera := cloudAgentPrevisFindByID(cameras, operation.ID) if camera == nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("目标摄影机不存在") } if operation.Type == "camera_remove" { if len(cameras) <= 1 { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("预演场景至少保留一台摄影机") } for _, shot := range shots { if stringValue(shot["cameraId"]) == operation.ID { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("摄影机仍被镜头引用,不能删除") } } next := make([]map[string]any, 0, len(cameras)-1) for _, item := range cameras { if stringValue(item["id"]) != operation.ID { next = append(next, item) } } scene["cameras"] = next return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(camera["name"]), Summary: fmt.Sprintf("删除摄影机《%s》", stringValue(camera["name"]))}, nil } if err := cloudAgentPrevisApplyName(camera, operation.Name, "摄影机名称"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if operation.FocalLength != nil { if err := cloudAgentPrevisFiniteInRange(*operation.FocalLength, "摄影机焦段", 1, 300); err != nil { return cloudAgentApprovalPreviewItem{}, err } camera["focalLength"] = *operation.FocalLength } if operation.Target != nil { if err := cloudAgentPrevisValidateVec3(*operation.Target, "target"); err != nil { return cloudAgentApprovalPreviewItem{}, err } camera["target"] = cloudAgentPrevisVec3(*operation.Target) } if err := cloudAgentPrevisApplyTransform(camera, operation); err != nil { return cloudAgentApprovalPreviewItem{}, err } return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(camera["name"]), Summary: fmt.Sprintf("修改摄影机《%s》", stringValue(camera["name"]))}, nil case "light_add", "light_update", "light_remove": if operation.Type == "light_add" { if cloudAgentPrevisFindByID(lights, operation.ID) != nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增灯光 ID 已存在") } name := "新灯光" if operation.Name != nil { name = *operation.Name } if err := cloudAgentPrevisValidateText(name, "灯光名称", cloudAgentPrevisTextLimit, true); err != nil { return cloudAgentApprovalPreviewItem{}, err } lightType := "point" if operation.LightType != nil { lightType = *operation.LightType } if err := cloudAgentPrevisValidateEnum(lightType, "灯光类型", "directional", "point", "spot", "ambient"); err != nil { return cloudAgentApprovalPreviewItem{}, err } position, rotation, scale := []float64{0, 3, 0}, []float64{0, 0, 0}, []float64{1, 1, 1} if operation.Position != nil { position = *operation.Position } if operation.Rotation != nil { rotation = *operation.Rotation } if operation.Scale != nil { scale = *operation.Scale } if err := cloudAgentPrevisValidateVec3(position, "position"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if err := cloudAgentPrevisValidateVec3(rotation, "rotation"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if err := cloudAgentPrevisValidateVec3(scale, "scale"); err != nil { return cloudAgentApprovalPreviewItem{}, err } intensity := 1.0 if operation.Intensity != nil { intensity = *operation.Intensity } if err := cloudAgentPrevisFiniteInRange(intensity, "灯光强度", 0, 100); err != nil { return cloudAgentApprovalPreviewItem{}, err } angle, penumbra := math.Pi/4, 0.35 if operation.Angle != nil { angle = *operation.Angle } if operation.Penumbra != nil { penumbra = *operation.Penumbra } if err := cloudAgentPrevisFiniteInRange(angle, "灯光角度", 0, 6.29); err != nil { return cloudAgentApprovalPreviewItem{}, err } if err := cloudAgentPrevisFiniteInRange(penumbra, "灯光 penumbra", 0, 1); err != nil { return cloudAgentApprovalPreviewItem{}, err } color := "#ffffff" if operation.Color != nil { if err := cloudAgentPrevisValidateColor(*operation.Color, "灯光颜色"); err != nil { return cloudAgentApprovalPreviewItem{}, err } color = *operation.Color } castShadow := lightType != "ambient" if operation.CastShadow != nil { castShadow = *operation.CastShadow } light := map[string]any{"id": operation.ID, "name": name, "type": lightType, "transform": map[string]any{"position": cloudAgentPrevisVec3(position), "rotation": cloudAgentPrevisVec3(rotation), "scale": cloudAgentPrevisVec3(scale)}, "color": color, "intensity": intensity, "angle": angle, "penumbra": penumbra, "castShadow": castShadow} scene["lights"] = append(lights, light) return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: name, Summary: fmt.Sprintf("新增灯光《%s》", name)}, nil } light := cloudAgentPrevisFindByID(lights, operation.ID) if light == nil { return cloudAgentApprovalPreviewItem{}, BadAuthRequest("目标灯光不存在") } if operation.Type == "light_remove" { next := make([]map[string]any, 0, len(lights)-1) for _, item := range lights { if stringValue(item["id"]) != operation.ID { next = append(next, item) } } scene["lights"] = next return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(light["name"]), Summary: fmt.Sprintf("删除灯光《%s》", stringValue(light["name"]))}, nil } if err := cloudAgentPrevisApplyName(light, operation.Name, "灯光名称"); err != nil { return cloudAgentApprovalPreviewItem{}, err } if operation.LightType != nil { if err := cloudAgentPrevisValidateEnum(*operation.LightType, "灯光类型", "directional", "point", "spot", "ambient"); err != nil { return cloudAgentApprovalPreviewItem{}, err } light["type"] = *operation.LightType } if operation.Intensity != nil { if err := cloudAgentPrevisFiniteInRange(*operation.Intensity, "灯光强度", 0, 100); err != nil { return cloudAgentApprovalPreviewItem{}, err } light["intensity"] = *operation.Intensity } if operation.Color != nil { if err := cloudAgentPrevisValidateColor(*operation.Color, "灯光颜色"); err != nil { return cloudAgentApprovalPreviewItem{}, err } light["color"] = *operation.Color } if operation.Angle != nil { if err := cloudAgentPrevisFiniteInRange(*operation.Angle, "灯光角度", 0, 6.29); err != nil { return cloudAgentApprovalPreviewItem{}, err } light["angle"] = *operation.Angle } if operation.Penumbra != nil { if err := cloudAgentPrevisFiniteInRange(*operation.Penumbra, "灯光 penumbra", 0, 1); err != nil { return cloudAgentApprovalPreviewItem{}, err } light["penumbra"] = *operation.Penumbra } if operation.CastShadow != nil { light["castShadow"] = *operation.CastShadow } if err := cloudAgentPrevisApplyTransform(light, operation); err != nil { return cloudAgentApprovalPreviewItem{}, err } return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(light["name"]), Summary: fmt.Sprintf("修改灯光《%s》", stringValue(light["name"]))}, nil default: return cloudAgentApprovalPreviewItem{}, BadAuthRequest("不支持的预演台语义操作") } } func applyCloudAgentPrevisMutation(repo *repository.Repository, userID, canvasID string, call cloudAgentCall, policy RuntimePolicySetting, recorder ...cloudAgentMutationRecorder) (any, error) { var plan *cloudAgentPrevisMutationPlan var err error switch call.Function.Name { case "previs_scene_create": plan, err = prepareCloudAgentPrevisSceneCreate(repo, userID, canvasID, call) case "previs_apply_patch": plan, err = prepareCloudAgentPrevisApplyPatch(repo, userID, canvasID, call) default: return nil, BadAuthRequest("不支持的预演台写工具") } if err != nil { return nil, err } if err = saveCloudAgentDocument(repo, plan.Canvas, plan.Document, policy); err != nil { return nil, err } if len(recorder) > 0 && recorder[0] != nil { preview := plan.Preview if err := recorder[0](repo, cloudAgentMutationInput{UserID: userID, CanvasID: canvasID, StepID: call.ID, Operation: plan.Operation, BeforeSnapshotHash: plan.BeforeSnapshotHash, AfterSnapshotHash: plan.AfterSnapshotHash, BeforeJSON: plan.BeforeJSON, Preview: &preview}); err != nil { return nil, err } } resultSnapshotHash := plan.ResultSnapshotHash if resultSnapshotHash == "" { resultSnapshotHash = plan.AfterSnapshotHash } return map[string]any{"canvasId": canvasID, "sceneId": plan.SceneID, "snapshotHash": resultSnapshotHash, "canvasSnapshotHash": plan.AfterSnapshotHash, "committed": true, "summary": plan.Preview.Description}, nil }