fix(video): NewAPI Channel 2 - 修复轮询与结果解析
创建成功后的短暂 task_not_exist 按退避策略重试,并在短期窗口耗尽后保留 running 状态,继续回查原 Provider Task ID。 补齐嵌套任务 ID 与视频结果地址解析,更新生成插件、协议文档和专项回归测试。
This commit is contained in:
1 parent
05d1bce876
commit
f4ced0ac59
13 files changed
+378
-12
No files matched your search
@@ -377,6 +377,55 @@ func TestOfficialRollDekWanVideoUsesVideosLifecycle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAPIVideoGenerationsParsesNestedTaskIDs(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, "newapi-video-generations-v1.yingce-plugin", "newapi-channel-2")
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
wantID string
|
||||
}{
|
||||
{name: "snake case", payload: `{"data":{"task_id":"task-snake","status":"queued"}}`, wantID: "task-snake"},
|
||||
{name: "camel case", payload: `{"data":{"taskId":"task-camel","status":"queued"}}`, wantID: "task-camel"},
|
||||
{name: "nested upstream id wins over wrapper id", payload: `{"id":"49137","data":{"task_id":"task-upstream","status":"queued"}}`, wantID: "task-upstream"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
created, err := adapter.ParseCreate(context.Background(), []byte(test.payload))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if created.TaskID != test.wantID || created.Status != StatusPending {
|
||||
t.Fatalf("created = %#v, want task ID %q and pending status", created, test.wantID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAPIVideoGenerationsParsesNestedVideoResults(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, "newapi-video-generations-v1.yingce-plugin", "newapi-channel-2")
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
}{
|
||||
{name: "channel result URL", payload: `{"code":"success","data":{"task_id":"task-upstream","status":"SUCCESS","result_url":"https://cdn.example/channel-result.mp4"}}`},
|
||||
{name: "provider nested video URL", payload: `{"code":"success","data":{"task_id":"task-upstream","status":"SUCCESS","data":{"status":"completed","video_url":"https://cdn.example/provider-result.mp4"}}}`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
state, err := adapter.ParsePoll(context.Background(), PollContext{TaskID: "task-upstream"}, []byte(test.payload))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state.Status != StatusSucceeded || state.Result == nil || len(state.Result.Videos) != 1 {
|
||||
t.Fatalf("state = %#v, want one completed video", state)
|
||||
}
|
||||
if state.Result.Videos[0].URL == "" {
|
||||
t.Fatalf("video = %#v, want a result URL", state.Result.Videos[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfficialOpenAIVideosDeclaresAuthenticatedResultDownload(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, "openai-videos.yingce-plugin", "newapi")
|
||||
capability, ok := adapter.(ResultCapability)
|
||||
|
||||
@@ -1000,7 +1000,11 @@ func (s *Service) enrichAPICallLogFailureSummary(log *model.ApiCallLog, response
|
||||
}
|
||||
|
||||
func (s *Service) enrichAPICallLogPayload(log *model.ApiCallLog, payload map[string]any) {
|
||||
nestedTaskID := ""
|
||||
if data, ok := payload["data"].(map[string]any); ok {
|
||||
if log.Capability == "video" && strings.Contains(log.Path, "/v1/video/generations") {
|
||||
nestedTaskID = firstNonEmpty(stringField(data, "task_id"), stringField(data, "taskId"))
|
||||
}
|
||||
for key, value := range data {
|
||||
if _, exists := payload[key]; !exists {
|
||||
payload[key] = value
|
||||
@@ -1064,7 +1068,7 @@ func (s *Service) enrichAPICallLogPayload(log *model.ApiCallLog, payload map[str
|
||||
}
|
||||
log.CachedTokens = firstInt64(usageMetadata, "cachedContentTokenCount")
|
||||
}
|
||||
log.ProviderRequestID = firstNonEmpty(stringField(payload, "task_id"), stringField(payload, "id"), stringField(payload, "request_id"), stringField(payload, "name"), log.ProviderRequestID)
|
||||
log.ProviderRequestID = firstNonEmpty(nestedTaskID, stringField(payload, "task_id"), stringField(payload, "id"), stringField(payload, "request_id"), stringField(payload, "name"), log.ProviderRequestID)
|
||||
log.ProviderStatus = strings.ToLower(firstNonEmpty(stringField(payload, "status"), log.ProviderStatus))
|
||||
if log.ProviderStatus == "failed" || log.ProviderStatus == "cancelled" || log.ProviderStatus == "expired" {
|
||||
log.Status = model.ApiCallStatusFailed
|
||||
|
||||
@@ -86,6 +86,25 @@ func TestEnrichAPICallLogReadsArkVideoUsage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichAPICallLogPrefersNewAPIChannel2NestedTaskID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
}{
|
||||
{name: "snake case", payload: `{"id":"wrapper-id","data":{"task_id":"task-snake"}}`},
|
||||
{name: "camel case", payload: `{"id":"wrapper-id","data":{"taskId":"task-camel"}}`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
log := &model.ApiCallLog{Capability: "video", Path: "/v1/video/generations"}
|
||||
(&Service{}).EnrichAPICallLog(log, []byte(test.payload))
|
||||
if !strings.HasPrefix(log.ProviderRequestID, "task-") {
|
||||
t.Fatalf("ProviderRequestID = %q, want nested upstream task ID", log.ProviderRequestID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichAPICallLogKeepsUserFacingAndUpstreamFailureDetails(t *testing.T) {
|
||||
log := &model.ApiCallLog{
|
||||
Status: model.ApiCallStatusFailed,
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/protocol"
|
||||
)
|
||||
@@ -342,6 +344,129 @@ func TestDeclarativeProtocolRuntimeExecutesCreatePollAndDownload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeclarativeNewAPIChannel2TaskNotExistRetry(t *testing.T) {
|
||||
adapter := newDeclarativeNewAPIChannel2TestAdapter(t)
|
||||
createCalls := 0
|
||||
pollCalls := 0
|
||||
var server *httptest.Server
|
||||
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/video/generations":
|
||||
createCalls++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"id":"provider-task-1","status":"pending"}`))
|
||||
case "/v1/video/generations/provider-task-1":
|
||||
pollCalls++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if pollCalls <= 2 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`{"code":"task_not_exist","data":null,"message":"task_not_exist"}`))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"id":"provider-task-1","status":"succeeded","video_url":"` + server.URL + `/media.mp4"}`))
|
||||
case "/media.mp4":
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
_, _ = w.Write([]byte("video"))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := providerConfig{BaseURL: server.URL + "/v1", APIKey: "key", Model: "video-model", InterfaceType: "newapi-channel-2", AllowLocalChannel: true}
|
||||
ctx := withProviderOutboundPolicy(context.Background(), config)
|
||||
result, err := runProtocolAdapterTaskWithTiming(ctx, canvasGenerationInput{Mode: "video", Prompt: "a clip", Config: config}, adapter, protocolPollTiming{
|
||||
InitialDelay: time.Millisecond,
|
||||
PollInterval: time.Millisecond,
|
||||
TaskNotExistWindow: time.Second,
|
||||
TaskNotExistRetryDelays: []time.Duration{time.Millisecond, time.Millisecond, time.Millisecond, time.Millisecond},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result["mode"] != "video" || createCalls != 1 || pollCalls != 3 {
|
||||
t.Fatalf("result = %#v, create calls = %d, poll calls = %d", result, createCalls, pollCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeclarativeNewAPIChannel2TaskNotExistExhaustion(t *testing.T) {
|
||||
adapter := newDeclarativeNewAPIChannel2TestAdapter(t)
|
||||
createCalls := 0
|
||||
pollCalls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/video/generations":
|
||||
createCalls++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"id":"provider-task-1","status":"pending"}`))
|
||||
case "/v1/video/generations/provider-task-1":
|
||||
pollCalls++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`{"code":"task_not_exist","message":"task_not_exist"}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := providerConfig{BaseURL: server.URL + "/v1", APIKey: "key", Model: "video-model", InterfaceType: "newapi-channel-2", AllowLocalChannel: true}
|
||||
ctx := withProviderOutboundPolicy(context.Background(), config)
|
||||
_, err := runProtocolAdapterTaskWithTiming(ctx, canvasGenerationInput{Mode: "video", Prompt: "a clip", Config: config}, adapter, protocolPollTiming{
|
||||
InitialDelay: time.Millisecond,
|
||||
PollInterval: time.Millisecond,
|
||||
TaskNotExistWindow: time.Second,
|
||||
TaskNotExistRetryDelays: []time.Duration{time.Millisecond, time.Millisecond, time.Millisecond, time.Millisecond},
|
||||
})
|
||||
var pendingErr providerStatePendingError
|
||||
var httpErr providerHTTPError
|
||||
if !errors.As(err, &pendingErr) || pendingErr.TaskID != "provider-task-1" || !errors.As(err, &httpErr) {
|
||||
t.Fatalf("error = %#v, want typed provider pending error wrapping HTTP error", err)
|
||||
}
|
||||
if createCalls != 1 || pollCalls != 5 {
|
||||
t.Fatalf("create calls = %d, poll calls = %d, want 1 and 5", createCalls, pollCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeclarativeNewAPIChannel2TaskNotExistStrictClassification(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
interfaceType string
|
||||
taskID string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{name: "error code", interfaceType: "newapi-channel-2", taskID: "task-1", err: providerHTTPError{StatusCode: 400, Body: `{"code":"task_not_exist"}`}, want: true},
|
||||
{name: "error message", interfaceType: "newapi-channel-2", taskID: "task-1", err: providerHTTPError{StatusCode: 400, Body: `{"message":"task_not_exist"}`}, want: true},
|
||||
{name: "other interface", interfaceType: "newapi-channel-1", taskID: "task-1", err: providerHTTPError{StatusCode: 400, Body: `{"code":"task_not_exist"}`}},
|
||||
{name: "missing task id", interfaceType: "newapi-channel-2", err: providerHTTPError{StatusCode: 400, Body: `{"code":"task_not_exist"}`}},
|
||||
{name: "other status", interfaceType: "newapi-channel-2", taskID: "task-1", err: providerHTTPError{StatusCode: 404, Body: `{"code":"task_not_exist"}`}},
|
||||
{name: "unstructured body", interfaceType: "newapi-channel-2", taskID: "task-1", err: providerHTTPError{StatusCode: 400, Body: "task_not_exist"}},
|
||||
{name: "partial match", interfaceType: "newapi-channel-2", taskID: "task-1", err: providerHTTPError{StatusCode: 400, Body: `{"code":"task_not_exist_later"}`}},
|
||||
{name: "other provider error", interfaceType: "newapi-channel-2", taskID: "task-1", err: providerHTTPError{StatusCode: 400, Body: `{"code":"invalid_parameter"}`}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := isNewAPIChannel2TaskNotReady(test.interfaceType, test.taskID, test.err); got != test.want {
|
||||
t.Fatalf("isNewAPIChannel2TaskNotReady() = %v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newDeclarativeNewAPIChannel2TestAdapter(t *testing.T) protocol.Adapter {
|
||||
t.Helper()
|
||||
adapter, err := protocol.LoadManifest([]byte(`{
|
||||
"apiVersion":"yingce.plugin/v1",
|
||||
"id":"newapi-channel-2","version":"1.0.0","name":"NewAPI Channel 2","author":"Test","documentation":"# Test",
|
||||
"contributes":{"providers":[{"id":"newapi-channel-2","label":"NewAPI Channel 2","capabilities":["video"],"scopes":["canvas"],"create":{"method":"POST","path":"/video/generations","fields":{"model":"request.model"}},"poll":{"method":"GET","path":"/video/generations/{{taskId}}"},"response":{"taskIdPaths":["id"],"statusPaths":["status"],"resultPaths":["video_url"],"resultKind":"video"}}]}
|
||||
}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return adapter
|
||||
}
|
||||
|
||||
func TestDeclarativeProtocolRecoveryQueriesExistingTaskWithoutCreating(t *testing.T) {
|
||||
manifest := []byte(`{
|
||||
"apiVersion":"yingce.plugin/v2",
|
||||
|
||||
@@ -150,6 +150,17 @@ type providerHTTPError struct {
|
||||
RetryAfter time.Duration
|
||||
}
|
||||
|
||||
type providerStatePendingError struct {
|
||||
TaskID string
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e providerStatePendingError) Error() string {
|
||||
return fmt.Sprintf("上游任务状态尚未同步,将继续查询原任务(任务 %s)", e.TaskID)
|
||||
}
|
||||
|
||||
func (e providerStatePendingError) Unwrap() error { return e.Cause }
|
||||
|
||||
type providerAnalyticsKey struct{}
|
||||
type providerOutboundPolicyKey struct{}
|
||||
|
||||
@@ -2297,10 +2308,32 @@ func runDeclarativeProtocolTask(ctx context.Context, input canvasGenerationInput
|
||||
return runProtocolAdapterTask(ctx, input, adapter)
|
||||
}
|
||||
|
||||
type protocolPollTiming struct {
|
||||
InitialDelay time.Duration
|
||||
PollInterval time.Duration
|
||||
TaskNotExistWindow time.Duration
|
||||
TaskNotExistMaxMisses int
|
||||
TaskNotExistRetryDelays []time.Duration
|
||||
}
|
||||
|
||||
var defaultProtocolPollTiming = protocolPollTiming{
|
||||
InitialDelay: 2 * time.Second,
|
||||
PollInterval: 2500 * time.Millisecond,
|
||||
TaskNotExistWindow: 15 * time.Second,
|
||||
TaskNotExistMaxMisses: 5,
|
||||
TaskNotExistRetryDelays: []time.Duration{2 * time.Second, 3 * time.Second, 5 * time.Second, 5 * time.Second},
|
||||
}
|
||||
|
||||
func runProtocolAdapterTask(ctx context.Context, input canvasGenerationInput, adapter protocol.Adapter) (map[string]interface{}, error) {
|
||||
return runProtocolAdapterTaskWithTiming(ctx, input, adapter, defaultProtocolPollTiming)
|
||||
}
|
||||
|
||||
func runProtocolAdapterTaskWithTiming(ctx context.Context, input canvasGenerationInput, adapter protocol.Adapter, timing protocolPollTiming) (map[string]interface{}, error) {
|
||||
request := protocolRequestFromInput(input)
|
||||
taskID := resumedProviderRequestID(ctx)
|
||||
var created protocol.CreateResult
|
||||
createdProviderTask := false
|
||||
syncWindowStartedAt := time.Now()
|
||||
if taskID == "" {
|
||||
spec, err := adapter.BuildCreate(ctx, protocol.RequestContext{BaseURL: input.Config.BaseURL, Request: request})
|
||||
if err != nil {
|
||||
@@ -2324,8 +2357,16 @@ func runProtocolAdapterTask(ctx context.Context, input canvasGenerationInput, ad
|
||||
if taskID == "" {
|
||||
return nil, errors.New("声明式协议创建请求没有返回任务 ID")
|
||||
}
|
||||
createdProviderTask = true
|
||||
syncWindowStartedAt = time.Now()
|
||||
}
|
||||
if createdProviderTask && input.Config.InterfaceType == string(model.ChannelInterfaceNewAPIChannel2) {
|
||||
if err := sleepContext(ctx, timing.InitialDelay); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
taskNotExistMisses := 0
|
||||
for deadline := providerPollingDeadline(ctx); time.Now().Before(deadline); {
|
||||
spec, err := adapter.BuildPoll(ctx, protocol.PollContext{BaseURL: input.Config.BaseURL, Model: request.Model, Request: request, TaskID: taskID})
|
||||
if err != nil {
|
||||
@@ -2333,8 +2374,26 @@ func runProtocolAdapterTask(ctx context.Context, input canvasGenerationInput, ad
|
||||
}
|
||||
body, err := executeProtocolRequest(withProviderRequestKind(ctx, "poll"), input.Config, spec)
|
||||
if err != nil {
|
||||
if isNewAPIChannel2TaskNotReady(input.Config.InterfaceType, taskID, err) {
|
||||
taskNotExistMisses++
|
||||
maxMisses := timing.TaskNotExistMaxMisses
|
||||
if maxMisses <= 0 {
|
||||
maxMisses = len(timing.TaskNotExistRetryDelays) + 1
|
||||
}
|
||||
if time.Since(syncWindowStartedAt) >= timing.TaskNotExistWindow || taskNotExistMisses >= maxMisses {
|
||||
return nil, providerStatePendingError{TaskID: taskID, Cause: err}
|
||||
}
|
||||
delayIndex := min(taskNotExistMisses-1, len(timing.TaskNotExistRetryDelays)-1)
|
||||
if delayIndex >= 0 {
|
||||
if sleepErr := sleepContext(ctx, timing.TaskNotExistRetryDelays[delayIndex]); sleepErr != nil {
|
||||
return nil, sleepErr
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
taskNotExistMisses = 0
|
||||
state, err := adapter.ParsePoll(ctx, protocol.PollContext{BaseURL: input.Config.BaseURL, Model: request.Model, Request: request, TaskID: taskID}, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2348,13 +2407,29 @@ func runProtocolAdapterTask(ctx context.Context, input canvasGenerationInput, ad
|
||||
case protocol.StatusFailed, protocol.StatusCancelled:
|
||||
return nil, protocolResultError(state.Message, taskID)
|
||||
}
|
||||
if err := sleepContext(ctx, 2500*time.Millisecond); err != nil {
|
||||
if err := sleepContext(ctx, timing.PollInterval); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("声明式协议任务超时(任务 %s)", taskID)
|
||||
}
|
||||
|
||||
func isNewAPIChannel2TaskNotReady(interfaceType string, taskID string, err error) bool {
|
||||
if strings.TrimSpace(interfaceType) != string(model.ChannelInterfaceNewAPIChannel2) || strings.TrimSpace(taskID) == "" {
|
||||
return false
|
||||
}
|
||||
var httpErr providerHTTPError
|
||||
if !errors.As(err, &httpErr) || httpErr.StatusCode != http.StatusBadRequest {
|
||||
return false
|
||||
}
|
||||
var payload map[string]any
|
||||
if json.Unmarshal([]byte(httpErr.Body), &payload) != nil {
|
||||
return false
|
||||
}
|
||||
code, message := providerFailureDetails(payload)
|
||||
return strings.EqualFold(strings.TrimSpace(code), "task_not_exist") || strings.EqualFold(strings.TrimSpace(message), "task_not_exist")
|
||||
}
|
||||
|
||||
// queryProtocolAdapterVideoTask performs exactly one read of an existing
|
||||
// declarative provider task. Manual recovery uses this path so it can never
|
||||
// create a second billable generation while checking a failed local task.
|
||||
|
||||
@@ -35,12 +35,22 @@ func TestOnlyResumableNewAPIChannel2VideoDeadlinesStayRunning(t *testing.T) {
|
||||
if !svc.shouldDeferVideoProviderTask(base, string(input), context.DeadlineExceeded) {
|
||||
t.Fatal("resumable NewAPI Channel 2 deadline should remain running")
|
||||
}
|
||||
pendingErr := providerStatePendingError{TaskID: base.ProviderRequestID, Cause: providerHTTPError{StatusCode: 400, Body: `{"code":"task_not_exist"}`}}
|
||||
if !svc.shouldDeferVideoProviderTask(base, string(input), pendingErr) {
|
||||
t.Fatal("resumable NewAPI Channel 2 provider sync delay should remain running")
|
||||
}
|
||||
if svc.shouldDeferVideoProviderTask(model.Task{ID: "task-2", Type: "canvas_video"}, string(input), context.DeadlineExceeded) {
|
||||
t.Fatal("missing provider task id must not be deferred")
|
||||
}
|
||||
if svc.shouldDeferVideoProviderTask(model.Task{ID: "task-2", Type: "canvas_video"}, string(input), pendingErr) {
|
||||
t.Fatal("provider sync delay without persisted provider task id must not be deferred")
|
||||
}
|
||||
if svc.shouldDeferVideoProviderTask(base, string(input), context.Canceled) {
|
||||
t.Fatal("explicit cancellation must not be deferred")
|
||||
}
|
||||
if svc.shouldDeferVideoProviderTask(base, string(input), providerHTTPError{StatusCode: 400, Body: `{"code":"task_not_exist"}`}) {
|
||||
t.Fatal("untyped provider error must not be deferred")
|
||||
}
|
||||
other, err := json.Marshal(canvasGenerationInput{Mode: "video", Config: providerConfig{BaseURL: "https://example.com", InterfaceType: string(model.ChannelInterfaceNewAPIVideo)}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -180,15 +180,22 @@ func (w *taskWorkerCoordinator) processClaimedTask(task *model.Task) error {
|
||||
return leaseErr
|
||||
default:
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
decryptedInput, decryptErr := s.decryptTaskInputJSON(task.InputJSON)
|
||||
if decryptErr == nil && s.shouldDeferVideoProviderTask(*task, decryptedInput, err) {
|
||||
if deferErr := s.repo.DeferRunningTaskForProviderPoll(task.ID, task.LeaseOwner, "后台仍在生成", 15*time.Second); deferErr != nil {
|
||||
return deferErr
|
||||
}
|
||||
_ = s.log(task.UserID, task.ID, "info", "前台等待结束,上游视频仍在生成,将继续回查原任务", task.PollStage)
|
||||
return nil
|
||||
decryptedInput, decryptErr := s.decryptTaskInputJSON(task.InputJSON)
|
||||
if decryptErr == nil && s.shouldDeferVideoProviderTask(*task, decryptedInput, err) {
|
||||
stage := "后台仍在生成"
|
||||
message := "前台等待结束,上游视频仍在生成,将继续回查原任务"
|
||||
var pendingErr providerStatePendingError
|
||||
if errors.As(err, &pendingErr) {
|
||||
stage = "等待上游任务同步"
|
||||
message = "上游任务状态暂未同步,将继续回查原任务"
|
||||
}
|
||||
if deferErr := s.repo.DeferRunningTaskForProviderPoll(task.ID, task.LeaseOwner, stage, 15*time.Second); deferErr != nil {
|
||||
return deferErr
|
||||
}
|
||||
_ = s.log(task.UserID, task.ID, "info", message, task.PollStage)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
err = errors.New(taskTimeoutMessage(task.Type))
|
||||
}
|
||||
return terminal.handleExecutionFailure(task, err, providerSucceeded, channelSlotFailedBeforeRequest)
|
||||
@@ -252,7 +259,16 @@ func taskExecutionTimeoutWithPolicy(taskType string, policy RuntimeTaskPolicy) t
|
||||
}
|
||||
|
||||
func (s *Service) shouldDeferVideoProviderTask(task model.Task, decryptedInput string, err error) bool {
|
||||
if !errors.Is(err, context.DeadlineExceeded) || strings.TrimSpace(task.ProviderRequestID) == "" || (!strings.HasPrefix(task.Type, "canvas_video") && !strings.HasPrefix(task.Type, "video_")) {
|
||||
providerRequestID := strings.TrimSpace(task.ProviderRequestID)
|
||||
if providerRequestID == "" || (!strings.HasPrefix(task.Type, "canvas_video") && !strings.HasPrefix(task.Type, "video_")) {
|
||||
return false
|
||||
}
|
||||
deferSignal := errors.Is(err, context.DeadlineExceeded)
|
||||
var pendingErr providerStatePendingError
|
||||
if errors.As(err, &pendingErr) {
|
||||
deferSignal = strings.TrimSpace(pendingErr.TaskID) == providerRequestID
|
||||
}
|
||||
if !deferSignal {
|
||||
return false
|
||||
}
|
||||
var input canvasGenerationInput
|
||||
|
||||
Reference in new issue
Block a user