168 lines
6.4 KiB
Go
168 lines
6.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"infinite-canvas/backend/internal/model"
|
|
"infinite-canvas/backend/internal/repository"
|
|
)
|
|
|
|
const providerTaskRecoveryLeaseDuration = 10 * time.Minute
|
|
|
|
type ProviderTaskQueryResult struct {
|
|
Task *model.Task `json:"task"`
|
|
ProviderStatus string `json:"providerStatus"`
|
|
Recovered bool `json:"recovered"`
|
|
BillingSettled bool `json:"billingSettled"`
|
|
}
|
|
|
|
func (s *Service) QueryFailedVideoTask(ctx context.Context, userID string, taskID string) (*ProviderTaskQueryResult, error) {
|
|
task, err := s.repo.TaskForUser(strings.TrimSpace(userID), strings.TrimSpace(taskID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.queryFailedVideoTask(ctx, task, strings.TrimSpace(userID))
|
|
}
|
|
|
|
func (s *Service) AdminQueryFailedVideoTask(ctx context.Context, actor *model.User, logID string) (*ProviderTaskQueryResult, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
log, err := s.repo.APICallLog(strings.TrimSpace(logID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if log.Capability != "video" || strings.TrimSpace(log.TaskID) == "" {
|
|
return nil, BadAuthRequest("该请求没有可查询的视频任务")
|
|
}
|
|
task, err := s.repo.Task(log.TaskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if task.UserID != log.UserID {
|
|
return nil, BadAuthRequest("请求与任务归属不一致")
|
|
}
|
|
if task.ProviderRequestID == "" {
|
|
task.ProviderRequestID = strings.TrimSpace(log.ProviderRequestID)
|
|
}
|
|
result, err := s.queryFailedVideoTask(ctx, task, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.appendAdminAudit(actor, "api_log.query_provider_task", "task", task.ID, "人工查询失败视频任务", map[string]any{
|
|
"apiCallLogId": log.ID, "providerRequestId": task.ProviderRequestID, "providerStatus": result.ProviderStatus, "recovered": result.Recovered,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Service) queryFailedVideoTask(ctx context.Context, task *model.Task, claimUserID string) (*ProviderTaskQueryResult, error) {
|
|
if task == nil || task.ID == "" {
|
|
return nil, BadAuthRequest("任务不存在")
|
|
}
|
|
if task.Status != model.TaskStatusFailed {
|
|
return nil, BadAuthRequest("只能人工查询状态为失败的任务")
|
|
}
|
|
if !strings.HasPrefix(task.Type, "canvas_video") && !strings.HasPrefix(task.Type, "video_") {
|
|
return nil, BadAuthRequest("该任务不是视频生成任务")
|
|
}
|
|
|
|
s.hydrateTaskProviderRequestID(task)
|
|
providerRequestID := strings.TrimSpace(task.ProviderRequestID)
|
|
if providerRequestID == "" {
|
|
return nil, BadAuthRequest("该任务没有可恢复的上游任务 ID")
|
|
}
|
|
if task.BillingOrderID != "" {
|
|
order, err := s.repo.BillingOrder(task.BillingOrderID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if order.UserID != task.UserID || order.TaskID != task.ID {
|
|
return nil, BadAuthRequest("任务与计费订单归属不一致")
|
|
}
|
|
if order.Status == model.BillingStatusRefunded {
|
|
return nil, BadAuthRequest("该任务已退款,不能恢复并重新扣费")
|
|
}
|
|
}
|
|
|
|
decryptedInput, err := s.decryptTaskInputJSON(task.InputJSON)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("读取任务配置失败:%w", err)
|
|
}
|
|
var input canvasGenerationInput
|
|
if err := json.Unmarshal([]byte(decryptedInput), &input); err != nil {
|
|
return nil, fmt.Errorf("任务输入解析失败:%w", err)
|
|
}
|
|
config, err := s.resolveProviderConfig(input.Config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if config.InterfaceType != string(model.ChannelInterfaceNewAPIChannel2) {
|
|
return nil, BadAuthRequest("该任务不使用 NewAPI Video Generations 协议")
|
|
}
|
|
input.Config = config
|
|
task.InputJSON = decryptedInput
|
|
task.ProviderRequestID = providerRequestID
|
|
if err := s.repo.UpdateTaskProviderState(task.ID, providerRequestID, task.PollStage, task.NextPollAt); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
owner := "manual-recovery:" + newID()
|
|
if err := s.repo.ClaimFailedTaskProviderRecovery(task.ID, claimUserID, owner, providerTaskRecoveryLeaseDuration); err != nil {
|
|
if errors.Is(err, repository.ErrTaskProviderRecoveryConflict) {
|
|
return nil, &AuthError{Status: 409, Message: "该任务正在查询上游状态,请稍后再试"}
|
|
}
|
|
return nil, err
|
|
}
|
|
task.LeaseOwner = owner
|
|
defer func() { _ = s.repo.ReleaseTaskProviderRecovery(task.ID, owner) }()
|
|
|
|
queryCtx := withProviderAnalytics(ctx, s, *task)
|
|
queryCtx = withProviderOutboundPolicy(queryCtx, input.Config)
|
|
result, providerStatus, err := queryNewAPIChannel2VideoTask(queryCtx, input, providerRequestID)
|
|
if err != nil {
|
|
_ = s.log(task.UserID, task.ID, "error", "人工查询上游视频任务失败", err.Error())
|
|
return nil, err
|
|
}
|
|
if result == nil {
|
|
_ = s.log(task.UserID, task.ID, "info", "人工查询完成,上游任务仍在处理", providerStatus)
|
|
return &ProviderTaskQueryResult{Task: taskForOutput(*task), ProviderStatus: providerStatus, Recovered: false}, nil
|
|
}
|
|
|
|
result, err = s.persistGeneratedMediaResult(task.UserID, result)
|
|
if err != nil {
|
|
_ = s.log(task.UserID, task.ID, "error", "人工查询已取得视频,但结果保存失败", err.Error())
|
|
return nil, err
|
|
}
|
|
resultJSON, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
task.Error = ""
|
|
task.PollStage = strings.ToLower(providerStatus)
|
|
task.NextPollAt = nil
|
|
if err := s.saveTaskCompletionWithinStorageQuota(task, resultJSON, nil, false); err != nil {
|
|
_ = s.MarkBillingUncertain(task.BillingOrderID, "人工查询确认上游成功,但任务结果未保存:"+err.Error())
|
|
_ = s.log(task.UserID, task.ID, "error", "人工查询已取得视频,但任务恢复失败", err.Error())
|
|
return nil, err
|
|
}
|
|
if err := s.RegisterTaskOutputFromTask(*task); err != nil {
|
|
_ = s.log(task.UserID, task.ID, "error", "任务恢复成功但项目产物登记失败", err.Error())
|
|
}
|
|
billingSettled := true
|
|
if err := s.SettleBilling(task.BillingOrderID, providerRequestID); err != nil {
|
|
billingSettled = false
|
|
_ = s.MarkBillingUncertain(task.BillingOrderID, "人工查询确认生成成功,但积分结算失败:"+err.Error())
|
|
_ = s.log(task.UserID, task.ID, "error", "任务恢复成功但积分结算失败,已进入待核对", err.Error())
|
|
} else {
|
|
_ = s.log(task.UserID, task.ID, "info", "人工查询确认生成成功,任务已恢复并完成结算", providerStatus)
|
|
}
|
|
return &ProviderTaskQueryResult{Task: taskForOutput(*task), ProviderStatus: providerStatus, Recovered: true, BillingSettled: billingSettled}, nil
|
|
}
|