feat(workflow): 增加工作流计费与执行生命周期管理

- 新增计费模块:执行开始建单、终态结算/取消/失败处理,支持按条/按秒/按token计费
- 新增执行生命周期跟踪:优雅关停时取消运行中执行并等待落库
- 新增异步任务等待/通知机制(Wait/Notify)
- 重构执行记录落库与进度上报,统一失败分类与重试语义
- 重命名文件:async_task.go→async.go、flow_checkpoint_store.go→exec_checkpoint.go、flow_graph_util.go→exec_record.go
- 更新 .gitignore 与数据库密码配置
This commit is contained in:
2026-09-03 13:22:22 +08:00
parent 67d049e586
commit d699f7ce14
46 changed files with 2690 additions and 2450 deletions
@@ -9,6 +9,7 @@ import (
commonHttp "gitea.redpowerfuture.com/red-future/common/http"
"gitea.redpowerfuture.com/red-future/common/oss"
"gitea.redpowerfuture.com/red-future/common/utils"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
@@ -158,7 +159,7 @@ func SubmitMergeAsync(ctx context.Context, videoURLs, audioURLs []string, upload
func submitMediaTask(ctx context.Context, kind TaskKind, req *mergeSubmitReq) (string, error) {
path := "media/video/" + string(kind) + "/async"
res := new(mergeSubmitRes)
if err := commonHttp.Post(ctx, path, requestHeaders(ctx), res, req); err != nil {
if err := commonHttp.Post(ctx, path, utils.HeadersFromCtx(ctx), res, req); err != nil {
return "", fmt.Errorf("提交%s任务失败: %v", kind, err)
}
if res.TaskID == "" {
@@ -199,31 +200,12 @@ func WaitMediaTask(ctx context.Context, kind TaskKind, taskID string, timeout ti
func getMediaTask(ctx context.Context, kind TaskKind, taskID string) (*MergeTask, error) {
path := "media/video/" + string(kind) + "/task/" + taskID
res := new(MergeTask)
if err := commonHttp.Get(ctx, path, requestHeaders(ctx), res); err != nil {
if err := commonHttp.Get(ctx, path, utils.HeadersFromCtx(ctx), res); err != nil {
return nil, err
}
return res, nil
}
// requestHeaders 透传当前请求头(含 Authorization / X-User-Info),供内部服务鉴权使用;
// 后台恢复续跑无 HTTP 请求时(ctx 携带合成 user),补充 X-User-Info 供下游 GetUserInfo 识别租户
func requestHeaders(ctx context.Context) map[string]string {
headers := make(map[string]string)
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
}
if headers["X-User-Info"] == "" {
if u := ctx.Value("user"); u != nil {
headers["X-User-Info"] = gconv.String(u)
}
}
return headers
}
// segmentResult 一段视频生成的产出(原 ai-agent/video/plan.SegmentResultplan 包已并入处理器树)。
type segmentResult struct {
SegmentIndex int `json:"segment_index"`