Files
ai-agent/gateway/model.go
T
19904408334andClaude Opus 4.7 3620e1ea61 refactor(gateway): ModelCallResult 拆分为 SubmitModelCall + WaitModelCallResult
- SubmitModelCall: 提交段(异步模型生成唯一 msgTopic),返回 (res, msgTopic, err)
- WaitModelCallResult: 订阅段(重订阅可恢复结果,供 Task5 AsyncModelCallWithRecovery 复用)
- ModelCallResult 签名不变,内部复用二者;订阅参数与拆分前一致

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-25 16:04:13 +08:00

226 lines
9.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
}
// 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
}