// Package gateway ai-agent 对外部服务的调用层:模型网关(model-gateway)的查询/提交/聊天调用,以及文件下载/上传(OSS)。 // 从 workflow/service/flow 抽离为独立目录,供工作流节点与视频管线(video/)共用,业务侧不再直接依赖 workflow 引擎。 package gateway import ( "context" "fmt" "net/http" "time" "ai-agent/workflow/consts/model" "ai-agent/workflow/consts/public" "gitea.redpowerfuture.com/red-future/common/beans" commonHttp "gitea.redpowerfuture.com/red-future/common/http" gmq "github.com/bjang03/gmq/core/gmq" "github.com/bjang03/gmq/mq" "github.com/bjang03/gmq/types" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" "github.com/google/uuid" ) // ListModelManageReq 配置列表 type ListModelManageReq struct { *beans.Page `json:"page"` ModelName string `p:"modelName" json:"modelName" dc:"模型名称(模糊查询,可选)"` ModelType int `p:"modelType" json:"modelType" dc:"模型类型"` } // ModelManageListItem 模型配置列表项。listModelManage 返回平铺的 model-gateway entity(id 在顶层), // 与 getModelManage 的 {modelManage:{...}} 嵌套不同,故独立成结构而非复用 GetModelInfoByIdRes。 type ModelManageListItem struct { Id int64 `json:"id" dc:"配置ID"` ModelName string `json:"modelName" dc:"模型名称"` } type ListModelManageRes struct { List []*ModelManageListItem `json:"list" dc:"列表数据"` Total int `json:"total" dc:"总数"` } // GetModelInfoByIdReq 查询模型配置 type GetModelInfoByIdReq struct { ModelId int64 `json:"id" dc:"模型ID"` } type GetModelInfoByIdRes struct { ModelManage struct { Id int64 `json:"id" dc:"模型ID"` ModelType model.ModelType `json:"modelType" description:"模型类型"` ModelName string `json:"modelName" description:"模型名称"` MaxTokens int `json:"maxTokens" description:"最大token数"` ChatModel *bool `json:"chatModel" description:"是否聊天模型"` ResponseType model.ResponseType `json:"responseType" description:"返回类型:1同步,2异步,3流"` Enabled *bool `json:"enabled" description:"是否启用"` RequestBodyMapping map[string]any `json:"requestBodyMapping" description:"请求体映射(约等于视频模型 schema)"` RequestBusinessFieldMapping map[string]string `json:"requestBusinessFieldMapping" description:"请求业务字段映射"` MinDuration int `json:"minDuration" description:"最小时长(秒)"` MaxDuration int `json:"maxDuration" description:"最大时长(秒)"` } `json:"modelManage" dc:"模型配置"` } // modelCallTopic 生成异步模型消息主题:带业务标识(bizName/modelId/sessionId)便于排查, // uuid 保证每次调用唯一,避免并发调用共用同一主题导致结果串扰 func modelCallTopic(bizName string, modelId int64, sessionId string) string { return fmt.Sprintf("model-call-%s-%d-%s-%s", bizName, modelId, sessionId, uuid.NewString()) } // ModelCallReq 调用模型网关 type ModelCallReq struct { ModelId int64 `json:"modelId" dc:"模型ID"` BizName string `json:"bizName" dc:"业务名称(调用方模块/系统,用于统计)"` SessionId string `json:"sessionId" dc:"会话ID"` RequestParams map[string]any `json:"requestParams" dc:"请求参数"` BusinessParams map[string]any `json:"businessParams" dc:"业务参数"` MsgTopic string `json:"msgTopic" dc:"消息主题(异步必要参数)"` } type ModelCallRes struct { TaskId int64 `json:"id" dc:"任务ID"` State int8 `json:"state" dc:"状态"` TotalTokens int64 `json:"totalTokens" dc:"总token"` PromptTokens int64 `json:"promptTokens" dc:"输入token"` CompletionTokens int64 `json:"completionTokens" dc:"输出token"` Tools []ModelTool `json:"tools" dc:"工具"` Content map[string]any `json:"content" dc:"内容"` Cost float64 `json:"cost" dc:"费用(元)"` ErrorMsg string `json:"errorMsg" dc:"错误消息"` } type ModelTool struct { Id string `json:"id" dc:"工具ID"` Type string `json:"type" dc:"工具类型"` Function struct { Name string `json:"name"` Arguments string `json:"arguments"` } `json:"function"` } // requestHeaders 透传当前 HTTP 请求头(鉴权/链路信息)。 // 浏览器 WebSocket 握手无法携带 Authorization 头,前端把 token 放在握手 URL query(?token=)里; // 若请求头没有 Authorization,则从 query 补回,保证下游(model-gateway → admin-go)能拿到用户 token。 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["Authorization"] == "" { if t := r.Request.URL.Query().Get("token"); t != "" { headers["Authorization"] = "Bearer " + t } } } return headers } // ListModelManage 配置列表 func ListModelManage(ctx context.Context, req *ListModelManageReq) (res *ListModelManageRes, err error) { res = new(ListModelManageRes) err = commonHttp.Get(ctx, "model-gateway/model/manage/listModelManage", requestHeaders(ctx), res, req) return } // GetModelInfoById 查询模型配置 func GetModelInfoById(ctx context.Context, req *GetModelInfoByIdReq) (res *GetModelInfoByIdRes, err error) { res = new(GetModelInfoByIdRes) err = commonHttp.Get(ctx, "model-gateway/model/manage/getModelManage", requestHeaders(ctx), res, req) return } // SubmitModelCall 提交模型调用(异步模型自动生成唯一 msgTopic),返回提交响应与消息主题。 // taskId 在 res.TaskId;同步模型 msgTopic 为空串。结果等待由 WaitModelCallResult 完成。 func SubmitModelCall(ctx context.Context, modelId int64, responseType model.ResponseType, sessionId string, requestParams map[string]any, businessParams map[string]any) (res *ModelCallRes, msgTopic string, err error) { // 异步模型必须绑定消息主题接收结果:自动生成唯一主题, // 带业务标识(bizName/modelId/sessionId)便于排查,每次调用唯一避免并发串结果 if responseType != nil && *responseType == *model.ResponseTypeAsync.Code() { msgTopic = modelCallTopic(g.Cfg().MustGet(ctx, "server.name").String(), modelId, sessionId) } req := ModelCallReq{ ModelId: modelId, BizName: g.Cfg().MustGet(ctx, "server.name").String(), SessionId: sessionId, RequestParams: requestParams, BusinessParams: businessParams, MsgTopic: msgTopic, } // 克隆 commonHttp 客户端(保留 Consul 服务发现),显式设置超时和 ResponseHeaderTimeout client := commonHttp.Httpclient.Clone() client.SetTimeout(30 * time.Minute) if tr, ok := client.Transport.(*http.Transport); ok { tr.ResponseHeaderTimeout = 30 * time.Minute } res = new(ModelCallRes) err = commonHttp.Post(ctx, "model-gateway/model/call/modelCall", requestHeaders(ctx), res, &req) if err != nil { return nil, "", err } if g.IsEmpty(res.TaskId) || !g.IsEmpty(res.ErrorMsg) { return nil, "", fmt.Errorf("创建模型任务失败:%v", res.ErrorMsg) } return res, msgTopic, nil } // WaitModelCallResult 订阅 msgTopic 等待异步模型结果。 // 重订阅可拿回已发布结果:gmq NATS 侧 Retention=LimitsPolicy+MaxAge=7天(消息不随 ack 删除、保留 7 天), // 订阅用 DeliverPolicy=DeliverAllPolicy(新订阅从 stream 序头重放),崩溃后重订阅同一 msgTopic 即可恢复。 func WaitModelCallResult(ctx context.Context, msgTopic string) (responseParams *ModelCallRes, err error) { resultCh := make(chan *ModelCallRes, 1) errCh := make(chan error, 1) _, err = gmq.GetGmq(public.GmqMsgPluginsName).GmqSubscribe(ctx, &mq.NatsSubMessage{ SubMessage: types.SubMessage{ Topic: msgTopic, ConsumerName: fmt.Sprintf("model-call-result-%s", uuid.NewString()), AutoAck: false, AutoUnsubscribe: true, FetchCount: 1, HandleFunc: func(ctx context.Context, msg any) error { r := new(ModelCallRes) if err := gconv.Struct(msg, r); err != nil { errCh <- err return nil } if g.IsEmpty(r.TaskId) || !g.IsEmpty(r.ErrorMsg) { errCh <- fmt.Errorf("创建模型任务失败:%v", r.ErrorMsg) return nil } resultCh <- r return nil }, }, Durable: true, }) if err != nil { return } select { case responseParams = <-resultCh: case err = <-errCh: return nil, err case <-ctx.Done(): return nil, ctx.Err() } return } // ModelCallResult 调模型网关生成一段内容并等待结果(内部复用 SubmitModelCall + WaitModelCallResult)。 // 同步模型提交即返回;异步模型为"提交+等待"一体。 func ModelCallResult(ctx context.Context, modelId int64, responseType model.ResponseType, sessionId string, requestParams map[string]any, businessParams map[string]any) (responseParams *ModelCallRes, err error) { res, msgTopic, err := SubmitModelCall(ctx, modelId, responseType, sessionId, requestParams, businessParams) if err != nil { return nil, err } if responseType != nil && *responseType == *model.ResponseTypeAsync.Code() { return WaitModelCallResult(ctx, msgTopic) } return res, nil }