Files
ai-agent/workflow/service/flow/async_task.go
T

118 lines
4.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"
"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"
)
// 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 统一异步模型调用入口(spec §5.4):
// 提交时把 task_id/msg_topic 落库 flow_async_task,崩溃后重订阅 msg_topic 拿回已完成结果复用,不重复调用。
// 同步模型直接走 gateway.ModelCallResult,不落库(无恢复语义)。
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
}