Files
ai-agent/workflow/service/flow/async.go
T
19904408334 d699f7ce14 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 与数据库密码配置
2026-09-03 13:22:22 +08:00

162 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package flow
import (
"context"
"encoding/json"
"errors"
"sync"
"time"
"github.com/gogf/gf/v2/frame/g"
"ai-agent/gateway"
"ai-agent/workflow/consts/model"
flowDao "ai-agent/workflow/dao/flow"
"ai-agent/workflow/model/entity"
)
// 全局等待任务回调的工具
var (
asyncMu sync.Mutex
asyncTasks = make(map[string]chan any)
)
// Wait 阻塞等待回调结果
// 调用后会一直卡住,直到 Notify 唤醒 或 超时/取消
func Wait(ctx context.Context, taskId string) (any, error) {
asyncMu.Lock()
ch := make(chan any, 1)
asyncTasks[taskId] = ch
asyncMu.Unlock()
defer close(ch)
for {
select {
case result := <-ch:
return result, nil
case <-ctx.Done():
asyncMu.Lock()
delete(asyncTasks, taskId)
asyncMu.Unlock()
return nil, ctx.Err()
}
}
}
// Notify 回调时调用,唤醒等待的任务
func Notify(taskId string, result any) {
asyncMu.Lock()
defer asyncMu.Unlock()
ch, exist := asyncTasks[taskId]
if !exist {
return
}
ch <- result
delete(asyncTasks, taskId)
}
// asyncRecoverWaitTimeout in-flight 行重订阅等结果的超时上限。
// 任务可能仍执行中(消息未发布)或提交即失败(不会发布消息);超时视为结果未知,清记录重提。
const asyncRecoverWaitTimeout = 10 * time.Minute
type asyncAction int
const (
asyncActionResubmit asyncAction = iota // 无记录 / failed / done 空结果 → 重新提交
asyncActionReuse // done 有结果 → 复用,不重调
asyncActionFinalize // in-flight → 重订阅 msgTopic 收尾
)
// asyncCallAction 异步任务缓存行动决策(纯函数,可单测)
func asyncCallAction(rec *entity.FlowAsyncTask) asyncAction {
if rec == nil {
return asyncActionResubmit
}
switch rec.State {
case flowDao.FlowAsyncStateDone:
if rec.Result != "" && rec.Result != "{}" {
return asyncActionReuse
}
return asyncActionResubmit
case flowDao.FlowAsyncStateFailed:
return asyncActionResubmit
default: // in-flight
return asyncActionFinalize
}
}
// AsyncModelCallWithRecovery 统一异步模型调用入口:
// 提交时把 task_id/msg_topic 落库 flow_async_task,崩溃后重订阅 msg_topic 拿回已完成结果复用,不重复调用。
// 同步模型直接走 gateway.ModelCallResult,不落库(无恢复语义)。
// 注意:本函数是节点内阻塞调用(WaitModelCallResult 等回调),不是独立并发触发方,
// 不参与 exec 并发仲裁(谁抢到执行权谁跑)——仲裁语义见《工作流执行并发仲裁设计.md》。
func AsyncModelCallWithRecovery(ctx context.Context, execId int64, nodeId string, segIdx int, modelId int64, responseType model.ResponseType, sessionId string, requestParams map[string]any, businessParams map[string]any) (*gateway.ModelCallRes, error) {
if responseType == nil || *responseType != *model.ResponseTypeAsync.Code() {
return gateway.ModelCallResult(ctx, modelId, responseType, sessionId, requestParams, businessParams)
}
rec, err := flowDao.FlowAsyncTaskDao.Get(ctx, execId, nodeId, segIdx)
if err != nil {
return nil, err
}
switch asyncCallAction(rec) {
case asyncActionReuse:
return unmarshalModelCallRes(rec.Result)
case asyncActionFinalize:
// 重订阅 msgTopic 收尾:拿回已发布结果(消息在 JetStream 保留 7 天);
// 成功 → 落 done 复用;超时/订阅失败 → 清记录重提
waitCtx, cancel := context.WithTimeout(ctx, asyncRecoverWaitTimeout)
res, waitErr := gateway.WaitModelCallResult(waitCtx, rec.MsgTopic)
cancel()
if waitErr != nil {
if errors.Is(waitErr, context.DeadlineExceeded) || errors.Is(waitErr, context.Canceled) {
_ = flowDao.FlowAsyncTaskDao.DeleteByKey(ctx, execId, nodeId, segIdx)
break // 落入下方重新提交
}
return nil, waitErr
}
_ = flowDao.FlowAsyncTaskDao.UpdateByKey(ctx, execId, nodeId, segIdx, flowDao.FlowAsyncStateDone, marshalModelCallRes(res))
return res, nil
case asyncActionResubmit:
// 清残留(failed 或 done 空结果)
if rec != nil {
_ = flowDao.FlowAsyncTaskDao.DeleteByKey(ctx, execId, nodeId, segIdx)
}
}
// 重新提交:先落库 in-flighttask_id/msg_topic),等待结果
res, msgTopic, err := gateway.SubmitModelCall(ctx, modelId, responseType, sessionId, requestParams, businessParams)
if err != nil {
return nil, err
}
if err := flowDao.FlowAsyncTaskDao.Upsert(ctx, execId, nodeId, segIdx, modelId, res.TaskId, msgTopic); err != nil {
return nil, err
}
waitRes, waitErr := gateway.WaitModelCallResult(ctx, msgTopic)
if waitErr != nil {
// 结果失败(model error/取消):落 failed,调用方(段重试/恢复)决定后续
_ = flowDao.FlowAsyncTaskDao.UpdateByKey(ctx, execId, nodeId, segIdx, flowDao.FlowAsyncStateFailed, "")
return nil, waitErr
}
_ = flowDao.FlowAsyncTaskDao.UpdateByKey(ctx, execId, nodeId, segIdx, flowDao.FlowAsyncStateDone, marshalModelCallRes(waitRes))
return waitRes, nil
}
func marshalModelCallRes(res *gateway.ModelCallRes) string {
b, err := json.Marshal(res)
if err != nil {
g.Log().Warningf(context.Background(), "序列化模型调用结果失败: %v", err)
return "{}"
}
return string(b)
}
func unmarshalModelCallRes(s string) (*gateway.ModelCallRes, error) {
res := new(gateway.ModelCallRes)
if err := json.Unmarshal([]byte(s), res); err != nil {
return nil, err
}
return res, nil
}