From 37d34619839bfa791c4e45394e2fb3b612764ee0 Mon Sep 17 00:00:00 2001 From: WangLiZhao <1838393649@qq.com> Date: Tue, 12 May 2026 13:45:08 +0800 Subject: [PATCH] gatway --- README.md | 35 +- config.yml | 29 +- consts/public/table_name.go | 8 +- controller/base.go | 23 -- controller/model_controller.go | 46 ++- controller/model_type_controller.go | 68 ---- controller/stat_controller.go | 2 - controller/task_controller.go | 6 - dao/model_dao.go | 114 +++++- dao/model_type_dao.go | 74 ---- dao/op_log_dao.go | 2 +- dao/stat_dao.go | 3 +- dao/task_dao_bg.go | 44 ++- go.mod | 15 +- go.sum | 185 +++++++++ main.go | 53 ++- model/dto/model_dto.go | 106 +++-- model/dto/model_type_dto.go | 74 ---- model/dto/stat_dto.go | 11 +- model/dto/task_dto.go | 22 +- model/entity/asynch_model.go | 63 ++- model/entity/asynch_model_stat.go | 16 - model/entity/asynch_task.go | 98 ++--- .../{asynch_op_log.go => logs_model_op.go} | 8 +- model/entity/logs_model_stat.go | 38 ++ service/callback.go | 135 ++++--- service/file_detect.go | 16 +- service/headers.go | 1 - service/model_invoker.go | 264 ++++++++++++- service/model_service.go | 322 ++++++++++----- service/model_type_service.go | 217 ----------- service/model_types_util.go | 52 --- service/stat_service.go | 11 +- service/storage_oss.go | 1 - service/task_service.go | 86 +++- service/utils.go | 113 ++++++ service/worker.go | 107 ++++- update.sql | 366 ++++++++---------- 38 files changed, 1721 insertions(+), 1113 deletions(-) delete mode 100644 controller/model_type_controller.go delete mode 100644 dao/model_type_dao.go delete mode 100644 model/dto/model_type_dto.go delete mode 100644 model/entity/asynch_model_stat.go rename model/entity/{asynch_op_log.go => logs_model_op.go} (92%) create mode 100644 model/entity/logs_model_stat.go delete mode 100644 service/model_type_service.go delete mode 100644 service/model_types_util.go create mode 100644 service/utils.go diff --git a/README.md b/README.md index 357ddce..1ec443c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# model-asynch(模型异步中间件) +# model-asynch(模型异步中间件)[2026.5.12前,暂时弃置] 一个独立的异步中间件服务:按模型配置路由调用不同模型服务,统一生成 `task_id`,后台异步执行,结果上传 OSS,并提供查询/批量领取/自动重试/自动清理能力,便于业务方“拿走结果并转移”。 @@ -69,6 +69,16 @@ 参数说明: - `modelName`:模型名称(唯一标识/路由键) - `modelsType`:模型类型ID列表(逗号分隔),示例:`1,2,3`(关联 `asynch_models_type.type_id`) + +### 模型类型同步 +- `POST /model/type/createModelType` 创建成功后,会同步 `POST` 到 `prompts-core` 的 `/prompt/createPrompt` +- 同步字段映射: + - `typeId` -> `modelTypeId` + - `type` -> `modelType` + - `promptInfo` -> `promptInfo` + - `responseJsonSchema` -> `responseJsonSchema` + - `version` -> `version` +- 若 `prompts-core` 同步失败,`model-gateway` 会回滚本地新建的模型类型,避免两边数据不一致 - `form`:动态表单配置(JSON数组),用于前端按模型渲染参数表单(字段示例:field/label/type/required) - `baseUrl`:模型服务地址(Base URL) - `route`:模型服务路由(拼接到 baseUrl 后) @@ -94,7 +104,7 @@ > > `callbackUrl` 用于任务成功后的回调通知:当任务 `state=2` 成功时,中间件会发起一次 GET 请求: > - 实际回调地址:`callbackUrl/{bizName}` -> - query 参数:`task_id/state/oss_file/file_type` +> - query 参数:`task_id/state/oss_file/file_type/text(可选,最多2000字符)` ### 第三步:同步任务进度(推荐批量) 业务方通过轮询/定时任务同步进度: @@ -107,9 +117,28 @@ ### 后台执行(由上层定时任务控制) 本项目不再在服务进程内常驻轮询 worker/cleaner,而是提供两个接口供上层定时任务触发: -- `POST /task/runWork`:执行一次 Worker(抢占并处理一批排队任务) +- `POST /task/runWork`:执行一次 Worker(抢占并处理一批排队任务;适合处理 createTask 立即执行时未处理到的任务和积压队列) - `POST /task/cleanWork`:执行一次 Cleaner(清理过期任务、失败重试、超时任务失败等) +创建任务执行策略: +- `POST /task/createTask` 成功入库后,会立即异步尝试执行当前任务。 +- 若当前模型并发已满,或当前任务未成功抢占,则会按 `asynch.worker.intervalSeconds` 对当前任务做轻量级定向轮询;只要任务仍为 `state=0` 就继续尝试,一旦进入 `state=1/2/3/4` 就立即停止,不会一直轮询。 +- 若任务执行成功且配置了 `callbackUrl + bizName`,会在成功落库后异步触发回调钩子。 + +本地调试(可选): +可在 `config.yml` 中开启自动执行,避免手工频繁调用接口: +```yml +asynch: + worker: + enabled: true + intervalSeconds: 5 + batchSize: 10 + goroutines: 1 + cleaner: + enabled: true + intervalSeconds: 30 +``` + ### 动态并发/队列调参(接口请求控制) 为支持根据最近一段时间的耗时与吞吐对 `max_concurrency/queue_limit` 做动态调整,本项目提供接口供上层定时任务触发(建议每小时一次): - `POST /model/autoTune` diff --git a/config.yml b/config.yml index 5648a2e..dc9f898 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,6 @@ server: address: ":8001" - name: "model-asynch" + name: "model-gateway" workerId: 1 # 雪花算法worker ID(用于 common/db/gfdb) # PostgreSQL(GoFrame driver pgsql) @@ -11,7 +11,7 @@ database: port: "15432" user: "postgres" pass: "Bjang09@686^*^" - name: "model-asynch" + name: "model-gateway" prefix: "" # (可选)表名前缀 role: "master" # (可选)数据库主从角色(master/slave),默认为master。如果不使用应用主从机制请不配置或留空即可。 debug: true # (可选)开启调试模式 @@ -29,11 +29,30 @@ database: redis: default: - address: 192.168.3.30:6379 + address: 116.204.74.41:6379 db: 0 consul: - address: 192.168.3.30:8500 + address: 116.204.74.41:8500 jaeger: - addr: 192.168.3.30:4318 + addr: 116.204.74.41:4318 + +# 本地调试用:可选自动执行 worker/cleaner(默认关闭) +asynch: + worker: + enabled: false + intervalSeconds: 5 + batchSize: 10 + goroutines: 1 + cleaner: + enabled: false + intervalSeconds: 30 + +modelType: + types: + 1: "推理模型" + 2: "图片模型" + 3: "音频模型" + 4: "向量化模型" + 5: "全模态模型" diff --git a/consts/public/table_name.go b/consts/public/table_name.go index 433fcbf..4f59f4e 100644 --- a/consts/public/table_name.go +++ b/consts/public/table_name.go @@ -1,9 +1,9 @@ package public const ( - TableNameModel = "asynch_models" // 异步模型表 + TableNameModel = "asynch_models" // 模型表 TableNameModelType = "asynch_models_type" // 模型类型表 - TableNameTask = "asynch_task" // 异步任务表 - TableNameOpLog = "asynch_op_log" // 异步操作日志表 - TableNameStat = "asynch_model_stat" // 按天统计表(请求次数) + TableNameTask = "asynch_task" // 任务表 + TableNameOpLog = "logs_model_op" // 操作日志表 + TableNameStat = "logs_model_stat" // 按天统计表(请求次数) ) diff --git a/controller/base.go b/controller/base.go index 08f113d..b0b429f 100644 --- a/controller/base.go +++ b/controller/base.go @@ -1,24 +1 @@ package controller - -import ( - "context" - - "gitea.com/red-future/common/beans" -) - -// ensureUser 用于本地/无网关环境下的兜底用户信息,避免 gfdb Hook 因缺少用户上下文而报错。 -// 生产环境建议由网关透传 X-User-Info 或鉴权中间件注入 ctx.Value("user")。 -func ensureUser(ctx context.Context) context.Context { - if ctx == nil { - ctx = context.Background() - } - if ctx.Value("user") != nil { - return ctx - } - u := &beans.User{ - UserName: "admin", - TenantId: 1, - } - return context.WithValue(ctx, "user", u) -} - diff --git a/controller/model_controller.go b/controller/model_controller.go index 331f7fb..af6653f 100644 --- a/controller/model_controller.go +++ b/controller/model_controller.go @@ -4,6 +4,7 @@ import ( "context" "model-asynch/model/dto" + "model-asynch/model/entity" "model-asynch/service" "gitea.com/red-future/common/beans" @@ -16,51 +17,42 @@ var Model = new(model) // CreateModel 添加配置 func (c *model) CreateModel(ctx context.Context, req *dto.CreateModelReq) (res *dto.CreateModelRes, err error) { - ctx = ensureUser(ctx) return service.Model.Create(ctx, req) } // UpdateModel 更改配置 func (c *model) UpdateModel(ctx context.Context, req *dto.UpdateModelReq) (res *beans.ResponseEmpty, err error) { - ctx = ensureUser(ctx) err = service.Model.Update(ctx, req) return } // DeleteModel 删除配置 func (c *model) DeleteModel(ctx context.Context, req *dto.DeleteModelReq) (res *beans.ResponseEmpty, err error) { - ctx = ensureUser(ctx) err = service.Model.Delete(ctx, req.ID) return } // GetModel 获取配置详情(按 modelName) func (c *model) GetModel(ctx context.Context, req *dto.GetModelReq) (res *dto.GetModelRes, err error) { - ctx = ensureUser(ctx) - m, err := service.Model.Get(ctx, req.ID) + model, err := service.Model.Get(ctx, req.ID) if err != nil { return nil, err } - return &dto.GetModelRes{Model: m}, nil + return &dto.GetModelRes{Model: model}, nil } // ListModel 配置列表 func (c *model) ListModel(ctx context.Context, req *dto.ListModelReq) (res *dto.ListModelRes, err error) { - ctx = ensureUser(ctx) pageNum, pageSize := 1, 10 //默认分页参数 - if req != nil && req.Page != nil { - if req.Page.PageNum > 0 { - pageNum = int(req.Page.PageNum) - } - if req.Page.PageSize > 0 { - pageSize = int(req.Page.PageSize) - } - } - modelName := "" if req != nil { - modelName = req.ModelName + if req.PageNum > 0 { + pageNum = req.PageNum + } + if req.PageSize > 0 { + pageSize = req.PageSize + } } - list, total, err := service.Model.List(ctx, pageNum, pageSize, modelName) + list, total, err := service.Model.List(ctx, pageNum, pageSize, req.ModelName, req.ModelType) if err != nil { return nil, err } @@ -72,7 +64,6 @@ func (c *model) ListModel(ctx context.Context, req *dto.ListModelReq) (res *dto. // AutoTune 动态调参(由上层定时任务每小时触发一次) func (c *model) AutoTune(ctx context.Context, req *dto.AutoTuneReq) (res *dto.AutoTuneRes, err error) { - ctx = ensureUser(ctx) windowSeconds := 3600 if req != nil && req.WindowSeconds > 0 { windowSeconds = req.WindowSeconds @@ -83,3 +74,20 @@ func (c *model) AutoTune(ctx context.Context, req *dto.AutoTuneReq) (res *dto.Au } return &dto.AutoTuneRes{List: list}, nil } + +func (c *model) ListType(ctx context.Context, req *dto.ListTypeReq) (res dto.TypeItem, err error) { + modelType := service.GetModelTypesFromConfig(ctx) + res.Type = modelType + return res, nil +} + +// UpdateChatModel 更新是否为聊天模型 +func (c *model) UpdateChatModel(ctx context.Context, req *dto.UpdateChatModelReq) (res *beans.ResponseEmpty, err error) { + err = service.Model.UpdateChatModel(ctx, req) + return +} + +// GetIsChatModel 获取是否为聊天模型 +func (c *model) GetIsChatModel(ctx context.Context, req *dto.GetIsChatModelReq) (res *entity.AsynchModel, err error) { + return service.Model.GetIsChatModel(ctx) +} diff --git a/controller/model_type_controller.go b/controller/model_type_controller.go deleted file mode 100644 index 450ef08..0000000 --- a/controller/model_type_controller.go +++ /dev/null @@ -1,68 +0,0 @@ -package controller - -import ( - "context" - - "model-asynch/model/dto" - "model-asynch/service" - - "gitea.com/red-future/common/beans" -) - -type modelType struct{} - -// ModelType 模型类型控制器 -var ModelType = new(modelType) - -func (c *modelType) CreateModelType(ctx context.Context, req *dto.CreateModelTypeReq) (res *dto.CreateModelTypeRes, err error) { - ctx = ensureUser(ctx) - return service.ModelType.Create(ctx, req) -} - -func (c *modelType) UpdateModelType(ctx context.Context, req *dto.UpdateModelTypeReq) (res *beans.ResponseEmpty, err error) { - ctx = ensureUser(ctx) - err = service.ModelType.Update(ctx, req) - return -} - -func (c *modelType) DeleteModelType(ctx context.Context, req *dto.DeleteModelTypeReq) (res *beans.ResponseEmpty, err error) { - ctx = ensureUser(ctx) - err = service.ModelType.Delete(ctx, req.ID) - return -} - -func (c *modelType) GetModelType(ctx context.Context, req *dto.GetModelTypeReq) (res *dto.GetModelTypeRes, err error) { - ctx = ensureUser(ctx) - t, err := service.ModelType.Get(ctx, req.ID) - if err != nil { - return nil, err - } - return &dto.GetModelTypeRes{Type: t}, nil -} - -func (c *modelType) ListModelType(ctx context.Context, req *dto.ListModelTypeReq) (res *dto.ListModelTypeRes, err error) { - ctx = ensureUser(ctx) - pageNum, pageSize := 1, 10 - if req != nil && req.Page != nil { - if req.Page.PageNum > 0 { - pageNum = int(req.Page.PageNum) - } - if req.Page.PageSize > 0 { - pageSize = int(req.Page.PageSize) - } - } - typeName := "" - if req != nil { - typeName = req.TypeName - } - list, total, err := service.ModelType.List(ctx, pageNum, pageSize, typeName) - if err != nil { - return nil, err - } - return &dto.ListModelTypeRes{List: list, Total: total}, nil -} - -func (c *modelType) ListModelTypeWithModels(ctx context.Context, req *dto.ListModelTypeWithModelsReq) (res []dto.ModelTypeWithModelsItem, err error) { - ctx = ensureUser(ctx) - return service.ModelType.ListWithModels(ctx, req) -} diff --git a/controller/stat_controller.go b/controller/stat_controller.go index e0938d9..fd77cb5 100644 --- a/controller/stat_controller.go +++ b/controller/stat_controller.go @@ -14,7 +14,5 @@ var Stat = new(stat) // ListModelStat 统计列表 func (c *stat) ListModelStat(ctx context.Context, req *dto.ListModelStatReq) (res *dto.ListModelStatRes, err error) { - ctx = ensureUser(ctx) return service.Stat.List(ctx, req) } - diff --git a/controller/task_controller.go b/controller/task_controller.go index e5fd8a4..64e0801 100644 --- a/controller/task_controller.go +++ b/controller/task_controller.go @@ -14,31 +14,26 @@ var Task = new(task) // CreateTask 根据 modelName 创建异步任务,返回 taskId func (c *task) CreateTask(ctx context.Context, req *dto.CreateTaskReq) (res *dto.CreateTaskRes, err error) { - ctx = ensureUser(ctx) return service.Task.Create(ctx, req) } // GetTaskResult 获取任务结果(只返回 oss 地址 + state) func (c *task) GetTaskResult(ctx context.Context, req *dto.GetTaskResultReq) (res *dto.GetTaskResultRes, err error) { - ctx = ensureUser(ctx) return service.Task.GetResult(ctx, req.TaskID) } // GetTaskBatch 批量查询任务(成功任务标记为已下载) func (c *task) GetTaskBatch(ctx context.Context, req *dto.GetTaskBatchReq) (res *dto.GetTaskBatchRes, err error) { - ctx = ensureUser(ctx) return service.Task.GetBatch(ctx, req) } // ListTask 任务列表分页查询 func (c *task) ListTask(ctx context.Context, req *dto.ListTaskReq) (res *dto.ListTaskRes, err error) { - ctx = ensureUser(ctx) return service.Task.List(ctx, req) } // RunWork 手动触发一次 worker(由上层定时任务调用) func (c *task) RunWork(ctx context.Context, req *dto.RunWorkReq) (res *dto.RunWorkRes, err error) { - ctx = ensureUser(ctx) batchSize, goroutines := 10, 1 if req != nil { if req.BatchSize > 0 { @@ -57,7 +52,6 @@ func (c *task) RunWork(ctx context.Context, req *dto.RunWorkReq) (res *dto.RunWo // CleanWork 手动触发一次 cleaner(由上层定时任务调用) func (c *task) CleanWork(ctx context.Context, req *dto.CleanWorkReq) (res *dto.CleanWorkRes, err error) { - ctx = ensureUser(ctx) service.Cleaner.RunOnce(ctx) return &dto.CleanWorkRes{Ok: true}, nil } diff --git a/dao/model_dao.go b/dao/model_dao.go index c293049..93f74ed 100644 --- a/dao/model_dao.go +++ b/dao/model_dao.go @@ -2,11 +2,14 @@ package dao import ( "context" + "fmt" "model-asynch/consts/public" + "model-asynch/model/dto" "model-asynch/model/entity" "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) @@ -22,12 +25,12 @@ func (d *modelDao) Insert(ctx context.Context, m *entity.AsynchModel) (id int64, return r.LastInsertId() } -func (d *modelDao) UpdateByID(ctx context.Context, id int64, data map[string]any) (rows int64, err error) { +func (d *modelDao) Update(ctx context.Context, m *dto.UpdateModelReq) (rows int64, err error) { // 触发 gfdb 的 updateHook 自动填充 updater,需要显式带 updater 字段 - data[entity.AsynchModelCol.Updater] = "" r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel). - Where(entity.AsynchModelCol.Id, id). - Data(data). + OmitEmpty(). + Where(entity.AsynchModelCol.Id, m.ID). + Data(m). Update() if err != nil { return 0, err @@ -35,7 +38,21 @@ func (d *modelDao) UpdateByID(ctx context.Context, id int64, data map[string]any return r.RowsAffected() } -func (d *modelDao) DeleteByID(ctx context.Context, id int64) (rows int64, err error) { +func (d *modelDao) UpdateByID(ctx context.Context, m *dto.UpdateModelReq) (rows int64, err error) { + // 专用于切换会话模型,只更新 is_chat_model 字段 + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel). + Where(entity.AsynchModelCol.Id, m.ID). + Data(g.Map{ + "is_chat_model": m.IsChatModel, + }). + Update() + if err != nil { + return 0, err + } + return r.RowsAffected() +} + +func (d *modelDao) DeleteByID(ctx context.Context, id string) (rows int64, err error) { r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel). Where(entity.AsynchModelCol.Id, id). Delete() @@ -59,7 +76,7 @@ func (d *modelDao) GetByModelName(ctx context.Context, modelName string) (m *ent return } -func (d *modelDao) GetByID(ctx context.Context, id int64) (m *entity.AsynchModel, err error) { +func (d *modelDao) Get(ctx context.Context, id int64) (m *entity.AsynchModel, err error) { r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel). Where(entity.AsynchModelCol.Id, id). One() @@ -73,11 +90,15 @@ func (d *modelDao) GetByID(ctx context.Context, id int64) (m *entity.AsynchModel return } -func (d *modelDao) List(ctx context.Context, pageNum, pageSize int, modelNameLike string) (list []*entity.AsynchModel, total int64, err error) { - model := gfdb.DB(ctx).Model(ctx, public.TableNameModel).Where("deleted_at IS NULL").OrderDesc(entity.AsynchModelCol.CreatedAt) +func (d *modelDao) List(ctx context.Context, pageNum, pageSize int, modelNameLike string, modelType int) (list []*entity.AsynchModel, total int64, err error) { + model := gfdb.DB(ctx).Model(ctx, public.TableNameModel). + OrderDesc(entity.AsynchModelCol.CreatedAt) if modelNameLike != "" { model = model.WhereLike(entity.AsynchModelCol.ModelName, "%"+modelNameLike+"%") } + if modelType != 0 { + model = model.Where(entity.AsynchModelCol.ModelsType, modelType) + } if pageNum > 0 && pageSize > 0 { model = model.Page(pageNum, pageSize) } @@ -90,10 +111,85 @@ func (d *modelDao) List(ctx context.Context, pageNum, pageSize int, modelNameLik return } +// ListByCreatorAndPlatform 普通用户:平台公共(tenant_id=0) + 自己创建的(creator=xxx) +func (d *modelDao) ListByCreatorAndPlatform(ctx context.Context, creator string, pageNum, pageSize int, modelNameLike string) (list []*entity.AsynchModel, total int64, err error) { + // 构建 Where 条件 + whereSQL := "deleted_at IS NULL AND (tenant_id = 1 OR creator = ?)" //1 代表超级管理员 + args := []any{creator} + + if modelNameLike != "" { + whereSQL += " AND model_name LIKE ?" + args = append(args, "%"+modelNameLike+"%") + } + + // 查总数 + countSQL := fmt.Sprintf("SELECT COUNT(1) FROM %s WHERE %s", public.TableNameModel, whereSQL) + countResult, err := gfdb.DB(ctx).GetAll(ctx, countSQL, args...) + if err != nil { + return nil, 0, err + } + if len(countResult) > 0 { + total = gconv.Int64(countResult[0]["count"]) + } + + // 查列表 + querySQL := fmt.Sprintf("SELECT * FROM %s WHERE %s ORDER BY created_at DESC", public.TableNameModel, whereSQL) + if pageNum > 0 && pageSize > 0 { + offset := (pageNum - 1) * pageSize + querySQL += fmt.Sprintf(" LIMIT %d OFFSET %d", pageSize, offset) + } + + r, err := gfdb.DB(ctx).GetAll(ctx, querySQL, args...) + if err != nil { + return nil, 0, err + } + + err = r.Structs(&list) + return +} + +func (d *modelDao) GetByCreatorAndPlatform(ctx context.Context, creator string, modelNameLike string, modelType int) (list []*entity.AsynchModel, err error) { + whereSQL := "deleted_at IS NULL AND (tenant_id = 1 OR creator = ?)" + args := []any{creator} + + if modelNameLike != "" { + whereSQL += " AND model_name LIKE ?" + args = append(args, "%"+modelNameLike+"%") + } + if modelType != 0 { + whereSQL += " AND models_type = ?" + args = append(args, modelType) + } + + querySQL := fmt.Sprintf("SELECT * FROM %s WHERE %s ORDER BY created_at DESC", public.TableNameModel, whereSQL) + + r, err := gfdb.DB(ctx).GetAll(ctx, querySQL, args...) + if err != nil { + return nil, err + } + + err = r.Structs(&list) + return +} + +func (d *modelDao) GetByIsChatModel(ctx context.Context, userName string) (m *entity.AsynchModel, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel). + Where(entity.AsynchModelCol.IsChatModel, 1). + Where(entity.AsynchModelCol.Creator, userName). + One() + if err != nil { + return nil, err + } + if r.IsEmpty() { + return nil, nil + } + err = r.Struct(&m) + return +} + // ListAll 用于分组展示:查询全部模型(不按类型过滤,类型拆分在 service 层处理) func (d *modelDao) ListAll(ctx context.Context) (list []*entity.AsynchModel, err error) { r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel). - Where("deleted_at IS NULL"). OrderDesc(entity.AsynchModelCol.CreatedAt). All() if err != nil { diff --git a/dao/model_type_dao.go b/dao/model_type_dao.go deleted file mode 100644 index a43c5a0..0000000 --- a/dao/model_type_dao.go +++ /dev/null @@ -1,74 +0,0 @@ -package dao - -import ( - "context" - - "model-asynch/consts/public" - "model-asynch/model/entity" - - "gitea.com/red-future/common/db/gfdb" - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/util/gconv" -) - -type modelTypeDao struct{} - -var ModelType = &modelTypeDao{} - -func (d *modelTypeDao) Insert(ctx context.Context, t *entity.AsynchModelType) (id int64, err error) { - r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModelType).Data(t).Insert() - if err != nil { - return 0, err - } - return r.LastInsertId() -} - -func (d *modelTypeDao) UpdateByID(ctx context.Context, id int64, data gdb.Map) (rows int64, err error) { - // 触发 gfdb 的 updateHook 自动填充 updater,需要显式带 updater 字段 - data[entity.AsynchModelTypeCol.Updater] = "" - r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModelType).Where(entity.AsynchModelTypeCol.Id, id).Data(data).Update() - if err != nil { - return 0, err - } - n, _ := r.RowsAffected() - return n, nil -} - -func (d *modelTypeDao) DeleteByID(ctx context.Context, id int64) (rows int64, err error) { - r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModelType).Where(entity.AsynchModelTypeCol.Id, id).Delete() - if err != nil { - return 0, err - } - n, _ := r.RowsAffected() - return n, nil -} - -func (d *modelTypeDao) GetByID(ctx context.Context, id int64) (*entity.AsynchModelType, error) { - r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModelType).Where(entity.AsynchModelTypeCol.Id, id).One() - if err != nil { - return nil, err - } - if r.IsEmpty() { - return nil, nil - } - var t *entity.AsynchModelType - _ = r.Struct(&t) - return t, nil -} - -func (d *modelTypeDao) List(ctx context.Context, pageNum, pageSize int, typeNameLike string) (list []*entity.AsynchModelType, total int64, err error) { - m := gfdb.DB(ctx).Model(ctx, public.TableNameModelType).Where("deleted_at IS NULL").OrderAsc(entity.AsynchModelTypeCol.TypeID) - if typeNameLike != "" { - m = m.WhereLike(entity.AsynchModelTypeCol.TypeName, "%"+typeNameLike+"%") - } - if pageNum > 0 && pageSize > 0 { - m = m.Page(pageNum, pageSize) - } - r, totalInt, err := m.AllAndCount(false) - if err != nil { - return nil, 0, err - } - total = gconv.Int64(totalInt) - err = r.Structs(&list) - return -} diff --git a/dao/op_log_dao.go b/dao/op_log_dao.go index 3293cd7..2d009de 100644 --- a/dao/op_log_dao.go +++ b/dao/op_log_dao.go @@ -13,7 +13,7 @@ type opLogDao struct{} var OpLog = &opLogDao{} -func (d *opLogDao) Insert(ctx context.Context, log *entity.AsynchOpLog) (id int64, err error) { +func (d *opLogDao) Insert(ctx context.Context, log *entity.LogsModelOp) (id int64, err error) { r, err := gfdb.DB(ctx).Model(ctx, public.TableNameOpLog).Data(log).Insert() if err != nil { return 0, err diff --git a/dao/stat_dao.go b/dao/stat_dao.go index edce22f..e6e89e2 100644 --- a/dao/stat_dao.go +++ b/dao/stat_dao.go @@ -29,7 +29,7 @@ DO UPDATE SET request_count = %s.request_count + 1, updated_at = NOW()`, return err } -func (d *statDao) List(ctx context.Context, pageNum, pageSize int, startDay, endDay string, tenantId *int64, creator, modelName string) (list []*entity.AsynchModelStat, total int64, err error) { +func (d *statDao) List(ctx context.Context, pageNum, pageSize int, startDay, endDay string, tenantId *int64, creator, modelName string) (list []*entity.LogsModelStat, total int64, err error) { m := gfdb.DB(ctx).Model(ctx, public.TableNameStat).Where("1=1") if startDay != "" { m = m.Where("day >= ?", startDay) @@ -58,4 +58,3 @@ func (d *statDao) List(ctx context.Context, pageNum, pageSize int, startDay, end err = r.Structs(&list) return } - diff --git a/dao/task_dao_bg.go b/dao/task_dao_bg.go index 5c49cba..3bbc9f5 100644 --- a/dao/task_dao_bg.go +++ b/dao/task_dao_bg.go @@ -20,7 +20,7 @@ func (d *taskDao) ClaimPendingGlobal(ctx context.Context, batchSize int) (tasks } err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { sql := fmt.Sprintf( - `SELECT id, tenant_id, creator, model_name, task_id, model_key, input_ref, request_payload, phase, tmp_file + `SELECT id, tenant_id, creator, model_name, task_id, biz_name, callback_url, model_key, input_ref, request_payload, phase, tmp_file FROM %s WHERE deleted_at IS NULL AND state = 0 ORDER BY enqueue_at ASC @@ -55,13 +55,51 @@ func (d *taskDao) ClaimPendingGlobal(ctx context.Context, batchSize int) (tasks return } -func (d *taskDao) UpdateSuccessGlobal(ctx context.Context, id int64, ossFile, fileType string, fileSize int64, expireAt *gtime.Time) error { +// ClaimPendingByTaskIDGlobal 按 task_id 定向抢占单个 pending 任务(不加 tenant 过滤) +// 用于 createTask 创建成功后立即异步尝试执行当前任务,避免只依赖后续 runWork 扫描队列。 +func (d *taskDao) ClaimPendingByTaskIDGlobal(ctx context.Context, taskID string) (task *entity.AsynchTask, err error) { + if taskID == "" { + return nil, nil + } + err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + sql := fmt.Sprintf( + `SELECT id, tenant_id, creator, model_name, task_id, biz_name, callback_url, model_key, input_ref, request_payload, phase, tmp_file + FROM %s + WHERE deleted_at IS NULL AND state = 0 AND task_id = ? + LIMIT 1 + FOR UPDATE SKIP LOCKED`, + public.TableNameTask, + ) + r, err := tx.GetOne(sql, taskID) + if err != nil { + return err + } + if r.IsEmpty() { + task = nil + return nil + } + if err := r.Struct(&task); err != nil { + return err + } + now := time.Now() + _, err = tx.Exec( + fmt.Sprintf(`UPDATE %s SET state=1, started_at=?, updated_at=? WHERE id=?`, public.TableNameTask), + now, now, task.Id, + ) + return err + }) + return +} + +func (d *taskDao) UpdateSuccessGlobal(ctx context.Context, id int64, ossFile, fileType, textResult string, fileSize int64, expireAt *gtime.Time, expendTokens int) error { now := gtime.Now() _, err := gfdb.DB(ctx).Exec(ctx, fmt.Sprintf(`UPDATE %s SET state=2, oss_file=?, file_type=?, + text_result=?, + expend_tokens=?, file_size=?, error_msg='', finished_at=?, @@ -71,7 +109,7 @@ SET state=2, tmp_file='', updated_at=? WHERE id=?`, public.TableNameTask), - ossFile, fileType, fileSize, now, now, now, id, + ossFile, fileType, textResult, expendTokens, fileSize, now, now, now, id, ) return err } diff --git a/go.mod b/go.mod index aa1ac10..ff0a09c 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,21 @@ module model-asynch go 1.26.0 require ( - gitea.com/red-future/common v0.0.12 + gitea.com/red-future/common v0.0.19 // indirect github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 github.com/gogf/gf/v2 v2.10.0 github.com/google/uuid v1.6.0 + github.com/tidwall/gjson v1.14.2 ) -// replace gitea.com/red-future/common v0.0.12 => ../common +require ( + github.com/r3labs/diff/v2 v2.15.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + google.golang.org/appengine v1.6.7 // indirect +) require ( github.com/BurntSushi/toml v1.5.0 // indirect @@ -29,7 +36,6 @@ require ( github.com/go-ego/gse v1.0.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/gogf/gf v1.16.9 // indirect github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 // indirect github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -39,7 +45,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/flatbuffers v1.12.1 // indirect - github.com/gorilla/websocket v1.5.3 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grokify/html-strip-tags-go v0.1.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/consul/api v1.26.1 // indirect @@ -65,6 +71,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/redis/go-redis/v9 v9.12.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/tidwall/sjson v1.2.5 github.com/tiger1103/gfast-token v1.0.10 // indirect github.com/vcaesar/cedar v0.30.0 // indirect go.mongodb.org/mongo-driver/v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index c1b9bcc..daabfa3 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,20 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= gitea.com/red-future/common v0.0.12 h1:whaCAiH33orl0P+oDpxzC4VoNluHKNYKGZ+FcUWw85Q= gitea.com/red-future/common v0.0.12/go.mod h1:3a7cwZNvgpKw5FzE8x5MZImd7NBePGXRGFSMjt90158= +gitea.com/red-future/common v0.0.19 h1:9/WrfCFUCeFUYwuhBYF+JOQi5F5xuOy+gVnf2ZvHZu4= +gitea.com/red-future/common v0.0.19/go.mod h1:6/nqIucVzmjOyqDTIq71feYBXXFNBy0rFwzaQ0/Ueoo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= @@ -25,6 +31,7 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -33,10 +40,15 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 h1:LdXxtjzvZYhhUaonAaAKArG3pyC67kGL3YY+6hGG8G4= github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -44,15 +56,23 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= +github.com/dgraph-io/badger/v4 v4.9.1 h1:DocZXZkg5JJHJPtUErA0ibyHxOVUDVoXLSCV6t8NC8w= +github.com/dgraph-io/badger/v4 v4.9.1/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= +github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= +github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU= +github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -65,15 +85,21 @@ github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/go-ego/gse v1.0.2 h1:+27lYFPhQEhA9igtdOsJPRKYL/k3TwYsxBF5jr6KFv4= github.com/go-ego/gse v1.0.2/go.mod h1:Fy35G+q7VV7Et1zIKO8o/sW1kkugV3znXap/lF/11zc= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -81,6 +107,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogf/gf v1.16.9 h1:Q803UmmRo59+Ws08sMVFOcd8oNpkSWL9vS33hlo/Cyk= github.com/gogf/gf v1.16.9/go.mod h1:8Q/kw05nlVRp+4vv7XASBsMe9L1tsVKiGoeP2AHnlkk= github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 h1:39+jbTenm7KBj4hO2C8ANAxVHpX/7OuRDs1VcGC9ylA= @@ -89,8 +119,12 @@ github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 h1:N/F9CuDdUZLoM1nVRqrDE/33pDZ github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0/go.mod h1:x6uoJGfZOtirIRQls8xUlYzC6f7T/eULPUa9er368X0= github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 h1:eUqwJ/qNH8lJ6yssiqskazgp1ACQuNU6zXlLOZVuXTQ= github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5/go.mod h1:sjQyMry9+0POYZCA6lHXBxO77WoNKkruJpRB4xKqk5k= +github.com/gogf/gf/contrib/registry/consul/v2 v2.10.0 h1:NF3xO+/bJ0Jve+BBVLX/f80aOmAtIVQPdoNk1IvaPs0= +github.com/gogf/gf/contrib/registry/consul/v2 v2.10.0/go.mod h1:tF3JjImw346aLSVNRpmYyMukLQGivBOpuAU39TvF6i0= github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 h1:tHUEZYB5GTqEYYVDYnlGobf1xISARKDE4KHVlgjwTec= github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5/go.mod h1:cfzTn2HS9RDX8f5pUVkbGxUWcSosouqfNQ1G6cY0V88= +github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.10.0 h1:9uQ29GvNTWBngPnltV+2C+FbofHbmcaiEdLgqhcHgu0= +github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.10.0/go.mod h1:wRPkw0CqBUe3DPHH2IA5en+Il7nPQpFhHDPqvuDNdjU= github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs= github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -104,6 +138,8 @@ github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -114,7 +150,10 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -123,14 +162,19 @@ github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUz github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -141,15 +185,22 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/consul/api v1.26.1 h1:5oSXOO5fboPZeW5SN+TdGFP/BILDgBm19OrPZ/pICIM= github.com/hashicorp/consul/api v1.26.1/go.mod h1:B4sQTeaSO16NtynqrAdwOlahJ7IUDZM9cj2420xYL8A= +github.com/hashicorp/consul/api v1.34.2 h1:B5jqSSKwWyY8U8WiGS5vmPEPkkF0bAvrECykdZkDR80= +github.com/hashicorp/consul/api v1.34.2/go.mod h1:+gAdHQa2zvgYX3ZfcgITtnYCSj6AgS/cgotvCKaE+b8= github.com/hashicorp/consul/sdk v0.15.0 h1:2qK9nDrr4tiJKRoxPGhm6B7xJjLVIQqkjiab2M4aKjU= github.com/hashicorp/consul/sdk v0.15.0/go.mod h1:r/OmRRPbHOe0yxNahLw7G9x5WG17E1BIECMtCjcPSNo= +github.com/hashicorp/consul/sdk v0.18.1 h1:RDTeBvAeOveI2xI86sV+8WkaN7OkP4zz+cG3fOobDCM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -158,9 +209,13 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= +github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -174,6 +229,7 @@ github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR3 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-sockaddr v1.0.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2wMvfPJU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -188,16 +244,26 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= +github.com/hashicorp/memberlist v0.5.2 h1:rJoNPWZ0juJBgqn48gjy59K5H4rNgvUoM1kUD7bXiuI= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc= +github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -208,6 +274,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -225,9 +293,13 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -244,13 +316,22 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U= +github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= +github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= +github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -266,29 +347,42 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg= +github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc= github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg= github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k= +github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -297,9 +391,26 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tiger1103/gfast-token v1.0.10 h1:fNiBE/Dq5iTHvTGlCx3DmXa2o4hr0NtumFpffZ39k6s= github.com/tiger1103/gfast-token v1.0.10/go.mod h1:a/21mxmj7zFeNvjhZSC0XpEAFHfb1aT2k6DXnufFU1s= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -307,32 +418,58 @@ github.com/vcaesar/cedar v0.30.0 h1:9fSDpM7FTjjUdPiBUUa0MWYMRGSEcqgFXvppZcZ4d7Y= github.com/vcaesar/cedar v0.30.0/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik= github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4= github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo= go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= +go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8= +go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.0.0/go.mod h1:AjRVh9A5/5DE7S+mZtTR6t8vpKKryam+0lREnfmS4cg= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/trace v1.0.0/go.mod h1:PXTWqayeFUlJV1YDNhsJYB184+IvAH814St6o6ajzIs= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -340,43 +477,58 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -389,29 +541,45 @@ golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -421,23 +589,33 @@ golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:U8orV30l6KpDsi9dxU0CoJZGbjS8EEpw+6ba+XwGPQA= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -445,6 +623,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -454,8 +634,12 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -465,6 +649,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go index 60a7772..d8873b3 100644 --- a/main.go +++ b/main.go @@ -5,10 +5,11 @@ import ( "os" "os/signal" "syscall" + "time" "model-asynch/controller" + "model-asynch/service" - _ "gitea.com/red-future/common/config" "gitea.com/red-future/common/http" "gitea.com/red-future/common/jaeger" _ "gitea.com/red-future/common/swagger" @@ -25,11 +26,13 @@ func main() { // 注册路由 http.RouteRegister([]interface{}{ controller.Model, - controller.ModelType, controller.Task, controller.Stat, }) + // 本地调试:可选自动触发 worker/cleaner(由配置文件控制) + startAutoRunner(ctx) + // 监听退出信号,确保 Ctrl+C 能完整退出(停止 worker/cleaner 并关闭 http server) quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) @@ -40,3 +43,49 @@ func main() { // 关闭 http server(RouteRegister 内部是 go Httpserver.Run() 启动的) _ = http.Httpserver.Shutdown() } + +func startAutoRunner(ctx context.Context) { + // worker + if g.Cfg().MustGet(ctx, "asynch.worker.enabled").Bool() { + interval := g.Cfg().MustGet(ctx, "asynch.worker.intervalSeconds").Int() + if interval <= 0 { + interval = 5 + } + batchSize := g.Cfg().MustGet(ctx, "asynch.worker.batchSize").Int() + goroutines := g.Cfg().MustGet(ctx, "asynch.worker.goroutines").Int() + ticker := time.NewTicker(time.Duration(interval) * time.Second) + go func() { + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if _, err := service.AsyncWorker.RunOnce(ctx, batchSize, goroutines); err != nil { + g.Log().Warningf(ctx, "[auto-worker] run once failed: %v", err) + } + } + } + }() + } + + // cleaner + if g.Cfg().MustGet(ctx, "asynch.cleaner.enabled").Bool() { + interval := g.Cfg().MustGet(ctx, "asynch.cleaner.intervalSeconds").Int() + if interval <= 0 { + interval = 30 + } + ticker := time.NewTicker(time.Duration(interval) * time.Second) + go func() { + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + service.Cleaner.RunOnce(ctx) + } + } + }() + } +} diff --git a/model/dto/model_dto.go b/model/dto/model_dto.go index ca90f92..9a1cb95 100644 --- a/model/dto/model_dto.go +++ b/model/dto/model_dto.go @@ -1,7 +1,6 @@ package dto import ( - "gitea.com/red-future/common/beans" "github.com/gogf/gf/v2/frame/g" ) @@ -9,20 +8,26 @@ import ( type CreateModelReq struct { g.Meta `path:"/createModel" method:"post" tags:"模型管理" summary:"创建模型配置" dc:"添加新的模型配置"` ModelName string `p:"modelName" json:"modelName" v:"required#modelName不能为空" dc:"模型名称(唯一标识)"` - ModelsType string `p:"modelsType" json:"modelsType" dc:"模型类型ID列表(逗号分隔),示例:1,2,3(关联 asynch_models_type.type_id,可选)"` + ModelsType int `p:"modelsType" json:"modelsType" v:"required#modelsType不能为空" dc:"模型类型:1-文本生成 2-图像生成 3-语音 4-视频 5-多模态"` BaseURL string `p:"baseUrl" json:"baseUrl" v:"required#baseUrl不能为空" dc:"模型服务基础地址(如 http(s)://host:port)"` - Route string `p:"route" json:"route" dc:"路由/路径(拼接到 BaseURL 之后的可选路径)"` HttpMethod string `p:"httpMethod" json:"httpMethod" dc:"请求方式:GET/POST(默认POST)"` - HeadMsg string `p:"headMsg" json:"headMsg" dc:"请求头绑定(支持多个,逗号分隔),示例:X-API-Key:xxx,operation:true"` - Form any `p:"form" json:"form" dc:"动态表单配置(JSON),用于前端渲染配置项,示例:[{field,label,type,required},...]"` - Enabled int `p:"enabled" json:"enabled" dc:"是否启用:0-禁用,1-启用"` - MaxConcurrency int `p:"maxConcurrency" json:"maxConcurrency" dc:"最大并发数"` - QueueLimit int `p:"queueLimit" json:"queueLimit" dc:"排队队列上限(超过则拒绝/限流)"` - TimeoutSeconds int `p:"timeoutSeconds" json:"timeoutSeconds" dc:"请求超时时间(秒)"` - ExpectedSeconds int `p:"expectedSeconds" json:"expectedSeconds" dc:"模型预计执行时间(秒,用于超时判定/排队策略等)"` - RetryTimes int `p:"retryTimes" json:"retryTimes" dc:"失败重试次数"` - RetryQueueMaxSeconds int `p:"retryQueueMaxSeconds" json:"retryQueueMaxSeconds" dc:"失败重试最大排队时间(秒);0表示失败重试插队到队首;>0表示排队超过该时间后插队,否则仍到队尾"` - AutoCleanSeconds int `p:"autoCleanSeconds" json:"autoCleanSeconds" dc:"自动清理间隔(秒)(如清理超时任务/队列)"` + HeadMsg string `p:"headMsg" json:"headMsg" dc:"请求头绑定(支持多个,逗号分隔),示例:Authorization:Bearer xxx,Content-Type:application/json"` + IsPrivate int `p:"isPrivate" json:"isPrivate" v:"in:0,1#私有化参数只能为0或1" dc:"是否私有化:0-私有(默认) 1-公共"` + Enabled int `p:"enabled" json:"enabled" v:"in:0,1#启用参数只能为0或1" dc:"是否启用:0-禁用,1-启用(默认1)"` + IsChatModel int `p:"isChatModel" json:"isChatModel" v:"in:0,1#对话模型参数只能为0或1" dc:"是否为对话模型:0-否,1-是(默认0)"` + ApiKey string `p:"apiKey" json:"apiKey" v:"required-if:isPrivate,1#公共模型必须填写API密钥" dc:"调用凭证/密钥,用于模型认证"` + Form any `p:"form" json:"form" dc:"动态表单配置(JSON),用于前端渲染配置项"` + RequestMapping any `p:"requestMapping" json:"requestMapping" dc:"请求映射"` + ResponseMapping any `p:"responseMapping" json:"responseMapping" dc:"返回映射"` + ResponseBody any `p:"responseBody" json:"responseBody" dc:"返回主体"` + TokenMapping string `p:"tokenMapping" json:"tokenMapping" dc:"token映射"` + MaxConcurrency int `p:"maxConcurrency" json:"maxConcurrency" dc:"最大并发数(默认10)"` + QueueLimit int `p:"queueLimit" json:"queueLimit" dc:"排队队列上限(默认1000)"` + TimeoutSeconds int `p:"timeoutSeconds" json:"timeoutSeconds" dc:"请求超时时间(秒,默认600)"` + ExpectedSeconds int `p:"expectedSeconds" json:"expectedSeconds" dc:"模型预计执行时间(秒,默认600)"` + RetryTimes int `p:"retryTimes" json:"retryTimes" dc:"失败重试次数(默认3)"` + RetryQueueMaxSeconds int `p:"retryQueueMaxSeconds" json:"retryQueueMaxSeconds" dc:"失败重试最大排队时间(秒,默认600)"` + AutoCleanSeconds int `p:"autoCleanSeconds" json:"autoCleanSeconds" dc:"任务完成后自动清理时间(秒,默认86400)"` Remark string `p:"remark" json:"remark" dc:"备注说明"` } @@ -30,36 +35,39 @@ type CreateModelRes struct { ID int64 `json:"id,string" dc:"配置ID"` } -// UpdateModelReq 更新模型配置 type UpdateModelReq struct { g.Meta `path:"/updateModel" method:"put" tags:"模型管理" summary:"更新模型配置" dc:"更新指定ID的模型配置"` - ID int64 `p:"id" json:"id,string" v:"required#id不能为空" dc:"配置ID"` - ModelsType *string `p:"modelsType" json:"modelsType" dc:"模型类型ID列表(逗号分隔)(可选更新)"` - BaseURL string `p:"baseUrl" json:"baseUrl" dc:"模型服务基础地址"` - Route string `p:"route" json:"route" dc:"路由/路径"` - HttpMethod *string `p:"httpMethod" json:"httpMethod" dc:"请求方式:GET/POST(可选更新)"` - HeadMsg *string `p:"headMsg" json:"headMsg" dc:"请求头绑定(可选更新)"` - Form any `p:"form" json:"form" dc:"动态表单配置(JSON)(可选更新)"` - Enabled *int `p:"enabled" json:"enabled" dc:"是否启用:0-禁用,1-启用(可选更新)"` - MaxConcurrency *int `p:"maxConcurrency" json:"maxConcurrency" dc:"最大并发数(可选更新)"` - QueueLimit *int `p:"queueLimit" json:"queueLimit" dc:"排队队列上限(可选更新)"` - TimeoutSeconds *int `p:"timeoutSeconds" json:"timeoutSeconds" dc:"请求超时时间(秒)(可选更新)"` - ExpectedSeconds *int `p:"expectedSeconds" json:"expectedSeconds" dc:"模型预计执行时间(秒)(可选更新)"` - RetryTimes *int `p:"retryTimes" json:"retryTimes" dc:"失败重试次数(可选更新)"` - RetryQueueMaxSeconds *int `p:"retryQueueMaxSeconds" json:"retryQueueMaxSeconds" dc:"失败重试最大排队时间(秒)(可选更新)"` - AutoCleanSeconds *int `p:"autoCleanSeconds" json:"autoCleanSeconds" dc:"自动清理间隔(秒)(可选更新)"` - Remark *string `p:"remark" json:"remark" dc:"备注说明(可选更新)"` + ID int64 `p:"id" json:"id" v:"required#id不能为空" dc:"配置ID"` + ModelsType string `p:"modelsType" json:"modelsType" dc:"模型类型ID列表(逗号分隔)(可选更新)"` + BaseURL string `p:"baseUrl" json:"baseUrl" dc:"模型服务基础地址"` + HttpMethod string `p:"httpMethod" json:"httpMethod" dc:"请求方式:GET/POST(可选更新)"` + HeadMsg string `p:"headMsg" json:"headMsg" dc:"请求头绑定(可选更新)"` + Form any `p:"form" json:"form" dc:"动态表单配置(JSON)(可选更新)"` + RequestMapping any `p:"requestMapping" json:"requestMapping" dc:"请求参数映射(可选更新)"` + ResponseMapping any `p:"responseMapping" json:"responseMapping" dc:"返回参数映射(可选更新)"` + ResponseBody any `p:"responseBody" json:"responseBody" dc:"返回主体(可选更新)"` + TokenMapping string `p:"tokenMapping" json:"tokenMapping" dc:"token映射(可选更新)"` + Enabled int `p:"enabled" json:"enabled" dc:"是否启用:0-禁用,1-启用(可选更新)"` + IsChatModel int `p:"isChatModel" json:"isChatModel" v:"in:0,1#对话模型参数只能为0或1" dc:"是否为对话模型:0-否,1-是(默认0)"` + MaxConcurrency int `p:"maxConcurrency" json:"maxConcurrency" dc:"最大并发数(可选更新)"` + QueueLimit int `p:"queueLimit" json:"queueLimit" dc:"排队队列上限(可选更新)"` + TimeoutSeconds int `p:"timeoutSeconds" json:"timeoutSeconds" dc:"请求超时时间(秒)(可选更新)"` + ExpectedSeconds int `p:"expectedSeconds" json:"expectedSeconds" dc:"模型预计执行时间(秒)(可选更新)"` + RetryTimes int `p:"retryTimes" json:"retryTimes" dc:"失败重试次数(可选更新)"` + RetryQueueMaxSeconds int `p:"retryQueueMaxSeconds" json:"retryQueueMaxSeconds" dc:"失败重试最大排队时间(秒)(可选更新)"` + AutoCleanSeconds int `p:"autoCleanSeconds" json:"autoCleanSeconds" dc:"自动清理间隔(秒)(可选更新)"` + Remark string `p:"remark" json:"remark" dc:"备注说明(可选更新)"` } // DeleteModelReq 删除模型配置 type DeleteModelReq struct { g.Meta `path:"/deleteModel" method:"delete" tags:"模型管理" summary:"删除模型配置" dc:"删除指定ID的模型配置"` - ID int64 `p:"id" json:"id,string" v:"required#id不能为空" dc:"配置ID"` + ID string `p:"id" json:"id,string" v:"required#id不能为空" dc:"配置ID"` } // GetModelReq 获取模型配置详情 type GetModelReq struct { - g.Meta `path:"/getModel" method:"get" tags:"模型管理" summary:"获取模型配置" dc:"根据模型名称获取配置详情"` + g.Meta `path:"/getModel" method:"get" tags:"模型管理" summary:"获取模型配置" dc:"根据模型ID获取配置详情"` ID int64 `p:"id" json:"id,string" v:"required#id不能为空" dc:"配置ID"` } @@ -69,9 +77,11 @@ type GetModelRes struct { // ListModelReq 配置列表 type ListModelReq struct { - g.Meta `path:"/listModel" method:"post" tags:"模型管理" summary:"模型配置列表" dc:"分页获取模型配置列表"` - Page *beans.Page `p:"page" json:"page" dc:"分页参数"` - ModelName string `p:"modelName" json:"modelName" dc:"模型名称(模糊查询,可选)"` + g.Meta `path:"/listModel" method:"get" tags:"模型管理" summary:"模型配置列表" dc:"分页获取模型配置列表"` + PageNum int `p:"pageNum" json:"pageNum" dc:"页码(默认1)"` + PageSize int `p:"pageSize" json:"pageSize" dc:"每页条数(默认10)"` + ModelName string `p:"modelName" json:"modelName" dc:"模型名称(模糊查询,可选)"` + ModelType int `p:"modelType" json:"modelType" dc:"模型类型"` } type ListModelRes struct { @@ -81,10 +91,34 @@ type ListModelRes struct { // AutoTuneReq 动态调参(由上层定时任务每小时触发一次) type AutoTuneReq struct { - g.Meta `path:"/autoTune" method:"post" tags:"模型管理" summary:"动态调参" dc:"按 model_name 维度统计指定时间窗口内执行耗时(P90),动态生成运行时 max_concurrency/queue_limit(不超过配置上限),写入 Redis 供 Worker/CreateTask 使用;windowSeconds 不传默认 3600"` + g.Meta `path:"/autoTune" method:"post" tags:"模型管理" summary:"动态调参" dc:"按 model_name 维度统计指定时间窗口内执行耗时(P90),动态生成运行时 max_concurrency/queue_limit(不超过配置上限),写入 Redis 供 Worker/CreateTask 使用;windowSeconds 不传默认 3600"` WindowSeconds int `p:"windowSeconds" json:"windowSeconds" dc:"统计窗口秒数;不传/<=0 默认 3600(1小时)"` } type AutoTuneRes struct { List any `json:"list" dc:"调参结果列表"` } + +type ModelTypeModelItem struct { + ID int64 `json:"id" dc:"模型主键ID"` + Name string `json:"name" dc:"模型名称"` + Form any `json:"form" dc:"动态表单配置(JSON数组),用于前端渲染"` +} + +// ListModelTypeReq 模型类型列表(分页) +type ListTypeReq struct { + g.Meta `path:"/listType" method:"get" tags:"模型类型列表" summary:"模型类型列表" dc:"分页获取模型类型列表"` +} + +type TypeItem struct { + Type map[int]string `json:"type" dc:"模型类型ID到名称的映射"` +} + +type UpdateChatModelReq struct { + g.Meta `path:"/updateChatModel" method:"post" tags:"模型管理" summary:"更新聊天模型" dc:"更新指定模型的聊天模型"` + Id int64 `p:"id" json:"id" v:"required#model不能为空" dc:"模型id"` +} + +type GetIsChatModelReq struct { + g.Meta `path:"/getIsChatModel" method:"get" tags:"模型管理" summary:"获取模型是否为聊天模型" dc:"根据模型ID获取是否为聊天模型"` +} diff --git a/model/dto/model_type_dto.go b/model/dto/model_type_dto.go deleted file mode 100644 index 3b2c606..0000000 --- a/model/dto/model_type_dto.go +++ /dev/null @@ -1,74 +0,0 @@ -package dto - -import ( - "gitea.com/red-future/common/beans" - "github.com/gogf/gf/v2/frame/g" -) - -// CreateModelTypeReq 创建模型类型 -type CreateModelTypeReq struct { - g.Meta `path:"/createModelType" method:"post" tags:"模型类型" summary:"创建模型类型" dc:"创建模型类型(图片/音频/视频等)"` - TypeID int `p:"typeId" json:"typeId" v:"required#typeId不能为空" dc:"模型类型ID(业务枚举)"` - TypeName string `p:"type" json:"type" v:"required#type不能为空" dc:"模型类型名称"` - Remark string `p:"remark" json:"remark" dc:"备注"` -} - -type CreateModelTypeRes struct { - ID int64 `json:"id,string" dc:"主键ID"` -} - -// UpdateModelTypeReq 更新模型类型 -type UpdateModelTypeReq struct { - g.Meta `path:"/updateModelType" method:"put" tags:"模型类型" summary:"更新模型类型" dc:"更新模型类型"` - ID int64 `p:"id" json:"id,string" v:"required#id不能为空" dc:"主键ID"` - TypeID *int `p:"typeId" json:"typeId" dc:"模型类型ID(可选更新)"` - TypeName *string `p:"type" json:"type" dc:"模型类型名称(可选更新)"` - Remark *string `p:"remark" json:"remark" dc:"备注(可选更新)"` -} - -// DeleteModelTypeReq 删除模型类型 -type DeleteModelTypeReq struct { - g.Meta `path:"/deleteModelType" method:"delete" tags:"模型类型" summary:"删除模型类型" dc:"删除模型类型"` - ID int64 `p:"id" json:"id,string" v:"required#id不能为空" dc:"主键ID"` -} - -// GetModelTypeReq 获取模型类型 -type GetModelTypeReq struct { - g.Meta `path:"/getModelType" method:"get" tags:"模型类型" summary:"获取模型类型" dc:"获取模型类型详情"` - ID int64 `p:"id" json:"id,string" v:"required#id不能为空" dc:"主键ID"` -} - -type GetModelTypeRes struct { - Type any `json:"type" dc:"模型类型详情"` -} - -// ListModelTypeReq 模型类型列表(分页) -type ListModelTypeReq struct { - g.Meta `path:"/listModelType" method:"post" tags:"模型类型" summary:"模型类型列表" dc:"分页获取模型类型列表"` - Page *beans.Page `p:"page" json:"page" dc:"分页参数(默认10条)"` - TypeName string `p:"type" json:"type" dc:"模型类型名称(模糊查询,可选)"` -} - -type ListModelTypeRes struct { - List any `json:"list" dc:"列表数据"` - Total int64 `json:"total" dc:"总数"` -} - -// ListModelTypeWithModelsReq 按类型分组返回模型列表 -type ListModelTypeWithModelsReq struct { - g.Meta `path:"/listModelTypeWithModels" method:"post" tags:"模型类型" summary:"按类型分组的模型列表" dc:"返回模型类型及其下的模型列表(用于前端分组展示)"` - TypeID int `p:"typeId" json:"typeId" dc:"按类型ID过滤(可选)"` - Type string `p:"type" json:"type" dc:"按类型名称过滤(可选,模糊匹配)"` -} - -type ModelTypeModelItem struct { - ID int64 `json:"id" dc:"模型主键ID"` - Name string `json:"name" dc:"模型名称"` - Form any `json:"form" dc:"动态表单配置(JSON数组),用于前端渲染"` -} - -type ModelTypeWithModelsItem struct { - TypeID int `json:"typeId" dc:"模型类型ID"` - Type string `json:"type" dc:"模型类型名称"` - Items []ModelTypeModelItem `json:"items" dc:"该类型下模型列表"` -} diff --git a/model/dto/stat_dto.go b/model/dto/stat_dto.go index 2341d66..321fa84 100644 --- a/model/dto/stat_dto.go +++ b/model/dto/stat_dto.go @@ -1,14 +1,12 @@ package dto -import ( - "gitea.com/red-future/common/beans" - "github.com/gogf/gf/v2/frame/g" -) +import "github.com/gogf/gf/v2/frame/g" // ListModelStatReq 统计列表 type ListModelStatReq struct { - g.Meta `path:"/listModelStat" method:"post" tags:"统计" summary:"模型请求统计列表" dc:"按天统计模型请求次数,支持分页与条件筛选"` - Page *beans.Page `p:"page" json:"page" dc:"分页参数(默认10条)"` + g.Meta `path:"/listModelStat" method:"get" tags:"统计" summary:"模型请求统计列表" dc:"按天统计模型请求次数,支持分页与条件筛选"` + PageNum int `p:"pageNum" json:"pageNum" dc:"页码(默认1)"` + PageSize int `p:"pageSize" json:"pageSize" dc:"每页条数(默认10)"` StartDay string `p:"startDay" json:"startDay" dc:"开始日期(YYYY-MM-DD,可选)"` EndDay string `p:"endDay" json:"endDay" dc:"结束日期(YYYY-MM-DD,可选)"` TenantID *int64 `p:"tenantId" json:"tenantId" dc:"租户ID(可选)"` @@ -20,4 +18,3 @@ type ListModelStatRes struct { List any `json:"list" dc:"列表数据"` Total int64 `json:"total" dc:"总数"` } - diff --git a/model/dto/task_dto.go b/model/dto/task_dto.go index 922073f..57f189f 100644 --- a/model/dto/task_dto.go +++ b/model/dto/task_dto.go @@ -1,19 +1,16 @@ package dto -import ( - "gitea.com/red-future/common/beans" - "github.com/gogf/gf/v2/frame/g" -) +import "github.com/gogf/gf/v2/frame/g" // CreateTaskReq 创建异步任务 type CreateTaskReq struct { - g.Meta `path:"/createTask" method:"post" tags:"任务管理" summary:"创建异步任务" dc:"创建异步任务并返回任务ID"` + g.Meta `path:"/createTask" method:"post" tags:"任务管理" summary:"创建异步任务" dc:"创建异步任务并返回任务ID;创建成功后会立即异步尝试执行当前任务,执行成功后按回调配置触发钩子"` ModelName string `p:"modelName" json:"modelName" v:"required#modelName不能为空" dc:"模型名称"` - ModelKey string `p:"modelKey" json:"modelKey" dc:"动态请求头(用于覆盖/补充模型配置 head_msg),示例:X-API-Key:xxx"` BizName string `p:"bizName" json:"bizName" dc:"业务名称(调用方模块/系统,用于统计)"` CallbackUrl string `p:"callbackUrl" json:"callbackUrl" dc:"回调地址(可选,用于后续业务通知)"` InputRef string `p:"inputRef" json:"inputRef" dc:"输入引用(如OSS/文件引用等)"` RequestPayload any `p:"requestPayload" json:"requestPayload" dc:"请求负载(透传给模型服务)"` + EpicycleId int64 `json:"epicycleId" dc:"轮次ID"` } type CreateTaskRes struct { @@ -49,11 +46,12 @@ type GetTaskBatchRes struct { // ListTaskReq 任务列表分页查询 type ListTaskReq struct { - g.Meta `path:"/listTask" method:"post" tags:"任务管理" summary:"任务列表" dc:"分页查询任务列表,支持按状态/模型名称/task_id过滤"` - Page *beans.Page `p:"page" json:"page" dc:"分页参数"` - ModelName string `p:"modelName" json:"modelName" dc:"模型名称(模糊匹配)"` - TaskID string `p:"taskId" json:"taskId" dc:"任务ID(模糊匹配)"` - State *int `p:"state" json:"state" dc:"任务状态(0/1/2/3/4,可选)"` + g.Meta `path:"/listTask" method:"get" tags:"任务管理" summary:"任务列表" dc:"分页查询任务列表,支持按状态/模型名称/task_id过滤"` + PageNum int `p:"pageNum" json:"pageNum" dc:"页码(默认1)"` + PageSize int `p:"pageSize" json:"pageSize" dc:"每页条数(默认10)"` + ModelName string `p:"modelName" json:"modelName" dc:"模型名称(模糊匹配)"` + TaskID string `p:"taskId" json:"taskId" dc:"任务ID(模糊匹配)"` + State *int `p:"state" json:"state" dc:"任务状态(0/1/2/3/4,可选)"` } type ListTaskRes struct { @@ -63,7 +61,7 @@ type ListTaskRes struct { // RunWorkReq 手动触发 worker 执行一次(由上层定时任务调用) type RunWorkReq struct { - g.Meta `path:"/runWork" method:"post" tags:"任务管理" summary:"执行一次Worker" dc:"手动触发一次Worker抢占并处理任务(用于由上层定时任务控制)"` + g.Meta `path:"/runWork" method:"post" tags:"任务管理" summary:"执行一次Worker" dc:"手动触发一次Worker抢占并处理排队中的任务;适合处理 createTask 立即执行时未处理到的任务以及积压队列"` BatchSize int `p:"batchSize" json:"batchSize" dc:"本次抢占任务数量(默认10)"` Goroutines int `p:"goroutines" json:"goroutines" dc:"本次并发数(默认1)"` } diff --git a/model/entity/asynch_model.go b/model/entity/asynch_model.go index 8ad9c5d..fdca743 100644 --- a/model/entity/asynch_model.go +++ b/model/entity/asynch_model.go @@ -5,12 +5,19 @@ import "gitea.com/red-future/common/beans" type asynchModelCol struct { beans.SQLBaseCol ModelName string + ModelsType string BaseURL string - Route string HttpMethod string HeadMsg string FormJSON string - ModelsType string + RequestMapping string + ResponseMapping string + ResponseBody string + TokenMapping string + Prompt string + IsPrivate string + IsChatModel string + ApiKey string Enabled string MaxConcurrency string QueueLimit string @@ -25,12 +32,19 @@ type asynchModelCol struct { var AsynchModelCol = asynchModelCol{ SQLBaseCol: beans.DefSQLBaseCol, ModelName: "model_name", + ModelsType: "models_type", BaseURL: "base_url", - Route: "route", HttpMethod: "http_method", HeadMsg: "head_msg", FormJSON: "form_json", - ModelsType: "models_type", + RequestMapping: "request_mapping", + ResponseMapping: "response_mapping", + ResponseBody: "response_body", + TokenMapping: "token_mapping", + Prompt: "prompt", + IsPrivate: "is_private", + IsChatModel: "is_chat_model", + ApiKey: "api_key", Enabled: "enabled", MaxConcurrency: "max_concurrency", QueueLimit: "queue_limit", @@ -44,21 +58,28 @@ var AsynchModelCol = asynchModelCol{ // AsynchModel 异步模型配置 type AsynchModel struct { - beans.SQLBaseDO `orm:",inline"` - ModelName string `orm:"model_name" json:"modelName"` - BaseURL string `orm:"base_url" json:"baseUrl"` - Route string `orm:"route" json:"route"` - HttpMethod string `orm:"http_method" json:"httpMethod"` - HeadMsg string `orm:"head_msg" json:"headMsg"` - Form any `orm:"form_json" json:"form"` - ModelsType string `orm:"models_type" json:"modelsType"` - Enabled int `orm:"enabled" json:"enabled"` - MaxConcurrency int `orm:"max_concurrency" json:"maxConcurrency"` - QueueLimit int `orm:"queue_limit" json:"queueLimit"` - TimeoutSeconds int `orm:"timeout_seconds" json:"timeoutSeconds"` - ExpectedSeconds int `orm:"expected_seconds" json:"expectedSeconds"` - RetryTimes int `orm:"retry_times" json:"retryTimes"` - RetryQueueMaxSecs int `orm:"retry_queue_max_seconds" json:"retryQueueMaxSeconds"` - AutoCleanSeconds int `orm:"auto_clean_seconds" json:"autoCleanSeconds"` - Remark string `orm:"remark" json:"remark"` + beans.SQLBaseDO `orm:",inline"` + ModelName string `orm:"model_name" json:"modelName"` + ModelsType int `orm:"models_type" json:"modelsType"` + BaseURL string `orm:"base_url" json:"baseUrl"` + HttpMethod string `orm:"http_method" json:"httpMethod"` + HeadMsg string `orm:"head_msg" json:"headMsg"` + Form any `orm:"form_json" json:"form"` + RequestMapping any `orm:"request_mapping" json:"requestMapping"` + ResponseMapping any `orm:"response_mapping" json:"responseMapping"` + ResponseBody any `orm:"response_body" json:"responseBody"` + TokenMapping string `orm:"token_mapping" json:"tokenMapping"` + Prompt string `orm:"prompt" json:"prompt"` + IsPrivate int `orm:"is_private" json:"isPrivate"` + IsChatModel int `orm:"is_chat_model" json:"isChatModel"` + ApiKey string `orm:"api_key" json:"apiKey"` + Enabled int `orm:"enabled" json:"enabled"` + MaxConcurrency int `orm:"max_concurrency" json:"maxConcurrency"` + QueueLimit int `orm:"queue_limit" json:"queueLimit"` + TimeoutSeconds int `orm:"timeout_seconds" json:"timeoutSeconds"` + ExpectedSeconds int `orm:"expected_seconds" json:"expectedSeconds"` + RetryTimes int `orm:"retry_times" json:"retryTimes"` + RetryQueueMaxSeconds int `orm:"retry_queue_max_seconds" json:"retryQueueMaxSeconds"` + AutoCleanSeconds int `orm:"auto_clean_seconds" json:"autoCleanSeconds"` + Remark string `orm:"remark" json:"remark"` } diff --git a/model/entity/asynch_model_stat.go b/model/entity/asynch_model_stat.go deleted file mode 100644 index 7ba8455..0000000 --- a/model/entity/asynch_model_stat.go +++ /dev/null @@ -1,16 +0,0 @@ -package entity - -import "github.com/gogf/gf/v2/os/gtime" - -// AsynchModelStat 按天统计:某天/租户/创建人/模型的请求次数 -// 注:这里不走通用 SQLBaseDO,采用联合唯一键(day,tenant_id,creator,model_name)做 UPSERT 原子累加。 -type AsynchModelStat struct { - Day *gtime.Time `orm:"day" json:"day"` // 日期(建议仅使用日期部分) - TenantId int64 `orm:"tenant_id" json:"tenantId,string"` - Creator string `orm:"creator" json:"creator"` - ModelName string `orm:"model_name" json:"modelName"` - RequestCount int64 `orm:"request_count" json:"requestCount"` - CreatedAt *gtime.Time `orm:"created_at" json:"createdAt"` - UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt"` -} - diff --git a/model/entity/asynch_task.go b/model/entity/asynch_task.go index 876838d..711751e 100644 --- a/model/entity/asynch_task.go +++ b/model/entity/asynch_task.go @@ -9,73 +9,81 @@ type asynchTaskCol struct { beans.SQLBaseCol ModelName string TaskID string - State string BizName string CallbackURL string ModelKey string + State string OssFile string FileType string FileSize string ErrorMsg string StartedAt string FinishedAt string - ExpireAt string DurationSeconds string + ExpireAt string RetryCount string EnqueueAt string Phase string TmpFile string InputRef string RequestPayload string + TextResult string + EpicycleId string + ExpendTokens string } var AsynchTaskCol = asynchTaskCol{ - SQLBaseCol: beans.DefSQLBaseCol, - ModelName: "model_name", - TaskID: "task_id", - State: "state", - BizName: "biz_name", - CallbackURL: "callback_url", - ModelKey: "model_key", - OssFile: "oss_file", - FileType: "file_type", - FileSize: "file_size", - ErrorMsg: "error_msg", - StartedAt: "started_at", - FinishedAt: "finished_at", - ExpireAt: "expire_at", + SQLBaseCol: beans.DefSQLBaseCol, + ModelName: "model_name", + TaskID: "task_id", + BizName: "biz_name", + CallbackURL: "callback_url", + ModelKey: "model_key", + State: "state", + OssFile: "oss_file", + FileType: "file_type", + FileSize: "file_size", + ErrorMsg: "error_msg", + StartedAt: "started_at", + FinishedAt: "finished_at", DurationSeconds: "duration_seconds", - RetryCount: "retry_count", - EnqueueAt: "enqueue_at", - Phase: "phase", - TmpFile: "tmp_file", - InputRef: "input_ref", - RequestPayload: "request_payload", + ExpireAt: "expire_at", + RetryCount: "retry_count", + EnqueueAt: "enqueue_at", + Phase: "phase", + TmpFile: "tmp_file", + InputRef: "input_ref", + RequestPayload: "request_payload", + TextResult: "text_result", + EpicycleId: "epicycle_id", + ExpendTokens: "expend_tokens", } // AsynchTask 异步任务 type AsynchTask struct { - beans.SQLBaseDO `orm:",inline"` - ModelName string `orm:"model_name" json:"modelName"` - TaskID string `orm:"task_id" json:"taskId"` - State int `orm:"state" json:"state"` // 0排队中/1执行中/2成功/3失败/4已下载 - BizName string `orm:"biz_name" json:"bizName"` - CallbackURL string `orm:"callback_url" json:"callbackUrl"` - ModelKey string `orm:"model_key" json:"modelKey"` - OssFile string `orm:"oss_file" json:"ossFile"` - FileType string `orm:"file_type" json:"fileType"` - FileSize int64 `orm:"file_size" json:"fileSize"` - ErrorMsg string `orm:"error_msg" json:"errorMsg"` - StartedAt *gtime.Time `orm:"started_at" json:"startedAt"` - FinishedAt *gtime.Time `orm:"finished_at" json:"finishedAt"` - ExpireAt *gtime.Time `orm:"expire_at" json:"expireAt"` // 已下载(state=4)后的过期时间 - DurationSeconds int64 `orm:"duration_seconds" json:"durationSeconds"` - RetryCount int `orm:"retry_count" json:"retryCount"` - EnqueueAt *gtime.Time `orm:"enqueue_at" json:"enqueueAt"` - Phase int `orm:"phase" json:"phase"` // 0模型阶段/1OSS阶段 - TmpFile string `orm:"tmp_file" json:"tmpFile"` // 临时结果文件路径 - // RetryQueueMaxSeconds 为 ListFailedRetryableGlobal 的 join 字段(非任务表字段) - RetryQueueMaxSeconds int `orm:"retry_queue_max_seconds" json:"-"` - InputRef string `orm:"input_ref" json:"inputRef"` - RequestPayload any `orm:"request_payload" json:"requestPayload"` + beans.SQLBaseDO `orm:",inline"` + ModelName string `orm:"model_name" json:"modelName"` + TaskID string `orm:"task_id" json:"taskId"` + BizName string `orm:"biz_name" json:"bizName"` + CallbackURL string `orm:"callback_url" json:"callbackUrl"` + ModelKey string `orm:"model_key" json:"modelKey"` + State int `orm:"state" json:"state"` // 0排队中/1执行中/2成功/3失败/4已下载 + OssFile string `orm:"oss_file" json:"ossFile"` + FileType string `orm:"file_type" json:"fileType"` + FileSize int64 `orm:"file_size" json:"fileSize"` + ErrorMsg string `orm:"error_msg" json:"errorMsg"` + StartedAt *gtime.Time `orm:"started_at" json:"startedAt"` + FinishedAt *gtime.Time `orm:"finished_at" json:"finishedAt"` + DurationSeconds int64 `orm:"duration_seconds" json:"durationSeconds"` + ExpireAt *gtime.Time `orm:"expire_at" json:"expireAt"` // 已下载(state=4)后的过期时间 + RetryCount int `orm:"retry_count" json:"retryCount"` + EnqueueAt *gtime.Time `orm:"enqueue_at" json:"enqueueAt"` + Phase int `orm:"phase" json:"phase"` // 0模型阶段/1OSS阶段 + TmpFile string `orm:"tmp_file" json:"tmpFile"` // 临时结果文件路径 + InputRef string `orm:"input_ref" json:"inputRef"` + RequestPayload any `orm:"request_payload" json:"requestPayload"` + TextResult string `orm:"text_result" json:"text"` + EpicycleId int64 `orm:"epicycle_id" json:"epicycleId"` // 轮次ID(用于标识同一轮次的任务) + ExpendTokens int64 `orm:"expend_tokens" json:"expendTokens"` // 消耗 token 数 + RetryQueueMaxSeconds int `orm:"retry_queue_max_seconds" json:"-"` } diff --git a/model/entity/asynch_op_log.go b/model/entity/logs_model_op.go similarity index 92% rename from model/entity/asynch_op_log.go rename to model/entity/logs_model_op.go index 0b821a4..b557cf5 100644 --- a/model/entity/asynch_op_log.go +++ b/model/entity/logs_model_op.go @@ -4,7 +4,7 @@ import ( "gitea.com/red-future/common/beans" ) -type asynchOpLogCol struct { +type LogsModelPpCol struct { beans.SQLBaseCol IP string UserAgent string @@ -21,7 +21,7 @@ type asynchOpLogCol struct { ResponsePayload string } -var AsynchOpLogCol = asynchOpLogCol{ +var LogsModelOpCol = LogsModelPpCol{ SQLBaseCol: beans.DefSQLBaseCol, IP: "ip", UserAgent: "user_agent", @@ -38,8 +38,8 @@ var AsynchOpLogCol = asynchOpLogCol{ ResponsePayload: "response_payload", } -// AsynchOpLog 操作日志(创建任务等) -type AsynchOpLog struct { +// LogsModelOp 操作日志(创建任务等) +type LogsModelOp struct { beans.SQLBaseDO `orm:",inline"` IP string `orm:"ip" json:"ip"` UserAgent string `orm:"user_agent" json:"userAgent"` diff --git a/model/entity/logs_model_stat.go b/model/entity/logs_model_stat.go new file mode 100644 index 0000000..e4583a9 --- /dev/null +++ b/model/entity/logs_model_stat.go @@ -0,0 +1,38 @@ +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// LogsModelStatCol 字段常量 +type LogsModelStatCol struct { + Day string + TenantId string + Creator string + ModelName string + RequestCount string + CreatedAt string + UpdatedAt string +} + +var LogsModelStatCols = LogsModelStatCol{ + Day: "day", + TenantId: "tenant_id", + Creator: "creator", + ModelName: "model_name", + RequestCount: "request_count", + CreatedAt: "created_at", + UpdatedAt: "updated_at", +} + +// LogsModelStat 按天统计:某天/租户/创建人/模型的请求次数 +// 注:这里不走通用 SQLBaseDO,采用联合唯一键(day,tenant_id,creator,model_name)做 UPSERT 原子累加。 +type LogsModelStat struct { + Day *gtime.Time `orm:"day" json:"day"` // 日期(建议仅使用日期部分) + TenantId int64 `orm:"tenant_id" json:"tenantId"` // 租户ID + Creator string `orm:"creator" json:"creator"` // 创建人/操作人 + ModelName string `orm:"model_name" json:"modelName"` // 模型名称 + RequestCount int64 `orm:"request_count" json:"requestCount"` // 请求次数 + CreatedAt *gtime.Time `orm:"created_at" json:"createdAt"` // 创建时间 + UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt"` // 更新时间 +} diff --git a/service/callback.go b/service/callback.go index ed9c86a..0d73838 100644 --- a/service/callback.go +++ b/service/callback.go @@ -2,87 +2,86 @@ package service import ( "context" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" + "encoding/json" "model-asynch/model/entity" + "gitea.com/red-future/common/http" "github.com/gogf/gf/v2/frame/g" ) -// triggerSuccessCallback 任务成功后的回调钩子: -// - 使用 GET 请求 -// - 回调地址为 callbackUrl + "/" + bizName -// - query 参数:task_id/state/oss_file/file_type -// 注意:回调失败不影响任务主流程,只记录日志。 -func triggerSuccessCallback(ctx context.Context, t *entity.AsynchTask) { - if t == nil { - return +// triggerCallback 任务成功后的回调: +// - JSON body 参数:task_id/state/oss_file/file_type/text(可选) +func triggerCallback(ctx context.Context, t *entity.AsynchTask) { + callbackURL := t.BizName + t.CallbackURL + headers := forwardHeaders(ctx) + var req struct{} + payload := map[string]interface{}{ + "task_id": t.TaskID, + "state": t.State, + "oss_file": t.OssFile, + "file_type": t.FileType, + "text": t.TextResult, + "error_msg": t.ErrorMsg, } - callbackURL := strings.TrimSpace(t.CallbackURL) - bizName := strings.TrimSpace(t.BizName) - if callbackURL == "" || bizName == "" { - return - } - - u, err := url.Parse(callbackURL) + jsonData, err := json.Marshal(payload) if err != nil { - g.Log().Warningf(ctx, "[callback] invalid callbackUrl=%s err=%v", callbackURL, err) - return - } - // 必须是可发起 HTTP 请求的绝对地址 - if u.Scheme == "" || u.Host == "" { - g.Log().Warningf(ctx, "[callback] callbackUrl must be absolute http(s) url, got=%s", callbackURL) + g.Log().Warningf(ctx, "[回调] JSON序列化失败 taskId=%s 错误=%v", t.TaskID, err) return } + g.Log().Infof(ctx, "[回调] 开始发送 taskId=%s 回调地址=%s 请求头数量=%d 消息体大小=%d字节", + t.TaskID, callbackURL, len(headers), len(jsonData)) - // path 末尾拼接 bizName - bizSeg := url.PathEscape(bizName) - if strings.HasSuffix(u.Path, "/") || u.Path == "" { - u.Path = strings.TrimRight(u.Path, "/") + "/" + bizSeg - } else { - u.Path = u.Path + "/" + bizSeg - } - - q := u.Query() - q.Set("task_id", t.TaskID) - q.Set("state", fmt.Sprintf("%d", t.State)) - q.Set("oss_file", t.OssFile) - q.Set("file_type", t.FileType) - u.RawQuery = q.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + err = http.Post(ctx, callbackURL, headers, &req, jsonData) if err != nil { - g.Log().Warningf(ctx, "[callback] build request failed url=%s err=%v", u.String(), err) + g.Log().Warningf(ctx, "[回调] 发送失败 taskId=%s 回调地址=%s 错误=%v", t.TaskID, callbackURL, err) return } - // 透传必要头部(如 Authorization / X-User-Info) - for k, v := range forwardHeaders(ctx) { - if v != "" { - req.Header.Set(k, v) - } - } - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - g.Log().Warningf(ctx, "[callback] request failed url=%s err=%v", u.String(), err) - return - } - defer resp.Body.Close() - b, _ := io.ReadAll(resp.Body) - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - msg := string(b) - if len(msg) > 2000 { - msg = msg[:2000] - } - g.Log().Warningf(ctx, "[callback] non-2xx url=%s code=%d body=%s", u.String(), resp.StatusCode, msg) - return - } - g.Log().Infof(ctx, "[callback] success url=%s code=%d", u.String(), resp.StatusCode) + g.Log().Infof(ctx, "[回调] 发送成功 taskId=%s 回调地址=%s 消息体大小=%d字节", t.TaskID, callbackURL, len(jsonData)) } +// triggerPromptsCallback 任务成功后的提示词回调 +// - JSON body 参数:epicycleId(轮次id)/textResult(模型回答消息) +func triggerPromptsCallback(ctx context.Context, t *entity.AsynchTask, epicycleId int64) { + callbackURL := "prompts-core/session/sessionCallback" + headers := forwardHeaders(ctx) + var req struct{} + payload := map[string]interface{}{ + "epicycleId": epicycleId, + "text": t.TextResult, + } + jsonData, err := json.Marshal(payload) + if err != nil { + g.Log().Warningf(ctx, "[提示词回调] JSON序列化失败 epicycleId=%d 错误=%v", epicycleId, err) + return + } + g.Log().Infof(ctx, "[提示词回调] 开始发送 epicycleId=%d 回调地址=%s 请求头数量=%d 消息体大小=%d字节", + t.EpicycleId, callbackURL, len(headers), len(jsonData)) + + err = http.Post(ctx, callbackURL, headers, &req, jsonData) + if err != nil { + g.Log().Warningf(ctx, "[提示词回调] 发送失败 epicycleId=%d 回调地址=%s 错误=%v", t.EpicycleId, callbackURL, err) + return + } + g.Log().Infof(ctx, "[提示词回调] 发送成功 epicycleId=%d 回调地址=%s 消息体大小=%d字节", t.EpicycleId, callbackURL, len(jsonData)) +} + +// IsSuperAdmin 调用admin-go服务检查是否是超级管理员 +func IsSuperAdmin(ctx context.Context) (res bool, err error) { + headers := forwardHeaders(ctx) + var r = make(map[string]bool) + if err = http.Get(ctx, "admin-go/api/v1/system/user/checkIsSuperAdmin", headers, &r); err != nil { + return false, err + } + return r["isSuperAdmin"], err +} + +// IsAdmin 调用admin-go服务检查是否是管理员 +func IsAdmin(ctx context.Context) (res bool, err error) { + headers := forwardHeaders(ctx) + var r = make(map[string]bool) + if err = http.Get(ctx, "admin-go/api/v1/system/user/checkIsSuperAdmin", headers, &r); err != nil { + return false, err + } + return r["isSuperAdmin"], err +} diff --git a/service/file_detect.go b/service/file_detect.go index aaf75c7..cf17e65 100644 --- a/service/file_detect.go +++ b/service/file_detect.go @@ -11,6 +11,10 @@ func DetectFileType(data []byte) (contentType string, ext string) { return "application/octet-stream", "" } ct := http.DetectContentType(data) + // http.DetectContentType 可能带 charset 等参数:text/plain; charset=utf-8 + if idx := strings.Index(ct, ";"); idx > 0 { + ct = strings.TrimSpace(ct[:idx]) + } switch ct { case "audio/mpeg": return ct, ".mp3" @@ -24,12 +28,20 @@ func DetectFileType(data []byte) (contentType string, ext string) { return ct, ".jpg" case "application/pdf": return ct, ".pdf" + case "text/plain": + return ct, ".txt" + case "application/json": + return ct, ".json" default: // 兜底:尝试从 ct 截取 subtype 作为后缀(例如 application/json) if parts := strings.Split(ct, "/"); len(parts) == 2 { - return ct, "." + parts[1] + sub := parts[1] + // 避免出现 "plain; charset=utf-8" 之类的后缀 + if idx := strings.Index(sub, ";"); idx > 0 { + sub = strings.TrimSpace(sub[:idx]) + } + return ct, "." + sub } return ct, "" } } - diff --git a/service/headers.go b/service/headers.go index b4033ed..e83ae4d 100644 --- a/service/headers.go +++ b/service/headers.go @@ -51,4 +51,3 @@ func forwardHeaders(ctx context.Context) map[string]string { } return headers } - diff --git a/service/model_invoker.go b/service/model_invoker.go index 83186ad..eb62e94 100644 --- a/service/model_invoker.go +++ b/service/model_invoker.go @@ -12,6 +12,11 @@ import ( "time" "model-asynch/model/entity" + + "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/frame/g" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" ) // parseHeadMsgHeaders 支持多个 header 绑定,逗号分隔: @@ -100,11 +105,14 @@ func InvokeModel(ctx context.Context, m *entity.AsynchModel, payload any, modelK if m == nil || m.BaseURL == "" { return nil, fmt.Errorf("模型配置不完整") } - url := strings.TrimRight(m.BaseURL, "/") + "/" + strings.TrimLeft(m.Route, "/") - if strings.TrimSpace(m.Route) == "" { - url = strings.TrimRight(m.BaseURL, "/") + + // ============ 新增:请求参数映射 ============ + mappedPayload, err := mapRequestPayload(m.RequestMapping, payload) + if err != nil { + return nil, fmt.Errorf("请求参数映射失败: %w", err) } + url := strings.TrimRight(m.BaseURL, "/") timeout := time.Duration(m.TimeoutSeconds) * time.Second if timeout <= 0 { timeout = 60 * time.Second @@ -118,11 +126,10 @@ func InvokeModel(ctx context.Context, m *entity.AsynchModel, payload any, modelK var ( req *http.Request - err error ) switch method { case http.MethodGet: - q, err := payloadToQuery(payload) + q, err := payloadToQuery(mappedPayload) // 使用映射后的payload if err != nil { return nil, err } @@ -135,7 +142,7 @@ func InvokeModel(ctx context.Context, m *entity.AsynchModel, payload any, modelK } req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, nil) default: - bodyBytes, err := json.Marshal(payload) + bodyBytes, err := json.Marshal(mappedPayload) // 使用映射后的payload if err != nil { return nil, err } @@ -145,20 +152,16 @@ func InvokeModel(ctx context.Context, m *entity.AsynchModel, payload any, modelK return nil, err } - // 先注入模型配置 head_msg(静态头部) + // 先注入模型配置 head_msg(静态头部,适合公共模型固定 API Key) for hk, hv := range parseHeadMsgHeaders(m.HeadMsg) { req.Header.Set(hk, hv) } - // 透传必要头部(如 Authorization / X-User-Info) - for k, v := range forwardHeaders(ctx) { - if v != "" { - req.Header.Set(k, v) - } - } - // 最后注入动态 modelKey(覆盖/补充静态 head_msg) + + // 最后注入动态 modelKey(允许覆盖/补充静态 head_msg),适合按请求动态传密钥。 for hk, hv := range parseHeadMsgHeaders(modelKey) { req.Header.Set(hk, hv) } + if method != http.MethodGet { req.Header.Set("Content-Type", "application/json") } @@ -174,12 +177,241 @@ func InvokeModel(ctx context.Context, m *entity.AsynchModel, payload any, modelK return nil, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { - // 尽量把错误体带回去,方便排查 msg := string(b) if len(msg) > 2000 { msg = msg[:2000] } return nil, fmt.Errorf("模型服务返回非2xx: %d, body=%s", resp.StatusCode, msg) } - return b, nil + + // ============ 新增:响应参数映射 ============ + mappedResponse, err := mapResponsePayload(m.ResponseMapping, b) + if err != nil { + // 响应映射失败不阻塞,返回原始数据 + g.Log().Warningf(ctx, "响应参数映射失败: %v,返回原始数据", err) + return b, nil + } + // ========================================= + + return mappedResponse, nil +} + +// ============================================ +// 映射相关函数 +// ============================================ + +// mapRequestPayload 将标准请求映射为模型特定格式 +func mapRequestPayload(mappingAny any, payload any) (any, error) { + // 1. 解析请求映射配置(值是any类型,支持bool、number等) + mapping, err := parseRequestMapping(mappingAny) + if err != nil { + return nil, err + } + + // 如果没有映射配置,直接返回原始payload + if len(mapping) == 0 { + return payload, nil + } + + // 2. 将payload转为map + var payloadMap map[string]any + switch v := payload.(type) { + case map[string]any: + payloadMap = v + case []map[string]any: + // 如果传进来的是纯messages数组,包装成标准格式 + payloadMap = map[string]any{ + "messages": v, + } + default: + // 通过JSON转换 + jsonBytes, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("序列化payload失败: %w", err) + } + if err := json.Unmarshal(jsonBytes, &payloadMap); err != nil { + return nil, fmt.Errorf("反序列化payload失败: %w", err) + } + } + + // 3. 用数据库固定参数覆盖/补充 + for key, value := range mapping { + if existingValue, exists := payloadMap[key]; !exists || isEmptyValue(existingValue) { + payloadMap[key] = value + } + } + + return payloadMap, nil +} + +// mapResponsePayload 将模型响应映射为标准格式 +func mapResponsePayload(mappingAny any, responseBytes []byte) ([]byte, error) { + mapping, err := parseResponseMapping(mappingAny) + if err != nil { + return nil, err + } + if len(mapping) == 0 { + return responseBytes, nil + } + + responseStr := string(responseBytes) + resultStr := `{}` + + for standardField, modelPath := range mapping { + value := gjson.Get(responseStr, modelPath) + if !value.Exists() { + continue + } + + resultStr, err = sjson.SetRaw(resultStr, standardField, value.Raw) + if err != nil { + return nil, fmt.Errorf("提取字段 %s <- %s 失败: %w", standardField, modelPath, err) + } + } + + return []byte(resultStr), nil +} + +func parseRequestMapping(mappingAny any) (map[string]any, error) { + if mappingAny == nil { + return nil, nil + } + + result := make(map[string]any) + + switch v := mappingAny.(type) { + case *gvar.Var: + if v == nil || v.IsNil() || v.IsEmpty() { + return nil, nil + } + // 尝试转成 map + if m := v.Map(); m != nil { + for k, val := range m { + result[k] = val + } + return result, nil + } + // 尝试转成 string + if s := v.String(); s != "" && s != "{}" && s != "null" { + if err := json.Unmarshal([]byte(s), &result); err != nil { + return nil, fmt.Errorf("解析请求映射字符串失败: %w", err) + } + return result, nil + } + return nil, nil + // ======================================================= + + case map[string]interface{}: + result = v + + case string: + if v == "" || v == "{}" || v == "null" { + return nil, nil + } + if err := json.Unmarshal([]byte(v), &result); err != nil { + return nil, fmt.Errorf("解析请求映射字符串失败: %w", err) + } + + case []byte: + if len(v) == 0 { + return nil, nil + } + if err := json.Unmarshal(v, &result); err != nil { + return nil, fmt.Errorf("解析请求映射字节失败: %w", err) + } + + default: + jsonBytes, err := json.Marshal(mappingAny) + if err != nil { + return nil, fmt.Errorf("序列化映射配置失败: %w", err) + } + if err := json.Unmarshal(jsonBytes, &result); err != nil { + return nil, fmt.Errorf("解析映射配置失败: %w", err) + } + } + + return result, nil +} + +// parseResponseMapping 解析响应映射配置 +// 返回值类型为 map[string]string,值都是JSON路径字符串 +func parseResponseMapping(mappingAny any) (map[string]string, error) { + if mappingAny == nil { + return nil, nil + } + + mapping := make(map[string]string) + + switch v := mappingAny.(type) { + case *gvar.Var: + if v == nil || v.IsNil() || v.IsEmpty() { + return nil, nil + } + if m := v.Map(); m != nil { + for k, val := range m { + if strVal, ok := val.(string); ok { + mapping[k] = strVal + } + } + return mapping, nil + } + if s := v.String(); s != "" && s != "{}" && s != "null" { + if err := json.Unmarshal([]byte(s), &mapping); err != nil { + return nil, fmt.Errorf("解析响应映射字符串失败: %w", err) + } + return mapping, nil + } + return nil, nil + case string: + if v == "" || v == "{}" || v == "null" { + return nil, nil + } + if err := json.Unmarshal([]byte(v), &mapping); err != nil { + return nil, fmt.Errorf("解析响应映射字符串失败: %w", err) + } + + case map[string]interface{}: + // 数据库JSONB直接返回的map + for k, val := range v { + if strVal, ok := val.(string); ok { + mapping[k] = strVal + } + } + + case []byte: + if len(v) == 0 { + return nil, nil + } + if err := json.Unmarshal(v, &mapping); err != nil { + return nil, fmt.Errorf("解析响应映射字节失败: %w", err) + } + + default: + jsonBytes, err := json.Marshal(mappingAny) + if err != nil { + return nil, fmt.Errorf("序列化响应映射配置失败: %w", err) + } + if err := json.Unmarshal(jsonBytes, &mapping); err != nil { + return nil, fmt.Errorf("解析响应映射配置失败: %w", err) + } + } + + return mapping, nil +} + +// isEmptyValue 判断值是否为空 +func isEmptyValue(v any) bool { + if v == nil { + return true + } + switch val := v.(type) { + case string: + return val == "" + case []any: + return len(val) == 0 + case map[string]any: + return len(val) == 0 + default: + return false + } } diff --git a/service/model_service.go b/service/model_service.go index 6f481ac..ab29b4a 100644 --- a/service/model_service.go +++ b/service/model_service.go @@ -3,10 +3,15 @@ package service import ( "context" "errors" + "sort" "model-asynch/dao" "model-asynch/model/dto" "model-asynch/model/entity" + + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" ) var Model = &modelService{} @@ -15,40 +20,28 @@ type modelService struct{} func (s *modelService) Create(ctx context.Context, req *dto.CreateModelReq) (res *dto.CreateModelRes, err error) { m := &entity.AsynchModel{ - ModelName: req.ModelName, - ModelsType: normalizeModelsType(req.ModelsType), - BaseURL: req.BaseURL, - Route: req.Route, - HttpMethod: req.HttpMethod, - HeadMsg: req.HeadMsg, - Form: req.Form, - Enabled: req.Enabled, - MaxConcurrency: req.MaxConcurrency, - QueueLimit: req.QueueLimit, - TimeoutSeconds: req.TimeoutSeconds, - ExpectedSeconds: req.ExpectedSeconds, - RetryTimes: req.RetryTimes, - RetryQueueMaxSecs: req.RetryQueueMaxSeconds, - AutoCleanSeconds: req.AutoCleanSeconds, - Remark: req.Remark, - } - if m.HttpMethod == "" { - m.HttpMethod = "POST" - } - if m.Enabled == 0 { - m.Enabled = 1 - } - if m.MaxConcurrency <= 0 { - m.MaxConcurrency = 10 - } - if m.QueueLimit <= 0 { - m.QueueLimit = 1000 - } - if m.TimeoutSeconds <= 0 { - m.TimeoutSeconds = 60 - } - if m.AutoCleanSeconds <= 0 { - m.AutoCleanSeconds = 86400 + ModelName: req.ModelName, + ModelsType: req.ModelsType, + BaseURL: req.BaseURL, + HttpMethod: req.HttpMethod, + HeadMsg: req.HeadMsg, + IsPrivate: req.IsPrivate, + Enabled: req.Enabled, + IsChatModel: req.IsChatModel, + ApiKey: req.ApiKey, + Form: req.Form, + RequestMapping: req.RequestMapping, + ResponseMapping: req.ResponseMapping, + ResponseBody: req.ResponseBody, + TokenMapping: req.TokenMapping, + MaxConcurrency: req.MaxConcurrency, + QueueLimit: req.QueueLimit, + TimeoutSeconds: req.TimeoutSeconds, + ExpectedSeconds: req.ExpectedSeconds, + RetryTimes: req.RetryTimes, + RetryQueueMaxSeconds: req.RetryQueueMaxSeconds, + AutoCleanSeconds: req.AutoCleanSeconds, + Remark: req.Remark, } id, err := dao.Model.Insert(ctx, m) if err != nil { @@ -58,68 +51,223 @@ func (s *modelService) Create(ctx context.Context, req *dto.CreateModelReq) (res } func (s *modelService) Update(ctx context.Context, req *dto.UpdateModelReq) error { - data := map[string]any{} - if req.BaseURL != "" { - data[entity.AsynchModelCol.BaseURL] = req.BaseURL + //根据当前 isChatModel 来判断是否更新模型 + if req.IsChatModel == 1 { + user, err := utils.GetUserInfo(ctx) + if err != nil { + return err + } + //判断当前用户是否有会话模型 + model, err := dao.Model.GetByIsChatModel(ctx, user.UserName) + if err != nil { + return err + } + if model != nil { + return errors.New("用户已存在会话模型,不能创建新的会话模型") + } + _, err = dao.Model.Update(ctx, req) + return err } - if req.Route != "" { - data[entity.AsynchModelCol.Route] = req.Route - } - if req.HttpMethod != nil && *req.HttpMethod != "" { - data[entity.AsynchModelCol.HttpMethod] = *req.HttpMethod - } - if req.HeadMsg != nil { - data[entity.AsynchModelCol.HeadMsg] = *req.HeadMsg - } - if req.Form != nil { - data[entity.AsynchModelCol.FormJSON] = req.Form - } - if req.ModelsType != nil { - data[entity.AsynchModelCol.ModelsType] = normalizeModelsType(*req.ModelsType) - } - if req.Enabled != nil { - data[entity.AsynchModelCol.Enabled] = *req.Enabled - } - if req.MaxConcurrency != nil { - data[entity.AsynchModelCol.MaxConcurrency] = *req.MaxConcurrency - } - if req.QueueLimit != nil { - data[entity.AsynchModelCol.QueueLimit] = *req.QueueLimit - } - if req.TimeoutSeconds != nil { - data[entity.AsynchModelCol.TimeoutSeconds] = *req.TimeoutSeconds - } - if req.ExpectedSeconds != nil { - data[entity.AsynchModelCol.ExpectedSeconds] = *req.ExpectedSeconds - } - if req.RetryTimes != nil { - data[entity.AsynchModelCol.RetryTimes] = *req.RetryTimes - } - if req.RetryQueueMaxSeconds != nil { - data[entity.AsynchModelCol.RetryQueueMaxSecs] = *req.RetryQueueMaxSeconds - } - if req.AutoCleanSeconds != nil { - data[entity.AsynchModelCol.AutoCleanSeconds] = *req.AutoCleanSeconds - } - if req.Remark != nil { - data[entity.AsynchModelCol.Remark] = *req.Remark - } - if len(data) == 0 { - return errors.New("无可更新字段") - } - _, err := dao.Model.UpdateByID(ctx, req.ID, data) + _, err := dao.Model.Update(ctx, req) return err } -func (s *modelService) Delete(ctx context.Context, id int64) error { +func (s *modelService) Delete(ctx context.Context, id string) error { _, err := dao.Model.DeleteByID(ctx, id) return err } func (s *modelService) Get(ctx context.Context, id int64) (*entity.AsynchModel, error) { - return dao.Model.GetByID(ctx, id) + model, err := dao.Model.Get(ctx, id) + if err != nil { + return nil, err + } + model.Form = ParseJSONField(model.Form) + model.RequestMapping = ParseJSONField(model.RequestMapping) + model.ResponseMapping = ParseJSONField(model.ResponseMapping) + model.ResponseBody = ParseJSONField(model.ResponseBody) + return model, nil } -func (s *modelService) List(ctx context.Context, pageNum, pageSize int, modelNameLike string) (list []*entity.AsynchModel, total int64, err error) { - return dao.Model.List(ctx, pageNum, pageSize, modelNameLike) +func (s *modelService) List(ctx context.Context, pageNum, pageSize int, modelNameLike string, modelType int) (list []*entity.AsynchModel, total int64, err error) { + isSuperAdmin, err := IsSuperAdmin(ctx) + if err != nil { + return nil, 0, err + } + user, err := utils.GetUserInfo(ctx) + if err != nil { + return nil, 0, err + } + + var models []*entity.AsynchModel + var count int64 + + if isSuperAdmin { + models, count, err = dao.Model.List(ctx, pageNum, pageSize, modelNameLike, modelType) + } else { + models, count, err = s.getModelsWithDedup(ctx, user.UserName, pageNum, pageSize, modelNameLike, modelType) + } + if err != nil { + return nil, 0, err + } + + // 处理列表中每条记录的 JSONB 字段 + for _, m := range models { + m.Form = ParseJSONField(m.Form) + m.RequestMapping = ParseJSONField(m.RequestMapping) + m.ResponseMapping = ParseJSONField(m.ResponseMapping) + m.ResponseBody = ParseJSONField(m.ResponseBody) + } + return models, count, nil +} + +// getModelsWithDedup 获取普通用户的模型列表并去重 +func (s *modelService) getModelsWithDedup(ctx context.Context, creator string, pageNum, pageSize int, modelNameLike string, modelType int) (list []*entity.AsynchModel, total int64, err error) { + // 1. 查全量数据(不分页,便于去重) + allModels, err := dao.Model.GetByCreatorAndPlatform(ctx, creator, modelNameLike, modelType) + if err != nil { + return nil, 0, err + } + + // 2. 按 modelName 去重,保留当前用户的 + modelMap := make(map[string]*entity.AsynchModel) + for _, m := range allModels { + if m == nil { + continue + } + name := m.ModelName + + _, ok := modelMap[name] + if !ok { + // 没有冲突,直接放进去 + modelMap[name] = m + } else { + // 有冲突,保留当前用户创建的 + if m.Creator == creator { + modelMap[name] = m + } + // 如果现有的就是当前用户的,不做任何替换 + } + } + + // 3. 转回切片并排序 + deduped := make([]*entity.AsynchModel, 0, len(modelMap)) + for _, m := range modelMap { + deduped = append(deduped, m) + } + sort.Slice(deduped, func(i, j int) bool { + return deduped[i].CreatedAt.After(deduped[j].CreatedAt) + }) + + // 4. 手动分页 + total = int64(len(deduped)) + if pageNum > 0 && pageSize > 0 { + start := (pageNum - 1) * pageSize + if start >= len(deduped) { + return []*entity.AsynchModel{}, total, nil + } + end := start + pageSize + if end > len(deduped) { + end = len(deduped) + } + deduped = deduped[start:end] + } + return deduped, total, nil +} + +// GetModelTypesFromConfig 从配置文件读取模型类型 +func GetModelTypesFromConfig(ctx context.Context) map[int]string { + typeMap := make(map[int]string) + + // 读取配置 + configMap := g.Cfg().MustGet(ctx, "modelType.types").Map() + for k, v := range configMap { + typeID := gconv.Int(k) + typeName := gconv.String(v) + if typeID > 0 && typeName != "" { + typeMap[typeID] = typeName + } + } + // 如果配置为空,使用默认值 + if len(typeMap) == 0 { + typeMap = map[int]string{ + 1: "推理模型", + 2: "图片模型", + 3: "音频模型", + 4: "向量化模型", + 5: "全模态模型", + } + } + return typeMap +} + +func (s *modelService) UpdateChatModel(ctx context.Context, req *dto.UpdateChatModelReq) error { + user, err := utils.GetUserInfo(ctx) + if err != nil { + return err + } + + // 校验新会话模型是否存在 + newModel, err := dao.Model.Get(ctx, req.Id) + if err != nil { + return err + } + if newModel == nil { + return errors.New("新会话模型不存在") + } + + // 获取当前用户会话模型 + currentModel, err := dao.Model.GetByIsChatModel(ctx, user.UserName) + if err != nil { + return err + } + if currentModel.ModelsType != 1 { + return errors.New("当前模型为非推理模型,不能设置为会话模型") + } + + // 如果点击的就是当前会话模型(已经是1),取消它(设为0) + if currentModel != nil && currentModel.Id == req.Id { + _, err = dao.Model.UpdateByID(ctx, &dto.UpdateModelReq{ + ID: req.Id, + IsChatModel: 0, + }) + return err + } + + // 如果之前有会话模型,取消它(设为0) + if currentModel != nil { + _, err = dao.Model.UpdateByID(ctx, &dto.UpdateModelReq{ + ID: currentModel.Id, + IsChatModel: 0, + }) + if err != nil { + return err + } + } + + // 设置当前为会话模型(设为1) + _, err = dao.Model.UpdateByID(ctx, &dto.UpdateModelReq{ + ID: req.Id, + IsChatModel: 1, + }) + return err +} + +func (s *modelService) GetIsChatModel(ctx context.Context) (*entity.AsynchModel, error) { + user, err := utils.GetUserInfo(ctx) + if err != nil { + return nil, err + } + model, err := dao.Model.GetByIsChatModel(ctx, user.UserName) + if err != nil { + return nil, err + } + if model == nil { + return nil, nil + } + model.Form = ParseJSONField(model.Form) + model.RequestMapping = ParseJSONField(model.RequestMapping) + model.ResponseMapping = ParseJSONField(model.ResponseMapping) + model.ResponseBody = ParseJSONField(model.ResponseBody) + return model, nil } diff --git a/service/model_type_service.go b/service/model_type_service.go deleted file mode 100644 index d8c405c..0000000 --- a/service/model_type_service.go +++ /dev/null @@ -1,217 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "errors" - "strings" - - "model-asynch/dao" - "model-asynch/model/dto" - "model-asynch/model/entity" - - "github.com/gogf/gf/v2/container/gvar" -) - -type modelTypeService struct{} - -var ModelType = &modelTypeService{} - -func normalizeFormValue(v any) any { - // 目标:对外永远返回 JSON 数组/对象,而不是字符串。 - if v == nil { - return []any{} - } - switch t := v.(type) { - case string: - s := strings.TrimSpace(t) - if s == "" { - return []any{} - } - return normalizeFormValueFromJSONString(s) - case []byte: - if len(t) == 0 { - return []any{} - } - return normalizeFormValueFromJSONBytes(t) - case *gvar.Var: - // goframe 常见的 DB 返回类型 - if t == nil { - return []any{} - } - b := t.Bytes() - if len(b) > 0 { - return normalizeFormValueFromJSONBytes(b) - } - s := strings.TrimSpace(t.String()) - if s == "" { - return []any{} - } - return normalizeFormValueFromJSONString(s) - default: - // 尝试兼容其他“像 JSON 的值类型”(例如实现了 Bytes/String 的包装类型) - if vb, ok := v.(interface{ Bytes() []byte }); ok { - if b := vb.Bytes(); len(b) > 0 { - return normalizeFormValueFromJSONBytes(b) - } - } - if vs, ok := v.(interface{ String() string }); ok { - if s := strings.TrimSpace(vs.String()); s != "" { - return normalizeFormValueFromJSONString(s) - } - } - // 已经是 []any / map[string]any 等结构 - return v - } -} - -// 兼容“JSONB 里存了 JSON 字符串”的历史数据: -// 例如 form_json = '"[]"' 或 '"[{...}]"'(外层是字符串,内层才是数组/对象) -func normalizeFormValueFromJSONString(s string) any { - var out any - if err := json.Unmarshal([]byte(s), &out); err != nil || out == nil { - return []any{} - } - // 如果解出来还是 string,且看起来是 JSON,再解一层 - if inner, ok := out.(string); ok { - inner = strings.TrimSpace(inner) - if inner == "" { - return []any{} - } - if strings.HasPrefix(inner, "[") || strings.HasPrefix(inner, "{") { - var out2 any - if err := json.Unmarshal([]byte(inner), &out2); err == nil && out2 != nil { - return out2 - } - } - return []any{} - } - return out -} - -func normalizeFormValueFromJSONBytes(b []byte) any { - var out any - if err := json.Unmarshal(b, &out); err != nil || out == nil { - return []any{} - } - // bytes 解出来也可能是 string(同上) - if inner, ok := out.(string); ok { - return normalizeFormValueFromJSONString(inner) - } - return out -} - -func (s *modelTypeService) Create(ctx context.Context, req *dto.CreateModelTypeReq) (res *dto.CreateModelTypeRes, err error) { - t := &entity.AsynchModelType{ - TypeID: req.TypeID, - TypeName: req.TypeName, - Remark: req.Remark, - } - id, err := dao.ModelType.Insert(ctx, t) - if err != nil { - return nil, err - } - return &dto.CreateModelTypeRes{ID: id}, nil -} - -func (s *modelTypeService) Update(ctx context.Context, req *dto.UpdateModelTypeReq) error { - data := map[string]any{} - if req.TypeID != nil { - data[entity.AsynchModelTypeCol.TypeID] = *req.TypeID - } - if req.TypeName != nil { - data[entity.AsynchModelTypeCol.TypeName] = *req.TypeName - } - if req.Remark != nil { - data[entity.AsynchModelTypeCol.Remark] = *req.Remark - } - if len(data) == 0 { - return errors.New("无可更新字段") - } - _, err := dao.ModelType.UpdateByID(ctx, req.ID, data) - return err -} - -func (s *modelTypeService) Delete(ctx context.Context, id int64) error { - _, err := dao.ModelType.DeleteByID(ctx, id) - return err -} - -func (s *modelTypeService) Get(ctx context.Context, id int64) (*entity.AsynchModelType, error) { - return dao.ModelType.GetByID(ctx, id) -} - -func (s *modelTypeService) List(ctx context.Context, pageNum, pageSize int, typeNameLike string) (list []*entity.AsynchModelType, total int64, err error) { - return dao.ModelType.List(ctx, pageNum, pageSize, typeNameLike) -} - -// ListWithModels 按类型分组返回模型(返回数组,便于前端直接渲染) -func (s *modelTypeService) ListWithModels(ctx context.Context, req *dto.ListModelTypeWithModelsReq) (res []dto.ModelTypeWithModelsItem, err error) { - types, _, err := dao.ModelType.List(ctx, 1, 1000, "") - if err != nil { - return nil, err - } - // 过滤类型(按 typeId / typeName 模糊) - filterTypeID := 0 - filterTypeName := "" - if req != nil { - filterTypeID = req.TypeID - filterTypeName = strings.TrimSpace(req.Type) - } - typeIDs := make([]int, 0, len(types)) - typeNameMap := make(map[int]string, len(types)) - for _, t := range types { - if t == nil { - continue - } - if filterTypeID > 0 && t.TypeID != filterTypeID { - continue - } - if filterTypeName != "" && !strings.Contains(t.TypeName, filterTypeName) { - continue - } - typeIDs = append(typeIDs, t.TypeID) - typeNameMap[t.TypeID] = t.TypeName - } - models, err := dao.Model.ListAll(ctx) - if err != nil { - return nil, err - } - itemsMap := map[int][]dto.ModelTypeModelItem{} - for _, m := range models { - if m == nil { - continue - } - form := normalizeFormValue(m.Form) - // 一个模型可能支持多个类型:models_type="1,2,3" - for _, tid := range parseModelsTypeIDs(m.ModelsType) { - // 若请求过滤了类型,则只输出该类型 - if filterTypeID > 0 && tid != filterTypeID { - continue - } - if filterTypeName != "" { - if _, ok := typeNameMap[tid]; !ok { - continue - } - } - itemsMap[tid] = append(itemsMap[tid], dto.ModelTypeModelItem{ - ID: m.Id, - Name: m.ModelName, - Form: form, - }) - } - } - out := make([]dto.ModelTypeWithModelsItem, 0, len(typeIDs)) - for _, tid := range typeIDs { - items := itemsMap[tid] - if items == nil { - items = make([]dto.ModelTypeModelItem, 0) - } - out = append(out, dto.ModelTypeWithModelsItem{ - TypeID: tid, - Type: typeNameMap[tid], - Items: items, - }) - } - return out, nil -} diff --git a/service/model_types_util.go b/service/model_types_util.go deleted file mode 100644 index c280987..0000000 --- a/service/model_types_util.go +++ /dev/null @@ -1,52 +0,0 @@ -package service - -import ( - "sort" - "strconv" - "strings" -) - -// normalizeModelsType 将 "1, 2,2,3" 归一化为 "1,2,3" -// - 去空格 -// - 去重 -// - 升序排序 -func normalizeModelsType(v string) string { - ids := parseModelsTypeIDs(v) - if len(ids) == 0 { - return "" - } - parts := make([]string, 0, len(ids)) - for _, id := range ids { - parts = append(parts, strconv.Itoa(id)) - } - return strings.Join(parts, ",") -} - -// parseModelsTypeIDs 解析 models_type 字段(支持 "1,2,3"),返回去重后的 int 列表(升序)。 -func parseModelsTypeIDs(v string) []int { - v = strings.TrimSpace(v) - if v == "" { - return nil - } - raw := strings.Split(v, ",") - seen := map[int]struct{}{} - out := make([]int, 0, len(raw)) - for _, s := range raw { - s = strings.TrimSpace(s) - if s == "" || s == "0" { - continue - } - id, err := strconv.Atoi(s) - if err != nil || id <= 0 { - continue - } - if _, ok := seen[id]; ok { - continue - } - seen[id] = struct{}{} - out = append(out, id) - } - sort.Ints(out) - return out -} - diff --git a/service/stat_service.go b/service/stat_service.go index 8509e96..c068e99 100644 --- a/service/stat_service.go +++ b/service/stat_service.go @@ -13,12 +13,12 @@ var Stat = &statService{} func (s *statService) List(ctx context.Context, req *dto.ListModelStatReq) (res *dto.ListModelStatRes, err error) { pageNum, pageSize := 1, 10 - if req != nil && req.Page != nil { - if req.Page.PageNum > 0 { - pageNum = int(req.Page.PageNum) + if req != nil { + if req.PageNum > 0 { + pageNum = req.PageNum } - if req.Page.PageSize > 0 { - pageSize = int(req.Page.PageSize) + if req.PageSize > 0 { + pageSize = req.PageSize } } startDay, endDay := "", "" @@ -37,4 +37,3 @@ func (s *statService) List(ctx context.Context, req *dto.ListModelStatReq) (res } return &dto.ListModelStatRes{List: list, Total: total}, nil } - diff --git a/service/storage_oss.go b/service/storage_oss.go index 60b09f8..a5295c6 100644 --- a/service/storage_oss.go +++ b/service/storage_oss.go @@ -62,7 +62,6 @@ func (s *ossStorage) UploadByTask(ctx context.Context, _ *entity.AsynchTask, dat if err := commonHttp.Post(ctx, fullURL, headers, &resp, body.Bytes()); err != nil { return "", err } - fmt.Println("打印结果 resp:", resp) g.Log().Infof(ctx, "[OSS] upload success url=%s size=%d format=%s", resp.FileURL, resp.FileSize, resp.FileFormat) return resp.FileURL, nil } diff --git a/service/task_service.go b/service/task_service.go index 11b6d5a..e28cf32 100644 --- a/service/task_service.go +++ b/service/task_service.go @@ -58,9 +58,10 @@ func (s *taskService) Create(ctx context.Context, req *dto.CreateTaskReq) (res * State: 0, BizName: req.BizName, CallbackURL: req.CallbackUrl, - ModelKey: req.ModelKey, + ModelKey: m.ApiKey, InputRef: req.InputRef, RequestPayload: storedPayload, + EpicycleId: req.EpicycleId, } _, err = dao.Task.Insert(ctx, t) if err != nil { @@ -80,7 +81,7 @@ func (s *taskService) Create(ctx context.Context, req *dto.CreateTaskReq) (res * apiPath = r.URL.Path httpMethod = r.Method } - _, _ = dao.OpLog.Insert(ctx, &entity.AsynchOpLog{ + _, _ = dao.OpLog.Insert(ctx, &entity.LogsModelOp{ IP: ip, UserAgent: ua, APIPath: apiPath, @@ -97,9 +98,80 @@ func (s *taskService) Create(ctx context.Context, req *dto.CreateTaskReq) (res * "taskId": taskID, }, }) + + // 4) 创建成功后立即异步尝试执行当前任务,并仅在任务仍处于 pending(state=0) 时做定向轮询。 + // 一旦任务进入 running/success/failed/downloaded,就停止轮询,避免一直空转。 + go s.pollAndRunUntilPicked(context.WithoutCancel(ctx), taskID, req.EpicycleId) + return &dto.CreateTaskRes{TaskID: taskID}, nil } +// pollAndRunUntilPicked 用于 createTask 创建后的“轻量级定向轮询”: +// - 目标:尽快把刚创建的任务拉起来执行 +// - 只在任务仍为 pending(state=0) 时继续尝试抢占 +// - 一旦任务进入 running(1) / success(2) / failed(3) / downloaded(4),立即停止 +// - 这样不会无限轮询;runWork 仍负责处理积压队列和未处理到的任务 +func (s *taskService) pollAndRunUntilPicked(ctx context.Context, taskID string, epicycleId int64) { + if taskID == "" { + return + } + interval := g.Cfg().MustGet(ctx, "asynch.worker.intervalSeconds").Int() + if interval <= 0 { + interval = 5 + } + g.Log().Infof(ctx, "[task-auto-run][start] taskId=%s interval=%ds", taskID, interval) + + ticker := time.NewTicker(time.Duration(interval) * time.Second) + defer ticker.Stop() + + tryRun := func() bool { + t, err := dao.Task.GetByTaskID(ctx, taskID) + if err != nil { + g.Log().Warningf(ctx, "[task-auto-run][stop] taskId=%s reason=query_failed err=%v", taskID, err) + return true + } + if t == nil { + g.Log().Warningf(ctx, "[task-auto-run][stop] taskId=%s reason=task_not_found", taskID) + return true + } + switch t.State { + case 0: + if err := AsyncWorker.RunByTaskID(ctx, taskID, epicycleId); err != nil { + g.Log().Warningf(ctx, "[task-auto-run][retry] taskId=%s state=0 err=%v", taskID, err) + } else { + g.Log().Infof(ctx, "[task-auto-run][triggered] taskId=%s state=0", taskID) + } + return false + case 1: + g.Log().Infof(ctx, "[task-auto-run][stop] taskId=%s reason=running", taskID) + return true + case 2, 3, 4: + g.Log().Infof(ctx, "[task-auto-run][stop] taskId=%s reason=terminal state=%d", taskID, t.State) + return true + default: + g.Log().Infof(ctx, "[task-auto-run][stop] taskId=%s reason=unknown_state state=%d", taskID, t.State) + return true + } + } + + // 先立即尝试一次 + if stop := tryRun(); stop { + return + } + + for { + select { + case <-ctx.Done(): + g.Log().Infof(ctx, "[task-auto-run][stop] taskId=%s reason=context_done", taskID) + return + case <-ticker.C: + if stop := tryRun(); stop { + return + } + } + } +} + func (s *taskService) GetResult(ctx context.Context, taskID string) (res *dto.GetTaskResultRes, err error) { t, err := dao.Task.GetByTaskID(ctx, taskID) if err != nil { @@ -168,12 +240,12 @@ func (s *taskService) GetBatch(ctx context.Context, req *dto.GetTaskBatchReq) (r func (s *taskService) List(ctx context.Context, req *dto.ListTaskReq) (res *dto.ListTaskRes, err error) { pageNum, pageSize := 1, 10 - if req != nil && req.Page != nil { - if req.Page.PageNum > 0 { - pageNum = int(req.Page.PageNum) + if req != nil { + if req.PageNum > 0 { + pageNum = req.PageNum } - if req.Page.PageSize > 0 { - pageSize = int(req.Page.PageSize) + if req.PageSize > 0 { + pageSize = req.PageSize } } modelName := "" diff --git a/service/utils.go b/service/utils.go new file mode 100644 index 0000000..72e4367 --- /dev/null +++ b/service/utils.go @@ -0,0 +1,113 @@ +package service + +import ( + "encoding/json" + "strings" + + "github.com/gogf/gf/v2/container/gvar" +) + +func normalizeFormValue(v any) any { + // 目标:对外永远返回 JSON 数组/对象,而不是字符串。 + if v == nil { + return []any{} + } + switch t := v.(type) { + case string: + s := strings.TrimSpace(t) + if s == "" { + return []any{} + } + return normalizeFormValueFromJSONString(s) + case []byte: + if len(t) == 0 { + return []any{} + } + return normalizeFormValueFromJSONBytes(t) + case *gvar.Var: + // goframe 常见的 DB 返回类型 + if t == nil { + return []any{} + } + b := t.Bytes() + if len(b) > 0 { + return normalizeFormValueFromJSONBytes(b) + } + s := strings.TrimSpace(t.String()) + if s == "" { + return []any{} + } + return normalizeFormValueFromJSONString(s) + default: + // 尝试兼容其他“像 JSON 的值类型”(例如实现了 Bytes/String 的包装类型) + if vb, ok := v.(interface{ Bytes() []byte }); ok { + if b := vb.Bytes(); len(b) > 0 { + return normalizeFormValueFromJSONBytes(b) + } + } + if vs, ok := v.(interface{ String() string }); ok { + if s := strings.TrimSpace(vs.String()); s != "" { + return normalizeFormValueFromJSONString(s) + } + } + // 已经是 []any / map[string]any 等结构 + return v + } +} + +// 兼容“JSONB 里存了 JSON 字符串”的历史数据: +// 例如 form_json = '"[]"' 或 '"[{...}]"'(外层是字符串,内层才是数组/对象) +func normalizeFormValueFromJSONString(s string) any { + var out any + if err := json.Unmarshal([]byte(s), &out); err != nil || out == nil { + return []any{} + } + // 如果解出来还是 string,且看起来是 JSON,再解一层 + if inner, ok := out.(string); ok { + inner = strings.TrimSpace(inner) + if inner == "" { + return []any{} + } + if strings.HasPrefix(inner, "[") || strings.HasPrefix(inner, "{") { + var out2 any + if err := json.Unmarshal([]byte(inner), &out2); err == nil && out2 != nil { + return out2 + } + } + return []any{} + } + return out +} + +func normalizeFormValueFromJSONBytes(b []byte) any { + var out any + if err := json.Unmarshal(b, &out); err != nil || out == nil { + return []any{} + } + // bytes 解出来也可能是 string(同上) + if inner, ok := out.(string); ok { + return normalizeFormValueFromJSONString(inner) + } + return out +} + +func ParseJSONField(field any) any { + var v *gvar.Var + switch val := field.(type) { + case *gvar.Var: + v = val + default: + return field + } + + if v == nil || v.IsNil() || v.IsEmpty() { + return nil + } + + str := v.String() + var result any + if json.Unmarshal([]byte(str), &result) == nil { + return result + } + return str +} diff --git a/service/worker.go b/service/worker.go index 243ed61..7dbed6a 100644 --- a/service/worker.go +++ b/service/worker.go @@ -5,12 +5,14 @@ import ( "fmt" "strings" "time" + "unicode/utf8" "model-asynch/dao" "model-asynch/model/entity" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/grpool" + "github.com/tidwall/gjson" ) var AsyncWorker = &asyncWorker{} @@ -43,7 +45,7 @@ func (w *asyncWorker) RunOnce(ctx context.Context, batchSize, goroutines int) (c for _, t := range tasks { task := t _ = pool.AddWithRecover(ctx, func(ctx context.Context) { - w.handleOne(ctx, task) + w.handleOne(ctx, task, 0) done <- struct{}{} }, func(ctx context.Context, e error) { if e != nil { @@ -59,8 +61,23 @@ func (w *asyncWorker) RunOnce(ctx context.Context, batchSize, goroutines int) (c return claimed, nil } -func (w *asyncWorker) handleOne(ctx context.Context, t *entity.AsynchTask) { - // 从任务入库的 request_payload 里恢复 payload + headers,给 OSS 上传透传鉴权用 +// RunByTaskID 创建任务后立即异步尝试执行当前任务: +// - 只定向抢占当前 taskId 对应的 pending 任务 +// - 若任务已被其它 worker 抢走/已不在 pending,则直接返回 +func (w *asyncWorker) RunByTaskID(ctx context.Context, taskID string, epicycleId int64) error { + task, err := dao.Task.ClaimPendingByTaskIDGlobal(ctx, taskID) + if err != nil { + return err + } + if task == nil { + return nil + } + w.handleOne(ctx, task, epicycleId) + return nil +} + +func (w *asyncWorker) handleOne(ctx context.Context, t *entity.AsynchTask, epicycleId int64) { + // 从任务入库的 request_payload 里恢复 payload + headers payload, headers := parseStoredPayload(t.RequestPayload) if len(headers) > 0 { ctx = setTaskHeadersToCtx(ctx, headers) @@ -71,26 +88,42 @@ func (w *asyncWorker) handleOne(ctx context.Context, t *entity.AsynchTask) { if err != nil { _ = dao.Task.UpdateFailedGlobal(ctx, t.Id, err.Error()) ReleaseQueueSlot(ctx, t.ModelName, t.TaskID) + // ============ 失败回调 ============ + t.State = 3 + t.ErrorMsg = err.Error() + go triggerCallback(context.WithoutCancel(ctx), t) + // ================================ return } if m == nil || m.Enabled != 1 { - _ = dao.Task.UpdateFailedGlobal(ctx, t.Id, "模型不存在或未启用") + errMsg := "模型不存在或未启用" + _ = dao.Task.UpdateFailedGlobal(ctx, t.Id, errMsg) ReleaseQueueSlot(ctx, t.ModelName, t.TaskID) + // ============ 失败回调 ============ + t.State = 3 + t.ErrorMsg = errMsg + go triggerCallback(context.WithoutCancel(ctx), t) + // ================================ return } - // 2) 分布式并发限制(按 model_name 全局维度) + // 2) 分布式并发限制 semKey := fmt.Sprintf("asynch:sem:%s", t.ModelName) - leaseSeconds := int64(3600) // 兜底1小时 + leaseSeconds := int64(3600) maxC := GetRuntimeMaxConcurrency(ctx, t.ModelName, m.MaxConcurrency) acquired, err := acquireSemaphore(ctx, semKey, maxC, leaseSeconds) if err != nil { _ = dao.Task.UpdateFailedGlobal(ctx, t.Id, err.Error()) ReleaseQueueSlot(ctx, t.ModelName, t.TaskID) + // ============ 失败回调 ============ + t.State = 3 + t.ErrorMsg = err.Error() + go triggerCallback(context.WithoutCancel(ctx), t) + // ================================ return } if !acquired { - // 并发满了:放回排队(重新置回 state=0),下一轮再抢占 + // 并发满了:放回排队,不回调(不是失败) _ = w.rollbackToPending(ctx, t.Id) return } @@ -109,30 +142,40 @@ func (w *asyncWorker) handleOne(ctx context.Context, t *entity.AsynchTask) { data []byte contentType string ext string + textResult string ) - // phase=1 表示模型已成功但 OSS 上传失败:优先从临时文件加载,避免重复跑模型 + // phase=1 表示模型已成功但 OSS 上传失败:优先从临时文件加载 if t.Phase == 1 && strings.TrimSpace(t.TmpFile) != "" { data, err = loadTmpResult(t.TmpFile) if err == nil && len(data) > 0 { contentType, ext = DetectFileType(data) } else { - // 临时文件不可用:回退重新调用模型 data = nil } } if data == nil { - // 统计:仅在真正请求模型时 +1(OSS 重试不计入) + // 统计 _ = dao.Stat.IncRequestCount(ctx, time.Now(), int64(t.TenantId), t.Creator, t.ModelName) - + // 核心调用 data, err = InvokeModel(ctx, m, payload, t.ModelKey) if err != nil { _ = dao.Task.UpdateFailedGlobal(ctx, t.Id, err.Error()) ReleaseQueueSlot(ctx, t.ModelName, t.TaskID) + // ============ 失败回调 ============ + t.State = 3 + t.ErrorMsg = err.Error() + go triggerCallback(context.WithoutCancel(ctx), t) + // ================================ return } contentType, ext = DetectFileType(data) - // 将模型输出写入临时文件,后续若 OSS 失败可只重试 OSS + if utf8.Valid(data) && (strings.HasPrefix(contentType, "text/") || contentType == "application/json") { + textResult = string(data) + if len(textResult) > 20000 { + textResult = textResult[:20000] + } + } tmpPath, err := saveTmpResult(t.TaskID, data, ext) if err == nil && tmpPath != "" { t.TmpFile = tmpPath @@ -147,26 +190,46 @@ func (w *asyncWorker) handleOne(ctx context.Context, t *entity.AsynchTask) { // OSS 阶段失败:保留临时文件,下一轮仅重试 OSS _ = dao.Task.UpdateFailedKeepTmpGlobal(ctx, t.Id, err.Error()) ReleaseQueueSlot(ctx, t.ModelName, t.TaskID) + // ============ OSS失败不回调(还会重试) ============ + // 注意:OSS失败保留临时文件,下次重试,所以这里不触发最终回调 + // 如果已经重试多次还没成功,需要在任务超时或超过最大重试次数时才回调失败 return } // 5) 更新任务状态成功 - // 注意:expire_at 的计算改为“已下载(state=4)后开始计时”,因此成功(state=2)不写 expire_at。 fileType := strings.TrimPrefix(ext, ".") if fileType == "" { fileType = contentType } - if err := dao.Task.UpdateSuccessGlobal(ctx, t.Id, ossURL, fileType, int64(len(data)), nil); err != nil { + if err := dao.Task.UpdateSuccessGlobal( + ctx, + t.Id, + ossURL, + fileType, + textResult, + int64(len(data)), + nil, + GetExpendTokens(m.TokenMapping, textResult), + ); err != nil { g.Log().Errorf(ctx, "[worker] update success failed: %v", err) return } - // 成功/失败均不再占用 queue_limit(state=0/1 才占用) + + // 成功/失败均不再占用 queue_limit ReleaseQueueSlot(ctx, t.ModelName, t.TaskID) - // 6) 成功回调(不影响主流程) + + // 6) 成功回调 t.State = 2 t.OssFile = ossURL t.FileType = fileType - go triggerSuccessCallback(context.WithoutCancel(ctx), t) + t.TextResult = textResult + g.Log().Infof(ctx, "[CALLBACK][DISPATCH] taskId=%s bizName=%s callbackUrl=%s", t.TaskID, t.BizName, t.CallbackURL) + go triggerCallback(context.WithoutCancel(ctx), t) + // ============ 如果有 epicycleId,也触发业务回调 ============ + if epicycleId != 0 { + go triggerPromptsCallback(context.WithoutCancel(ctx), t, epicycleId) + } + // 成功后清理临时文件 deleteTmpResult(t.TmpFile) } @@ -174,3 +237,13 @@ func (w *asyncWorker) handleOne(ctx context.Context, t *entity.AsynchTask) { func (w *asyncWorker) rollbackToPending(ctx context.Context, id int64) error { return dao.Task.RollbackToPendingGlobal(ctx, id) } + +// GetExpendTokens 根据映射路径从 textResult 中提取消耗 token 值 +func GetExpendTokens(tokenMapping string, textResult string) int { + value := gjson.Get(textResult, tokenMapping) + if value.Exists() { + return int(value.Int()) + } else { + return len(textResult) + } +} diff --git a/update.sql b/update.sql index fb1a2e1..9f740ab 100644 --- a/update.sql +++ b/update.sql @@ -1,164 +1,130 @@ --- model-asynch 核心表(pgsql) --- 1) asynch_models_type:模型类型 --- 2) asynch_models:模型配置 --- 3) asynch_task:异步任务 --- 4) asynch_op_log:操作日志(统计用) --- 5) asynch_model_stat:按天模型请求统计(限流/监控用) - --- ========================= --- 0) asynch_models_type --- ========================= -CREATE TABLE IF NOT EXISTS asynch_models_type ( - -- 基础字段(与现有表保持一致) - id BIGINT PRIMARY KEY, -- 主键ID(非自增) - tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID - creator VARCHAR(64) NOT NULL, -- 创建人 - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 - updater VARCHAR(64) NOT NULL, -- 更新人 - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 - deleted_at TIMESTAMP(6), -- 删除时间(软删) - - -- 业务字段 - type_id INT NOT NULL, -- 模型类型ID(业务枚举) - type_name VARCHAR(64) NOT NULL, -- 模型类型名称(图片模型/音频模型/...) - remark TEXT DEFAULT '' -- 备注 -); - -CREATE UNIQUE INDEX IF NOT EXISTS uk_asynch_models_type_tenant_type_id - ON asynch_models_type(tenant_id, type_id); -CREATE INDEX IF NOT EXISTS idx_asynch_models_type_tenant_id ON asynch_models_type(tenant_id); -CREATE INDEX IF NOT EXISTS idx_asynch_models_type_type_name ON asynch_models_type(type_name); -CREATE INDEX IF NOT EXISTS idx_asynch_models_type_deleted_at ON asynch_models_type(deleted_at); - -COMMENT ON TABLE asynch_models_type IS '模型类型表'; -COMMENT ON COLUMN asynch_models_type.id IS '主键ID(非自增)'; -COMMENT ON COLUMN asynch_models_type.tenant_id IS '租户ID'; -COMMENT ON COLUMN asynch_models_type.creator IS '创建人'; -COMMENT ON COLUMN asynch_models_type.created_at IS '创建时间'; -COMMENT ON COLUMN asynch_models_type.updater IS '更新人'; -COMMENT ON COLUMN asynch_models_type.updated_at IS '更新时间'; -COMMENT ON COLUMN asynch_models_type.deleted_at IS '删除时间(软删)'; -COMMENT ON COLUMN asynch_models_type.type_id IS '模型类型ID(业务枚举)'; -COMMENT ON COLUMN asynch_models_type.type_name IS '模型类型名称'; -COMMENT ON COLUMN asynch_models_type.remark IS '备注'; - +-- model-asynch 核心表(pgsql) +-- 1) asynch_models:模型配置 +-- 2) asynch_task:异步任务 +-- 3) logs_model_op:操作日志(统计用) +-- 4) logs_model_stat:按天模型请求统计(限流/监控用) -- ========================= -- 1) asynch_models -- ========================= CREATE TABLE IF NOT EXISTS asynch_models ( - -- 基础字段(与现有表保持一致) - id BIGINT PRIMARY KEY, -- 主键ID(非自增) - tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID - creator VARCHAR(64) NOT NULL, -- 创建人 - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 - updater VARCHAR(64) NOT NULL, -- 更新人 - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 - deleted_at TIMESTAMP(6), -- 删除时间(软删) - + -- 基础字段 + id BIGINT PRIMARY KEY, -- 主键ID(非自增) + tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID + creator VARCHAR(64) NOT NULL, -- 创建人 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + updater VARCHAR(64) NOT NULL, -- 更新人 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 + deleted_at TIMESTAMP(6), -- 删除时间(软删) -- 业务字段 - model_name VARCHAR(128) NOT NULL, -- 模型名称(路由键) - models_type VARCHAR(128) NOT NULL DEFAULT '', -- 模型类型ID列表(逗号分隔),示例:1,2,3(关联 asynch_models_type.type_id) - base_url VARCHAR(256) NOT NULL, -- 模型服务基础地址(如 http://1.2.3.4:8080) - route VARCHAR(256) NOT NULL DEFAULT '',-- 模型服务路由(如 /v1/infer) - http_method VARCHAR(8) NOT NULL DEFAULT 'POST', -- 请求方式:GET/POST - head_msg VARCHAR(1024) DEFAULT '', -- 请求头绑定(支持多个,逗号分隔):X-API-Key:xxx,operation:true - form_json JSONB NOT NULL DEFAULT '[]'::jsonb, -- 动态表单配置(JSON数组),用于前端渲染 - - enabled SMALLINT NOT NULL DEFAULT 1, -- 是否启用:1启用/0停用 - max_concurrency INT NOT NULL DEFAULT 10, -- 单模型最大并发 - queue_limit INT NOT NULL DEFAULT 1000, -- 排队上限(近似控制) - timeout_seconds INT NOT NULL DEFAULT 60, -- 调用模型服务超时(秒) - expected_seconds INT NOT NULL DEFAULT 0, -- 模型预计执行时间(秒,用于超时判定/排队策略等) - - retry_times SMALLINT NOT NULL DEFAULT 0, -- 失败后最多再重试 N 次(不含首次) - retry_queue_max_seconds INT NOT NULL DEFAULT 0, -- 失败重试最大排队时间(秒):0=插队到队首;>0=排队超过该时间后插队,否则仍到队尾 - - auto_clean_seconds INT NOT NULL DEFAULT 86400, -- 已下载(state=4)后的保留时间(秒) - remark TEXT DEFAULT '' -- 备注 + model_name VARCHAR(128) NOT NULL, -- 模型名称 + models_type SMALLINT NOT NULL DEFAULT 0, -- 模型类型 + base_url VARCHAR(256) NOT NULL, -- 模型地址 + http_method VARCHAR(8) NOT NULL DEFAULT 'POST', -- 请求方式 GET/POST + head_msg VARCHAR(1024) DEFAULT '', -- 请求头绑定(支持多个,逗号分隔)示例 X-API:xxx,operation:true + is_private SMALLINT NOT NULL DEFAULT 0, -- 是否私有化 0-私有 1-公共 + enabled SMALLINT NOT NULL DEFAULT 1, -- 是否启用 0停用 1-启用 + is_chat_model SMALLINT NOT NULL DEFAULT 0, -- 是否为对话模型 0-否 1-是 + api_key VARCHAR(256) NOT NULL DEFAULT '', -- 调用凭证,密钥 + prompt TEXT NOT NULL DEFAULT '', -- 提示词内容(文本) + form_json JSONB NOT NULL DEFAULT '{}'::jsonb, -- 表单结构(用于前端渲染) + request_mapping JSONB NOT NULL DEFAULT '{}'::jsonb -- 请求映射 + response_mapping JSONB NOT NULL DEFAULT '{}'::jsonb, -- 返回映射 + response_body JSONB NOT NULL DEFAULT '{}'::jsonb, -- 返回主体 + max_concurrency INT NOT NULL DEFAULT 10, -- 单模型最大并发 + queue_limit INT NOT NULL DEFAULT 1000, -- 排队上限(近似控制) + timeout_seconds INT NOT NULL DEFAULT 600, -- 调用模型服务超时(秒) + expected_seconds INT NOT NULL DEFAULT 600, -- 模型预计执行时间(秒) + retry_times SMALLINT NOT NULL DEFAULT 3, -- 失败重试次数 + retry_queue_max_seconds INT NOT NULL DEFAULT 600, -- 失败重试最大排队时间(秒 0=插队到队首;>0=排队超过该时间后插队,否则仍到队尾) + auto_clean_seconds INT NOT NULL DEFAULT 86400, -- 已下载(state=4 后的保留时间(秒),到期清理) + remark TEXT DEFAULT '' -- 备注 + token_mapping VARCHAR(128) NOT NULL DEFAULT ''; -- token 映射 ); -CREATE UNIQUE INDEX IF NOT EXISTS uk_asynch_models_tenant_model_name - ON asynch_models(tenant_id, model_name); +CREATE UNIQUE INDEX IF NOT EXISTS uk_asynch_models_tenant_creator_chat ON asynch_models(tenant_id, creator) WHERE is_chat_model = 1 AND deleted_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS uk_asynch_models_tenant_model_name ON asynch_models(tenant_id, creator, model_name); CREATE INDEX IF NOT EXISTS idx_asynch_models_tenant_id ON asynch_models(tenant_id); CREATE INDEX IF NOT EXISTS idx_asynch_models_model_name ON asynch_models(model_name); CREATE INDEX IF NOT EXISTS idx_asynch_models_models_type ON asynch_models(models_type); CREATE INDEX IF NOT EXISTS idx_asynch_models_enabled ON asynch_models(enabled); CREATE INDEX IF NOT EXISTS idx_asynch_models_deleted_at ON asynch_models(deleted_at); -COMMENT ON TABLE asynch_models IS '异步模型表(模型服务配置)'; -COMMENT ON COLUMN asynch_models.id IS '主键ID(非自增)'; +COMMENT ON TABLE asynch_models IS '模型配置表'; +COMMENT ON COLUMN asynch_models.id IS '主键ID(非自增)'; COMMENT ON COLUMN asynch_models.tenant_id IS '租户ID'; COMMENT ON COLUMN asynch_models.creator IS '创建人'; COMMENT ON COLUMN asynch_models.created_at IS '创建时间'; COMMENT ON COLUMN asynch_models.updater IS '更新人'; COMMENT ON COLUMN asynch_models.updated_at IS '更新时间'; -COMMENT ON COLUMN asynch_models.deleted_at IS '删除时间(软删)'; -COMMENT ON COLUMN asynch_models.model_name IS '模型名称(路由键)'; -COMMENT ON COLUMN asynch_models.models_type IS '模型类型ID列表(逗号分隔),示例:1,2,3(关联 asynch_models_type.type_id)'; -COMMENT ON COLUMN asynch_models.base_url IS '模型服务基础地址(如 http://1.2.3.4:8080)'; -COMMENT ON COLUMN asynch_models.route IS '模型服务路由(如 /v1/infer)'; -COMMENT ON COLUMN asynch_models.http_method IS '请求方式:GET/POST'; -COMMENT ON COLUMN asynch_models.head_msg IS '请求头绑定(支持多个,逗号分隔):X-API-Key:xxx,operation:true'; -COMMENT ON COLUMN asynch_models.form_json IS '动态表单配置(JSON数组),用于前端渲染'; -COMMENT ON COLUMN asynch_models.enabled IS '是否启用:1启用/0停用'; +COMMENT ON COLUMN asynch_models.deleted_at IS '删除时间(软删)'; + +COMMENT ON COLUMN asynch_models.model_name IS '模型名称'; +COMMENT ON COLUMN asynch_models.models_type IS '模型类型'; +COMMENT ON COLUMN asynch_models.base_url IS '模型地址'; +COMMENT ON COLUMN asynch_models.http_method IS '请求方式 GET/POST'; +COMMENT ON COLUMN asynch_models.head_msg IS '请求头绑定(支持多个,逗号分隔)示例 X-API:xxx,operation:true'; +COMMENT ON COLUMN asynch_models.is_private IS '是否私有化 0-私有 1-公共'; +COMMENT ON COLUMN asynch_models.enabled IS '是否启用 0停用 1-启用'; +COMMENT ON COLUMN asynch_models.is_chat_model IS '是否为对话模型 0-否 1-是'; +COMMENT ON COLUMN asynch_models.api_key IS '调用凭证,密钥'; +COMMENT ON COLUMN asynch_models.prompt IS '提示词内容(文本)'; +COMMENT ON COLUMN asynch_models.form_json IS '表单结构(用于前端渲染,也用于后端校验)'; +COMMENT ON COLUMN asynch_models.request_mapping IS '请求映射'; +COMMENT ON COLUMN asynch_models.response_mapping IS '返回映射'; +COMMENT ON COLUMN asynch_models.response_body IS '返回主体'; COMMENT ON COLUMN asynch_models.max_concurrency IS '单模型最大并发'; -COMMENT ON COLUMN asynch_models.queue_limit IS '排队上限(近似控制)'; -COMMENT ON COLUMN asynch_models.timeout_seconds IS '调用模型服务超时(秒)'; -COMMENT ON COLUMN asynch_models.expected_seconds IS '模型预计执行时间(秒,用于超时判定/排队策略等)'; -COMMENT ON COLUMN asynch_models.retry_times IS '失败后最多再重试 N 次(不含首次)'; -COMMENT ON COLUMN asynch_models.retry_queue_max_seconds IS '失败重试最大排队时间(秒):0=插队到队首;>0=排队超过该时间后插队,否则仍到队尾'; -COMMENT ON COLUMN asynch_models.auto_clean_seconds IS '已下载(state=4)后的保留时间(秒),到期清理'; +COMMENT ON COLUMN asynch_models.queue_limit IS '排队上限(近似控制)'; +COMMENT ON COLUMN asynch_models.timeout_seconds IS '调用模型服务超时(秒)'; +COMMENT ON COLUMN asynch_models.expected_seconds IS '模型预计执行时间(秒)'; +COMMENT ON COLUMN asynch_models.retry_times IS '失败重试次数'; +COMMENT ON COLUMN asynch_models.retry_queue_max_seconds IS '失败重试最大排队时间(秒 0=插队到队首;>0=排队超过该时间后插队,否则仍到队尾)'; +COMMENT ON COLUMN asynch_models.auto_clean_seconds IS '已下载(state=4 后的保留时间(秒),到期清理)'; COMMENT ON COLUMN asynch_models.remark IS '备注'; +COMMENT ON COLUMN asynch_models.token_mapping IS 'token映射'; + -- ========================= -- 2) asynch_task -- ========================= CREATE TABLE IF NOT EXISTS asynch_task ( - -- 基础字段(与现有表保持一致) - id BIGINT PRIMARY KEY, -- 主键ID(非自增) - tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID - creator VARCHAR(64) NOT NULL, -- 创建人 - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 - updater VARCHAR(64) NOT NULL, -- 更新人 - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 - deleted_at TIMESTAMP(6), -- 删除时间(软删) - - -- 任务核心字段 - model_name VARCHAR(128) NOT NULL, -- 模型名称 - task_id VARCHAR(64) NOT NULL, -- 任务ID(对外返回) - biz_name VARCHAR(128) NOT NULL DEFAULT '', -- 业务名称(调用方模块/系统) - callback_url VARCHAR(512) DEFAULT '', -- 回调地址(可选,用于后续业务通知) - model_key VARCHAR(1024) DEFAULT '', -- 动态请求头(用于覆盖/补充模型配置 head_msg),如 X-API-Key:xxx - state SMALLINT NOT NULL DEFAULT 0, -- 0排队中/1执行中/2成功/3失败/4已下载 - - oss_file VARCHAR(512) DEFAULT '', -- 结果文件OSS地址 - file_type VARCHAR(32) DEFAULT '', -- 文件类型(mp3/mp4/png/...) - file_size BIGINT NOT NULL DEFAULT 0, -- 文件大小(字节) - error_msg TEXT DEFAULT '', -- 错误信息 - - started_at TIMESTAMP, -- 开始执行时间 - finished_at TIMESTAMP, -- 执行结束时间 - duration_seconds BIGINT NOT NULL DEFAULT 0, -- 耗时(秒):从创建到完成(成功/失败)整体耗时 - - expire_at TIMESTAMP, -- state=4 后写入,用于清理 - - -- 重试/排队 - retry_count INT NOT NULL DEFAULT 0, -- 已重试次数(不含首次) - enqueue_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 入队时间(用于排队顺序) - - -- 任务执行阶段:用于区分“重试模型”与“仅重试 OSS” - phase SMALLINT NOT NULL DEFAULT 0, -- 0模型阶段/1OSS阶段 - tmp_file TEXT DEFAULT '', -- 临时结果文件路径(phase=1 时仅重试 OSS 上传) - - -- 输入信息(可选) - input_ref TEXT DEFAULT '', -- 输入引用(如OSS/业务资源ID等) - request_payload JSONB -- 请求参数(可选) + -- 基础字段 + id BIGINT PRIMARY KEY, -- 主键ID(非自增) + tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID + creator VARCHAR(64) NOT NULL, -- 创建人 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + updater VARCHAR(64) NOT NULL, -- 更新人 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 + deleted_at TIMESTAMP(6), -- 删除时间(软删) + + -- 业务字段 + model_name VARCHAR(128) NOT NULL, -- 模型名称 + task_id VARCHAR(64) NOT NULL, -- 任务ID(对外返回) + biz_name VARCHAR(128) NOT NULL DEFAULT '', -- 业务名称(调用方模块/系统) + callback_url VARCHAR(512) DEFAULT '', -- 回调地址(可选,用于后续业务通知) + model_key VARCHAR(1024) DEFAULT '', -- 动态请求头(用于覆盖/补充模型配置 head_msg),如 X-API-Key:xxx + state SMALLINT NOT NULL DEFAULT 0, -- 0排队中/1执行中/2成功/3失败/4已下载 + oss_file VARCHAR(512) DEFAULT '', -- 结果文件OSS地址 + file_type VARCHAR(32) DEFAULT '', -- 文件类型(mp3/mp4/png/...) + file_size BIGINT NOT NULL DEFAULT 0, -- 文件大小(字节) + error_msg TEXT DEFAULT '', -- 错误信息 + started_at TIMESTAMP, -- 开始执行时间 + finished_at TIMESTAMP, -- 执行结束时间 + duration_seconds BIGINT NOT NULL DEFAULT 0, -- 耗时(秒):从创建到完成(成功/失败)整体耗时 + expire_at TIMESTAMP, -- state=4 后写入,用于清理 + retry_count INT NOT NULL DEFAULT 0, -- 已重试次数(不含首次) + enqueue_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 入队时间(用于排队顺序) + phase SMALLINT NOT NULL DEFAULT 0, -- 0模型阶段/1OSS阶段 + tmp_file TEXT DEFAULT '', -- 临时结果文件路径(phase=1 时仅重试 OSS 上传) + input_ref TEXT DEFAULT '', -- 输入引用(如OSS/业务资源ID等) + request_payload JSONB, -- 请求参数(可选) + text_result TEXT DEFAULT '', -- 文本类结果(可选,支持直接回调) + epicycle_id VARCHAR(64) DEFAULT '', -- 轮次ID + expend_tokens BIGINT NOT NULL DEFAULT 0 -- 消耗 token 数 ); -CREATE UNIQUE INDEX IF NOT EXISTS uk_asynch_task_tenant_task_id - ON asynch_task(tenant_id, task_id); +CREATE UNIQUE INDEX IF NOT EXISTS uk_asynch_task_tenant_task_id ON asynch_task(tenant_id, task_id); CREATE INDEX IF NOT EXISTS idx_asynch_task_tenant_id ON asynch_task(tenant_id); CREATE INDEX IF NOT EXISTS idx_asynch_task_model_name ON asynch_task(model_name); CREATE INDEX IF NOT EXISTS idx_asynch_task_biz_name ON asynch_task(biz_name); @@ -168,42 +134,48 @@ CREATE INDEX IF NOT EXISTS idx_asynch_task_enqueue_at ON asynch_task(enqueue_at) CREATE INDEX IF NOT EXISTS idx_asynch_task_updated_at ON asynch_task(updated_at); CREATE INDEX IF NOT EXISTS idx_asynch_task_expire_at ON asynch_task(expire_at); CREATE INDEX IF NOT EXISTS idx_asynch_task_deleted_at ON asynch_task(deleted_at); +CREATE INDEX IF NOT EXISTS idx_asynch_task_epicycle_id ON asynch_task(epicycle_id); +CREATE INDEX IF NOT EXISTS idx_asynch_task_expend_tokens ON asynch_task(expend_tokens); COMMENT ON TABLE asynch_task IS '异步任务表'; -COMMENT ON COLUMN asynch_task.id IS '主键ID(非自增)'; +COMMENT ON COLUMN asynch_task.id IS '主键ID(非自增)'; COMMENT ON COLUMN asynch_task.tenant_id IS '租户ID'; COMMENT ON COLUMN asynch_task.creator IS '创建人'; COMMENT ON COLUMN asynch_task.created_at IS '创建时间'; COMMENT ON COLUMN asynch_task.updater IS '更新人'; COMMENT ON COLUMN asynch_task.updated_at IS '更新时间'; -COMMENT ON COLUMN asynch_task.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN asynch_task.deleted_at IS '删除时间(软删)'; COMMENT ON COLUMN asynch_task.model_name IS '模型名称'; -COMMENT ON COLUMN asynch_task.task_id IS '任务ID(对外返回)'; -COMMENT ON COLUMN asynch_task.biz_name IS '业务名称(调用方模块/系统)'; -COMMENT ON COLUMN asynch_task.callback_url IS '回调地址(可选,用于后续业务通知)'; -COMMENT ON COLUMN asynch_task.model_key IS '动态请求头(用于覆盖/补充模型配置 head_msg),如 X-API-Key:xxx'; +COMMENT ON COLUMN asynch_task.task_id IS '任务ID(对外返回)'; +COMMENT ON COLUMN asynch_task.biz_name IS '业务名称(调用方模块/系统)'; +COMMENT ON COLUMN asynch_task.callback_url IS '回调地址(可选,用于后续业务通知)'; +COMMENT ON COLUMN asynch_task.model_key IS '动态请求头(用于覆盖/补充模型配置 head_msg),如 X-API-Key:xxx'; COMMENT ON COLUMN asynch_task.state IS '0排队中/1执行中/2成功/3失败/4已下载'; COMMENT ON COLUMN asynch_task.oss_file IS '结果文件OSS地址'; -COMMENT ON COLUMN asynch_task.file_type IS '文件类型(mp3/mp4/png/...)'; -COMMENT ON COLUMN asynch_task.file_size IS '文件大小(字节)'; +COMMENT ON COLUMN asynch_task.file_type IS '文件类型(mp3/mp4/png/...)'; +COMMENT ON COLUMN asynch_task.file_size IS '文件大小(字节)'; COMMENT ON COLUMN asynch_task.error_msg IS '错误信息'; COMMENT ON COLUMN asynch_task.started_at IS '开始执行时间'; COMMENT ON COLUMN asynch_task.finished_at IS '执行结束时间'; -COMMENT ON COLUMN asynch_task.duration_seconds IS '耗时(秒):从创建到完成(成功/失败)整体耗时'; +COMMENT ON COLUMN asynch_task.duration_seconds IS '耗时(秒):从创建到完成(成功/失败)整体耗时'; COMMENT ON COLUMN asynch_task.expire_at IS 'state=4 后写入,用于清理'; -COMMENT ON COLUMN asynch_task.retry_count IS '已重试次数(不含首次)'; -COMMENT ON COLUMN asynch_task.enqueue_at IS '入队时间(用于排队顺序)'; -COMMENT ON COLUMN asynch_task.phase IS '执行阶段:0模型阶段/1OSS阶段(模型已成功,等待上传OSS)'; -COMMENT ON COLUMN asynch_task.tmp_file IS '临时结果文件路径(phase=1 时仅重试 OSS 上传)'; -COMMENT ON COLUMN asynch_task.input_ref IS '输入引用(如OSS/业务资源ID等)'; -COMMENT ON COLUMN asynch_task.request_payload IS '请求参数(可选,JSON)'; +COMMENT ON COLUMN asynch_task.retry_count IS '已重试次数(不含首次)'; +COMMENT ON COLUMN asynch_task.enqueue_at IS '入队时间(用于排队顺序)'; +COMMENT ON COLUMN asynch_task.phase IS '执行阶段 模型阶段/1OSS阶段(模型已成功,等待上传OSS)'; +COMMENT ON COLUMN asynch_task.tmp_file IS '临时结果文件路径(phase=1 时仅重试 OSS 上传)'; +COMMENT ON COLUMN asynch_task.input_ref IS '输入引用(如OSS/业务资源ID等)'; +COMMENT ON COLUMN asynch_task.request_payload IS '请求参数(可选,JSON)'; +COMMENT ON COLUMN asynch_task.text_result IS '文本类结果(可选,支持直接回调)'; +COMMENT ON COLUMN asynch_task.epicycle_id IS '轮次ID(用于标识同一轮次的任务)'; +COMMENT ON COLUMN asynch_task.expend_tokens IS '消耗 token 数'; + -- ========================= --- 3) asynch_op_log +-- 3) logs_model_op -- ========================= -CREATE TABLE IF NOT EXISTS asynch_op_log ( - -- 基础字段(与现有表保持一致) +CREATE TABLE IF NOT EXISTS logs_model_op ( + -- 基础字段 id BIGINT PRIMARY KEY, tenant_id BIGINT NOT NULL DEFAULT 0, creator VARCHAR(64) NOT NULL, @@ -211,64 +183,60 @@ CREATE TABLE IF NOT EXISTS asynch_op_log ( updater VARCHAR(64) NOT NULL, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP(6), - -- 基础审计信息 ip VARCHAR(64) DEFAULT '', user_agent VARCHAR(256) DEFAULT '', api_path VARCHAR(256) DEFAULT '', http_method VARCHAR(16) DEFAULT '', - -- 业务信息 biz_name VARCHAR(128) NOT NULL DEFAULT '', -- 调用方业务模块/系统 model_name VARCHAR(128) NOT NULL DEFAULT '', task_id VARCHAR(64) NOT NULL DEFAULT '', - -- 统计字段 - op_type VARCHAR(64) NOT NULL DEFAULT 'createTask', -- 操作类型(默认创建任务) + op_type VARCHAR(64) NOT NULL DEFAULT 'createTask', -- 操作类型(默认创建任务) success SMALLINT NOT NULL DEFAULT 1, -- 1成功/0失败 error_msg TEXT DEFAULT '', - cost_ms BIGINT NOT NULL DEFAULT 0, -- 耗时(毫秒) - - -- 请求/响应 JSON(用于后期统计分析) + cost_ms BIGINT NOT NULL DEFAULT 0, -- 耗时(毫秒) + -- 请求/响应 JSON(用于后期统计分析) request_payload JSONB, response_payload JSONB ); -CREATE INDEX IF NOT EXISTS idx_asynch_op_log_tenant_time ON asynch_op_log(tenant_id, created_at); -CREATE INDEX IF NOT EXISTS idx_asynch_op_log_model_name ON asynch_op_log(model_name); -CREATE INDEX IF NOT EXISTS idx_asynch_op_log_biz_name ON asynch_op_log(biz_name); -CREATE INDEX IF NOT EXISTS idx_asynch_op_log_task_id ON asynch_op_log(task_id); -CREATE INDEX IF NOT EXISTS idx_asynch_op_log_op_type ON asynch_op_log(op_type); -CREATE INDEX IF NOT EXISTS idx_asynch_op_log_deleted_at ON asynch_op_log(deleted_at); +CREATE INDEX IF NOT EXISTS idx_logs_model_op_tenant_time ON logs_model_op(tenant_id, created_at); +CREATE INDEX IF NOT EXISTS idx_logs_model_op_model_name ON logs_model_op(model_name); +CREATE INDEX IF NOT EXISTS idx_logs_model_op_biz_name ON logs_model_op(biz_name); +CREATE INDEX IF NOT EXISTS idx_logs_model_op_task_id ON logs_model_op(task_id); +CREATE INDEX IF NOT EXISTS idx_logs_model_op_op_type ON logs_model_op(op_type); +CREATE INDEX IF NOT EXISTS idx_logs_model_op_deleted_at ON logs_model_op(deleted_at); -COMMENT ON TABLE asynch_op_log IS '操作记录日志表(创建任务等,用于统计)'; -COMMENT ON COLUMN asynch_op_log.id IS '主键ID(非自增)'; -COMMENT ON COLUMN asynch_op_log.tenant_id IS '租户ID'; -COMMENT ON COLUMN asynch_op_log.creator IS '创建人'; -COMMENT ON COLUMN asynch_op_log.created_at IS '创建时间'; -COMMENT ON COLUMN asynch_op_log.updater IS '更新人'; -COMMENT ON COLUMN asynch_op_log.updated_at IS '更新时间'; -COMMENT ON COLUMN asynch_op_log.deleted_at IS '删除时间(软删)'; -COMMENT ON COLUMN asynch_op_log.ip IS '客户端IP'; -COMMENT ON COLUMN asynch_op_log.user_agent IS 'User-Agent'; -COMMENT ON COLUMN asynch_op_log.api_path IS '接口路径'; -COMMENT ON COLUMN asynch_op_log.http_method IS 'HTTP方法'; -COMMENT ON COLUMN asynch_op_log.biz_name IS '业务名称(调用方模块/系统)'; -COMMENT ON COLUMN asynch_op_log.model_name IS '模型名称'; -COMMENT ON COLUMN asynch_op_log.task_id IS '任务ID'; -COMMENT ON COLUMN asynch_op_log.op_type IS '操作类型(如 createTask/getTaskResult/getTaskBatch 等)'; -COMMENT ON COLUMN asynch_op_log.success IS '是否成功:1成功/0失败'; -COMMENT ON COLUMN asynch_op_log.error_msg IS '错误信息(失败时)'; -COMMENT ON COLUMN asynch_op_log.cost_ms IS '耗时(毫秒)'; -COMMENT ON COLUMN asynch_op_log.request_payload IS '请求 JSON'; -COMMENT ON COLUMN asynch_op_log.response_payload IS '响应 JSON'; +COMMENT ON TABLE logs_model_op IS '操作记录日志表(创建任务等,用于统计)'; +COMMENT ON COLUMN logs_model_op.id IS '主键ID(非自增)'; +COMMENT ON COLUMN logs_model_op.tenant_id IS '租户ID'; +COMMENT ON COLUMN logs_model_op.creator IS '创建人'; +COMMENT ON COLUMN logs_model_op.created_at IS '创建时间'; +COMMENT ON COLUMN logs_model_op.updater IS '更新人'; +COMMENT ON COLUMN logs_model_op.updated_at IS '更新时间'; +COMMENT ON COLUMN logs_model_op.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN logs_model_op.ip IS '客户端IP'; +COMMENT ON COLUMN logs_model_op.user_agent IS 'User-Agent'; +COMMENT ON COLUMN logs_model_op.api_path IS '接口路径'; +COMMENT ON COLUMN logs_model_op.http_method IS 'HTTP方法'; +COMMENT ON COLUMN logs_model_op.biz_name IS '业务名称(调用方模块/系统)'; +COMMENT ON COLUMN logs_model_op.model_name IS '模型名称'; +COMMENT ON COLUMN logs_model_op.task_id IS '任务ID'; +COMMENT ON COLUMN logs_model_op.op_type IS '操作类型(如 createTask/getTaskResult/getTaskBatch 等)'; +COMMENT ON COLUMN logs_model_op.success IS '是否成功:1成功/0失败'; +COMMENT ON COLUMN logs_model_op.error_msg IS '错误信息(失败时)'; +COMMENT ON COLUMN logs_model_op.cost_ms IS '耗时(毫秒)'; +COMMENT ON COLUMN logs_model_op.request_payload IS '请求 JSON'; +COMMENT ON COLUMN logs_model_op.response_payload IS '响应 JSON'; -- ========================= --- 4) asynch_model_stat +-- 4) logs_model_stat -- ========================= -CREATE TABLE IF NOT EXISTS asynch_model_stat ( - day DATE NOT NULL, -- 天(YYYY-MM-DD) +CREATE TABLE IF NOT EXISTS logs_model_stat ( + day DATE NOT NULL, -- 天(YYYY-MM-DD) tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID creator VARCHAR(64) NOT NULL DEFAULT '', -- 创建人 model_name VARCHAR(128) NOT NULL DEFAULT '', -- 模型名称 @@ -279,16 +247,16 @@ CREATE TABLE IF NOT EXISTS asynch_model_stat ( ); -- 便于时间段/租户/人/模型过滤 -CREATE INDEX IF NOT EXISTS idx_asynch_model_stat_tenant_day ON asynch_model_stat(tenant_id, day); -CREATE INDEX IF NOT EXISTS idx_asynch_model_stat_day ON asynch_model_stat(day); -CREATE INDEX IF NOT EXISTS idx_asynch_model_stat_model_name ON asynch_model_stat(model_name); -CREATE INDEX IF NOT EXISTS idx_asynch_model_stat_creator ON asynch_model_stat(creator); +CREATE INDEX IF NOT EXISTS idx_logs_model_stat_tenant_day ON logs_model_stat(tenant_id, day); +CREATE INDEX IF NOT EXISTS idx_logs_model_stat_day ON logs_model_stat(day); +CREATE INDEX IF NOT EXISTS idx_logs_model_stat_model_name ON logs_model_stat(model_name); +CREATE INDEX IF NOT EXISTS idx_logs_model_stat_creator ON logs_model_stat(creator); -COMMENT ON TABLE asynch_model_stat IS '按天模型请求统计(用于限流/监控)'; -COMMENT ON COLUMN asynch_model_stat.day IS '天(YYYY-MM-DD)'; -COMMENT ON COLUMN asynch_model_stat.tenant_id IS '租户ID'; -COMMENT ON COLUMN asynch_model_stat.creator IS '创建人'; -COMMENT ON COLUMN asynch_model_stat.model_name IS '模型名称'; -COMMENT ON COLUMN asynch_model_stat.request_count IS '请求次数'; -COMMENT ON COLUMN asynch_model_stat.created_at IS '创建时间'; -COMMENT ON COLUMN asynch_model_stat.updated_at IS '更新时间'; +COMMENT ON TABLE logs_model_stat IS '按天模型请求统计(用于限流/监控)'; +COMMENT ON COLUMN logs_model_stat.day IS '天(YYYY-MM-DD)'; +COMMENT ON COLUMN logs_model_stat.tenant_id IS '租户ID'; +COMMENT ON COLUMN logs_model_stat.creator IS '创建人'; +COMMENT ON COLUMN logs_model_stat.model_name IS '模型名称'; +COMMENT ON COLUMN logs_model_stat.request_count IS '请求次数'; +COMMENT ON COLUMN logs_model_stat.created_at IS '创建时间'; +COMMENT ON COLUMN logs_model_stat.updated_at IS '更新时间';