feat(workflow): 工作流提供方 - 集成 RunningHub 与 ComfyUI Bridge
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/repository"
|
||||
@@ -108,10 +109,73 @@ func (w *taskLifecycleCoordinator) cancelTask(_ context.Context, userID string,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 任务创建后可能已经产生上游费用。取消接口保留用于兼容旧客户端,
|
||||
// 但不能再改变任务状态、退款或结束关联会话;页面离开只应停止监听。
|
||||
if task.Status == model.TaskStatusQueued || task.Status == model.TaskStatusRunning || task.ProviderRequestID != "" {
|
||||
return nil, errors.New("任务已发起,无法取消,请等待任务完成")
|
||||
if task.Status != model.TaskStatusQueued && task.Status != model.TaskStatusRunning {
|
||||
if task.Status == model.TaskStatusCancelled {
|
||||
return taskForOutput(*task), nil
|
||||
}
|
||||
return nil, fmt.Errorf("任务当前状态为 %s,无法取消", task.Status)
|
||||
}
|
||||
return nil, fmt.Errorf("任务当前状态为 %s,无法取消", task.Status)
|
||||
|
||||
// 先从账单和请求日志补齐上游 ID,再做条件更新。取消与 worker 完成之间
|
||||
// 以数据库终态为准,避免“用户已取消但迟到结果又把任务写成成功”。
|
||||
s.hydrateTaskProviderRequestID(task)
|
||||
originalStatus := task.Status
|
||||
now := time.Now()
|
||||
cancelled, err := s.repo.CancelTaskIfStatus(userID, id, task.Status, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !cancelled {
|
||||
latest, latestErr := s.repo.TaskForUser(userID, id)
|
||||
if latestErr != nil {
|
||||
return nil, latestErr
|
||||
}
|
||||
if latest.Status == model.TaskStatusCancelled {
|
||||
return taskForOutput(*latest), nil
|
||||
}
|
||||
return nil, errors.New("任务状态已变化,请刷新后重试")
|
||||
}
|
||||
|
||||
task.Status = model.TaskStatusCancelled
|
||||
task.Stage = "任务已取消"
|
||||
task.Error = "任务已取消"
|
||||
task.CompletedAt = &now
|
||||
s.cancelActiveTask(task.ID)
|
||||
|
||||
// 这些收尾操作必须幂等;任何单项失败都记录日志,但不能让已经落库的
|
||||
// cancelled 状态重新对用户表现为“取消失败”。
|
||||
if err := s.markSessionFailed(*task, "会话任务已取消。"); err != nil {
|
||||
_ = s.log(task.UserID, task.ID, "error", "取消任务后更新会话状态失败", err.Error())
|
||||
}
|
||||
if err := s.finalizeTaskTextReplay(task.ID, model.TaskStatusCancelled); err != nil {
|
||||
_ = s.log(task.UserID, task.ID, "error", "取消任务后归并文本回放失败", err.Error())
|
||||
}
|
||||
_ = s.log(task.UserID, task.ID, "warn", "用户主动取消任务", "")
|
||||
|
||||
if task.ProviderRequestID != "" {
|
||||
// 上游取消可能需要轮询确认,不能阻塞取消接口;请求上下文也不能因
|
||||
// 浏览器关闭而中断。后台对账会继续负责退款或费用核对。
|
||||
cancelTask := *task
|
||||
go func() {
|
||||
requestCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if err := s.requestProviderCancellation(requestCtx, &cancelTask); err != nil {
|
||||
_ = s.log(cancelTask.UserID, cancelTask.ID, "error", "发送上游取消请求失败", err.Error())
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
var billingErr error
|
||||
if originalStatus == model.TaskStatusQueued {
|
||||
billingErr = s.taskBilling().RefundBilling(task.BillingOrderID, "用户主动取消,且任务尚未开始执行")
|
||||
} else {
|
||||
// running 任务可能已经发起上游调用但尚未把 request ID 写回,不能
|
||||
// 直接退款后放任上游继续生成,先冻结为待核对更安全。
|
||||
billingErr = s.taskBilling().MarkBillingUncertain(task.BillingOrderID, "用户取消时上游请求 ID 尚未确认,费用待核对")
|
||||
}
|
||||
if billingErr != nil {
|
||||
_ = s.log(task.UserID, task.ID, "error", "取消任务后处理积分失败,已保留人工核对线索", billingErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return taskForOutput(*task), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user