fix(*): 持续集成 - 修复锁文件、格式门禁与生成链路构建失败

This commit is contained in:
ddCat committed 2026-08-07 12:35:03 +08:00
1 parent cab0baba56
commit 5cb17acbc6
18 files changed
+289 -238

No files matched your search

+1 -1
View File
@@ -123,7 +123,7 @@ type PublicChannelModelPrice struct {
InputTokenPriceMicrocredits int64 `json:"inputTokenPriceMicrocredits"`
OutputTokenPriceMicrocredits int64 `json:"outputTokenPriceMicrocredits"`
CachedTokenPriceMicrocredits int64 `json:"cachedTokenPriceMicrocredits"`
CapabilityConfig *ModelCapabilityConfig `json:"capabilityConfig,omitempty"`
CapabilityConfig *ModelCapabilityConfig `json:"capabilityConfig,omitempty"`
}
func (s *Service) RequireAdmin(user *model.User) error {
@@ -6,8 +6,8 @@ import (
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
"gorm.io/driver/sqlite"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
@@ -154,7 +154,7 @@ func newBulkUserTestDB(t *testing.T) *gorm.DB {
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.User{}, &model.AuthSession{}, &model.AdminAuditEvent{}, &model.CreditAccount{}, &model.CreditLedgerEntry{}, &model.SystemSetting{}); err != nil {
if err := db.AutoMigrate(&model.User{}, &model.AuthSession{}, &model.AdminAuditEvent{}, &model.CreditAccount{}, &model.CreditLedgerEntry{}, &model.SystemSetting{}, &model.TaskTextDelta{}); err != nil {
t.Fatal(err)
}
return db
@@ -15,7 +15,7 @@ func TestRedeemBatchCanBeReviewedAndRecordsAuditIP(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.SystemSetting{}, &model.User{}, &model.CreditAccount{}, &model.CreditLedgerEntry{}, &model.RedeemBatch{}, &model.RedeemCode{}); err != nil {
if err := db.AutoMigrate(&model.SystemSetting{}, &model.User{}, &model.CreditAccount{}, &model.CreditLedgerEntry{}, &model.RedeemBatch{}, &model.RedeemCode{}, &model.AdminAuditEvent{}); err != nil {
t.Fatal(err)
}
admin := &model.User{ID: "admin-1", Username: "admin", DisplayName: "管理员", Role: model.UserRoleAdmin, Status: model.UserStatusActive}
+1 -1
View File
@@ -349,7 +349,7 @@ func (s *Service) ConfirmProjectAssetCandidate(userID string, projectID string,
if candidate.Category == model.AssetCategoryCharacter {
kind = "entity"
var characterPayload string
characterPayload, marshalErr = characterAssetPayload(assetID, versionID, candidate.Name, json.RawMessage(candidate.DetailsJSON), now, now)
characterPayload, marshalErr = characterAssetPayload(assetID, versionID, candidate.Name, json.RawMessage(candidate.DetailsJSON), now, now)
payload = []byte(characterPayload)
} else {
payload, marshalErr = json.Marshal(map[string]any{
@@ -46,15 +46,15 @@ type CharacterRepresentationSummary struct {
}
type VoiceProfileSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
VoiceKey string `json:"voiceKey"`
Language string `json:"language"`
Timbre string `json:"timbre"`
SampleResourceID string `json:"sampleResourceId,omitempty"`
CompatibleModels []string `json:"compatibleModels"`
Status string `json:"status"`
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
VoiceKey string `json:"voiceKey"`
Language string `json:"language"`
Timbre string `json:"timbre"`
SampleResourceID string `json:"sampleResourceId,omitempty"`
CompatibleModels []string `json:"compatibleModels"`
Status string `json:"status"`
}
type CharacterVoiceSummary struct {
+12 -12
View File
@@ -27,15 +27,15 @@ type PromptTemplateVariable struct {
}
type PromptOperationDefinition struct {
Operation string `json:"operation"`
Label string `json:"label"`
Category string `json:"category"`
Description string `json:"description"`
OutputType string `json:"outputType"`
SchemaKey string `json:"schemaKey,omitempty"`
Variables []PromptTemplateVariable `json:"variables"`
OutputContract string `json:"outputContract"`
DefaultContent string `json:"-"`
Operation string `json:"operation"`
Label string `json:"label"`
Category string `json:"category"`
Description string `json:"description"`
OutputType string `json:"outputType"`
SchemaKey string `json:"schemaKey,omitempty"`
Variables []PromptTemplateVariable `json:"variables"`
OutputContract string `json:"outputContract"`
DefaultContent string `json:"-"`
}
type PromptTemplateRequest struct {
@@ -51,10 +51,10 @@ type UserPromptCustomizationRequest struct {
}
type UserPromptPreference struct {
Definition PromptOperationDefinition `json:"definition"`
Template *model.PromptTemplate `json:"template"`
Definition PromptOperationDefinition `json:"definition"`
Template *model.PromptTemplate `json:"template"`
Customization *model.UserPromptCustomization `json:"customization,omitempty"`
Outdated bool `json:"outdated"`
Outdated bool `json:"outdated"`
}
type CompiledPrompt struct {
@@ -18,20 +18,20 @@ func defaultPromptDefinitions() []PromptOperationDefinition {
return []PromptOperationDefinition{
{
Operation: promptOperationStoryboardPlan, Label: "分镜规划", Category: "分镜", OutputType: "json", SchemaKey: "storyboard-plan/v3",
Description: "把剧情、项目画风和当前角色版本规划为可执行镜头。",
Variables: []PromptTemplateVariable{{Label: "项目名称", Placeholder: "{{项目名称}}"}, {Label: "项目画风", Placeholder: "{{项目画风}}"}, {Label: "用户要求", Placeholder: "{{用户要求}}"}},
Description: "把剧情、项目画风和当前角色版本规划为可执行镜头。",
Variables: []PromptTemplateVariable{{Label: "项目名称", Placeholder: "{{项目名称}}"}, {Label: "项目画风", Placeholder: "{{项目画风}}"}, {Label: "用户要求", Placeholder: "{{用户要求}}"}},
DefaultContent: defaultStoryboardPromptTemplate(),
},
{
Operation: promptOperationStoryboardRepair, Label: "分镜修复", Category: "分镜", OutputType: "json", SchemaKey: "storyboard-plan/v3",
Description: "修复模型返回的分镜结构、字段和镜头复杂度。",
Variables: []PromptTemplateVariable{{Label: "校验错误", Placeholder: "{{校验错误}}"}, {Label: "项目画风", Placeholder: "{{项目画风}}"}},
Description: "修复模型返回的分镜结构、字段和镜头复杂度。",
Variables: []PromptTemplateVariable{{Label: "校验错误", Placeholder: "{{校验错误}}"}, {Label: "项目画风", Placeholder: "{{项目画风}}"}},
DefaultContent: `你是影视分镜 JSON 修复导演。修复结构时必须保留剧情信息,通过拆镜或重新分配内容解决复杂度超限,不得用删除关键剧情掩盖错误。保持原项目的视觉媒介、角色身份、服装、道具和连续性,不要擅自改写画风。只修复校验错误和由此引发的镜头组织问题。`,
},
{
Operation: promptOperationStoryboardFirstFrame, Label: "分镜首帧", Category: "生成", OutputType: "text",
Description: "把单镜头结构转换为图片模型使用的首帧提示词。",
Variables: []PromptTemplateVariable{{Label: "项目视觉", Placeholder: "{{项目视觉}}"}, {Label: "首帧构图", Placeholder: "{{首帧构图}}"}, {Label: "表演起始状态", Placeholder: "{{表演起始状态}}"}, {Label: "负面要求", Placeholder: "{{负面要求}}"}},
Variables: []PromptTemplateVariable{{Label: "项目视觉", Placeholder: "{{项目视觉}}"}, {Label: "首帧构图", Placeholder: "{{首帧构图}}"}, {Label: "表演起始状态", Placeholder: "{{表演起始状态}}"}, {Label: "负面要求", Placeholder: "{{负面要求}}"}},
DefaultContent: `生成单一、可执行的分镜首帧。严格继承项目视觉媒介和角色资产;画面明确主体左右位置、视线、前中后景、遮挡、视觉焦点、可信光源与材质。只描述静止首帧,不提前写后续运动,不添加画外人物、无来源光线、文字或水印。
【项目视觉】
@@ -49,7 +49,7 @@ func defaultPromptDefinitions() []PromptOperationDefinition {
{
Operation: promptOperationStoryboardVideo, Label: "分镜视频", Category: "生成", OutputType: "text",
Description: "把单镜头结构转换为视频模型使用的紧凑执行提示词。",
Variables: []PromptTemplateVariable{{Label: "项目视觉", Placeholder: "{{项目视觉}}"}, {Label: "镜头意图", Placeholder: "{{镜头意图}}"}, {Label: "首帧构图", Placeholder: "{{首帧构图}}"}, {Label: "表演与调度", Placeholder: "{{表演与调度}}"}, {Label: "摄影机", Placeholder: "{{摄影机}}"}, {Label: "时间节拍", Placeholder: "{{时间节拍}}"}, {Label: "运动与结尾", Placeholder: "{{运动与结尾}}"}, {Label: "声音", Placeholder: "{{声音}}"}, {Label: "执行优先级", Placeholder: "{{执行优先级}}"}, {Label: "负面要求", Placeholder: "{{负面要求}}"}},
Variables: []PromptTemplateVariable{{Label: "项目视觉", Placeholder: "{{项目视觉}}"}, {Label: "镜头意图", Placeholder: "{{镜头意图}}"}, {Label: "首帧构图", Placeholder: "{{首帧构图}}"}, {Label: "表演与调度", Placeholder: "{{表演与调度}}"}, {Label: "摄影机", Placeholder: "{{摄影机}}"}, {Label: "时间节拍", Placeholder: "{{时间节拍}}"}, {Label: "运动与结尾", Placeholder: "{{运动与结尾}}"}, {Label: "声音", Placeholder: "{{声音}}"}, {Label: "执行优先级", Placeholder: "{{执行优先级}}"}, {Label: "负面要求", Placeholder: "{{负面要求}}"}},
DefaultContent: `生成单一连续镜头的视频执行提示词。一个镜头只保留一个叙事目标、一个主运镜和一条主要动作链;摄影机运动必须有起点、动机和停止点。优先保证角色身份、表演、关键动作和连续性,次要环境效果可以简化。
【项目视觉】
@@ -84,14 +84,14 @@ func defaultPromptDefinitions() []PromptOperationDefinition {
},
{
Operation: promptOperationCharacterExtract, Label: "角色卡提取", Category: "角色", OutputType: "json", SchemaKey: "character-breakdown/v1",
Description: "从章节正文提取需要跨镜头保持一致的角色资产。",
Variables: []PromptTemplateVariable{{Label: "项目名称", Placeholder: "{{项目名称}}"}, {Label: "章节名称", Placeholder: "{{章节名称}}"}, {Label: "项目画风", Placeholder: "{{项目画风}}"}},
Description: "从章节正文提取需要跨镜头保持一致的角色资产。",
Variables: []PromptTemplateVariable{{Label: "项目名称", Placeholder: "{{项目名称}}"}, {Label: "章节名称", Placeholder: "{{章节名称}}"}, {Label: "项目画风", Placeholder: "{{项目画风}}"}},
DefaultContent: `你是短剧角色资产导演。只提取章节中实际出场、发言或对剧情产生明确作用,并且后续制作需要保持视觉或声音一致的角色。忽略系统播报、纯物件、无身份群众和没有持续角色价值的一次性路人;合并同一角色的姓名、专属称谓和别名。正文未明确的信息必须写“正文未明确”,不得自行改变人物关系、时代背景或编造编号式姓名。角色设定要具体、稳定、可用于后续三视图和视频一致性控制。`,
},
{
Operation: promptOperationCharacterTurnaround, Label: "角色三视图", Category: "角色", OutputType: "text",
Description: "按当前角色版本和项目画风生成正、侧、背三视图。",
Variables: []PromptTemplateVariable{{Label: "角色名称", Placeholder: "{{角色名称}}"}, {Label: "项目画风", Placeholder: "{{项目画风}}"}, {Label: "角色设定", Placeholder: "{{角色设定}}"}},
Description: "按当前角色版本和项目画风生成正、侧、背三视图。",
Variables: []PromptTemplateVariable{{Label: "角色名称", Placeholder: "{{角色名称}}"}, {Label: "项目画风", Placeholder: "{{项目画风}}"}, {Label: "角色设定", Placeholder: "{{角色设定}}"}},
DefaultContent: `制作专业人物三视图设定表。画面严格分成三个等宽竖向区域,从左到右依次为正面全身、右侧面全身、背面全身。三个视角必须是同一角色、同一服装、同一发型、同一体型和同一比例,采用站立中性姿势,完整显示头顶到脚底。背景使用纯净中性浅色和均匀设定稿光线,只负责分离轮廓,不得改变项目画风的绘画或渲染媒介。禁止文字、边框、道具说明、表情变化和额外人物。`,
},
}
@@ -22,6 +22,8 @@ const (
type providerCancellationOutcome string
type geminiOperation map[string]any
const (
providerCancellationPending providerCancellationOutcome = "pending"
providerCancellationConfirmed providerCancellationOutcome = "confirmed"
@@ -213,7 +215,9 @@ func deleteProviderTask(ctx context.Context, config providerConfig, path string)
func queryProviderCancellation(ctx context.Context, config providerConfig, providerRequestID string) (providerCancellationOutcome, string, error) {
switch config.InterfaceType {
case string(model.ChannelInterfaceGeminiVeo):
var operation map[string]any
// Gemini 用 operation.error 表示取消终态,此处必须保留该字段再判断,
// 不能让通用 JSON 解包提前把它转换成请求失败。
var operation geminiOperation
if err := getGeminiJSON(ctx, config, "/"+strings.TrimLeft(providerRequestID, "/"), &operation); err != nil {
return "", "", err
}
@@ -9,6 +9,7 @@ import (
)
func TestGeminiProviderCancellationAndConfirmation(t *testing.T) {
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("x-goog-api-key") != "test-key" {
@@ -42,6 +43,7 @@ func TestGeminiProviderCancellationAndConfirmation(t *testing.T) {
}
func TestVolcengineArkProviderCancellationUsesDelete(t *testing.T) {
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete || r.URL.Path != "/api/v3/contents/generations/tasks/task-1" {
http.NotFound(w, r)
+41 -53
View File
@@ -236,7 +236,7 @@ func TestTextReferenceImageRejectsInternalAssetURL(t *testing.T) {
}
func TestSeedanceVideosBodyUsesVideosEndpointFields(t *testing.T) {
body, err := seedanceVideosBody(canvasGenerationInput{
body, err := seedanceVideosRequestBody(canvasGenerationInput{
Prompt: "make it move",
Config: providerConfig{
Model: "seedance-2.0-mini-480p",
@@ -254,32 +254,26 @@ func TestSeedanceVideosBodyUsesVideosEndpointFields(t *testing.T) {
if err != nil {
t.Fatalf("seedanceVideosBody() error = %v", err)
}
if body["model"] != "seedance-2.0-mini-480p" {
t.Fatalf("model = %#v", body["model"])
if body.Model != "seedance-2.0-mini-480p" {
t.Fatalf("model = %#v", body.Model)
}
if body["aspect_ratio"] != "9:16" || body["duration"] != 8 {
t.Fatalf("size fields = %#v %#v", body["aspect_ratio"], body["duration"])
if body.AspectRatio != "9:16" || body.Duration != 8 {
t.Fatalf("size fields = %#v %#v", body.AspectRatio, body.Duration)
}
if body["generate_audio"] != true {
t.Fatalf("generate_audio = %#v, want true", body["generate_audio"])
if body.GenerateAudio == nil || !*body.GenerateAudio {
t.Fatalf("generate_audio = %#v, want true", body.GenerateAudio)
}
if body["image_url"] != testReferenceImageDataURL {
t.Fatalf("image_url = %#v", body["image_url"])
if body.ImageURL != testReferenceImageDataURL {
t.Fatalf("image_url = %#v", body.ImageURL)
}
referenceImages, ok := body["reference_image_urls"].([]string)
if !ok || len(referenceImages) != 1 || referenceImages[0] != "data:image/png;base64,d29ybGQ=" {
t.Fatalf("reference_image_urls = %#v", body["reference_image_urls"])
if len(body.ReferenceImageURLs) != 1 || body.ReferenceImageURLs[0] != "data:image/png;base64,d29ybGQ=" {
t.Fatalf("reference_image_urls = %#v", body.ReferenceImageURLs)
}
referenceVideos, ok := body["reference_videos"].([]string)
if !ok || len(referenceVideos) != 1 || referenceVideos[0] != "https://example.com/ref.mp4" {
t.Fatalf("reference_videos = %#v", body["reference_videos"])
if len(body.ReferenceVideos) != 1 || body.ReferenceVideos[0] != "https://example.com/ref.mp4" {
t.Fatalf("reference_videos = %#v", body.ReferenceVideos)
}
referenceAudios, ok := body["reference_audios"].([]string)
if !ok || len(referenceAudios) != 1 || referenceAudios[0] != "data:audio/mpeg;base64,AAAA" {
t.Fatalf("reference_audios = %#v", body["reference_audios"])
}
if body["content"] != nil || body["ratio"] != nil {
t.Fatalf("unexpected agent-plan fields in body: %#v", body)
if len(body.ReferenceAudios) != 1 || body.ReferenceAudios[0] != "data:audio/mpeg;base64,AAAA" {
t.Fatalf("reference_audios = %#v", body.ReferenceAudios)
}
}
@@ -294,7 +288,7 @@ func TestSeedanceVideosBodyHonorsGenerateAudio(t *testing.T) {
{name: "explicit disabled", value: "false", want: false},
} {
t.Run(test.name, func(t *testing.T) {
body, err := seedanceVideosBody(canvasGenerationInput{
body, err := seedanceVideosRequestBody(canvasGenerationInput{
Prompt: "make it move",
Config: providerConfig{
Model: "seedance-2.0-mini-480p",
@@ -304,15 +298,15 @@ func TestSeedanceVideosBodyHonorsGenerateAudio(t *testing.T) {
if err != nil {
t.Fatalf("seedanceVideosBody() error = %v", err)
}
if body["generate_audio"] != test.want {
t.Fatalf("generate_audio = %#v, want %v", body["generate_audio"], test.want)
if body.GenerateAudio == nil || *body.GenerateAudio != test.want {
t.Fatalf("generate_audio = %#v, want %v", body.GenerateAudio, test.want)
}
})
}
}
func TestSeedanceVideosBodyUsesOrderedFrameImageURLsWhenConfigured(t *testing.T) {
body, err := seedanceVideosBody(canvasGenerationInput{
body, err := seedanceVideosRequestBody(canvasGenerationInput{
Prompt: "make it move",
Config: providerConfig{Model: "seedance-2.0-mini-480p"},
ReferenceImages: []providerMedia{
@@ -325,9 +319,9 @@ func TestSeedanceVideosBodyUsesOrderedFrameImageURLsWhenConfigured(t *testing.T)
if err != nil {
t.Fatalf("seedanceVideosBody() error = %v", err)
}
imageURLs, ok := body["image_urls"].([]string)
if !ok || len(imageURLs) != 3 {
t.Fatalf("image_urls = %#v", body["image_urls"])
imageURLs := body.ImageURLs
if len(imageURLs) != 3 {
t.Fatalf("image_urls = %#v", imageURLs)
}
want := []string{testReferenceImageDataURL, "data:image/png;base64,d29ybGQ=", "data:image/png;base64,Y2hhcmFjdGVy"}
for index := range want {
@@ -335,11 +329,11 @@ func TestSeedanceVideosBodyUsesOrderedFrameImageURLsWhenConfigured(t *testing.T)
t.Fatalf("image_urls = %#v, want %#v", imageURLs, want)
}
}
if body["image_url"] != nil || body["reference_image_urls"] != nil {
if body.ImageURL != "" || body.ReferenceImageURLs != nil {
t.Fatalf("unexpected legacy image fields in body: %#v", body)
}
if prompt := body["prompt"]; prompt != "make it move" {
t.Fatalf("prompt = %#v", body["prompt"])
if body.Prompt != "make it move" {
t.Fatalf("prompt = %#v", body.Prompt)
}
}
@@ -549,7 +543,7 @@ func TestRunVideoTaskUsesXAIVideoGenerationEndpoint(t *testing.T) {
}
func TestXAIVideoBodyUsesOfficialImageShapeAndNormalizesSettings(t *testing.T) {
body, err := grokVideoBody(canvasGenerationInput{
body, err := xaiVideoRequestBody(canvasGenerationInput{
Prompt: "make it move",
Config: providerConfig{
Model: "grok-imagine-video-1.5",
@@ -564,22 +558,16 @@ func TestXAIVideoBodyUsesOfficialImageShapeAndNormalizesSettings(t *testing.T) {
if err != nil {
t.Fatalf("grokVideoBody() error = %v", err)
}
if body["duration"] != 20 || body["aspect_ratio"] != "9:16" || body["resolution"] != "1080p" {
if body.Duration != 20 || body.AspectRatio != "9:16" || body.Resolution != "1080p" {
t.Fatalf("xAI settings = %#v", body)
}
image, ok := body["image"].(map[string]interface{})
if !ok || image["url"] != testReferenceImageDataURL {
t.Fatalf("image = %#v", body["image"])
}
for _, legacyField := range []string{"seconds", "size", "images"} {
if _, exists := body[legacyField]; exists {
t.Fatalf("body includes legacy field %q: %#v", legacyField, body)
}
if body.Image == nil || body.Image.URL != testReferenceImageDataURL {
t.Fatalf("image = %#v", body.Image)
}
}
func TestXAIVideoBodyRejectsMultipleStartImages(t *testing.T) {
_, err := grokVideoBody(canvasGenerationInput{
_, err := xaiVideoRequestBody(canvasGenerationInput{
Config: providerConfig{Model: "grok-imagine-video-1.5", InterfaceType: "xai-video"},
ReferenceImages: []providerMedia{
{ID: "image-1", DataURL: testReferenceImageDataURL},
@@ -635,7 +623,7 @@ func TestNewAPIVideoOmitsImagesForTextToVideoOperation(t *testing.T) {
}
func TestSeedanceVideosBodyRequiresImageForVideoOrAudioReferences(t *testing.T) {
_, err := seedanceVideosBody(canvasGenerationInput{
_, err := seedanceVideosRequestBody(canvasGenerationInput{
Prompt: "make it move",
Config: providerConfig{Model: "seedance-2.0-mini-480p"},
ReferenceVideos: []providerMedia{{ID: "video-1", URL: "https://example.com/ref.mp4"}},
@@ -918,7 +906,7 @@ func TestRunGeminiVeoVideoTaskUsesLongRunningOperation(t *testing.T) {
}
func TestNewAPIChannel2SingleImageModelsRequireOneReference(t *testing.T) {
_, err := newAPIChannel2VideoBody(canvasGenerationInput{Config: providerConfig{Model: "grok-video-1.5", VideoSeconds: "6"}})
_, err := newAPIChannel2VideoRequestBody(canvasGenerationInput{Config: providerConfig{Model: "grok-video-1.5", VideoSeconds: "6"}})
if err == nil {
t.Fatal("newAPIChannel2VideoBody() error = nil")
}
@@ -928,7 +916,7 @@ func TestNewAPIChannel2SingleImageModelsRequireOneReference(t *testing.T) {
}
func TestNewAPIChannel2SingleImageModelUsesReferenceForStaleTextToVideoMetadata(t *testing.T) {
body, err := newAPIChannel2VideoBody(canvasGenerationInput{
body, err := newAPIChannel2VideoRequestBody(canvasGenerationInput{
Config: providerConfig{Model: "grok-video-1.5", VideoSeconds: "6"},
ReferenceImages: []providerMedia{{ID: "image-1", DataURL: testReferenceImageDataURL}},
Metadata: map[string]interface{}{"videoEditOperation": "text_to_video"},
@@ -936,14 +924,14 @@ func TestNewAPIChannel2SingleImageModelUsesReferenceForStaleTextToVideoMetadata(
if err != nil {
t.Fatalf("newAPIChannel2VideoBody() error = %v", err)
}
images, ok := body["image_urls"].([]string)
if !ok || len(images) != 1 || images[0] != testReferenceImageDataURL {
t.Fatalf("image_urls = %#v", body["image_urls"])
images := body.ImageURLs
if len(images) != 1 || images[0] != testReferenceImageDataURL {
t.Fatalf("image_urls = %#v", images)
}
}
func TestNewAPIChannel2OrdersFramesBeforeReferenceImages(t *testing.T) {
body, err := newAPIChannel2VideoBody(canvasGenerationInput{
body, err := newAPIChannel2VideoRequestBody(canvasGenerationInput{
Config: providerConfig{Model: "Seedance 2 Mini", VideoSeconds: "10"},
ReferenceImages: []providerMedia{
{ID: "character", DataURL: "data:image/png;base64,Y2hhcmFjdGVy"},
@@ -955,15 +943,15 @@ func TestNewAPIChannel2OrdersFramesBeforeReferenceImages(t *testing.T) {
if err != nil {
t.Fatalf("newAPIChannel2VideoBody() error = %v", err)
}
images, ok := body["image_urls"].([]string)
images := body.ImageURLs
want := []string{"data:image/png;base64,Zmlyc3Q=", "data:image/png;base64,bGFzdA==", "data:image/png;base64,Y2hhcmFjdGVy"}
if !ok || !reflect.DeepEqual(images, want) {
t.Fatalf("image_urls = %#v, want %#v", body["image_urls"], want)
if !reflect.DeepEqual(images, want) {
t.Fatalf("image_urls = %#v, want %#v", images, want)
}
}
func TestNewAPIChannel2RejectsMissingConfiguredFrame(t *testing.T) {
_, err := newAPIChannel2VideoBody(canvasGenerationInput{
_, err := newAPIChannel2VideoRequestBody(canvasGenerationInput{
Config: providerConfig{Model: "Seedance 2 Mini", VideoSeconds: "10"},
ReferenceImages: []providerMedia{{ID: "character", DataURL: testReferenceImageDataURL}},
Metadata: map[string]interface{}{"videoStartFrameNodeId": "missing-frame", "videoEditOperation": "image_to_video"},
+28 -28
View File
@@ -15,13 +15,13 @@ import (
const runtimePolicySettingKey = "runtime_policy"
const (
maxRuntimeUploadMB int64 = 999
maxRuntimeStorageGB int64 = 999
maxRuntimeDataMB int64 = 999_999
maxRuntimeCount int64 = 999_999_999
maxRuntimeRate = 999_999
maxRuntimeConcurrency = 999
maxRuntimeTimeoutMinutes = 9_999
maxRuntimeUploadMB int64 = 999
maxRuntimeStorageGB int64 = 999
maxRuntimeDataMB int64 = 999_999
maxRuntimeCount int64 = 999_999_999
maxRuntimeRate = 999_999
maxRuntimeConcurrency = 999
maxRuntimeTimeoutMinutes = 9_999
)
type RuntimeResourcePolicy struct {
@@ -52,27 +52,27 @@ type RuntimeTaskPolicy struct {
}
type RuntimeRequestPolicy struct {
TaskCreatePerMinute int `json:"taskCreatePerMinute"`
SessionCreatePerMinute int `json:"sessionCreatePerMinute"`
ResourceUploadPerMinute int `json:"resourceUploadPerMinute"`
ResourceImportPerMinute int `json:"resourceImportPerMinute"`
SessionFilePerMinute int `json:"sessionFilePerMinute"`
AssetWritePerMinute int `json:"assetWritePerMinute"`
CanvasWritePerMinute int `json:"canvasWritePerMinute"`
RegisterPerHour int `json:"registerPerHour"`
EmailCodePerHour int `json:"emailCodePerHour"`
LoginIPPerTenMinutes int `json:"loginIPPerTenMinutes"`
LoginAccountPerTenMinutes int `json:"loginAccountPerTenMinutes"`
SystemRelayPerMinute int `json:"systemRelayPerMinute"`
CustomRelayPerMinute int `json:"customRelayPerMinute"`
CustomRelayConcurrency int `json:"customRelayConcurrency"`
CustomRelayRequestMB int64 `json:"customRelayRequestMB"`
CustomRelayResponseMB int64 `json:"customRelayResponseMB"`
CustomRelayTimeoutMinutes int `json:"customRelayTimeoutMinutes"`
SystemRelayRequestMB int64 `json:"systemRelayRequestMB"`
SystemRelayResponseMB int64 `json:"systemRelayResponseMB"`
ChannelCircuitFailureCount int `json:"channelCircuitFailureCount"`
ChannelCircuitOpenSeconds int `json:"channelCircuitOpenSeconds"`
TaskCreatePerMinute int `json:"taskCreatePerMinute"`
SessionCreatePerMinute int `json:"sessionCreatePerMinute"`
ResourceUploadPerMinute int `json:"resourceUploadPerMinute"`
ResourceImportPerMinute int `json:"resourceImportPerMinute"`
SessionFilePerMinute int `json:"sessionFilePerMinute"`
AssetWritePerMinute int `json:"assetWritePerMinute"`
CanvasWritePerMinute int `json:"canvasWritePerMinute"`
RegisterPerHour int `json:"registerPerHour"`
EmailCodePerHour int `json:"emailCodePerHour"`
LoginIPPerTenMinutes int `json:"loginIPPerTenMinutes"`
LoginAccountPerTenMinutes int `json:"loginAccountPerTenMinutes"`
SystemRelayPerMinute int `json:"systemRelayPerMinute"`
CustomRelayPerMinute int `json:"customRelayPerMinute"`
CustomRelayConcurrency int `json:"customRelayConcurrency"`
CustomRelayRequestMB int64 `json:"customRelayRequestMB"`
CustomRelayResponseMB int64 `json:"customRelayResponseMB"`
CustomRelayTimeoutMinutes int `json:"customRelayTimeoutMinutes"`
SystemRelayRequestMB int64 `json:"systemRelayRequestMB"`
SystemRelayResponseMB int64 `json:"systemRelayResponseMB"`
ChannelCircuitFailureCount int `json:"channelCircuitFailureCount"`
ChannelCircuitOpenSeconds int `json:"channelCircuitOpenSeconds"`
}
type RuntimePolicySetting struct {
@@ -47,7 +47,7 @@ func TestUserStorageUsageCountsPersistedPayloads(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.SystemSetting{}, &model.Asset{}, &model.CanvasProject{}, &model.Session{}, &model.Message{}, &model.Task{}, &model.TaskLog{}, &model.Result{}, &model.ApiCallLog{}); err != nil {
if err := db.AutoMigrate(&model.SystemSetting{}, &model.Asset{}, &model.CanvasProject{}, &model.Session{}, &model.Message{}, &model.Task{}, &model.TaskLog{}, &model.Result{}, &model.ApiCallLog{}, &model.TaskTextDelta{}); err != nil {
t.Fatal(err)
}
items := []any{
@@ -79,7 +79,7 @@ func TestSaveTaskCompletionPersistsRelatedRowsTogether(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.SystemSetting{}, &model.Asset{}, &model.CanvasProject{}, &model.Session{}, &model.Message{}, &model.Task{}, &model.TaskLog{}, &model.Result{}, &model.ApiCallLog{}); err != nil {
if err := db.AutoMigrate(&model.SystemSetting{}, &model.Asset{}, &model.CanvasProject{}, &model.Session{}, &model.Message{}, &model.Task{}, &model.TaskLog{}, &model.Result{}, &model.ApiCallLog{}, &model.TaskTextDelta{}); err != nil {
t.Fatal(err)
}
session := model.Session{ID: "session-1", UserID: "user-1", Status: model.SessionStatusActive}
@@ -68,7 +68,7 @@ func removeFixedMediaRestrictions(template string) string {
func defaultStoryboardPromptTemplate() string {
return `你是影视分镜导演和 AI 视频提示词专家。先理解故事目标、人物动机、冲突、情绪曲线和结尾,再把剧情转译为可执行、可拍摄的连续镜头,不要把剧情段落直接改写成镜头摘要。
每个镜头先确定 narrativeIntent、viewerPOV 和 performanceBlocking,再选择景别、机位、焦段、构图和运镜。摄影机设计必须说明叙事动机,不得用术语数量代替导演判断;一个镜头只保留一个主运镜,必须有起点、动机和停止点,并为人物表演让出注意力。
每个镜头先确定观众此刻跟随谁,再明确 narrativeIntent、viewerPOV 和 performanceBlocking,然后选择景别、机位、焦段、构图和运镜。摄影机设计必须说明叙事动机,不得用术语数量代替导演判断;一个镜头只保留一个主运镜,必须有起点、动机和停止点,并为人物表演让出注意力。
大远景只承担空间、规模和处境建立;人物对白与喜剧反应优先使用中景、近景、过肩或独立反应镜头。按叙事需要使用反应、停顿、空镜、插入和匹配剪辑,避免每镜都推进、环绕、航拍或慢动作。