From 46b9e6cdf36767403ad574a4cb14f3a011c2c63b Mon Sep 17 00:00:00 2001 From: qian Date: Tue, 22 Sep 2026 23:01:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(task):=E5=AE=8C=E5=96=84wan3.0=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin/v1/task_management.go | 28 +- internal/cmd/cmd.go | 2 +- internal/logic/admin/task_management.go | 21 + internal/logic/billing/pricing.go | 10 + internal/logic/billing/task_billing_test.go | 11 + internal/logic/task/async_polling.go | 23 +- relay/common/task_adaptor.go | 10 + relay/handler/task_handler.go | 13 + relay/taskchannel/ali/adaptor.go | 40 +- relay/taskchannel/ali/adaptor_test.go | 108 +++ web/admin/src/views/ai/TaskLogsPage.vue | 653 +++++++++++++++++- web/tenant/src/views/service/TaskLogsPage.vue | 386 ++++++++--- .../src/views/service/playground/VideoTab.vue | 4 +- 13 files changed, 1165 insertions(+), 144 deletions(-) diff --git a/api/admin/v1/task_management.go b/api/admin/v1/task_management.go index 3e412e3..bb68ed8 100644 --- a/api/admin/v1/task_management.go +++ b/api/admin/v1/task_management.go @@ -19,13 +19,15 @@ type TaskListReq struct { } type ModelTaskItem struct { - ID int64 `json:"id"` - PublicTaskID string `json:"public_task_id"` - Platform string `json:"platform"` - Action string `json:"action"` - Status string `json:"status"` - Progress string `json:"progress"` - ModelName string `json:"model_name"` + ID int64 `json:"id"` + PublicTaskID string `json:"public_task_id"` + Platform string `json:"platform"` + Action string `json:"action"` + Status string `json:"status"` + Progress string `json:"progress"` + ModelName string `json:"model_name"` + // UpstreamModel 上游供应商实际使用的模型名(仅任务详情返回,排障时用于和请求模型对照) + UpstreamModel string `json:"upstream_model,omitempty"` FailReason string `json:"fail_reason,omitempty"` PreDeductAmount float64 `json:"pre_deduct_amount"` ActualCost float64 `json:"actual_cost"` @@ -35,8 +37,16 @@ type ModelTaskItem struct { TenantID int64 `json:"tenant_id"` UserID int64 `json:"user_id"` SubmitTime string `json:"submit_time,omitempty"` - FinishTime string `json:"finish_time,omitempty"` - CreatedAt string `json:"created_at"` + // StartTime 上游开始执行任务的时间(仅任务详情返回;详情时间线用它区分「排队耗时」与「执行耗时」) + StartTime string `json:"start_time,omitempty"` + FinishTime string `json:"finish_time,omitempty"` + CreatedAt string `json:"created_at"` + // RequestID 任务提交时的原始请求 ID(req_xxxxx,仅任务详情返回),关联 aud_request_logs.request_id + RequestID string `json:"request_id,omitempty"` + // 上游排障标识(仅任务详情返回;从 private_data 提取,其余内部字段不外露): + // UpstreamTaskID 任务句柄(上游轮询查询用);UpstreamRequestID 提交调用追踪 ID(部分上游返回) + UpstreamTaskID string `json:"upstream_task_id,omitempty"` + UpstreamRequestID string `json:"upstream_request_id,omitempty"` } type TaskListRes struct { diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index a4814f9..8be7637 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -598,7 +598,7 @@ func registerRelayRoutes(server *ghttp.Server) { // 阿里 DashScope 官方视频协议端点(wan2.x / wan3.0 通用,阿里 SDK 换 base_url 直连; // 查询回放官方响应形态,轮询计费由网关后台完成) - server.Group("/api/v1", func(group *ghttp.RouterGroup) { + server.Group("/v1", func(group *ghttp.RouterGroup) { group.Middleware(middleware.ApiMaintenance, middleware.MaintenanceMode, middleware.ApiKeyAuth, middleware.RelayBodyLimit, middleware.ContentFilter) group.POST("/services/aigc/video-generation/video-synthesis", relay.HandleAliVideoSubmit) group.GET("/tasks/{task_id}", relay.HandleAliVideoRetrieve) diff --git a/internal/logic/admin/task_management.go b/internal/logic/admin/task_management.go index 39f9d00..693dc68 100644 --- a/internal/logic/admin/task_management.go +++ b/internal/logic/admin/task_management.go @@ -2,6 +2,7 @@ package admin import ( "context" + "encoding/json" "time" "github.com/gogf/gf/v2/errors/gcode" @@ -132,6 +133,8 @@ func (s *sAdmin) TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (*v1.Tas ResultUrl string `json:"result_url"` TenantId int64 `json:"tenant_id"` UserId int64 `json:"user_id"` + PrivateData string `json:"private_data"` + RequestId string `json:"request_id"` SubmitTime *gtime.Time `json:"submit_time"` StartTime *gtime.Time `json:"start_time"` FinishTime *gtime.Time `json:"finish_time"` @@ -153,6 +156,7 @@ func (s *sAdmin) TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (*v1.Tas Status: task.Status, Progress: task.Progress, ModelName: task.ModelName, + UpstreamModel: task.UpstreamModel, FailReason: task.FailReason, PreDeductAmount: task.PreDeductAmount, ActualCost: task.ActualCost, @@ -160,15 +164,32 @@ func (s *sAdmin) TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (*v1.Tas ResultURL: task.ResultUrl, TenantID: task.TenantId, UserID: task.UserId, + RequestID: task.RequestId, CreatedAt: task.CreatedAt.Format("Y-m-d H:i:s"), } if task.SubmitTime != nil { item.SubmitTime = task.SubmitTime.Format("Y-m-d H:i:s") } + if task.StartTime != nil { + item.StartTime = task.StartTime.Format("Y-m-d H:i:s") + } if task.FinishTime != nil { item.FinishTime = task.FinishTime.Format("Y-m-d H:i:s") } + // 从 private_data 提取上游排障标识:仅取 upstream_task_id / upstream_request_id 两个键, + // 计费上下文等其余内部数据不外露(同步图片任务无上游任务 ID,两字段为空) + if task.PrivateData != "" { + var pd struct { + UpstreamTaskID string `json:"upstream_task_id"` + UpstreamRequestID string `json:"upstream_request_id"` + } + if json.Unmarshal([]byte(task.PrivateData), &pd) == nil { + item.UpstreamTaskID = pd.UpstreamTaskID + item.UpstreamRequestID = pd.UpstreamRequestID + } + } + // 对 re-host 到对象存储的结果图,生成新鲜的缩略图 URL 供详情弹窗内联预览,并刷新原图 URL // (规避 result_url 24h 预签名过期)。非图片/上游直链任务查不到文件记录,保持原 result_url。 if thumb, orig := common.TaskResultImageURLs(ctx, task.TenantId, task.PublicTaskId, 600); thumb != "" { diff --git a/internal/logic/billing/pricing.go b/internal/logic/billing/pricing.go index 2be9c91..c780790 100644 --- a/internal/logic/billing/pricing.go +++ b/internal/logic/billing/pricing.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "math" + "strings" "time" "github.com/gogf/gf/v2/errors/gerror" @@ -111,6 +112,15 @@ func LookupPerSecondPrice(prices map[string]float64, spec string) float64 { if p, ok := prices[spec]; ok && p > 0 { return p } + // 大小写不敏感兜底:配置键形态因供应商词汇而异("720p" vs "720P"), + // 语义同档应优先于 "*" 通配回退,防止大写上报查不中而按兜底价计费 + if lower := strings.ToLower(spec); lower != "" { + for k, p := range prices { + if strings.ToLower(k) == lower && p > 0 { + return p + } + } + } if p, ok := prices[perSecondWildcard]; ok && p > 0 { return p } diff --git a/internal/logic/billing/task_billing_test.go b/internal/logic/billing/task_billing_test.go index b8ce809..cdf3dfa 100644 --- a/internal/logic/billing/task_billing_test.go +++ b/internal/logic/billing/task_billing_test.go @@ -352,4 +352,15 @@ func TestLookupPerSecondPrice(t *testing.T) { if p := LookupPerSecondPrice(map[string]float64{"720p": 0}, "720p"); p != 0 { t.Errorf("zero price entry = %v", p) } + // 大小写不敏感兜底:同档不同形态(配置小写/上报大写,或反之)优先于 "*" 通配回退 + if p := LookupPerSecondPrice(map[string]float64{"720p": 0.5, "*": 0.9}, "720P"); p != 0.5 { + t.Errorf("case-insensitive hit = %v", p) + } + if p := LookupPerSecondPrice(map[string]float64{"768P": 0.35}, "768p"); p != 0.35 { + t.Errorf("case-insensitive hit (uppercase key) = %v", p) + } + // 大小写不敏感未命中("2k")仍回退 "*" + if p := LookupPerSecondPrice(map[string]float64{"720p": 0.5, "1080p": 1.0, "*": 0.9}, "2K"); p != 0.9 { + t.Errorf("wildcard after case-insensitive miss = %v", p) + } } diff --git a/internal/logic/task/async_polling.go b/internal/logic/task/async_polling.go index b2a0bab..b398431 100644 --- a/internal/logic/task/async_polling.go +++ b/internal/logic/task/async_polling.go @@ -326,9 +326,10 @@ func processChannelTasks(ctx context.Context, channelID int64, tasks []*common.A // privateData PrivateData 反序列化结构 type privateData struct { - UpstreamTaskID string `json:"upstream_task_id"` - TaskType string `json:"task_type"` - BillingContext struct { + UpstreamTaskID string `json:"upstream_task_id"` + UpstreamRequestID string `json:"upstream_request_id"` // 上游提交调用追踪 ID(部分上游返回,可空) + TaskType string `json:"task_type"` + BillingContext struct { Ratios map[string]any `json:"ratios"` ModelName string `json:"model_name"` PreDeduct float64 `json:"pre_deduct"` @@ -541,18 +542,24 @@ func recordTaskUsage(task *common.AsyncTask, channel *common.ChannelBasicInfo, s // 按秒计费任务的视频时长:取提交时计费上下文里的 spec.duration(真实秒数)。 // 只认 spec.* 事实键——旧 duration 键在部分 adaptor 是乘数语义(如 gemini 1.3),不可作为时长 - // 同时提取 upstream_task_id 作为上游请求 ID 落用量日志(排障时凭它在上游定位任务)。 + // 提取上游请求 ID 落用量日志(排障时凭它在上游定位任务): + // 优先 upstream_request_id(上游调用追踪 ID,如 DashScope 顶层 request_id), + // 未记录时回退 upstream_task_id(任务句柄,轮询同款标识) durationSeconds := 0 upstreamRequestID := "" if len(task.PrivateData) > 0 { var pdDur struct { - UpstreamTaskID string `json:"upstream_task_id"` - BillingContext struct { + UpstreamTaskID string `json:"upstream_task_id"` + UpstreamRequestID string `json:"upstream_request_id"` + BillingContext struct { Ratios map[string]any `json:"ratios"` } `json:"billing_context"` } if json.Unmarshal(task.PrivateData, &pdDur) == nil { - upstreamRequestID = pdDur.UpstreamTaskID + upstreamRequestID = pdDur.UpstreamRequestID + if upstreamRequestID == "" { + upstreamRequestID = pdDur.UpstreamTaskID + } if v, ok := pdDur.BillingContext.Ratios["spec.duration"].(float64); ok && v > 0 { durationSeconds = int(v) } @@ -587,7 +594,7 @@ func recordTaskUsage(task *common.AsyncTask, channel *common.ChannelBasicInfo, s DurationSeconds: durationSeconds, - // 任务的上游请求 ID = 上游提交响应体中的任务 ID(轮询同款标识) + // 任务的上游请求 ID = 调用追踪 ID(上游返回时),否则为任务句柄(轮询同款标识) UpstreamRequestID: upstreamRequestID, } diff --git a/relay/common/task_adaptor.go b/relay/common/task_adaptor.go index 80424bd..411708f 100644 --- a/relay/common/task_adaptor.go +++ b/relay/common/task_adaptor.go @@ -55,6 +55,16 @@ type TaskAdaptor interface { GetChannelName() string } +// UpstreamRequestIDExtractor 供应商私有的「上游调用追踪 ID 提取」可选能力。 +// 为什么需要:部分异步上游(如阿里 DashScope)不在响应头返回请求 ID,而是放在 +// 提交响应体顶层(request_id 字段),通用管线无法用统一键提取。实现方从提交 +// 响应体解析自家私有的追踪 ID,与 output.task_id(任务句柄)互补——提工单或 +// 在上游日志定位一次提交调用时用它。未实现的上游走响应头提取或为空。 +type UpstreamRequestIDExtractor interface { + // ExtractUpstreamRequestID 从上游提交响应体提取调用追踪 ID,未返回时为空串 + ExtractUpstreamRequestID(body []byte) string +} + // TaskInfo 异步任务查询结果 type TaskInfo struct { Status TaskStatusEnum diff --git a/relay/handler/task_handler.go b/relay/handler/task_handler.go index 450402f..8f4aa23 100644 --- a/relay/handler/task_handler.go +++ b/relay/handler/task_handler.go @@ -317,6 +317,16 @@ func HandleTaskSubmit( } now := time.Now() + // 上游调用追踪 ID:优先响应头(与同步对话链路 helper.ExtractUpstreamRequestID 同源, + // 覆盖 OpenAI/Anthropic 等返回标准头的上游);无候选头时由供应商可选能力从提交 + // 响应体提取(如 DashScope 顶层 request_id)。与 upstream_task_id(任务句柄)互补 + upstreamRequestID := helper.ExtractUpstreamRequestID(resp.Header) + if upstreamRequestID == "" { + if ex, ok := adaptor.(common.UpstreamRequestIDExtractor); ok { + upstreamRequestID = ex.ExtractUpstreamRequestID(taskData) + } + } + privateDataMap := map[string]any{ "upstream_task_id": upstreamTaskID, "task_type": platform, @@ -329,6 +339,9 @@ func HandleTaskSubmit( "pre_deduct": preDeductAmount.InexactFloat64(), }, } + if upstreamRequestID != "" { + privateDataMap["upstream_request_id"] = upstreamRequestID + } if rc.RequestEcho != nil { privateDataMap["request_echo"] = rc.RequestEcho } diff --git a/relay/taskchannel/ali/adaptor.go b/relay/taskchannel/ali/adaptor.go index 414a746..3ced7ac 100644 --- a/relay/taskchannel/ali/adaptor.go +++ b/relay/taskchannel/ali/adaptor.go @@ -173,9 +173,15 @@ func (a *AliAdaptor) EstimateBilling(_ context.Context, _ *common.RelayInfo, bod metadata := taskchannel.ExtractMetadata(req) var meta aliMetadata if taskchannel.UnmarshalMetadata(metadata, &meta) == nil { - // 分辨率事实值:per_second 矩阵查价键(480P/720P/1080P 原值) + // 分辨率事实值:per_second 矩阵查价键(480P/720P/1080P 原值)。 + // OpenAI Videos 路径无 resolution 字段,wan3.0 按 size 归一档位上报, + // 与 buildVideoRequest 的折算口径同源 if meta.Resolution != "" { ratios["spec.resolution"] = meta.Resolution + } else if strings.HasPrefix(strings.ToLower(modelName), "wan3.") && meta.Size != "" { + if r := normalizeWan3Resolution(meta.Size); r != "" { + ratios["spec.resolution"] = r + } } // 时长事实值:duration>0 用请求值;duration=-1(wan3.0 智能时长)按输出上限冻结, // 结算以 usage.output_video_duration 多退少补 @@ -293,8 +299,13 @@ func (a *AliAdaptor) buildVideoRequest(info *common.RelayInfo, req map[string]an params.Audio = &audio } if params.Resolution == "" && meta.Size != "" { - params.Resolution = resolutionFromSize(meta.Size) + if r := normalizeWan3Resolution(meta.Size); r != "" { + params.Resolution = r + } } + // wan3.0 协议无 size 参数:size 只作档位折算源,任何情况不透传上游 + //(wan2.x 的 size 是原生尺寸语义,不进本分支、不受影响) + params.Size = "" // ratio 兜底:通用词汇 aspect_ratio(OpenAI Videos 双写键,部分链路只写该键) if params.Ratio == "" { if ar, ok := metadata["aspect_ratio"].(string); ok { @@ -355,6 +366,18 @@ func buildWan3Media(req map[string]any, metadata map[string]any) []dashScopeMedi return nil } +// normalizeWan3Resolution 把 size 值归一为 wan3.0 分辨率档位: +// 裸档位词汇("720P"/"480p")直接归一为大写档位;WxH 尺寸("1280x720")按短边归档; +// 其余返回空串(上游取默认 1080P)。wan2.x 的 size 是原生尺寸语义,不得走此函数。 +func normalizeWan3Resolution(size string) string { + upper := strings.ToUpper(strings.TrimSpace(size)) + switch upper { + case "480P", "720P", "1080P": + return upper + } + return resolutionFromSize(size) +} + // resolutionFromSize 从尺寸字符串(如 "1280x720")按短边映射 wan3.0 分辨率档位。 // 竖版尺寸(720x1280)同样按短边归档;解析失败返回空串(上游取默认 1080P) func resolutionFromSize(size string) string { @@ -484,6 +507,19 @@ func (a *AliAdaptor) DoResponse(_ context.Context, resp *http.Response, _ *commo return result.Output.TaskID, body, nil } +// ExtractUpstreamRequestID 从 DashScope 提交响应体顶层提取 request_id(调用追踪 ID)。 +// 与 output.task_id(任务句柄,轮询查询用)互补:在阿里云日志/工单中定位一次提交调用时用它 +func (a *AliAdaptor) ExtractUpstreamRequestID(body []byte) string { + var resp dashScopeSubmitResponse + if json.Unmarshal(body, &resp) != nil { + return "" + } + return resp.RequestID +} + +// 确保实现上游调用追踪 ID 提取可选能力 +var _ common.UpstreamRequestIDExtractor = (*AliAdaptor)(nil) + func (a *AliAdaptor) FetchTask(ctx context.Context, baseURL, apiKey string, taskData []byte) (*http.Response, error) { var data struct { TaskID string `json:"task_id"` diff --git a/relay/taskchannel/ali/adaptor_test.go b/relay/taskchannel/ali/adaptor_test.go index a392fbc..2b14766 100644 --- a/relay/taskchannel/ali/adaptor_test.go +++ b/relay/taskchannel/ali/adaptor_test.go @@ -3,6 +3,7 @@ package ali import ( "context" "encoding/json" + "strings" "testing" "github.com/qianfree/team-api/relay/common" @@ -67,6 +68,25 @@ func TestEstimateBilling_VideoSpecSignals(t *testing.T) { t.Fatalf("seconds should override metadata duration: %v", ratios) } + // OpenAI Videos 路径无 resolution 字段:wan3.0 按 size 归一档位上报(裸档位/WxH 均可) + ratios = a.EstimateBilling(context.Background(), newTestRelayInfo(), []byte( + `{"model":"wan3.0-video","prompt":"p","metadata":{"size":"720P","duration":5}}`)) + if ratios["spec.resolution"] != "720P" { + t.Fatalf("size tier vocabulary should report spec.resolution: %v", ratios) + } + ratios = a.EstimateBilling(context.Background(), newTestRelayInfo(), []byte( + `{"model":"wan3.0-video","prompt":"p","metadata":{"size":"1280x720","duration":5}}`)) + if ratios["spec.resolution"] != "720P" { + t.Fatalf("size WxH should report spec.resolution: %v", ratios) + } + + // wan2.x 的 size 是原生尺寸语义,不得折算档位(预扣按矩阵兜底档走) + ratios = a.EstimateBilling(context.Background(), newTestRelayInfo(), []byte( + `{"model":"wan2.2-t2v-plus","prompt":"p","metadata":{"size":"1024x576","duration":5}}`)) + if _, has := ratios["spec.resolution"]; has { + t.Fatalf("wan2 size must not derive spec.resolution: %v", ratios) + } + // 未传时长:不产出 spec.duration(引擎按默认 5s 估) ratios = a.EstimateBilling(context.Background(), newTestRelayInfo(), []byte( `{"model":"wan3.0-video","prompt":"p"}`)) @@ -142,6 +162,9 @@ func TestBuildVideoRequest_OpenAIVocabulary(t *testing.T) { if req.Parameters.Resolution != "720P" { t.Fatalf("size short-edge → resolution lost: %+v", req.Parameters) } + if req.Parameters.Size != "" { + t.Fatalf("wan3.0 must not carry size after resolution derived: %+v", req.Parameters) + } if req.Parameters.Ratio != "9:16" { t.Fatalf("aspect_ratio → ratio lost: %+v", req.Parameters) } @@ -235,6 +258,27 @@ func TestParseTaskResult_Wan3MaterialUsage(t *testing.T) { } } +func TestExtractUpstreamRequestID(t *testing.T) { + a := &AliAdaptor{} + // DashScope 提交成功响应:顶层 request_id 提取,与 output.task_id 互不干扰 + if got := a.ExtractUpstreamRequestID([]byte( + `{"output":{"task_id":"tsk-abc","task_status":"PENDING"},"request_id":"req-123","code":null,"message":""}`)); got != "req-123" { + t.Fatalf("request_id lost: %q", got) + } + // 错误响应同样带顶层 request_id(DashScope 错误时 code/message 非空) + if got := a.ExtractUpstreamRequestID([]byte( + `{"request_id":"req-err","code":"InvalidApiKey","message":"Invalid API-key"}`)); got != "req-err" { + t.Fatalf("error response request_id lost: %q", got) + } + // 缺失/非法 body:返回空串不报错 + if got := a.ExtractUpstreamRequestID([]byte(`{"output":{"task_id":"t"}}`)); got != "" { + t.Fatalf("missing request_id should yield empty: %q", got) + } + if got := a.ExtractUpstreamRequestID([]byte(`not json`)); got != "" { + t.Fatalf("invalid body should yield empty: %q", got) + } +} + func TestResolutionFromSize(t *testing.T) { cases := map[string]string{ "1280x720": "720P", @@ -252,3 +296,67 @@ func TestResolutionFromSize(t *testing.T) { } } } + +// 裸档位词汇(OpenAI 端点 size 直传 wan3.0 原生词汇)归一; +// WxH 尺寸走短边归档,无效值返回空串 +func TestNormalizeWan3Resolution(t *testing.T) { + cases := map[string]string{ + "720P": "720P", + "480p": "480P", // 小写归一为大写档位 + " 1080p ": "1080P", + "1280x720": "720P", + "1024x576": "720P", + "4K": "", // 非法键必须返回空串,防止把 "4K" 之类键透传上游 + "768P": "", // MiniMax 档位非 wan3.0 词汇,不归一 + "not-a-tie": "", + } + for size, want := range cases { + if got := normalizeWan3Resolution(size); got != want { + t.Errorf("normalizeWan3Resolution(%q) = %q, want %q", size, got, want) + } + } +} + +// OpenAI 端点 size 直传 wan3.0 原生档位词汇("720P"/"480p"): +// 直接作为 resolution 下发上游,且不再透传 wan3.0 不认识的 size 参数 +func TestBuildVideoRequest_SizeTierVocabulary(t *testing.T) { + for _, size := range []string{"720P", "480p", "1080p"} { + a := &AliAdaptor{} + body := []byte(`{"model":"wan3.0-video","prompt":"p","metadata":{"size":"` + size + `"}}`) + r, err := a.BuildRequestBody(context.Background(), newTestRelayInfo(), body) + if err != nil { + t.Fatalf("build (%s): %v", size, err) + } + var req dashScopeVideoRequest + if err := json.NewDecoder(r).Decode(&req); err != nil { + t.Fatalf("decode (%s): %v", size, err) + } + want := strings.ToUpper(size) + if req.Parameters == nil || req.Parameters.Resolution != want { + t.Fatalf("size=%s should derive resolution=%s: %+v", size, want, req.Parameters) + } + if req.Parameters.Size != "" { + t.Fatalf("wan3.0 request must not carry size param (input %s): %+v", size, req.Parameters) + } + } +} + +// 回归:metadata.size 无法归一为档位(如 MiniMax 词汇误传给 wan3.0)时, +// resolution 留空走上游默认 1080P,原 size 值不再透传 +func TestBuildVideoRequest_SizeUnrecognized(t *testing.T) { + a := &AliAdaptor{} + body := []byte(`{"model":"wan3.0-video","prompt":"p","metadata":{"size":"768P"}}`) + r, err := a.BuildRequestBody(context.Background(), newTestRelayInfo(), body) + if err != nil { + t.Fatalf("build: %v", err) + } + var req dashScopeVideoRequest + if err := json.NewDecoder(r).Decode(&req); err != nil { + t.Fatalf("decode: %v", err) + } + // 归一失败且无其他参数时 Parameters 整体为 nil(不下发 parameters 节点), + // 非 nil 时 resolution/size 必须为空(上游默认 1080P) + if req.Parameters != nil && (req.Parameters.Resolution != "" || req.Parameters.Size != "") { + t.Fatalf("unrecognized size should leave resolution/size empty: %+v", req.Parameters) + } +} diff --git a/web/admin/src/views/ai/TaskLogsPage.vue b/web/admin/src/views/ai/TaskLogsPage.vue index 12cdc8a..da8058e 100644 --- a/web/admin/src/views/ai/TaskLogsPage.vue +++ b/web/admin/src/views/ai/TaskLogsPage.vue @@ -1,5 +1,5 @@