212 lines
8.5 KiB
Go
212 lines
8.5 KiB
Go
// 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
|
||
}
|
||
|
||
// 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
|
||
}
|