diff --git a/common/util/files.go b/common/util/files.go index 34268a1..45e28fc 100644 --- a/common/util/files.go +++ b/common/util/files.go @@ -1,6 +1,7 @@ package util import ( + "encoding/json" "fmt" "net/http" "os" @@ -67,3 +68,48 @@ func SaveTmpResult(taskID string, data []byte, ext string) (string, error) { } return path, nil } + +// SaveTempFileByType +// 根据传入的数据自动判断: +// 若是 []byte 且后缀为 .mp3 → 保存二进制音频 +// 若是任意结构体/map → 自动转 JSON 保存 +// 返回:新临时文件路径、错误 +func SaveTempFileByType(taskID string, data any, oldTmpFile string) (string, error) { + // 1. 先清理旧临时文件(统一逻辑) + if oldTmpFile != "" { + _ = os.Remove(oldTmpFile) + } + + var tmpPath string + var tmpErr error + + // 2. 判断是否是二进制音频([]byte + .mp3) + if audioData, ok := data.([]byte); ok { + tmpPath, tmpErr = saveTmpResult(taskID, audioData, ".mp3") + } else { + // 3. 其他类型 → 序列化为 JSON 保存 + mappedBytes, err := json.Marshal(data) + if err != nil { + return "", err + } + if len(mappedBytes) == 0 { + return "", nil + } + tmpPath, tmpErr = saveTmpResult(taskID, mappedBytes, ".json") + } + + if tmpErr != nil || tmpPath == "" { + return "", tmpErr + } + + return tmpPath, nil +} + +// saveTmpResult 你原有的底层保存文件方法(保留不动) +func saveTmpResult(taskID string, data []byte, ext string) (string, error) { + // 你原来实现,比如: + filename := taskID + ext + tmpPath := filepath.Join(os.TempDir(), filename) + err := os.WriteFile(tmpPath, data, 0644) + return tmpPath, err +} diff --git a/common/util/headers.go b/common/util/headers.go index 581ab3e..25d9728 100644 --- a/common/util/headers.go +++ b/common/util/headers.go @@ -77,14 +77,3 @@ func SetTaskHeadersToCtx(ctx context.Context, headers map[string]string) context } return ctx } - -// ParseStoredPayload 解析入库的 request_payload,拆出模型调用核心数据 -func ParseStoredPayload(v map[string]any) map[string]any { - if v == nil { - return nil - } - if p, ok := v["payload"]; ok { - return gconv.Map(p) - } - return v -} diff --git a/common/util/mapping.go b/common/util/mapping.go index f7c344b..f4acb86 100644 --- a/common/util/mapping.go +++ b/common/util/mapping.go @@ -1,11 +1,17 @@ package util import ( + "bytes" + "context" "encoding/json" "fmt" + "io" "model-gateway/model/entity" + "net/http" "net/url" + "regexp" "strings" + "time" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" @@ -18,14 +24,9 @@ import ( func ValidatePromptResult(raw map[string]any, model *entity.AsynchModel) error { // 1) 获取校验配置,并取值 requestMapping := model.RequestMapping - contentKey := "" - for k := range model.ResponseBody { - contentKey = k - break - } - contentStr, ok := raw[contentKey].(string) + contentStr, ok := raw[model.ResponseBody].(string) if !ok || contentStr == "" { - return fmt.Errorf("%s 字段为空或不是字符串", contentKey) + return fmt.Errorf("%s 字段为空或不是字符串", model.ResponseBody) } // 2) 解析 content 为 JSON 数组 @@ -105,56 +106,39 @@ func MapResponsePayload(mapping map[string]any, result map[string]any) (map[stri return mapped, nil } -// ParseHeadMsgHeaders 支持多个 header 绑定,逗号分隔: -// 示例: -// - X-API-Key:qwen3-tts-key,operation:true,count:123 -// - X-API-Key:"qwen3-tts-key",operation:"true" +// ParseHeadMsgHeaders 从 head_msg JSON 中提取请求头 +// head_msg 格式示例: // -// 说明: -// - HTTP Header 最终都是字符串,这里做的是“值的字符串化表达”。 -// - 若 value 用双引号包裹,会去掉外层引号再注入,便于在配置中区分字符串/布尔/数字等表达(以及避免值中包含特殊字符时歧义)。 -func ParseHeadMsgHeaders(headMsg string) map[string]string { - headMsg = strings.TrimSpace(headMsg) - if headMsg == "" { +// { +// "Authorization": "Bearer xxx", +// "Content-Type": "application/json", +// "X-Api-App-Id": "5147401364", +// "X-Api-Access-Key": "VCqRX7..." +// } +func ParseHeadMsgHeaders(headMsg map[string]any) map[string]string { + if len(headMsg) == 0 { return nil } - out := map[string]string{} - parts := strings.Split(headMsg, ",") - for _, p := range parts { - p = strings.TrimSpace(p) - if p == "" { - continue - } - // HeaderName:HeaderValue(推荐) / HeaderName=HeaderValue(兼容) - if strings.Contains(p, ":") { - kv := strings.SplitN(p, ":", 2) - k := strings.TrimSpace(kv[0]) - v := strings.TrimSpace(kv[1]) - v = strings.Trim(v, "\"") - if k != "" && v != "" { - out[k] = v - } - continue - } - if strings.Contains(p, "=") { - kv := strings.SplitN(p, "=", 2) - k := strings.TrimSpace(kv[0]) - v := strings.TrimSpace(kv[1]) - v = strings.Trim(v, "\"") - if k != "" && v != "" { - out[k] = v - } - continue - } - } - if len(out) == 0 { - return nil + out := make(map[string]string, len(headMsg)) + for k, v := range headMsg { + out[k] = gconv.String(v) } return out } -// PayloadToQuery 将 payload 转为 url.Values -func PayloadToQuery(payload map[string]any) (url.Values, error) { +// GetModelBody 获取数据库中保存的模型信息 +func GetModelBody(v map[string]any) map[string]any { + if v == nil { + return nil + } + if p, ok := v["body"]; ok { + return gconv.Map(p) + } + return v +} + +// BodyToQuery 将 body 转为 url.Values +func BodyToQuery(payload map[string]any) (url.Values, error) { q := url.Values{} for k, v := range payload { if v == nil { @@ -164,3 +148,142 @@ func PayloadToQuery(payload map[string]any) (url.Values, error) { } return q, nil } + +// PullTaskResult 轮询查询任务结果直到完成 +func PullTaskResult(ctx context.Context, taskID string, queryConfig map[string]any) (map[string]any, error) { + // 1. 解析配置 + url := gconv.String(queryConfig["url"]) + method := gconv.String(queryConfig["method"]) + headers, _ := queryConfig["headers"].(map[string]any) + interval := gconv.Int(queryConfig["interval_seconds"]) + if interval <= 0 { + interval = 2 + } + + if method == "" { + method = "GET" + } + + // 2. 构建参数 + params := map[string]any{"id": taskID} + + // 3. 替换 URL 中的 {id} + finalURL := replaceURLParams(url, params) + + // 4. 构建请求体 + bodyCfg, _ := queryConfig["body"].(map[string]any) + body := buildParams(bodyCfg, params) + + // 5. 轮询 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + var reqBody io.Reader + if method == "POST" && body != nil { + bs, _ := json.Marshal(body) + reqBody = bytes.NewReader(bs) + } + + req, err := http.NewRequestWithContext(ctx, method, finalURL, reqBody) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %w", err) + } + + for k, v := range headers { + req.Header.Set(k, gconv.String(v)) + } + if req.Header.Get("Content-Type") == "" && reqBody != nil { + req.Header.Set("Content-Type", "application/json") + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + g.Log().Warningf(ctx, "[PullTaskResult] 请求失败 taskID=%s err=%v", taskID, err) + time.Sleep(time.Duration(interval) * time.Second) + continue + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + all, _ := io.ReadAll(resp.Body) + resp.Body.Close() + g.Log().Warningf(ctx, "[PullTaskResult] 请求异常 taskID=%s status=%d body=%s", taskID, resp.StatusCode, string(all)) + time.Sleep(time.Duration(interval) * time.Second) + continue + } + + var result map[string]any + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + resp.Body.Close() + g.Log().Warningf(ctx, "[PullTaskResult] 解析失败 taskID=%s err=%v", taskID, err) + time.Sleep(time.Duration(interval) * time.Second) + continue + } + resp.Body.Close() + + status := gconv.String(result["status"]) + g.Log().Infof(ctx, "[PullTaskResult] 轮询 taskID=%s status=%s", taskID, status) + + switch status { + case "succeeded": + return result, nil + case "failed", "expired": + return result, fmt.Errorf("任务失败: status=%s", status) + case "queued", "running": + time.Sleep(time.Duration(interval) * time.Second) + continue + default: + // 兼容没有 status 字段的情况,直接返回 + return result, nil + } + } +} + +// buildParams 构建请求参数,用 params 覆盖 bodyCfg 中对应 key +func buildParams(bodyCfg map[string]any, params map[string]any) map[string]any { + result := make(map[string]any, len(bodyCfg)+len(params)) + for k, v := range bodyCfg { + result[k] = v + } + for k, v := range params { + result[k] = v + } + return result +} + +// replaceURLParams 替换 URL 中的 {key} +func replaceURLParams(url string, params map[string]any) string { + re := regexp.MustCompile(`\{([^}]+)\}`) + return re.ReplaceAllStringFunc(url, func(s string) string { + key := strings.Trim(s, "{}") + if val, ok := params[key]; ok { + return gconv.String(val) + } + return s + }) +} + +// replaceBodyParams 用 params 覆盖 body 中对应 key +func replaceBodyParams(bodyCfg map[string]any, params map[string]any) map[string]any { + result := make(map[string]any) + for k, v := range bodyCfg { + result[k] = v + } + for k, v := range params { + result[k] = v + } + return result +} + +// InjectCallbackURL 将回调地址注入到请求体中 +func InjectCallbackURL(ctx context.Context, payload map[string]any, callbackURL string) map[string]any { + if callbackURL == "" { + return payload + } + payload[callbackURL] = GetCallbackURL(ctx, "/task/modelCallback") + return payload +} diff --git a/config.yml b/config.yml index 5e5be98..e7d2193 100644 --- a/config.yml +++ b/config.yml @@ -61,6 +61,10 @@ jaeger: # 本地调试用:可选自动执行 worker/cleaner(默认关闭) asynch: + queryPending: + enabled: false + intervalSeconds: 10 # 每10秒轮询一次 + limit: 10 # 每次查10条 worker: enabled: false intervalSeconds: 5 diff --git a/controller/base.go b/controller/base.go deleted file mode 100644 index b0b429f..0000000 --- a/controller/base.go +++ /dev/null @@ -1 +0,0 @@ -package controller diff --git a/controller/task_controller.go b/controller/task_controller.go index 44be51f..51bdbe9 100644 --- a/controller/task_controller.go +++ b/controller/task_controller.go @@ -18,6 +18,16 @@ func (c *task) CreateTask(ctx context.Context, req *dto.CreateTaskReq) (res *dto return taskService.Task.Create(ctx, req) } +// ModelTaskCallback 接收模型异步任务的回调通知 +func (c *task) ModelTaskCallback(ctx context.Context, req *dto.ModelTaskCallbackReq) (res *dto.ModelTaskCallbackRes, err error) { + return taskService.Task.ModelTaskCallback(ctx, req) +} + +// QueryPendingTasks 批量轮询进行中的异步任务 +func (c *task) QueryPendingTasks(ctx context.Context, req *dto.QueryPendingTasksReq) (res *dto.QueryPendingTasksRes, err error) { + return taskService.Task.QueryPendingTasks(ctx, req) +} + // GetTaskResult 获取任务结果(只返回 oss 地址 + state) func (c *task) GetTaskResult(ctx context.Context, req *dto.GetTaskResultReq) (res *dto.GetTaskResultRes, err error) { return taskService.Task.GetResult(ctx, req.TaskID) diff --git a/dao/task_dao.go b/dao/task_dao.go index b5b8f26..9adb27a 100644 --- a/dao/task_dao.go +++ b/dao/task_dao.go @@ -76,7 +76,7 @@ func (d *taskDao) MarkDownloadedByID(ctx context.Context, id int64, expireAt *gt // List 任务分页查询(受 gfdb 租户 Hook 影响) func (d *taskDao) List(ctx context.Context, pageNum, pageSize int, modelNameLike, taskIDLike string, state *int) (list []*entity.AsynchTask, total int64, err error) { - m := gfdb.DB(ctx).Model(ctx, public.TableNameTask).Where("deleted_at IS NULL") + m := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).Where("deleted_at IS NULL") if modelNameLike != "" { m = m.WhereLike(entity.AsynchTaskCol.ModelName, "%"+modelNameLike+"%") } @@ -98,3 +98,14 @@ func (d *taskDao) List(ctx context.Context, pageNum, pageSize int, modelNameLike err = r.Structs(&list) return } + +// GetPendingAsyncTasks 获取进行中的异步任务 +func (d *taskDao) GetPendingAsyncTasks(ctx context.Context, limit int) ([]*entity.AsynchTask, error) { + var tasks []*entity.AsynchTask + err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask). + Where("state", 1). + Where("deleted_at IS NULL"). + Limit(limit). + Scan(&tasks) + return tasks, err +} diff --git a/go.mod b/go.mod deleted file mode 100644 index 3732215..0000000 --- a/go.mod +++ /dev/null @@ -1,96 +0,0 @@ -module model-gateway - -go 1.26.0 - -require ( - gitea.com/red-future/common v0.0.19 - 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 -) - -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 - github.com/armon/go-metrics v0.4.1 // indirect - github.com/bwmarrin/snowflake v0.3.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clbanning/mxj/v2 v2.7.0 // indirect - github.com/dgraph-io/badger/v4 v4.2.0 // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - 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/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 - github.com/golang-jwt/jwt/v5 v5.3.1 // indirect - github.com/golang/glog v1.2.5 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - 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.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 - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/golang-lru v1.0.2 // indirect - github.com/hashicorp/serf v0.10.1 // indirect - github.com/klauspost/compress v1.18.2 // indirect - github.com/lib/pq v1.10.9 // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.0.9 // indirect - github.com/olekukonko/tablewriter v1.1.0 // indirect - 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 - go.opencensus.io v0.23.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/sdk v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.1 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/grpc v1.75.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 044b9a6..0000000 --- a/go.sum +++ /dev/null @@ -1,468 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -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/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/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= -github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -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/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= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -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/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/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= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -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/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= -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-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/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= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -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-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-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= -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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 h1:39+jbTenm7KBj4hO2C8ANAxVHpX/7OuRDs1VcGC9ylA= -github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0/go.mod h1:B0s0fVzn0W220E8UTpSGzrrGKsop5KcB90twBeLCiz0= -github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 h1:N/F9CuDdUZLoM1nVRqrDE/33pDZuhVxpNY4wYdeIaBs= -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/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/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= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= -github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= -github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -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/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= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -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.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -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= -github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -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/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= -github.com/google/flatbuffers v1.12.1/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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.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/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/sdk v0.15.0 h1:2qK9nDrr4tiJKRoxPGhm6B7xJjLVIQqkjiab2M4aKjU= -github.com/hashicorp/consul/sdk v0.15.0/go.mod h1:r/OmRRPbHOe0yxNahLw7G9x5WG17E1BIECMtCjcPSNo= -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= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -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-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-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= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -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-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= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= -github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -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/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= -github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= -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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -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/konsorten/go-windows-terminal-sequences v1.0.1/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= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -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/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= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -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= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -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/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= -github.com/olekukonko/errors v1.1.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/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= -github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= -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= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -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_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/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/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/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/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/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.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= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -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.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -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/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/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/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= -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= -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.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -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/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/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/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/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -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/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -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/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= -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= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -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/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/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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -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-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-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.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -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-20210220032951-036812b2e83c/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/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= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-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-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-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-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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -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= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= -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= -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.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -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/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/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= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -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/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= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -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.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -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= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 922fe7e..75f77ed 100644 --- a/main.go +++ b/main.go @@ -93,4 +93,24 @@ func startAutoRunner(ctx context.Context) { } }() } + + // queryPending + if g.Cfg().MustGet(ctx, "asynch.queryPending.enabled").Bool() { + interval := g.Cfg().MustGet(ctx, "asynch.queryPending.intervalSeconds", 10).Int() + limit := g.Cfg().MustGet(ctx, "asynch.queryPending.limit", 10).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 := task.Task.QueryPendingTasks(ctx, &dto.QueryPendingTasksReq{Limit: limit}); err != nil { + g.Log().Warningf(ctx, "[auto-queryPending] run once failed: %v", err) + } + } + } + }() + } } diff --git a/model/dto/model_dto.go b/model/dto/model_dto.go index 3fc7de0..18151f8 100644 --- a/model/dto/model_dto.go +++ b/model/dto/model_dto.go @@ -9,34 +9,36 @@ import ( // CreateModelReq 添加模型配置 type CreateModelReq struct { - g.Meta `path:"/createModel" method:"post" tags:"模型管理" summary:"创建模型配置" dc:"添加新的模型配置"` - ModelName string `p:"modelName" json:"modelName" v:"required#modelName不能为空" dc:"模型名称(唯一标识)"` - ModelType int `p:"modelType" json:"modelType" v:"required#modelType不能为空" dc:"模型类型:1-文本生成 2-图像生成 3-语音 4-视频 5-多模态"` - BaseURL string `p:"baseUrl" json:"baseUrl" v:"required#baseUrl不能为空" dc:"模型服务基础地址(如 gateway(s)://host:port)"` - HttpMethod string `p:"httpMethod" json:"httpMethod" dc:"请求方式:GET/POST(默认POST)"` - 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)"` - IsOwner *int `p:"isOwner" json:"isOwner" v:"in:0,1#是否为所有者参数只能为0或1" dc:"是否为所有者:0-否,1-是(默认0)"` - OperatorName string `p:"operatorName" json:"operatorName" v:"required#operatorName不能为空" dc:"运营商名称"` - TokenConfig map[string]any `p:"tokenConfig" json:"tokenConfig" dc:"token计算配置"` - ExtendMapping map[string]any `p:"extendMapping" json:"extendMapping" dc:"附加映射"` - QueryConfig map[string]any `p:"queryConfig" json:"queryConfig" dc:"结果配置"` - ApiKey string `p:"apiKey" json:"apiKey" dc:"调用凭证/密钥,用于模型认证"` - Form []map[string]any `p:"form" json:"form" dc:"动态表单配置(JSON),用于前端渲染配置项"` - RequestMapping map[string]any `p:"requestMapping" json:"requestMapping" dc:"请求映射"` - ResponseMapping map[string]any `p:"responseMapping" json:"responseMapping" dc:"返回映射"` - ResponseBody map[string]any `p:"responseBody" json:"responseBody" dc:"返回主体"` - ResponseTokenField string `p:"responseTokenField" json:"responseTokenField" 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:"备注说明"` + g.Meta `path:"/createModel" method:"post" tags:"模型管理" summary:"创建模型配置" dc:"添加新的模型配置"` + ModelName string `p:"modelName" json:"modelName" v:"required#模型名称不能为空" dc:"模型名称(唯一标识)"` + ModelType int `p:"modelType" json:"modelType" v:"required#模型类型不能为空" dc:"模型类型"` + BaseURL string `p:"baseUrl" json:"baseUrl" v:"required#模型地址不能为空" dc:"模型服务地址"` + HttpMethod string `p:"httpMethod" json:"httpMethod" dc:"请求方式:GET/POST(默认POST)"` + HeadMsg map[string]any `p:"headMsg" json:"headMsg" dc:"请求头JSON结构"` + IsPrivate *int `p:"isPrivate" json:"isPrivate" dc:"是否私有化:0-私有 1-公共"` + Enabled *int `p:"enabled" json:"enabled" dc:"是否启用:0-停用 1-启用"` + IsChatModel *int `p:"isChatModel" json:"isChatModel" dc:"是否为对话模型:0-否 1-是"` + IsAsync *int `p:"isAsync" json:"isAsync" dc:"是否异步:0-同步 1-异步"` + IsStream *int `p:"isStream" json:"isStream" dc:"是否流式:0-非流式 1-流式"` + IsOwner *int `p:"isOwner" json:"isOwner" dc:"是否为所有者:0-否 1-是"` + ApiKey string `p:"apiKey" json:"apiKey" dc:"调用凭证/密钥"` + Form []map[string]any `p:"form" json:"form" dc:"动态表单配置"` + RequestMapping map[string]any `p:"requestMapping" json:"requestMapping" dc:"请求映射"` + ResponseMapping map[string]any `p:"responseMapping" json:"responseMapping" dc:"返回映射"` + ResponseBody string `p:"responseBody" json:"responseBody" dc:"返回主体"` + ResponseTokenField string `p:"responseTokenField" json:"responseTokenField" dc:"响应中消耗token的字段映射"` + OperatorName string `p:"operatorName" json:"operatorName" dc:"运营商名称"` + TokenConfig map[string]any `p:"tokenConfig" json:"tokenConfig" dc:"token计算配置"` + ExtendMapping map[string]any `p:"extendMapping" json:"extendMapping" dc:"附加映射"` + QueryConfig map[string]any `p:"queryConfig" json:"queryConfig" dc:"查询/回调配置"` + StreamConfig map[string]any `p:"streamConfig" json:"streamConfig" dc:"流式输出配置"` + FirstFrame string `p:"firstFrame" json:"firstFrame" dc:"首帧图片参数"` + LastFrame string `p:"lastFrame" json:"lastFrame" dc:"尾帧图片参数"` + MaxConcurrency int `p:"maxConcurrency" json:"maxConcurrency" dc:"最大并发数(默认10)"` + TimeoutSeconds int `p:"timeoutSeconds" json:"timeoutSeconds" dc:"请求超时时间(秒,默认600)"` + RetryTimes int `p:"retryTimes" json:"retryTimes" dc:"失败重试次数(默认3)"` + AutoCleanSeconds int `p:"autoCleanSeconds" json:"autoCleanSeconds" dc:"任务完成后自动清理时间(秒,默认86400)"` + CallbackUrl string `p:"callbackUrl" json:"callbackUrl" dc:"回调地址"` } type CreateModelRes struct { @@ -44,35 +46,37 @@ type CreateModelRes struct { } type UpdateModelReq struct { - g.Meta `path:"/updateModel" method:"put" tags:"模型管理" summary:"更新模型配置" dc:"更新指定ID的模型配置"` - ID int64 `p:"id" json:"id" v:"required#id不能为空" dc:"配置ID"` - ModelName string `p:"modelName" json:"modelName" dc:"模型名称(唯一标识)"` - ModelType int `p:"modelType" json:"modelType" 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:"请求头绑定(可选更新)"` - ApiKey string `p:"apiKey" json:"apiKey" dc:"调用凭证/密钥,用于模型认证(可选更新)"` - Form []map[string]any `p:"form" json:"form" dc:"动态表单配置(JSON)(可选更新)"` - RequestMapping map[string]any `p:"requestMapping" json:"requestMapping" dc:"请求参数映射(可选更新)"` - ResponseMapping map[string]any `p:"responseMapping" json:"responseMapping" dc:"返回参数映射(可选更新)"` - ResponseBody map[string]any `p:"responseBody" json:"responseBody" dc:"返回主体(可选更新)"` - ResponseTokenField string `p:"responseTokenField" json:"responseTokenField" dc:"响应中消耗token的字段映射"` - Enabled *int `p:"enabled" json:"enabled" dc:"是否启用:0-禁用,1-启用(可选更新)"` - IsPrivate *int `p:"isPrivate" json:"isPrivate" v:"in:0,1#私有化参数只能为0或1" dc:"是否私有化:0-私有(默认) 1-公共"` - IsChatModel *int `p:"isChatModel" json:"isChatModel" v:"in:0,1#对话模型参数只能为0或1" dc:"是否为对话模型:0-否,1-是(默认0)"` - IsOwner *int `p:"isOwner" json:"isOwner" v:"in:0,1#是否为所有者参数只能为0或1" dc:"是否为所有者:0-否,1-是(默认0)"` - OperatorName string `p:"operatorName" json:"operatorName" v:"required#operatorName不能为空" dc:"运营商名称"` - TokenConfig map[string]any `p:"tokenConfig" json:"tokenConfig" dc:"token计算配置"` - ExtendMapping map[string]any `p:"extendMapping" json:"extendMapping" dc:"附加映射"` - QueryConfig map[string]any `p:"queryConfig" json:"queryConfig" dc:"结果配置"` - 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:"备注说明(可选更新)"` + g.Meta `path:"/updateModel" method:"put" tags:"模型管理" summary:"更新模型配置" dc:"更新指定ID的模型配置"` + ID int64 `p:"id" json:"id" v:"required#id不能为空" dc:"配置ID"` + ModelName string `p:"modelName" json:"modelName" dc:"模型名称"` + ModelType int `p:"modelType" json:"modelType" dc:"模型类型"` + BaseURL string `p:"baseUrl" json:"baseUrl" dc:"模型服务地址"` + HttpMethod string `p:"httpMethod" json:"httpMethod" dc:"请求方式:GET/POST"` + HeadMsg map[string]any `p:"headMsg" json:"headMsg" dc:"请求头JSON结构"` + IsPrivate *int `p:"isPrivate" json:"isPrivate" dc:"是否私有化:0-私有 1-公共"` + Enabled *int `p:"enabled" json:"enabled" dc:"是否启用:0-停用 1-启用"` + IsChatModel *int `p:"isChatModel" json:"isChatModel" dc:"是否为对话模型:0-否 1-是"` + IsAsync *int `p:"isAsync" json:"isAsync" dc:"是否异步:0-同步 1-异步"` + IsStream *int `p:"isStream" json:"isStream" dc:"是否流式:0-非流式 1-流式"` + IsOwner *int `p:"isOwner" json:"isOwner" dc:"是否为所有者:0-否 1-是"` + ApiKey string `p:"apiKey" json:"apiKey" dc:"调用凭证/密钥"` + Form []map[string]any `p:"form" json:"form" dc:"动态表单配置"` + RequestMapping map[string]any `p:"requestMapping" json:"requestMapping" dc:"请求映射"` + ResponseMapping map[string]any `p:"responseMapping" json:"responseMapping" dc:"返回映射"` + ResponseBody string `p:"responseBody" json:"responseBody" dc:"返回主体"` + ResponseTokenField string `p:"responseTokenField" json:"responseTokenField" dc:"响应中消耗token的字段映射"` + OperatorName string `p:"operatorName" json:"operatorName" dc:"运营商名称"` + TokenConfig map[string]any `p:"tokenConfig" json:"tokenConfig" dc:"token计算配置"` + ExtendMapping map[string]any `p:"extendMapping" json:"extendMapping" dc:"附加映射"` + QueryConfig map[string]any `p:"queryConfig" json:"queryConfig" dc:"查询/回调配置"` + StreamConfig map[string]any `p:"streamConfig" json:"streamConfig" dc:"流式输出配置"` + FirstFrame string `p:"firstFrame" json:"firstFrame" dc:"首帧图片参数"` + LastFrame string `p:"lastFrame" json:"lastFrame" dc:"尾帧图片参数"` + MaxConcurrency int `p:"maxConcurrency" json:"maxConcurrency" dc:"最大并发数"` + TimeoutSeconds int `p:"timeoutSeconds" json:"timeoutSeconds" dc:"请求超时时间(秒)"` + RetryTimes int `p:"retryTimes" json:"retryTimes" dc:"失败重试次数"` + AutoCleanSeconds int `p:"autoCleanSeconds" json:"autoCleanSeconds" dc:"任务完成后自动清理时间(秒)"` + CallbackUrl string `p:"callbackUrl" json:"callbackUrl" dc:"回调地址"` } type UpdateModelRes struct { diff --git a/model/dto/task_dto.go b/model/dto/task_dto.go index 0efa7d7..ebc3eb8 100644 --- a/model/dto/task_dto.go +++ b/model/dto/task_dto.go @@ -18,6 +18,38 @@ type CreateTaskRes struct { TaskID string `json:"taskId" dc:"任务ID"` } +type ModelTaskCallbackReq struct { + g.Meta `path:"/modelCallback" method:"post" tags:"异步任务" summary:"模型任务回调通知"` + TaskID string `json:"id" dc:"任务ID"` + Status string `json:"status" dc:"queued/running/succeeded/failed/expired"` + Content map[string]any `json:"content,omitempty" dc:"任务结果内容"` + Usage map[string]any `json:"usage,omitempty" dc:"token用量"` +} + +type ModelTaskCallbackRes struct { + Success bool `json:"success" dc:"是否接收成功"` +} + +// QueryPendingTasksReq 批量轮询请求 +type QueryPendingTasksReq struct { + g.Meta `path:"/queryPending" method:"get" tags:"异步任务" summary:"批量轮询进行中的任务"` + Limit int `p:"limit" json:"limit" dc:"查询数量,默认10"` +} + +// QueryPendingTasksRes 批量轮询响应 +type QueryPendingTasksRes struct { + Total int `json:"total" dc:"本次查询数量"` + Results []QueryTaskItem `json:"results" dc:"查询结果列表"` +} + +// QueryTaskItem 单个任务查询结果 +type QueryTaskItem struct { + TaskID string `json:"taskId" dc:"任务ID"` + Status string `json:"status" dc:"任务状态"` + Content map[string]any `json:"content,omitempty" dc:"结果内容"` + Usage map[string]any `json:"usage,omitempty" dc:"token用量"` +} + // GetTaskResultReq 获取结果(只返回 oss 地址) type GetTaskResultReq struct { g.Meta `path:"/getTaskResult" method:"get" tags:"任务管理" summary:"获取任务结果" dc:"根据任务ID获取结果(只返回OSS地址)"` diff --git a/model/entity/asynch_model.go b/model/entity/asynch_model.go index 505781d..5202e45 100644 --- a/model/entity/asynch_model.go +++ b/model/entity/asynch_model.go @@ -14,25 +14,25 @@ type asynchModelCol struct { ResponseMapping string ResponseBody string ResponseTokenField string - Prompt string IsPrivate string IsChatModel string + IsAsync string + IsStream string ApiKey string Enabled string MaxConcurrency string - QueueLimit string TimeoutSeconds string - ExpectedSeconds string RetryTimes string - RetryQueueMaxSecs string AutoCleanSeconds string - Remark string IsOwner string OperatorName string TokenConfig string ExtendMapping string QueryConfig string StreamConfig string + FirstFrame string + LastFrame string + CallbackUrl string } var AsynchModelCol = asynchModelCol{ @@ -47,57 +47,57 @@ var AsynchModelCol = asynchModelCol{ ResponseMapping: "response_mapping", ResponseBody: "response_body", ResponseTokenField: "response_token_field", - Prompt: "prompt", IsPrivate: "is_private", IsChatModel: "is_chat_model", + IsAsync: "is_async", + IsStream: "is_stream", ApiKey: "api_key", Enabled: "enabled", MaxConcurrency: "max_concurrency", - QueueLimit: "queue_limit", TimeoutSeconds: "timeout_seconds", - ExpectedSeconds: "expected_seconds", RetryTimes: "retry_times", - RetryQueueMaxSecs: "retry_queue_max_seconds", AutoCleanSeconds: "auto_clean_seconds", - Remark: "remark", IsOwner: "is_owner", OperatorName: "operator_name", TokenConfig: "token_config", ExtendMapping: "extend_mapping", QueryConfig: "query_config", StreamConfig: "stream_config", + FirstFrame: "first_frame", + LastFrame: "last_frame", + CallbackUrl: "callback_url", } // AsynchModel 异步模型配置 type AsynchModel struct { - beans.SQLBaseDO `orm:",inline"` - ModelName string `orm:"model_name" json:"modelName"` - ModelType int `orm:"model_type" json:"modelType"` - BaseURL string `orm:"base_url" json:"baseUrl"` - HttpMethod string `orm:"http_method" json:"httpMethod"` - HeadMsg string `orm:"head_msg" json:"headMsg"` - Form []map[string]any `orm:"form_json" json:"form"` - RequestMapping map[string]any `orm:"request_mapping" json:"requestMapping"` - ResponseMapping map[string]any `orm:"response_mapping" json:"responseMapping"` - ResponseBody map[string]any `orm:"response_body" json:"responseBody"` - ResponseTokenField string `orm:"response_token_field" json:"responseTokenField"` - 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"` - IsOwner *int `json:"isOwner" orm:"is_owner"` - OperatorName string `orm:"operator_name" json:"operatorName"` - TokenConfig map[string]any `orm:"token_config" json:"tokenConfig"` - ExtendMapping map[string]any `orm:"extend_mapping" json:"extendMapping"` - QueryConfig map[string]any `orm:"query_config" json:"queryConfig"` - StreamConfig map[string]any `orm:"stream_config" json:"streamConfig"` + beans.SQLBaseDO `orm:",inline"` + ModelName string `orm:"model_name" json:"modelName"` + ModelType int `orm:"model_type" json:"modelType"` + BaseURL string `orm:"base_url" json:"baseUrl"` + HttpMethod string `orm:"http_method" json:"httpMethod"` + HeadMsg map[string]any `orm:"head_msg" json:"headMsg"` + Form []map[string]any `orm:"form_json" json:"form"` + RequestMapping map[string]any `orm:"request_mapping" json:"requestMapping"` + ResponseMapping map[string]any `orm:"response_mapping" json:"responseMapping"` + ResponseBody string `orm:"response_body" json:"responseBody"` + ResponseTokenField string `orm:"response_token_field" json:"responseTokenField"` + IsPrivate *int `orm:"is_private" json:"isPrivate"` + IsChatModel *int `orm:"is_chat_model" json:"isChatModel"` + IsAsync *int `orm:"is_async" json:"isAsync"` + IsStream *int `orm:"is_stream" json:"isStream"` + ApiKey string `orm:"api_key" json:"apiKey"` + Enabled *int `orm:"enabled" json:"enabled"` + MaxConcurrency int `orm:"max_concurrency" json:"maxConcurrency"` + TimeoutSeconds int `orm:"timeout_seconds" json:"timeoutSeconds"` + RetryTimes int `orm:"retry_times" json:"retryTimes"` + AutoCleanSeconds int `orm:"auto_clean_seconds" json:"autoCleanSeconds"` + IsOwner *int `json:"isOwner" orm:"is_owner"` + OperatorName string `orm:"operator_name" json:"operatorName"` + TokenConfig map[string]any `orm:"token_config" json:"tokenConfig"` + ExtendMapping map[string]any `orm:"extend_mapping" json:"extendMapping"` + QueryConfig map[string]any `orm:"query_config" json:"queryConfig"` + StreamConfig map[string]any `orm:"stream_config" json:"streamConfig"` + FirstFrame string `orm:"first_frame" json:"firstFrame"` + LastFrame string `orm:"last_frame" json:"lastFrame"` + CallbackUrl string `orm:"callback_url" json:"callbackUrl"` } diff --git a/service/job/cleaner.go b/service/job/cleaner.go index b6b7d47..c133c56 100644 --- a/service/job/cleaner.go +++ b/service/job/cleaner.go @@ -55,9 +55,9 @@ func (c *cleaner) RunOnce(ctx context.Context) (res *dto.CleanWorkRes, err error if err != nil || m == nil { continue } - limit := queue.GetRuntimeQueueLimit(ctx, t.ModelName, m.QueueLimit) + limit := queue.GetRuntimeQueueLimit(ctx, t.ModelName, m.MaxConcurrency*2) if limit > 0 { - ok, _ := queue.AcquireQueueSlot(ctx, t.ModelName, t.TaskID, limit, m.ExpectedSeconds) + ok, _ := queue.AcquireQueueSlot(ctx, t.ModelName, t.TaskID, limit, m.TimeoutSeconds) if !ok { continue } diff --git a/service/queue/auto_tune.go b/service/queue/auto_tune.go index f3d06ea..06f5803 100644 --- a/service/queue/auto_tune.go +++ b/service/queue/auto_tune.go @@ -28,7 +28,6 @@ type AutoTuneResult struct { OldQueueLimit int `json:"oldQueueLimit"` // 调参前运行时值(Redis),若无则等于 cap NewQueueLimit int `json:"newQueueLimit"` // 本次计算出的运行时值(将写入 Redis),受 ±50% 约束且不超过 cap - ExpectedSeconds int `json:"expectedSeconds"` // 模型预计执行时间(秒):asynch_models.expected_seconds(用于 queue_limit 计算绑定) } // AutoTune 由上层定时任务通过接口触发: @@ -65,11 +64,11 @@ func AutoTune(ctx context.Context, req *dto.AutoTuneReq) (res *dto.AutoTuneRes, if m.MaxConcurrency > cur.MaxConcurrency { cur.MaxConcurrency = m.MaxConcurrency } - if m.QueueLimit > cur.QueueLimit { - cur.QueueLimit = m.QueueLimit + if m.MaxConcurrency*2 > cur.MaxConcurrency*2 { + cur.MaxConcurrency = m.MaxConcurrency } - if m.ExpectedSeconds > cur.ExpectedSeconds { - cur.ExpectedSeconds = m.ExpectedSeconds + if m.TimeoutSeconds > cur.TimeoutSeconds { + cur.TimeoutSeconds = m.TimeoutSeconds } } if len(modelMap) == 0 { @@ -113,7 +112,7 @@ SELECT model_name, for modelName, m := range modelMap { s := statMap[modelName] capMax := m.MaxConcurrency - capQueue := m.QueueLimit + capQueue := m.MaxConcurrency * 2 oldMax := GetRuntimeMaxConcurrency(ctx, modelName, capMax) oldQueue := GetRuntimeQueueLimit(ctx, modelName, capQueue) @@ -129,7 +128,6 @@ SELECT model_name, CapQueueLimit: capQueue, OldQueueLimit: oldQueue, NewQueueLimit: oldQueue, - ExpectedSeconds: m.ExpectedSeconds, }) continue } @@ -155,7 +153,7 @@ SELECT model_name, setRuntimeInt(ctx, runtimeMaxConcurrencyKey(modelName), newMax) // queue_limit:W_target = expected_seconds * queueFactor - exp := m.ExpectedSeconds + exp := m.TimeoutSeconds if exp <= 0 { exp = 60 } @@ -190,7 +188,6 @@ SELECT model_name, CapQueueLimit: capQueue, OldQueueLimit: oldQueue, NewQueueLimit: newQueue, - ExpectedSeconds: m.ExpectedSeconds, }) } diff --git a/service/task/task_service.go b/service/task/task_service.go index fe65c9a..5e05984 100644 --- a/service/task/task_service.go +++ b/service/task/task_service.go @@ -3,6 +3,7 @@ package task import ( "context" "errors" + "fmt" "model-gateway/common/util" "model-gateway/service/queue" "time" @@ -11,9 +12,12 @@ import ( "model-gateway/model/dto" "model-gateway/model/entity" + "gitea.com/red-future/common/beans" + "gitea.com/red-future/common/utils" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv" "github.com/google/uuid" ) @@ -25,22 +29,29 @@ type taskService struct{} func (s *taskService) Create(ctx context.Context, req *dto.CreateTaskReq) (res *dto.CreateTaskRes, err error) { startAt := time.Now() taskID := uuid.NewString() - - // 1) 检查模型配置 - m, err := dao.Model.Get(ctx, &entity.AsynchModel{ + // 1) 检查模型配置,并且获取模型 + userInfo, err := utils.GetUserInfo(ctx) + if err != nil { + return nil, err + } + model, err := dao.Model.Get(ctx, &entity.AsynchModel{ + SQLBaseDO: beans.SQLBaseDO{ + TenantId: userInfo.TenantId, + Creator: userInfo.UserName, + }, ModelName: req.ModelName, }) if err != nil { return nil, err } - if m == nil || (m.Enabled != nil && *m.Enabled != 1) { + if model == nil || (model.Enabled != nil && *model.Enabled != 1) { return nil, errors.New("模型不存在或未启用") } // 2) 排队上限(严格控制:Redis 原子闸门) - limit := queue.GetRuntimeQueueLimit(ctx, req.ModelName, m.QueueLimit) + limit := queue.GetRuntimeQueueLimit(ctx, req.ModelName, model.MaxConcurrency*2) if limit > 0 { - ok, err := queue.AcquireQueueSlot(ctx, req.ModelName, taskID, limit, m.ExpectedSeconds) + ok, err := queue.AcquireQueueSlot(ctx, req.ModelName, taskID, limit, model.TimeoutSeconds) if err != nil { return nil, err } @@ -50,9 +61,13 @@ func (s *taskService) Create(ctx context.Context, req *dto.CreateTaskReq) (res * } // 3) 插入任务记录 + if model.IsAsync != nil && *model.IsAsync == 1 { + // 异步调用:注入回调地址后提交,拿到 task_id 轮询 + req.RequestPayload = util.InjectCallbackURL(ctx, req.RequestPayload, model.CallbackUrl) + } storedPayload := map[string]any{ - "payload": req.RequestPayload, - "headers": util.ForwardHeaders(ctx), + "headers": util.ParseHeadMsgHeaders(model.HeadMsg), + "body": req.RequestPayload, } _, err = dao.Task.Insert(ctx, &entity.AsynchTask{ ModelName: req.ModelName, @@ -60,13 +75,12 @@ func (s *taskService) Create(ctx context.Context, req *dto.CreateTaskReq) (res * State: 0, BizName: req.BizName, CallbackURL: req.CallbackUrl, - ModelKey: m.ApiKey, + ModelKey: model.ApiKey, InputRef: req.InputRef, RequestPayload: storedPayload, EpicycleId: req.EpicycleId, }) - if err != nil { - // 入库失败:回滚闸门占位 + if err != nil { // 入库失败:回滚闸门占位 queue.ReleaseQueueSlot(ctx, req.ModelName, taskID) return nil, err } @@ -100,75 +114,96 @@ func (s *taskService) Create(ctx context.Context, req *dto.CreateTaskReq) (res * }, }) - // 5) 创建成功后立即异步尝试执行当前任务,并仅在任务仍处于 pending(state=0) 时做定向轮询。 - // 一旦任务进入 running/success/failed/downloaded,就停止轮询,避免一直空转。 - go s.pollAndRunUntilPicked(util.AsyncCtx(ctx), taskID, req) + // 5) 获取任务信息 + task, err := dao.Task.ClaimPendingByTaskIDGlobal(ctx, taskID) + if err != nil { + return nil, err + } + if task == nil { + return nil, err + } + + // 5) 创建成功后立即异步尝试执行当前任务 + go AsyncWorker.handleOne(util.AsyncCtx(ctx), task, model, req) + return &dto.CreateTaskRes{TaskID: taskID}, nil } -// pollAndRunUntilPicked 定向轮询执行刚创建的任务 -// - 目标:尽快把刚创建的任务拉起来执行 -// - 只在任务仍为 pending(state=0) 时继续尝试抢占 -// - 一旦任务进入 running(1) / success(2) / failed(3) / downloaded(4),立即停止 -// - 不会无限轮询;runWork 仍负责处理积压队列和未处理到的任务 -func (s *taskService) pollAndRunUntilPicked(ctx context.Context, taskID string, req *dto.CreateTaskReq) { - interval := g.Cfg().MustGet(ctx, "asynch.worker.intervalSeconds", 5).Int() - pollTimeout := g.Cfg().MustGet(ctx, "asynch.worker.pollTimeoutSeconds", 300).Int() - pollCtx, cancel := context.WithTimeout(ctx, time.Duration(pollTimeout)*time.Second) - defer cancel() +func (s *taskService) ModelTaskCallback(ctx context.Context, req *dto.ModelTaskCallbackReq) (*dto.ModelTaskCallbackRes, error) { + g.Log().Infof(ctx, "[模型回调] 收到通知 taskID=%s status=%s", req.TaskID, req.Status) + // 1. 查本地任务 + task, err := dao.Task.Get(ctx, &entity.AsynchTask{ + TaskID: req.TaskID, + }) + if err != nil || task == nil { + return nil, fmt.Errorf("任务不存在: %s", req.TaskID) + } - ticker := time.NewTicker(time.Duration(interval) * time.Second) - defer ticker.Stop() + // 2. 成功:取 video_url 和 usage + if req.Status == "succeeded" { + result := map[string]any{ + "video_url": req.Content["video_url"], + "usage": req.Usage, + } + NotifyAsyncResult(req.TaskID, result, nil) + return &dto.ModelTaskCallbackRes{Success: true}, nil + } - g.Log().Infof(ctx, "[任务自动执行][开始] taskId=%s 轮询间隔=%ds 超时=%ds", taskID, interval, pollTimeout) - tryRun := func() bool { - t, err := dao.Task.Get(ctx, &entity.AsynchTask{ - TaskID: taskID, - }) + // 3. 失败/过期 + if req.Status == "failed" || req.Status == "expired" { + NotifyAsyncResult(req.TaskID, nil, fmt.Errorf(req.Status)) + return &dto.ModelTaskCallbackRes{Success: true}, nil + } + + return &dto.ModelTaskCallbackRes{Success: true}, nil +} + +// QueryPendingTasks 批量轮询进行中的异步任务 +func (s *taskService) QueryPendingTasks(ctx context.Context, req *dto.QueryPendingTasksReq) (*dto.QueryPendingTasksRes, error) { + limit := req.Limit + if limit <= 0 { + limit = g.Cfg().MustGet(ctx, "asynch.queryPending.limit", 10).Int() + } + + // 1. 查 state=1(执行中)的异步任务 + tasks, err := dao.Task.GetPendingAsyncTasks(ctx, limit) + if err != nil { + return nil, err + } + + // 2. 逐个查询 + var results []dto.QueryTaskItem + for _, t := range tasks { + // 拿到模型配置 + model, err := dao.Model.GetByModelNameForTenant(ctx, t.TenantId, t.ModelName) + if err != nil || model == nil || model.QueryConfig == nil { + continue + } + result, err := util.PullTaskResult(ctx, t.TaskID, model.QueryConfig) if err != nil { - g.Log().Warningf(ctx, "[任务自动执行][停止] taskId=%s 原因=查询失败 err=%v", taskID, err) - return true - } - if t == nil { - g.Log().Warningf(ctx, "[任务自动执行][停止] taskId=%s 原因=任务不存在", taskID) - return true + g.Log().Warningf(ctx, "[轮询] 查询失败 taskID=%s err=%v", t.TaskID, err) + continue } - switch t.State { - case 0: - //RunByTaskID 尝试执行任务 - if err = AsyncWorker.RunByTaskID(ctx, taskID, req); err != nil { - g.Log().Warningf(ctx, "[任务自动执行][重试] taskId=%s 状态=待处理 err=%v", taskID, err) - } else { - g.Log().Infof(ctx, "[任务自动执行][已触发] taskId=%s 状态=待处理", taskID) - } - return false - case 1: - g.Log().Infof(ctx, "[任务自动执行][停止] taskId=%s 原因=执行中", taskID) - return true - case 2, 3, 4: - g.Log().Infof(ctx, "[任务自动执行][停止] taskId=%s 原因=终态 状态=%d", taskID, t.State) - return true - default: - g.Log().Infof(ctx, "[任务自动执行][停止] taskId=%s 原因=未知状态 状态=%d", taskID, t.State) - return true - } - } - // 立即尝试一次 - if stop := tryRun(); stop { - return - } - for { - select { - case <-pollCtx.Done(): - g.Log().Infof(ctx, "[任务自动执行][停止] taskId=%s 原因=轮询超时", taskID) - return - case <-ticker.C: - if stop := tryRun(); stop { - return - } + status := gconv.String(result["status"]) + item := dto.QueryTaskItem{ + TaskID: t.TaskID, + Status: status, + Content: result["content"].(map[string]any), + Usage: result["usage"].(map[string]any), + } + results = append(results, item) + + // 如果任务完成,通知等待通道 + if status == "succeeded" || status == "failed" || status == "expired" { + NotifyAsyncResult(t.TaskID, result["content"].(map[string]any), nil) } } + + return &dto.QueryPendingTasksRes{ + Total: len(results), + Results: results, + }, nil } // GetResult 获取任务结果 diff --git a/service/task/worker.go b/service/task/worker.go index 0edeb98..0f2f8bd 100644 --- a/service/task/worker.go +++ b/service/task/worker.go @@ -13,8 +13,8 @@ import ( "model-gateway/service/queue" "net/http" "os" - "path/filepath" "strings" + "sync" "time" "unicode/utf8" @@ -55,7 +55,7 @@ func (w *asyncWorker) RunOnce(ctx context.Context, req *dto.RunWorkReq) (res *dt for _, t := range tasks { task := t _ = pool.AddWithRecover(ctx, func(ctx context.Context) { - w.handleOne(ctx, task, &dto.CreateTaskReq{EpicycleId: 0}) + //w.handleOne(ctx, task, &dto.CreateTaskReq{EpicycleId: 0}) done <- struct{}{} }, func(ctx context.Context, e error) { if e != nil { @@ -74,185 +74,159 @@ func (w *asyncWorker) RunOnce(ctx context.Context, req *dto.RunWorkReq) (res *dt }, nil } -// RunByTaskID 创建任务后立即异步尝试执行当前任务: -// - 只定向抢占当前 taskId 对应的 pending 任务 -// - 若任务已被其它 worker 抢走/已不在 pending,则直接返回 -func (w *asyncWorker) RunByTaskID(ctx context.Context, taskID string, req *dto.CreateTaskReq) error { - task, err := dao.Task.ClaimPendingByTaskIDGlobal(ctx, taskID) - if err != nil { - return err - } - if task == nil { - return nil - } - w.handleOne(ctx, task, req) - return nil -} - // handleOne 执行一次完整的任务 -func (w *asyncWorker) handleOne(ctx context.Context, t *entity.AsynchTask, req *dto.CreateTaskReq) { - payload := util.ParseStoredPayload(t.RequestPayload) - maxRetry := 0 // 后面从 model 取 - g.Log().Infof(ctx, "[执行任务][开始] taskId=%s model=%s", t.TaskID, t.ModelName) +func (w *asyncWorker) handleOne(ctx context.Context, task *entity.AsynchTask, model *entity.AsynchModel, req *dto.CreateTaskReq) { + body := util.GetModelBody(task.RequestPayload) //核心请求参数 + maxRetry := model.RetryTimes //重试次数 - // 1) 获取模型配置 - model, err := dao.Model.GetByModelNameForTenant(ctx, t.TenantId, t.ModelName) - if err != nil || model == nil { - w.failTask(ctx, t, "模型不存在或未启用") - return - } - maxRetry = model.RetryTimes - - // 2) 分布式并发控制 - semKey := fmt.Sprintf("asynch:sem:%s", t.ModelName) - maxC := queue.GetRuntimeMaxConcurrency(ctx, t.ModelName, model.MaxConcurrency) + g.Log().Infof(ctx, "[执行任务][开始] taskId=%s model=%s", task.TaskID, task.ModelName) + // 1) 分布式并发控制 + semKey := fmt.Sprintf("asynch:sem:%s", task.ModelName) + maxC := queue.GetRuntimeMaxConcurrency(ctx, task.ModelName, model.MaxConcurrency) acquired, err := queue.AcquireSemaphore(ctx, semKey, maxC, 3600) if err != nil { - w.failTask(ctx, t, err.Error()) + w.failTask(ctx, task, err.Error()) return } if !acquired { - g.Log().Infof(ctx, "[执行任务][排队] 并发已满,放回队列 taskId=%s", t.TaskID) - _ = w.rollbackToPending(ctx, t.Id) + g.Log().Infof(ctx, "[执行任务][排队] 并发已满,放回队列 taskId=%s", task.TaskID) + _ = w.rollbackToPending(ctx, task.Id) return } defer func() { _ = queue.ReleaseSemaphore(ctx, semKey) }() - // 3) request_payload 校验 - if payload == nil { - w.failTask(ctx, t, "request_payload 为空") + // 2) request_payload 校验 + if body == nil { + w.failTask(ctx, task, "请求模型为空") return } - // 4) 调用模型 - var textResult map[string]any - if streamEnabled, _ := model.StreamConfig["enabled"].(bool); streamEnabled { - rawBytes, modelErr := w.callModelRaw(ctx, t, model, payload) - if modelErr != nil { - w.failTask(ctx, t, modelErr.Error()) + // 3) 调用模型 + switch { + case model.IsStream != nil && *model.IsStream == 1: // 流式调用 + rawBytes, err := w.callModelStream(ctx, task, model, body) + if err != nil { + w.failTask(ctx, task, err.Error()) return } - textResult, err = util.ParseStreamResponse(rawBytes, model.StreamConfig) + // 解析流式结果 + body, err = util.ParseStreamResponse(rawBytes, model.StreamConfig) if err != nil { - w.failTask(ctx, t, err.Error()) + w.failTask(ctx, task, err.Error()) return } - } else { - textResult, err = w.callModel(ctx, t, model, payload) + case model.IsAsync != nil && *model.IsAsync == 1: // 异步调用:注入回调地址后提交,拿到 task_id 轮询 + // 异步调用:提交任务 + body, err = w.callModel(ctx, task, model, body) if err != nil { - w.failTask(ctx, t, err.Error()) + w.failTask(ctx, task, err.Error()) + return + } + // 拿到 task_id,启动轮询 + taskID := gjson.New(body).Get(model.ResponseBody).String() + body, err = util.PullTaskResult(ctx, taskID, model.QueryConfig) + if err != nil { + w.failTask(ctx, task, err.Error()) + return + } + default: // 同步调用 + body, err = w.callModel(ctx, task, model, body) + if err != nil { + w.failTask(ctx, task, err.Error()) return } } - // 5) 模型返回映射处理 - textResult, err = util.MapResponsePayload(model.ResponseMapping, textResult) + // 5) 解析响应映射 + body, err = util.MapResponsePayload(model.ResponseMapping, body) if err != nil { - w.failTask(ctx, t, err.Error()) + w.failTask(ctx, task, err.Error()) return } - // 6) 保存临时文件(区分二进制音频和JSON文本) - if audioData, ok := textResult["audio"].([]byte); ok { - tmpPath, tmpErr := saveTmpResult(t.TaskID, audioData, ".mp3") - if tmpErr == nil && tmpPath != "" { - if t.TmpFile != "" { - _ = os.Remove(t.TmpFile) - } - t.TmpFile = tmpPath - t.Phase = 1 - _ = dao.Task.UpdateTmpAfterModelGlobal(ctx, t.Id, tmpPath) - } - } else { - mappedBytes, _ := json.Marshal(textResult) - if len(mappedBytes) > 0 { - tmpPath, tmpErr := saveTmpResult(t.TaskID, mappedBytes, ".json") - if tmpErr == nil && tmpPath != "" { - if t.TmpFile != "" { - _ = os.Remove(t.TmpFile) - } - t.TmpFile = tmpPath - t.Phase = 1 - _ = dao.Task.UpdateTmpAfterModelGlobal(ctx, t.Id, tmpPath) - } - } + // 5) 保存临时文件(通用工具方法) + tmpPath, tmpErr := util.SaveTempFileByType(task.TaskID, body, task.TmpFile) + if tmpErr == nil && tmpPath != "" { + task.TmpFile = tmpPath + task.Phase = 1 + _ = dao.Task.UpdateTmpAfterModelGlobal(ctx, task.Id, tmpPath) } - // 7) 上传 OSS(可重试) + // 6) 上传 OSS(可重试) var oss *gateway.UploadFileResponse for attempt := 0; attempt <= maxRetry; attempt++ { if attempt > 0 { - g.Log().Infof(ctx, "[执行任务][重试] OSS上传 第%d/%d次 taskId=%s", attempt, maxRetry, t.TaskID) + g.Log().Infof(ctx, "[执行任务][重试] OSS上传 第%d/%d次 taskId=%s", attempt, maxRetry, task.TaskID) } - oss, err = w.uploadOSS(ctx, t) + oss, err = w.uploadOSS(ctx, task) if err == nil { break } g.Log().Errorf(ctx, "[执行任务][失败] OSS上传失败 taskId=%s attempt=%d/%d err=%v", - t.TaskID, attempt, maxRetry, err) + task.TaskID, attempt, maxRetry, err) if attempt == maxRetry { - _ = dao.Task.UpdateFailedKeepTmpGlobal(ctx, t.Id, err.Error()) - w.failTask(ctx, t, fmt.Sprintf("OSS上传重试耗尽: %v", err)) + _ = dao.Task.UpdateFailedKeepTmpGlobal(ctx, task.Id, err.Error()) + w.failTask(ctx, task, fmt.Sprintf("OSS上传重试耗尽: %v", err)) return } } - //8) 解析校验(可重试,失败重新调模型) + // 7) 解析校验(可重试,失败重新调模型) if req.BuildType == 1 { for attempt := 0; attempt <= maxRetry; attempt++ { if attempt > 0 { - g.Log().Infof(ctx, "[执行任务][重试] JSON解析 第%d/%d次 taskId=%s", attempt, maxRetry, t.TaskID) + g.Log().Infof(ctx, "[执行任务][重试] JSON解析 第%d/%d次 taskId=%s", attempt, maxRetry, task.TaskID) } // 6.1) 校验数据 - err = util.ValidatePromptResult(textResult, model) + err = util.ValidatePromptResult(body, model) if err == nil { break } g.Log().Warningf(ctx, "[执行任务][解析失败] taskId=%s attempt=%d/%d err=%v", - t.TaskID, attempt, maxRetry, err) + task.TaskID, attempt, maxRetry, err) if attempt == maxRetry { - w.failTask(ctx, t, fmt.Sprintf("JSON解析重试耗尽: %v", err)) + w.failTask(ctx, task, fmt.Sprintf("JSON解析重试耗尽: %v", err)) return } // 6.2) 重新调模型 - newResult, modelErr := w.callModel(ctx, t, model, payload) + newResult, modelErr := w.callModel(ctx, task, model, body) if modelErr != nil { g.Log().Warningf(ctx, "[执行任务][重试] 重新调模型失败 taskId=%s attempt=%d/%d err=%v", - t.TaskID, attempt, maxRetry, modelErr) + task.TaskID, attempt, maxRetry, modelErr) continue } - textResult = newResult + body = newResult } } - // 9) 成功回调 - t.State = 2 - t.OssFile = oss.FileAddressPrefix + oss.FileURL - t.FileType = oss.FileFormat - t.TextResult = textResult - t.FileSize = int64(oss.FileSize) - t.ExpendTokens = int64(GetExpendTokens(model.ResponseTokenField, textResult)) + // 8) 成功回调 + task.State = 2 + task.OssFile = oss.FileAddressPrefix + oss.FileURL + task.FileType = oss.FileFormat + task.TextResult = body + task.FileSize = int64(oss.FileSize) + task.ExpendTokens = int64(GetExpendTokens(model.ResponseTokenField, body)) - if err = dao.Task.UpdateSuccessGlobal(ctx, t); err != nil { - g.Log().Errorf(ctx, "[执行任务][失败] 更新数据库失败 taskId=%s err=%v", t.TaskID, err) + if err = dao.Task.UpdateSuccessGlobal(ctx, task); err != nil { + g.Log().Errorf(ctx, "[执行任务][失败] 更新数据库失败 taskId=%s err=%v", task.TaskID, err) return } - queue.ReleaseQueueSlot(ctx, t.ModelName, t.TaskID) - go gateway.TriggerCallback(context.WithoutCancel(ctx), t) + queue.ReleaseQueueSlot(ctx, task.ModelName, task.TaskID) + go gateway.TriggerCallback(context.WithoutCancel(ctx), task) if req.EpicycleId != 0 { - go gateway.TriggerPromptsCallback(context.WithoutCancel(ctx), t, req.EpicycleId) + go gateway.TriggerPromptsCallback(context.WithoutCancel(ctx), task, req.EpicycleId) } g.Log().Infof(ctx, "[执行任务][成功] taskId=%s fileType=%s textLen=%d callbackUrl=%s", - t.TaskID, oss.FileFormat, len(textResult), t.CallbackURL) + task.TaskID, oss.FileFormat, len(body), task.CallbackURL) - // 10) 删除临时文件 - _ = os.Remove(t.TmpFile) + // 9) 删除临时文件 + _ = os.Remove(task.TmpFile) } -// callModelRaw 调用模型,返回原始字节(不做响应映射,用于流式输出) -func (w *asyncWorker) callModelRaw(ctx context.Context, task *entity.AsynchTask, model *entity.AsynchModel, payload map[string]any) ([]byte, error) { +// callModelStream 调用模型,返回原始字节(不做响应映射,用于流式输出) +func (w *asyncWorker) callModelStream(ctx context.Context, task *entity.AsynchTask, model *entity.AsynchModel, body map[string]any) ([]byte, error) { var data []byte var err error @@ -265,11 +239,11 @@ func (w *asyncWorker) callModelRaw(ctx context.Context, task *entity.AsynchTask, if data == nil { _ = dao.Stat.IncRequestCount(ctx, time.Now(), int64(task.TenantId), task.Creator, task.ModelName) - data, err = InvokeModel(ctx, model, payload, task.ModelKey) + data, err = InvokeModel(ctx, model, body, task.ModelKey) if err != nil { return nil, err } - tmpPath, tmpErr := saveTmpResult(task.TaskID, data, "") + tmpPath, tmpErr := util.SaveTmpResult(task.TaskID, data, "") if tmpErr == nil && tmpPath != "" { task.TmpFile = tmpPath task.Phase = 1 @@ -280,9 +254,61 @@ func (w *asyncWorker) callModelRaw(ctx context.Context, task *entity.AsynchTask, return data, nil } +// asyncResult 异步任务结果 +type asyncResult struct { + result map[string]any + err error +} + +// asyncTaskChan 全局异步任务等待通道 +var asyncTaskChan = sync.Map{} // taskID → chan asyncResult + +func (w *asyncWorker) callModelAsync(ctx context.Context, task *entity.AsynchTask, model *entity.AsynchModel, body map[string]any) (map[string]any, error) { + // 1. 提交异步任务 + result, err := w.callModel(ctx, task, model, body) + if err != nil { + return nil, err + } + // 2. 拿到 task_id + taskID := gjson.New(result).Get(model.ResponseBody).String() + + // 3. 创建等待通道 + ch := make(chan asyncResult, 1) + asyncTaskChan.Store(taskID, ch) + defer func() { + asyncTaskChan.Delete(taskID) + close(ch) + }() + + // 4. 阻塞等待回调或超时 + timeout := time.Duration(model.TimeoutSeconds) * time.Second + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + g.Log().Infof(ctx, "[异步任务] 开始等待结果 taskID=%s timeout=%v", taskID, timeout) + + select { + case res, ok := <-ch: + if !ok { + return nil, fmt.Errorf("异步任务通道已关闭: taskID=%s", taskID) + } + g.Log().Infof(ctx, "[异步任务] 获取结果成功 taskID=%s", taskID) + return res.result, res.err + case <-ctx.Done(): + return nil, fmt.Errorf("异步任务超时: taskID=%s", taskID) + } +} + +// NotifyAsyncResult 回调接口调用此方法通知结果 +func NotifyAsyncResult(taskID string, result map[string]any, err error) { + if ch, ok := asyncTaskChan.Load(taskID); ok { + ch.(chan asyncResult) <- asyncResult{result: result, err: err} + } +} + // 返回: ossURL(成功时有值), fileType, textResult(失败时是错误信息), retryable(是否可重试) // callModel 调用模型 + 检测文件类型 + 保存临时文件 -func (w *asyncWorker) callModel(ctx context.Context, task *entity.AsynchTask, model *entity.AsynchModel, payload map[string]any) (map[string]any, error) { +func (w *asyncWorker) callModel(ctx context.Context, task *entity.AsynchTask, model *entity.AsynchModel, body map[string]any) (map[string]any, error) { var data []byte var contentType, ext, textResult string var err error @@ -296,11 +322,11 @@ func (w *asyncWorker) callModel(ctx context.Context, task *entity.AsynchTask, mo if data == nil { _ = dao.Stat.IncRequestCount(ctx, time.Now(), int64(task.TenantId), task.Creator, task.ModelName) - data, err = InvokeModel(ctx, model, payload, task.ModelKey) + data, err = InvokeModel(ctx, model, body, task.ModelKey) if err != nil { return nil, err } - tmpPath, tmpErr := saveTmpResult(task.TaskID, data, ext) + tmpPath, tmpErr := util.SaveTmpResult(task.TaskID, data, ext) if tmpErr == nil && tmpPath != "" { task.TmpFile = tmpPath task.Phase = 1 @@ -317,7 +343,7 @@ func (w *asyncWorker) callModel(ctx context.Context, task *entity.AsynchTask, mo // InvokeModel 调用模型服务,返回二进制结果 // modelKey 用于覆盖/补充模型配置 head_msg(例如每次请求携带不同的 X-API-Key) -func InvokeModel(ctx context.Context, model *entity.AsynchModel, payload map[string]any, modelKey string) ([]byte, error) { +func InvokeModel(ctx context.Context, model *entity.AsynchModel, body map[string]any, modelKey string) ([]byte, error) { // 1)请求参数映射:将标准 payload 按模型配置的 requestMapping 转为模型需要的格式 //mappedPayload := util.ReverseMap(model.RequestMapping, payload) @@ -331,7 +357,7 @@ func InvokeModel(ctx context.Context, model *entity.AsynchModel, payload map[str var req *http.Request switch method { case http.MethodGet: - q, err := util.PayloadToQuery(payload) + q, err := util.BodyToQuery(body) if err != nil { return nil, err } @@ -344,7 +370,7 @@ func InvokeModel(ctx context.Context, model *entity.AsynchModel, payload map[str } req, err = http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil) default: - bodyBytes, err := json.Marshal(payload) + bodyBytes, err := json.Marshal(body) if err != nil { return nil, err } @@ -355,8 +381,8 @@ func InvokeModel(ctx context.Context, model *entity.AsynchModel, payload map[str for hk, hv := range util.ParseHeadMsgHeaders(model.HeadMsg) { req.Header.Set(hk, hv) } - for hk, hv := range util.ParseHeadMsgHeaders(modelKey) { - req.Header.Set(hk, hv) + if modelKey != "" { + req.Header.Set("Authorization", "Bearer "+modelKey) } if method != http.MethodGet { req.Header.Set("Content-Type", "application/json") @@ -456,25 +482,7 @@ func (w *asyncWorker) failTask(ctx context.Context, t *entity.AsynchTask, errMsg go gateway.TriggerCallback(context.WithoutCancel(ctx), t) } -// saveTmpResult 将模型输出写入临时文件,用于 OSS 上传失败后的“仅重试 OSS”。 -func saveTmpResult(taskID string, data []byte, ext string) (string, error) { - dir := filepath.Join(os.TempDir(), "model-asynch") - if err := os.MkdirAll(dir, 0o755); err != nil { - return "", err - } - if ext == "" { - ext = ".bin" - } - if ext[0] != '.' { - ext = "." + ext - } - path := filepath.Join(dir, fmt.Sprintf("%s%s", taskID, ext)) - if err := os.WriteFile(path, data, 0o644); err != nil { - return "", err - } - return path, nil -} - +// rollbackToPending 恢复任务状态为 PENDING func (w *asyncWorker) rollbackToPending(ctx context.Context, id int64) error { return dao.Task.RollbackToPendingGlobal(ctx, id) } diff --git a/update.sql b/update.sql index 672a85d..e1cdef1 100644 --- a/update.sql +++ b/update.sql @@ -8,45 +8,57 @@ -- 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), -- 删除时间(软删) - -- 业务字段 - model_name VARCHAR(128) NOT NULL, -- 模型名称 - model_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-是 - is_owner SMALLINT NOT NULL DEFAULT 99, -- 1=当前用户创建的,0=超级管理员的 - 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 '' -- 备注 - response_token_field VARCHAR(128) NOT NULL DEFAULT ''; -- 响应中消耗token的字段映射 - operator_name VARCHAR(64) NOT NULL DEFAULT '', -- 运营商名称 - stream_config JSONB NOT NULL DEFAULT '{}'::jsonb, -- 流式配置 - token_config JSONB NOT NULL DEFAULT '{}'::jsonb -- Token配置 - extend_mapping JSONB NOT NULL DEFAULT '{}'::jsonb, - query_config JSONB NOT NULL DEFAULT '{}'::jsonb; + -- ========== 基础字段 ========== + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL DEFAULT 0, + 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, + model_type SMALLINT NOT NULL DEFAULT 0, + operator_name VARCHAR(64) NOT NULL DEFAULT '', + + -- ========== 请求配置 ========== + base_url VARCHAR(256) NOT NULL, + http_method VARCHAR(8) NOT NULL DEFAULT 'POST', + head_msg JSONB NOT NULL DEFAULT '{}'::jsonb, + api_key VARCHAR(256) NOT NULL DEFAULT '', + + -- ========== 状态开关 ========== + is_private SMALLINT NOT NULL DEFAULT 0, + enabled SMALLINT NOT NULL DEFAULT 1, + is_chat_model SMALLINT NOT NULL DEFAULT 0, + is_async SMALLINT NOT NULL DEFAULT 0, + is_stream SMALLINT NOT NULL DEFAULT 0, + is_owner SMALLINT NOT NULL DEFAULT 99, + + -- ========== 配置相关 ========== + 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, + token_config JSONB NOT NULL DEFAULT '{}'::jsonb, + extend_mapping JSONB NOT NULL DEFAULT '{}'::jsonb, + query_config JSONB NOT NULL DEFAULT '{}'::jsonb, + stream_config JSONB NOT NULL DEFAULT '{}'::jsonb, + first_frame VARCHAR(128) NOT NULL DEFAULT '', + last_frame VARCHAR(128) NOT NULL DEFAULT '', + + -- ========== 限制与重试 ========== + max_concurrency INT NOT NULL DEFAULT 10, + timeout_seconds INT NOT NULL DEFAULT 600, + retry_times SMALLINT NOT NULL DEFAULT 3, + auto_clean_seconds INT NOT NULL DEFAULT 86400, + + -- ========== 其他 ========== + response_token_field VARCHAR(128) NOT NULL DEFAULT '', ); + +-- ========== 索引 ========== 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); @@ -55,7 +67,9 @@ CREATE INDEX IF NOT EXISTS idx_asynch_models_model_type ON asynch_models(model_t 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 COLUMN asynch_models.tenant_id IS '租户ID'; COMMENT ON COLUMN asynch_models.creator IS '创建人'; @@ -66,40 +80,32 @@ COMMENT ON COLUMN asynch_models.deleted_at IS '删除时间(软删)'; COMMENT ON COLUMN asynch_models.model_name IS '模型名称'; COMMENT ON COLUMN asynch_models.model_type IS '模型类型'; +COMMENT ON COLUMN asynch_models.operator_name 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.is_owner IS '1=当前用户创建的,0=超级管理员的'; -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.head_msg IS '请求头信息'; +COMMENT ON COLUMN asynch_models.api_key IS '调用凭证/密钥'; +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.is_async IS '是否异步:0-同步 1-异步'; +COMMENT ON COLUMN asynch_models.is_stream IS '是否流式:0-非流式 1-流式'; +COMMENT ON COLUMN asynch_models.is_owner IS '1=当前用户创建 0=超级管理员'; +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.token_config IS 'Token计算配置'; +COMMENT ON COLUMN asynch_models.extend_mapping IS '附加映射'; +COMMENT ON COLUMN asynch_models.query_config IS '查询/回调配置'; +COMMENT ON COLUMN asynch_models.stream_config IS '流式输出配置'; +COMMENT ON COLUMN asynch_models.first_frame IS '首帧图片参数'; +COMMENT ON COLUMN asynch_models.last_frame IS '尾帧图片参数'; +COMMENT ON COLUMN asynch_models.max_concurrency IS '最大并发数'; +COMMENT ON COLUMN asynch_models.timeout_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.auto_clean_seconds IS '任务完成后自动清理时间(秒)'; COMMENT ON COLUMN asynch_models.response_token_field IS '响应中消耗token的字段映射'; -COMMENT ON COLUMN asynch_models.operator_name IS '运营商名称'; -COMMENT ON COLUMN asynch_models.extend_mapping IS '附加映射(请求时候额外字段)'; -COMMENT ON COLUMN asynch_models.query_config IS '查询结果配置(通过task_id查询结果相关配置)'; -COMMENT ON COLUMN asynch_models.token_config IS '{ - "zh_ratio": 1.0, // 中文字符→token系数 - "en_ratio": 1.3, // 英文单词→token系数 - "space_ratio": 0.1, // 空格系数 - "punctuation_ratio": 0.1, // 标点系数 - "max_window_size": 8192, // 模型最大窗口 - "reserve_ratio": 0.2, // 预留回复空间比例 - "min_reserve": 512, // 最少预留token数 -}'; -- =========================