Files
ai-agent/gateway/model.go
T
19904408334 f0f8724bd9 fix: 修复并行分支续跑输出丢失及模型空指针
合并续跑时各分支独立的 ConfigMap 副本,避免汇合节点输出丢失;并对模型配置缺失和空 responseType 增加保护,防止空指针崩溃。
2026-08-24 16:04:45 +08:00

212 lines
8.5 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 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 entityid 在顶层),
// 与 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
}
// ModelCallResult 调模型网关生成一段内容并等待结果。
// 视频等异步模型为"提交+等待"一体:内部订阅 GMQ 直到结果返回。
func ModelCallResult(ctx context.Context, modelId int64, responseType model.ResponseType, sessionId string, requestParams map[string]any, businessParams map[string]any) (responseParams *ModelCallRes, err error) {
// 异步模型必须绑定消息主题接收结果:自动生成唯一主题,
// 带业务标识(bizName/modelId/sessionId)便于排查,每次调用唯一避免并发串结果
msgTopic := ""
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,
}
// 2. 克隆 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)
}
// 3. 订阅模型结果(异步模型)
if responseType != nil && *responseType == *model.ResponseTypeAsync.Code() {
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
}
return res, nil
}