Compare commits
45 Commits
50d2eadbd1
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 445ee02c5a | |||
| b3b111995e | |||
| 1c6c9bae14 | |||
| afd60caf56 | |||
| 196d2069ac | |||
| 7596cbde09 | |||
| 7ec18926e3 | |||
| a6b32bfeb3 | |||
| 2dc88ae587 | |||
| e906248b0a | |||
| e5781aca06 | |||
| 0cf8948cd2 | |||
| 96e8bdfe62 | |||
| 26de41d04e | |||
| 0bee3685fb | |||
| 9049e0d2e8 | |||
| aae46a4f29 | |||
| bcfcc7ed47 | |||
| 2c7838807b | |||
| 52124385a1 | |||
| c7e9eb889b | |||
| 558fd49ec1 | |||
| d409b84b58 | |||
| e487b4bb5e | |||
| a28fcbaee9 | |||
| 5416e7a983 | |||
| 0e2ac286e9 | |||
| a88dc84d99 | |||
| 4d2d4fd93d | |||
| 7129bd2de7 | |||
| 09474eb997 | |||
| 4946220185 | |||
| b6cdb8ff1d | |||
| 4626d819b5 | |||
| 170568e03e | |||
| a080a5536d | |||
| 142fea1e91 | |||
| a585233c4d | |||
| 09290fecbe | |||
| 6409b612bd | |||
| bac9d7713f | |||
| acfddb98d9 | |||
| adf1d0ae6e | |||
| 37d3461983 | |||
| e81df5ce5a |
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
# 阶段1: 构建
|
||||
FROM golang:alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
ENV GO111MODULE=on
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOTOOLCHAIN=auto
|
||||
WORKDIR /build
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go mod download && go mod tidy
|
||||
|
||||
RUN go build -ldflags="-s -w" -o main ./main.go
|
||||
|
||||
|
||||
EXPOSE 3004
|
||||
|
||||
CMD ["./main"]
|
||||
206
README.md
Normal file
206
README.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# model-asynch(模型异步中间件)[2026.5.12前,暂时弃置]
|
||||
|
||||
一个独立的异步中间件服务:按模型配置路由调用不同模型服务,统一生成 `task_id`,后台异步执行,结果上传 OSS,并提供查询/批量领取/自动重试/自动清理能力,便于业务方“拿走结果并转移”。
|
||||
|
||||
> 分支约定:`dev` 为开发分支;`main`(或 master)为线上主分支。
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心功能
|
||||
|
||||
### 1.1 模型配置(asynch_models)
|
||||
- 增删改查模型服务配置(`model_name` 唯一标识)
|
||||
- 支持配置:
|
||||
- 请求地址:`base_url + route`
|
||||
- 请求方式:`http_method`(GET/POST)
|
||||
- 请求头:`head_msg`(以请求头注入,支持多个 header)
|
||||
- 超时:`timeout_seconds`
|
||||
- 并发:`max_concurrency`(按租户+模型的 Redis 分布式信号量限流)
|
||||
- 重试:`retry_times`(失败后最多再重试 N 次)
|
||||
- 保留:`auto_clean_seconds`(任务被业务领取到 `state=4` 后的保留秒数,到期清理)
|
||||
|
||||
### 1.2 异步任务(asynch_task)
|
||||
- 创建任务:生成 `task_id`,入库排队
|
||||
- 后台 Worker:
|
||||
- PostgreSQL `FOR UPDATE SKIP LOCKED` 抢占任务,支持多实例不重复消费
|
||||
- 调用模型服务(GET/POST)
|
||||
- 结果上传 OSS(调用你们的 OSS 文件服务 `oss/file/uploadFile`,透传 `Authorization/X-User-Info`)
|
||||
- 批量领取结果:批量查询 `task_id` 列表,返回 `task_id/state/oss_file`,并把成功的任务从 `state=2` 更新为 `state=4`
|
||||
- 自动重试:失败 `state=3` 会由清理器按 `retry_times` 重新入队到队尾
|
||||
- 自动清理:
|
||||
- `state=4` 且 `expire_at` 到期 → 硬删除任务
|
||||
- 失败重试耗尽仍失败 → 硬删除任务
|
||||
- `state=0/1` 超时 → 标记失败(防止卡死)
|
||||
|
||||
### 1.3 统计(asynch_model_stat)
|
||||
- 按天统计:`day + tenant_id + creator + model_name -> request_count`
|
||||
- 统计口径:仅在 Worker 真正调用模型服务时计数(OSS 重试不计数)
|
||||
- 用途:给其他服务提供全局限流/监控依据
|
||||
|
||||
---
|
||||
|
||||
## 2. 使用流程(业务方如何接入)
|
||||
|
||||
### 第一步:创建模型配置
|
||||
业务方(或运维)先在中间件里创建/更新模型配置(`model_name` 为唯一键),例如:
|
||||
- `POST /model/createModel`(或 `/model/updateModel`)
|
||||
|
||||
请求示例(JSON):
|
||||
```json
|
||||
{
|
||||
"modelName": "model-service",
|
||||
"modelsType": "1,2,3",
|
||||
"baseUrl": "http://127.0.0.1:8000",
|
||||
"route": "/api/v1/chat",
|
||||
"httpMethod": "POST",
|
||||
"headMsg": "API_KEY:model-key,API_STATE:true,API_NUM:123",
|
||||
"enabled": 1,
|
||||
"maxConcurrency": 5,
|
||||
"queueLimit": 20,
|
||||
"timeoutSeconds": 1800,
|
||||
"expectedSeconds": 600,
|
||||
"retryTimes": 3,
|
||||
"retryQueueMaxSeconds": 600,
|
||||
"autoCleanSeconds": 3600,
|
||||
"remark": "Model-Service 模型服务"
|
||||
}
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- `modelName`:模型名称(唯一标识/路由键)
|
||||
- `modelsType`:模型类型ID列表(逗号分隔),示例:`1,2,3`(关联 `asynch_models_type.type_id`)
|
||||
|
||||
### 模型类型同步
|
||||
- `POST /model/type/createModelType` 创建成功后,会同步 `POST` 到 `prompts-core` 的 `/prompt/createPrompt`
|
||||
- 同步字段映射:
|
||||
- `typeId` -> `modelTypeId`
|
||||
- `type` -> `modelType`
|
||||
- `promptInfo` -> `promptInfo`
|
||||
- `responseJsonSchema` -> `responseJsonSchema`
|
||||
- `version` -> `version`
|
||||
- 若 `prompts-core` 同步失败,`model-gateway` 会回滚本地新建的模型类型,避免两边数据不一致
|
||||
- `form`:动态表单配置(JSON数组),用于前端按模型渲染参数表单(字段示例:field/label/type/required)
|
||||
- `baseUrl`:模型服务地址(Base URL)
|
||||
- `route`:模型服务路由(拼接到 baseUrl 后)
|
||||
- `httpMethod`:请求方式(GET/POST)
|
||||
- `headMsg`:请求头绑定(支持多个 header,逗号分隔,格式 `Key:Value`;布尔/数字也会以字符串形式注入 header)
|
||||
- `enabled`:是否启用(0禁用/1启用)
|
||||
- `maxConcurrency`:单模型最大并发(按租户+模型维度限流)
|
||||
- `queueLimit`:排队上限(严格控制)。创建任务时通过 Redis Lua 原子闸门校验并占位,保证分布式并发创建不会超限;任务进入成功/失败态后释放占位,失败重试重新入队时会再次占位。
|
||||
- `timeoutSeconds`:调用模型服务超时(秒)
|
||||
- `expectedSeconds`:模型预计执行时间(秒,用于超时判定/排队策略等)
|
||||
- `retryTimes`:失败后最多再重试 N 次(不含首次)
|
||||
- `retryQueueMaxSeconds`:失败重试最大排队时间(秒);0 表示重试插队到队首;>0 表示排队超过该时间后插队,否则仍到队尾
|
||||
- `autoCleanSeconds`:任务被领取到 `state=4` 后的保留时间(秒),到期清理
|
||||
- `remark`:备注说明
|
||||
|
||||
### 第二步:创建任务拿到 task_id
|
||||
业务方发起推理请求时调用:
|
||||
- `POST /task/createTask`(传 `modelName + requestPayload + bizName + callbackUrl(可选) + modelKey(可选)`)
|
||||
- 中间件返回 `task_id`
|
||||
- 业务方将 `task_id` 落到自己的业务表,并把业务状态置为「生成中」
|
||||
|
||||
> `modelKey` 用于“动态覆盖/补充”模型配置中的 `head_msg`(例如每次请求携带不同的 `X-API-Key:xxx`)。
|
||||
>
|
||||
> `callbackUrl` 用于任务成功后的回调通知:当任务 `state=2` 成功时,中间件会发起一次 GET 请求:
|
||||
> - 实际回调地址:`callbackUrl/{bizName}`
|
||||
> - query 参数:`task_id/state/oss_file/file_type/text(可选,最多2000字符)`
|
||||
|
||||
### 第三步:同步任务进度(推荐批量)
|
||||
业务方通过轮询/定时任务同步进度:
|
||||
- 推荐:`POST /task/getTaskBatch`(批量传 `taskIds`,返回每个任务的 `state + oss_file`)
|
||||
- 或单条:`GET /task/getTaskResult?taskId=...`
|
||||
|
||||
业务侧拿到 `oss_file` 后自行做资源处理(直接保存或转存),并把业务状态更新为「成功/失败」。
|
||||
|
||||
> 说明:批量接口对 `state=2(成功)` 的任务会自动标记为 `state=4(已下载)` 并写入 `expire_at`,用于后续清理。
|
||||
|
||||
### 后台执行(由上层定时任务控制)
|
||||
本项目不再在服务进程内常驻轮询 worker/cleaner,而是提供两个接口供上层定时任务触发:
|
||||
- `POST /task/runWork`:执行一次 Worker(抢占并处理一批排队任务;适合处理 createTask 立即执行时未处理到的任务和积压队列)
|
||||
- `POST /task/cleanWork`:执行一次 Cleaner(清理过期任务、失败重试、超时任务失败等)
|
||||
|
||||
创建任务执行策略:
|
||||
- `POST /task/createTask` 成功入库后,会立即异步尝试执行当前任务。
|
||||
- 若当前模型并发已满,或当前任务未成功抢占,则会按 `asynch.worker.intervalSeconds` 对当前任务做轻量级定向轮询;只要任务仍为 `state=0` 就继续尝试,一旦进入 `state=1/2/3/4` 就立即停止,不会一直轮询。
|
||||
- 若任务执行成功且配置了 `callbackUrl + bizName`,会在成功落库后异步触发回调钩子。
|
||||
|
||||
本地调试(可选):
|
||||
可在 `config.yml` 中开启自动执行,避免手工频繁调用接口:
|
||||
```yml
|
||||
asynch:
|
||||
worker:
|
||||
enabled: true
|
||||
intervalSeconds: 5
|
||||
batchSize: 10
|
||||
goroutines: 1
|
||||
cleaner:
|
||||
enabled: true
|
||||
intervalSeconds: 30
|
||||
```
|
||||
|
||||
### 动态并发/队列调参(接口请求控制)
|
||||
为支持根据最近一段时间的耗时与吞吐对 `max_concurrency/queue_limit` 做动态调整,本项目提供接口供上层定时任务触发(建议每小时一次):
|
||||
- `POST /model/autoTune`
|
||||
|
||||
请求参数(JSON,可选):
|
||||
```json
|
||||
{
|
||||
"windowSeconds": 3600
|
||||
}
|
||||
```
|
||||
> `windowSeconds` 不传/<=0 默认 3600(1小时)。
|
||||
|
||||
动态调参口径(默认近 1 小时窗口,按 `model_name` 维度):
|
||||
- 执行耗时:`finished_at - started_at`(取 P90)
|
||||
- 吞吐:近 1 小时完成数 / 3600
|
||||
|
||||
调参结果不会覆盖 `asynch_models` 中配置的最大上限(cap),而是写入 Redis 运行时参数(带 TTL,默认 2 小时):
|
||||
- `asynch:runtime:max_concurrency:{model_name}`
|
||||
- `asynch:runtime:queue_limit:{model_name}`
|
||||
|
||||
生效位置:
|
||||
- CreateTask 入队时,严格 queue_limit 闸门会优先使用运行时 `queue_limit`(若无运行时值则回退 cap)。
|
||||
- Worker 获取并发令牌时,优先使用运行时 `max_concurrency`(若无运行时值则回退 cap)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 状态机说明(asynch_task.state)
|
||||
|
||||
| state | 含义 | 产生方 |
|
||||
|---:|---|---|
|
||||
| 0 | 排队中 | 创建任务/重试入队 |
|
||||
| 1 | 执行中 | Worker 抢占后 |
|
||||
| 2 | 成功(已上传 OSS) | Worker |
|
||||
| 3 | 失败 | Worker / 超时处理 |
|
||||
| 4 | 已下载(已领取) | 批量领取接口(2→4) |
|
||||
|
||||
字段补充:
|
||||
- `retry_count`:已重试次数(不含首次)
|
||||
- `enqueue_at`:入队时间(用于排队顺序,重试会更新为 NOW() 放到队尾)
|
||||
- `expire_at`:仅对 `state=4` 生效,表示保留到期时间
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置说明(config.yml)
|
||||
|
||||
关键配置:
|
||||
- `database.default`: PostgreSQL 连接
|
||||
- `redis.default`: Redis 连接(并发令牌、可扩展用途)
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据库初始化
|
||||
|
||||
项目根目录提供 `update.sql`:首次部署执行建表 SQL。
|
||||
|
||||
---
|
||||
|
||||
## 6. 开发与发布建议(Git)
|
||||
|
||||
- `dev`:日常开发与联调
|
||||
- `main`:线上稳定分支
|
||||
- 推荐流程:
|
||||
1) 从 `main` 拉出 `dev`
|
||||
2) 功能完成后提 MR/PR 合并回 `main`
|
||||
3) 打 tag / 发布镜像
|
||||
10
common/util/convert.go
Normal file
10
common/util/convert.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package util
|
||||
|
||||
import "github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
// ConvertTo 转换为指定类型
|
||||
func ConvertTo[T any](v interface{}) *T {
|
||||
var t T
|
||||
_ = gconv.Struct(v, &t)
|
||||
return &t
|
||||
}
|
||||
115
common/util/files.go
Normal file
115
common/util/files.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DetectFileType 根据返回的二进制内容推断 contentType + 扩展名(尽量稳定)
|
||||
func DetectFileType(data []byte) (contentType string, ext string) {
|
||||
if len(data) == 0 {
|
||||
return "application/octet-stream", ""
|
||||
}
|
||||
ct := http.DetectContentType(data)
|
||||
// gateway.DetectContentType 可能带 charset 等参数:text/plain; charset=utf-8
|
||||
if idx := strings.Index(ct, ";"); idx > 0 {
|
||||
ct = strings.TrimSpace(ct[:idx])
|
||||
}
|
||||
switch ct {
|
||||
case "audio/mpeg":
|
||||
return ct, ".mp3"
|
||||
case "audio/wave", "audio/wav", "audio/x-wav":
|
||||
return ct, ".wav"
|
||||
case "video/mp4":
|
||||
return ct, ".mp4"
|
||||
case "image/png":
|
||||
return ct, ".png"
|
||||
case "image/jpeg":
|
||||
return ct, ".jpg"
|
||||
case "application/pdf":
|
||||
return ct, ".pdf"
|
||||
case "text/plain":
|
||||
return ct, ".txt"
|
||||
case "application/json":
|
||||
return ct, ".json"
|
||||
default:
|
||||
// 兜底:尝试从 ct 截取 subtype 作为后缀(例如 application/json)
|
||||
if parts := strings.Split(ct, "/"); len(parts) == 2 {
|
||||
sub := parts[1]
|
||||
// 避免出现 "plain; charset=utf-8" 之类的后缀
|
||||
if idx := strings.Index(sub, ";"); idx > 0 {
|
||||
sub = strings.TrimSpace(sub[:idx])
|
||||
}
|
||||
return ct, "." + sub
|
||||
}
|
||||
return ct, ""
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
79
common/util/headers.go
Normal file
79
common/util/headers.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// AsyncCtx 固化异步上下文中的 token 和用户信息,避免请求结束后丢失
|
||||
func AsyncCtx(ctx context.Context) context.Context {
|
||||
asyncCtx := context.WithoutCancel(ctx)
|
||||
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
if token := r.Header.Get("Authorization"); token != "" {
|
||||
asyncCtx = context.WithValue(asyncCtx, "token", token)
|
||||
}
|
||||
if userInfo := r.Header.Get("X-User-Info"); userInfo != "" {
|
||||
asyncCtx = context.WithValue(asyncCtx, "xUserInfo", userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if user, err := utils.GetUserInfo(ctx); err == nil && user != nil {
|
||||
asyncCtx = context.WithValue(asyncCtx, "user", user)
|
||||
}
|
||||
|
||||
return asyncCtx
|
||||
}
|
||||
|
||||
// ForwardHeaders 透传调用链路的头信息,优先使用 ctx 中的固化值
|
||||
func ForwardHeaders(ctx context.Context) map[string]string {
|
||||
headers := make(map[string]string)
|
||||
SetHeaderFromContext(headers, ctx, "Authorization", "token")
|
||||
SetHeaderFromContext(headers, ctx, "X-User-Info", "xUserInfo")
|
||||
FallbackToRequestHeaders(headers, ctx)
|
||||
return headers
|
||||
}
|
||||
|
||||
// SetHeaderFromContext 从上下文中设置 header
|
||||
func SetHeaderFromContext(headers map[string]string, ctx context.Context, headerKey, ctxKey string) {
|
||||
if value, ok := ctx.Value(ctxKey).(string); ok && value != "" {
|
||||
headers[headerKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
// FallbackToRequestHeaders 从请求头中获取作为兜底
|
||||
func FallbackToRequestHeaders(headers map[string]string, ctx context.Context) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if headers["Authorization"] == "" {
|
||||
if token := r.Header.Get("Authorization"); token != "" {
|
||||
headers["Authorization"] = token
|
||||
}
|
||||
}
|
||||
|
||||
if headers["X-User-Info"] == "" {
|
||||
if userInfo := r.Header.Get("X-User-Info"); userInfo != "" {
|
||||
headers["X-User-Info"] = userInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetTaskHeadersToCtx 把任务入库时保存的 header 信息注入 ctx,给 worker 调 OSS 用
|
||||
func SetTaskHeadersToCtx(ctx context.Context, headers map[string]string) context.Context {
|
||||
if headers == nil {
|
||||
return ctx
|
||||
}
|
||||
if v := gconv.String(headers["Authorization"]); v != "" {
|
||||
ctx = context.WithValue(ctx, "token", v)
|
||||
}
|
||||
if v := gconv.String(headers["X-User-Info"]); v != "" {
|
||||
ctx = context.WithValue(ctx, "xUserInfo", v)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
303
common/util/mapping.go
Normal file
303
common/util/mapping.go
Normal file
@@ -0,0 +1,303 @@
|
||||
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"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
tgjson "github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// ParseAndValidate 解析并校验结果
|
||||
func ParseAndValidate(raw map[string]any, model *entity.ModelGatewayModel) (map[string]any, error) {
|
||||
// 1) 解析 content 字符串为 rounds 数组
|
||||
contentVal, ok := raw[entity.ResponseBody]
|
||||
if !ok {
|
||||
return raw, fmt.Errorf("字段 %s 不存在", entity.ResponseBody)
|
||||
}
|
||||
contentStr, ok := contentVal.(string)
|
||||
if !ok || strings.TrimSpace(contentStr) == "" {
|
||||
return raw, fmt.Errorf("字段 %s 为空或不是字符串", entity.ResponseBody)
|
||||
}
|
||||
var arr []any
|
||||
if err := json.Unmarshal([]byte(contentStr), &arr); err != nil {
|
||||
return raw, fmt.Errorf("JSON解析失败: %w", err)
|
||||
}
|
||||
if len(arr) == 0 {
|
||||
return raw, fmt.Errorf("解析后数组为空")
|
||||
}
|
||||
|
||||
// 2) 校验必填字段
|
||||
if len(model.RequiredFields) > 0 {
|
||||
for i, r := range arr {
|
||||
round, ok := r.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, field := range model.RequiredFields {
|
||||
if gjson.New(round).Get(field).IsNil() {
|
||||
return raw, fmt.Errorf("rounds[%d] 缺少必填字段: %s", i, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{"total_rounds": len(arr), "rounds": arr}, nil
|
||||
}
|
||||
|
||||
// ParseStructResult 解析结构结果
|
||||
func ParseStructResult(raw map[string]any, responseBody string) map[string]any {
|
||||
contentVal := raw[responseBody]
|
||||
// 是字符串,尝试解析
|
||||
contentStr := gconv.String(contentVal)
|
||||
if contentStr == "" || contentStr == "0" {
|
||||
return map[string]any{
|
||||
"total_rounds": 1,
|
||||
"rounds": []map[string]any{{responseBody: raw}},
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试解析为数组
|
||||
var arr []any
|
||||
if err := json.Unmarshal([]byte(contentStr), &arr); err == nil && len(arr) > 0 {
|
||||
return map[string]any{
|
||||
"total_rounds": 1,
|
||||
"rounds": []map[string]any{{responseBody: arr}},
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试解析为单个对象
|
||||
var parsed any
|
||||
if err := json.Unmarshal([]byte(contentStr), &parsed); err == nil {
|
||||
return map[string]any{
|
||||
"total_rounds": 1,
|
||||
"rounds": []map[string]any{{responseBody: parsed}},
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底:原始字符串作为内容
|
||||
return map[string]any{
|
||||
"total_rounds": 1,
|
||||
"rounds": []map[string]any{{responseBody: contentStr}},
|
||||
}
|
||||
}
|
||||
|
||||
// ParseHeadMsgHeaders 从 head_msg JSON 中提取请求头
|
||||
// head_msg 格式示例:
|
||||
//
|
||||
// {
|
||||
// "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 := make(map[string]string, len(headMsg))
|
||||
for k, v := range headMsg {
|
||||
out[k] = gconv.String(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MapResponsePayload 映射模型响应为标准格式
|
||||
func MapResponsePayload(mapping map[string]any, result map[string]any) (map[string]any, error) {
|
||||
if len(mapping) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 把 result 转成 JSON 字符串,tidwall/gjson 需要字符串输入
|
||||
resultBytes, _ := json.Marshal(result)
|
||||
resultStr := string(resultBytes)
|
||||
|
||||
mapped := make(map[string]any)
|
||||
|
||||
for standardField, modelPath := range mapping {
|
||||
path := gconv.String(modelPath)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
value := tgjson.Get(resultStr, path)
|
||||
if !value.Exists() {
|
||||
continue
|
||||
}
|
||||
// 如果是数组路径(含 #),取 Array;否则取单值
|
||||
if strings.Contains(path, "#") {
|
||||
var arr []any
|
||||
for _, v := range value.Array() {
|
||||
arr = append(arr, v.Value())
|
||||
}
|
||||
mapped[standardField] = arr
|
||||
} else {
|
||||
mapped[standardField] = value.Value()
|
||||
}
|
||||
}
|
||||
|
||||
return mapped, nil
|
||||
}
|
||||
|
||||
//
|
||||
//// 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 {
|
||||
continue
|
||||
}
|
||||
q.Set(k, gconv.String(v))
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
// PullTaskResult 轮询查询异步任务结果直到完成
|
||||
func PullTaskResult(ctx context.Context, body map[string]any, queryConfig map[string]any, headMsg map[string]any) (map[string]any, error) {
|
||||
// 1) 解析配置
|
||||
// 1.1 提取 taskID
|
||||
taskIDPath := gconv.String(queryConfig["task_id"])
|
||||
taskID := gconv.String(gjson.New(body).Get(taskIDPath).Val())
|
||||
if taskID == "" {
|
||||
return nil, fmt.Errorf("无法从路径 %s 提取 taskID", taskIDPath)
|
||||
}
|
||||
g.Log().Infof(ctx, "[PullTaskResult] taskID=%s", taskID)
|
||||
|
||||
// 1.2 请求地址,替换 {id}
|
||||
queryUrl := gconv.String(queryConfig["url"])
|
||||
queryUrl = replaceURLParams(queryUrl, map[string]any{"id": taskID})
|
||||
|
||||
// 1.3 请求方式
|
||||
method := gconv.String(queryConfig["method"])
|
||||
if method == "" {
|
||||
method = "GET"
|
||||
}
|
||||
|
||||
// 1.4 状态判断配置
|
||||
statusPath := gconv.String(queryConfig["status_path"])
|
||||
statusValues, _ := queryConfig["status_values"].(map[string]any)
|
||||
if statusPath == "" {
|
||||
statusPath = "status"
|
||||
}
|
||||
|
||||
// 1.5 轮询间隔
|
||||
interval := gconv.Int(queryConfig["interval_seconds"])
|
||||
if interval <= 0 {
|
||||
interval = 2
|
||||
}
|
||||
|
||||
// 1.6 请求体
|
||||
reqBodyMap := map[string]any{"task_id": taskID}
|
||||
|
||||
// 2) 轮询请求
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
var reqBody io.Reader
|
||||
if method == "POST" {
|
||||
bs, _ := json.Marshal(reqBodyMap)
|
||||
reqBody = bytes.NewReader(bs)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, queryUrl, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 统一用 headMsg 注入请求头
|
||||
for hk, hv := range ParseHeadMsgHeaders(headMsg) {
|
||||
req.Header.Set(hk, hv)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
|
||||
g.Log().Infof(ctx, "[PullTaskResult] taskID=%s statusCode=%d body=%s", taskID, resp.StatusCode, string(raw))
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
time.Sleep(time.Duration(interval) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
_ = json.Unmarshal(raw, &result)
|
||||
|
||||
statusVal := gjson.New(result).Get(statusPath).Val()
|
||||
statusStr := gconv.String(statusVal)
|
||||
g.Log().Infof(ctx, "[PullTaskResult] 状态 taskID=%s status=%v", taskID, statusVal)
|
||||
|
||||
if matchStatus(statusStr, statusValues["succeeded"]) {
|
||||
g.Log().Infof(ctx, "[PullTaskResult] 任务成功 taskID=%s", taskID)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if matchStatus(statusStr, statusValues["failed"]) {
|
||||
g.Log().Errorf(ctx, "[PullTaskResult] 任务失败 taskID=%s", taskID)
|
||||
return result, fmt.Errorf("任务失败")
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(interval) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func matchStatus(actual string, expected any) bool {
|
||||
expectedStr := gconv.String(expected)
|
||||
if actual == expectedStr {
|
||||
return true
|
||||
}
|
||||
switch v := expected.(type) {
|
||||
case []any:
|
||||
for _, item := range v {
|
||||
if actual == gconv.String(item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
150
common/util/streaming.go
Normal file
150
common/util/streaming.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
)
|
||||
|
||||
// ================================================================
|
||||
|
||||
// ParseStreamResponse 流式响应解析(通用入口)
|
||||
func ParseStreamResponse(rawBytes []byte, streamConfig map[string]any) (map[string]any, error) {
|
||||
enabled, _ := streamConfig["enabled"].(bool)
|
||||
if !enabled {
|
||||
return gjson.New(string(rawBytes)).Map(), nil
|
||||
}
|
||||
|
||||
parser, _ := streamConfig["parser"].(string)
|
||||
if parser == "base64_concat" {
|
||||
return parseBase64Stream(rawBytes)
|
||||
}
|
||||
|
||||
return parseSSEStream(rawBytes, streamConfig)
|
||||
}
|
||||
|
||||
// parseBase64Stream 拼接流式 base64 并解码为二进制(TTS 等音频模型)
|
||||
func parseBase64Stream(rawBytes []byte) (map[string]any, error) {
|
||||
lines := strings.Split(string(rawBytes), "\n")
|
||||
var audioBase64 strings.Builder
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var chunk map[string]any
|
||||
if err := json.Unmarshal([]byte(line), &chunk); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if data, ok := chunk["data"].(string); ok && data != "" {
|
||||
audioBase64.WriteString(data)
|
||||
}
|
||||
}
|
||||
|
||||
cleanBase64 := strings.Map(func(r rune) rune {
|
||||
if r == ' ' || r == '\n' || r == '\r' || r == '\t' {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, audioBase64.String())
|
||||
|
||||
audioBytes, err := base64.StdEncoding.DecodeString(cleanBase64)
|
||||
if err != nil {
|
||||
audioBytes, err = base64.RawStdEncoding.DecodeString(cleanBase64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("base64 解码失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{"audio": audioBytes}, nil
|
||||
}
|
||||
|
||||
// parseSSEStream SSE 流式解析(图片模型等)
|
||||
func parseSSEStream(rawBytes []byte, streamConfig map[string]any) (map[string]any, error) {
|
||||
events, _ := streamConfig["events"].([]any)
|
||||
if len(events) == 0 {
|
||||
return gjson.New(string(rawBytes)).Map(), nil
|
||||
}
|
||||
|
||||
lines := strings.Split(string(rawBytes), "\n")
|
||||
result := make(map[string]any)
|
||||
var partials []map[string]any
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line == "[DONE]" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "event:") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
line = strings.TrimPrefix(line, "data:")
|
||||
line = strings.TrimSpace(line)
|
||||
}
|
||||
|
||||
var chunk map[string]any
|
||||
if err := json.Unmarshal([]byte(line), &chunk); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
chunkType, _ := chunk["type"].(string)
|
||||
|
||||
for _, evt := range events {
|
||||
e, _ := evt.(map[string]any)
|
||||
match, _ := e["match"].(string)
|
||||
if !strings.Contains(chunkType, match) {
|
||||
continue
|
||||
}
|
||||
|
||||
fields, _ := e["fields"].(map[string]any)
|
||||
aggregateTo, _ := e["aggregate_to"].(string)
|
||||
evtType, _ := e["type"].(string)
|
||||
|
||||
switch evtType {
|
||||
case "partial":
|
||||
item := make(map[string]any)
|
||||
for localKey, chunkKey := range fields {
|
||||
item[localKey] = chunk[chunkKey.(string)]
|
||||
}
|
||||
partials = append(partials, item)
|
||||
|
||||
case "final":
|
||||
for localKey, chunkKey := range fields {
|
||||
val := gjson.New(chunk).Get(chunkKey.(string))
|
||||
if !val.IsNil() {
|
||||
if _, exists := result[aggregateTo]; !exists {
|
||||
result[aggregateTo] = make(map[string]any)
|
||||
}
|
||||
result[aggregateTo].(map[string]any)[localKey] = val.Val()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(partials) > 0 {
|
||||
for _, evt := range events {
|
||||
e, _ := evt.(map[string]any)
|
||||
if e["type"] == "partial" {
|
||||
if orderBy, ok := e["order_by"].(string); ok {
|
||||
sort.Slice(partials, func(i, j int) bool {
|
||||
return fmt.Sprint(partials[i][orderBy]) < fmt.Sprint(partials[j][orderBy])
|
||||
})
|
||||
}
|
||||
result[e["aggregate_to"].(string)] = partials
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mergedBytes, _ := json.Marshal(result)
|
||||
return gjson.New(mergedBytes).Map(), nil
|
||||
}
|
||||
75
config.yml
Normal file
75
config.yml
Normal file
@@ -0,0 +1,75 @@
|
||||
server:
|
||||
address: ":3004"
|
||||
name: "model-gateway"
|
||||
workerId: 1 # 雪花算法worker ID(用于 common/db/gfdb)
|
||||
|
||||
# PostgreSQL(GoFrame driver pgsql)
|
||||
database:
|
||||
default:
|
||||
- type: "pgsql"
|
||||
host: "116.204.74.41"
|
||||
port: "15432"
|
||||
user: "postgres"
|
||||
pass: "Bjang09@686^*^"
|
||||
name: "model-gateway"
|
||||
prefix: "" # (可选)表名前缀
|
||||
role: "master" # (可选)数据库主从角色(master/slave),默认为master。如果不使用应用主从机制请不配置或留空即可。
|
||||
debug: true # (可选)开启调试模式
|
||||
dryRun: false # (可选)ORM空跑(只读不写)
|
||||
charset: "utf8" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312),一般设置为utf8mb4。默认为utf8。
|
||||
timezone: "Asia/Shanghai" # (可选)时区配置,例如:Local
|
||||
maxIdle: 5 # (可选)连接池最大闲置的连接数(默认10)
|
||||
maxOpen: 20 # (可选)连接池最大打开的连接数(默认无限制)
|
||||
maxLifetime: "30s" # (可选)连接对象可重复使用的时间长度(默认30秒)
|
||||
maxIdleConnTime: "30s" # (可选,v2.10新增)连接池中空闲连接的最大生存时间(默认30秒)。可以通过配置文件或SetConnMaxIdleTime方法设置,避免长时间空闲连接占用资源。
|
||||
createdAt: "created_at" # (可选)自动创建时间字段名称
|
||||
updatedAt: "updated_at" # (可选)自动更新时间字段名称
|
||||
deletedAt: "deleted_at" # (可选)软删除时间字段名称
|
||||
timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效
|
||||
model_gateway:
|
||||
- type: "pgsql"
|
||||
host: "116.204.74.41"
|
||||
port: "15432"
|
||||
user: "postgres"
|
||||
pass: "Bjang09@686^*^"
|
||||
name: "model-gateway"
|
||||
prefix: ""
|
||||
role: "master"
|
||||
debug: true
|
||||
dryRun: false
|
||||
charset: "utf8"
|
||||
timezone: "Asia/Shanghai"
|
||||
maxIdle: 5
|
||||
maxOpen: 20
|
||||
maxLifetime: "30s"
|
||||
maxIdleConnTime: "30s"
|
||||
createdAt: "created_at"
|
||||
updatedAt: "updated_at"
|
||||
deletedAt: "deleted_at"
|
||||
timeMaintainDisabled: false
|
||||
|
||||
redis:
|
||||
default:
|
||||
address: 192.168.3.30:6379
|
||||
db: 0
|
||||
|
||||
consul:
|
||||
address: 192.168.3.30:8500
|
||||
|
||||
jaeger:
|
||||
addr: 192.168.3.30:4318
|
||||
|
||||
# 本地调试用:可选自动执行 worker/cleaner(默认关闭)
|
||||
asynch:
|
||||
queryPending:
|
||||
enabled: false
|
||||
intervalSeconds: 10 # 每10秒轮询一次
|
||||
limit: 10 # 每次查10条
|
||||
worker:
|
||||
enabled: false
|
||||
intervalSeconds: 5
|
||||
batchSize: 10
|
||||
goroutines: 1
|
||||
cleaner:
|
||||
enabled: false
|
||||
intervalSeconds: 30
|
||||
123
consts/public/public.go
Normal file
123
consts/public/public.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package public
|
||||
|
||||
const (
|
||||
CallModeSync = 0 // 同步调用
|
||||
CallModeAsync = 1 // 异步调用
|
||||
CallModeStream = 2 // 流式调用
|
||||
)
|
||||
|
||||
const (
|
||||
TaskStatusPending = 0 // 排队中
|
||||
TaskStatusRunning = 1 // 执行中
|
||||
TaskStatusSuccess = 2 // 成功
|
||||
TaskStatusFailed = 3 // 失败
|
||||
TaskStatusDownloaded = 4 // 已下载
|
||||
)
|
||||
|
||||
const (
|
||||
BuildTypePrompt = 1 //提示词构建
|
||||
BuildTypeNode = 2 //节点构建
|
||||
BuildTypeStruct = 3 //结构构建
|
||||
)
|
||||
|
||||
// ModelType 模型类型常量
|
||||
const (
|
||||
ModelTypeInference = 100 // 推理模型
|
||||
|
||||
ModelTypeImage = 200 // 图片模型
|
||||
ImageSubTypeTextToImage = 201 // 图片模型-文生图
|
||||
ImageSubTypeImageToImage = 202 // 图片模型-图生图
|
||||
ImageSubTypeImageEdit = 203 // 图片模型-图片编辑
|
||||
ImageSubTypeImageVariation = 204 // 图片模型-图片变体
|
||||
ImageSubTypeImageTextToImage = 205 // 图片模型-图文生图
|
||||
|
||||
ModelTypeAudio = 300 // 音频模型
|
||||
AudioSubTypeTextToSpeech = 301 // 音频模型-文生音
|
||||
AudioSubTypeSpeechToText = 302 // 音频模型-音生文
|
||||
AudioSubTypeSpeechToSpeech = 303 // 音频模型-音生音
|
||||
|
||||
ModelTypeVector = 400 // 向量化模型
|
||||
VectorSubTypeEmbedding = 401 // 向量化模型-文本嵌入
|
||||
VectorSubTypeRerank = 402 // 向量化模型-重排序
|
||||
|
||||
ModelTypeOmni = 500 // 全模态模型
|
||||
OmniSubTypeTextImageAudio = 501 // 全模态模型-文图音
|
||||
OmniSubTypeVision = 502 // 全模态模型-视觉理解
|
||||
|
||||
ModelTypeVideo = 600 // 视频模型
|
||||
VideoSubTypeTextToVideo = 601 // 视频模型-文生视频
|
||||
VideoSubTypeImageToVideo = 602 // 视频模型-图生视频
|
||||
VideoSubTypeImageTextToVideo = 603 // 视频模型-图文生视频
|
||||
VideoSubTypeVideoToVideo = 604 // 视频模型-视频生视频
|
||||
)
|
||||
|
||||
// ModelTypeName 模型类型名称映射
|
||||
var ModelTypeName = map[int]string{
|
||||
ModelTypeInference: "推理模型",
|
||||
|
||||
ModelTypeImage: "图片模型",
|
||||
ImageSubTypeTextToImage: "图片模型-文生图",
|
||||
ImageSubTypeImageToImage: "图片模型-图生图",
|
||||
ImageSubTypeImageEdit: "图片模型-图片编辑",
|
||||
ImageSubTypeImageVariation: "图片模型-图片变体",
|
||||
ImageSubTypeImageTextToImage: "图片模型-图文生图",
|
||||
|
||||
ModelTypeAudio: "音频模型",
|
||||
AudioSubTypeTextToSpeech: "音频模型-文生音",
|
||||
AudioSubTypeSpeechToText: "音频模型-音生文",
|
||||
AudioSubTypeSpeechToSpeech: "音频模型-音生音",
|
||||
|
||||
ModelTypeVector: "向量化模型",
|
||||
VectorSubTypeEmbedding: "向量化模型-文本嵌入",
|
||||
VectorSubTypeRerank: "向量化模型-重排序",
|
||||
|
||||
ModelTypeOmni: "全模态模型",
|
||||
OmniSubTypeTextImageAudio: "全模态模型-文图音",
|
||||
OmniSubTypeVision: "全模态模型-视觉理解",
|
||||
|
||||
ModelTypeVideo: "视频模型",
|
||||
VideoSubTypeTextToVideo: "视频模型-文生视频",
|
||||
VideoSubTypeImageToVideo: "视频模型-图生视频",
|
||||
VideoSubTypeImageTextToVideo: "视频模型-图文生视频",
|
||||
VideoSubTypeVideoToVideo: "视频模型-视频生视频",
|
||||
}
|
||||
|
||||
// 运营商常量
|
||||
const (
|
||||
OperatorAliyun = "阿里云百炼"
|
||||
OperatorVolcengine = "火山引擎"
|
||||
OperatorTencent = "腾讯云"
|
||||
OperatorHuawei = "华为云"
|
||||
OperatorBaidu = "百度智能云"
|
||||
OperatorOpenAI = "OpenAI"
|
||||
OperatorAzure = "Azure OpenAI"
|
||||
OperatorAWS = "AWS Bedrock"
|
||||
OperatorGoogle = "Google Cloud"
|
||||
OperatorDeepSeek = "DeepSeek"
|
||||
OperatorMoonshot = "Moonshot"
|
||||
OperatorZhipu = "智谱AI"
|
||||
OperatorBaichuan = "百川智能"
|
||||
OperatorMinimax = "MiniMax"
|
||||
OperatorXunfei = "科大讯飞"
|
||||
OperatorOthers = "其他"
|
||||
)
|
||||
|
||||
// OperatorList 运营商列表(供前端下拉框使用)
|
||||
var OperatorList = []string{
|
||||
OperatorAliyun,
|
||||
OperatorVolcengine,
|
||||
OperatorTencent,
|
||||
OperatorHuawei,
|
||||
OperatorBaidu,
|
||||
OperatorOpenAI,
|
||||
OperatorAzure,
|
||||
OperatorAWS,
|
||||
OperatorGoogle,
|
||||
OperatorDeepSeek,
|
||||
OperatorMoonshot,
|
||||
OperatorZhipu,
|
||||
OperatorBaichuan,
|
||||
OperatorMinimax,
|
||||
OperatorXunfei,
|
||||
OperatorOthers,
|
||||
}
|
||||
12
consts/public/table_name.go
Normal file
12
consts/public/table_name.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package public
|
||||
|
||||
const (
|
||||
DbNameModelGateway = "model_gateway" //数据库名称
|
||||
)
|
||||
|
||||
const (
|
||||
TableNameModel = "model_gateway_models" // 模型表
|
||||
TableNameTask = "model_gateway_task" // 任务表
|
||||
TableNameOpLog = "model_gateway_logs_op" // 操作日志表
|
||||
TableNameStat = "model_gateway_logs_stat" // 按天统计表
|
||||
)
|
||||
18
controller/model_gateway_logs_stat_controller.go
Normal file
18
controller/model_gateway_logs_stat_controller.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
statService "model-gateway/service/stat"
|
||||
|
||||
"model-gateway/model/dto"
|
||||
)
|
||||
|
||||
// ModelGatewayLogsStat 统计控制器
|
||||
var ModelGatewayLogsStat = new(stat)
|
||||
|
||||
type stat struct{}
|
||||
|
||||
// ListModelStat 统计列表
|
||||
func (c *stat) ListModelStat(ctx context.Context, req *dto.ListModelStatReq) (res *dto.ListModelStatRes, err error) {
|
||||
return statService.ModelGatewayLogsStat.List(ctx, req)
|
||||
}
|
||||
66
controller/model_gateway_models_controller.go
Normal file
66
controller/model_gateway_models_controller.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"model-gateway/model/dto"
|
||||
modelService "model-gateway/service/model"
|
||||
"model-gateway/service/queue"
|
||||
)
|
||||
|
||||
// ModelGatewayModels 模型配置控制器
|
||||
var ModelGatewayModels = new(model)
|
||||
|
||||
type model struct{}
|
||||
|
||||
// CreateModel 添加配置
|
||||
func (c *model) CreateModel(ctx context.Context, req *dto.CreateModelReq) (res *dto.CreateModelRes, err error) {
|
||||
return modelService.ModelGatewayModels.Create(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateModel 更改配置
|
||||
func (c *model) UpdateModel(ctx context.Context, req *dto.UpdateModelReq) (res *dto.UpdateModelRes, err error) {
|
||||
err = modelService.ModelGatewayModels.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteModel 删除配置
|
||||
func (c *model) DeleteModel(ctx context.Context, req *dto.DeleteModelReq) (res *dto.DeleteModelRes, err error) {
|
||||
err = modelService.ModelGatewayModels.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// GetModel 获取配置详情
|
||||
func (c *model) GetModel(ctx context.Context, req *dto.GetModelReq) (res *dto.GetModelRes, err error) {
|
||||
return modelService.ModelGatewayModels.Get(ctx, req)
|
||||
}
|
||||
|
||||
// ListModel 配置列表
|
||||
func (c *model) ListModel(ctx context.Context, req *dto.ListModelReq) (res *dto.ListModelRes, err error) {
|
||||
return modelService.ModelGatewayModels.List(ctx, req)
|
||||
}
|
||||
|
||||
// AutoTune 动态调参(由上层定时任务每小时触发一次)
|
||||
func (c *model) AutoTune(ctx context.Context, req *dto.AutoTuneReq) (res *dto.AutoTuneRes, err error) {
|
||||
return queue.AutoTune(ctx, req)
|
||||
}
|
||||
|
||||
// ListType 模型类型列表
|
||||
func (c *model) ListType(ctx context.Context, req *dto.ListTypeReq) (res *dto.TypeItem, err error) {
|
||||
return modelService.GetModelTypesFromConfig()
|
||||
}
|
||||
|
||||
// ListOperator 运营商列表
|
||||
func (c *model) ListOperator(ctx context.Context, req *dto.ListOperatorReq) (res *dto.ListOperatorRes, err error) {
|
||||
return modelService.GetOperatorList()
|
||||
}
|
||||
|
||||
// UpdateChatModel 更新是否为聊天模型
|
||||
func (c *model) UpdateChatModel(ctx context.Context, req *dto.UpdateChatModelReq) (res *dto.UpdateChatModelRes, err error) {
|
||||
err = modelService.ModelGatewayModels.UpdateChatModel(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// GetIsChatModel 获取当前会话模型
|
||||
func (c *model) GetIsChatModel(ctx context.Context, req *dto.GetIsChatModelReq) (res *dto.GetIsChatModelRes, err error) {
|
||||
return modelService.ModelGatewayModels.GetIsChatModel(ctx)
|
||||
}
|
||||
43
controller/model_gateway_task_controller.go
Normal file
43
controller/model_gateway_task_controller.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
taskService "model-gateway/service/task"
|
||||
|
||||
"model-gateway/model/dto"
|
||||
)
|
||||
|
||||
// ModelGatewayTask 任务控制器
|
||||
var ModelGatewayTask = new(task)
|
||||
|
||||
type task struct{}
|
||||
|
||||
// CreateTask 根据 modelName 创建异步任务,返回 taskId
|
||||
func (c *task) CreateTask(ctx context.Context, req *dto.CreateTaskReq) (res *dto.CreateTaskRes, err error) {
|
||||
return taskService.ModelGatewayTask.Create(ctx, req)
|
||||
}
|
||||
|
||||
// GetTaskResult 获取单条任务结果(返回 *dto.GetTaskResultRes)
|
||||
func (c *task) GetTaskResult(ctx context.Context, req *dto.GetTaskResultReq) (res *dto.GetTaskResultRes, err error) {
|
||||
return taskService.ModelGatewayTask.GetResult(ctx, req.TaskID)
|
||||
}
|
||||
|
||||
// GetTaskBatch 批量查询任务(返回 *[]dto.GetTaskBatchItem)
|
||||
func (c *task) GetTaskBatch(ctx context.Context, req *dto.GetTaskBatchReq) (res *dto.GetTaskBatchRes, err error) {
|
||||
return taskService.ModelGatewayTask.GetBatch(ctx, req)
|
||||
}
|
||||
|
||||
// ListTask 任务列表分页查询
|
||||
func (c *task) ListTask(ctx context.Context, req *dto.ListTaskReq) (res *dto.ListTaskRes, err error) {
|
||||
return taskService.ModelGatewayTask.List(ctx, req)
|
||||
}
|
||||
|
||||
// ModelTaskCallback 接收模型异步任务的回调通知 —— 待调整
|
||||
func (c *task) ModelTaskCallback(ctx context.Context, req *dto.ModelTaskCallbackReq) (res *dto.ModelTaskCallbackRes, err error) {
|
||||
return taskService.ModelGatewayTask.ModelTaskCallback(ctx, req)
|
||||
}
|
||||
|
||||
// QueryPendingTasks 批量轮询进行中的异步任务 —— 待调整
|
||||
func (c *task) QueryPendingTasks(ctx context.Context, req *dto.QueryPendingTasksReq) (res *dto.QueryPendingTasksRes, err error) {
|
||||
return taskService.ModelGatewayTask.QueryPendingTasks(ctx, req)
|
||||
}
|
||||
23
dao/model_gateway_logs_op.go
Normal file
23
dao/model_gateway_logs_op.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"model-gateway/consts/public"
|
||||
"model-gateway/model/entity"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||
)
|
||||
|
||||
var ModelGatewayLogsOp = &modelGatewayLogsOpDao{}
|
||||
|
||||
type modelGatewayLogsOpDao struct{}
|
||||
|
||||
// Insert 插入操作日志
|
||||
func (d *modelGatewayLogsOpDao) Insert(ctx context.Context, req *entity.ModelGatewayLogsOp) (int64, error) {
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameOpLog).Insert(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.LastInsertId()
|
||||
}
|
||||
52
dao/model_gateway_logs_stat_dao.go
Normal file
52
dao/model_gateway_logs_stat_dao.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"model-gateway/consts/public"
|
||||
"model-gateway/model/entity"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var ModelGatewayLogsStat = &modelGatewayLogsStatDao{}
|
||||
|
||||
type modelGatewayLogsStatDao struct{}
|
||||
|
||||
// IncRequestCount 原子累加:按天+租户+创建人+模型 +1
|
||||
func (d *modelGatewayLogsStatDao) IncRequestCount(ctx context.Context, day time.Time, tenantId uint64, creator, modelName string) error {
|
||||
_, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameStat).
|
||||
Data(&entity.ModelGatewayLogsStat{
|
||||
Day: gtime.New(day),
|
||||
TenantId: tenantId,
|
||||
Creator: creator,
|
||||
ModelName: modelName,
|
||||
RequestCount: 1,
|
||||
}).
|
||||
OnDuplicate("request_count", "request_count+1").
|
||||
Insert()
|
||||
return err
|
||||
}
|
||||
|
||||
// List 分页查询统计
|
||||
func (d *modelGatewayLogsStatDao) List(ctx context.Context, pageNum, pageSize int, req *entity.ModelGatewayLogsStat) (list []*entity.ModelGatewayLogsStat, total int64, err error) {
|
||||
model := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameStat).
|
||||
OmitEmpty().
|
||||
Where(entity.ModelGatewayLogsStatCols.Creator, req.Creator).
|
||||
WhereLike(entity.ModelGatewayLogsStatCols.ModelName, "%"+req.ModelName+"%").
|
||||
OrderDesc(entity.ModelGatewayLogsStatCols.Day).
|
||||
OrderDesc(entity.ModelGatewayLogsStatCols.RequestCount)
|
||||
if pageNum > 0 && pageSize > 0 {
|
||||
model = model.Page(pageNum, pageSize)
|
||||
}
|
||||
r, totalInt, err := model.AllAndCount(false)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total = gconv.Int64(totalInt)
|
||||
err = r.Structs(&list)
|
||||
return
|
||||
}
|
||||
201
dao/model_gateway_models_dao.go
Normal file
201
dao/model_gateway_models_dao.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"model-gateway/consts/public"
|
||||
"model-gateway/model/dto"
|
||||
"model-gateway/model/entity"
|
||||
"strconv"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var ModelGatewayModels = &modelGatewayModelsDao{}
|
||||
|
||||
type modelGatewayModelsDao struct{}
|
||||
|
||||
// Insert 插入
|
||||
func (d *modelGatewayModelsDao) Insert(ctx context.Context, req *entity.ModelGatewayModel) (int64, error) {
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameModel).Insert(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.LastInsertId()
|
||||
}
|
||||
|
||||
// Update 更新
|
||||
func (d *modelGatewayModelsDao) Update(ctx context.Context, req *entity.ModelGatewayModel) (int64, error) {
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameModel).
|
||||
OmitEmpty().
|
||||
Data(req).
|
||||
Where(entity.ModelGatewayModelCol.Id, req.Id).
|
||||
Update()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.RowsAffected()
|
||||
}
|
||||
|
||||
// Delete 删除
|
||||
func (d *modelGatewayModelsDao) Delete(ctx context.Context, req *entity.ModelGatewayModel) (int64, error) {
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameModel).
|
||||
OmitEmpty().
|
||||
Where(entity.ModelGatewayModelCol.Id, req.Id).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.RowsAffected()
|
||||
}
|
||||
|
||||
// Get 获取模型
|
||||
func (d *modelGatewayModelsDao) Get(ctx context.Context, req *entity.ModelGatewayModel, fields ...string) (*entity.ModelGatewayModel, error) {
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameModel).
|
||||
OmitEmpty().
|
||||
Where(entity.ModelGatewayModelCol.Id, req.Id).
|
||||
Where(entity.ModelGatewayModelCol.Creator, req.Creator).
|
||||
Where(entity.ModelGatewayModelCol.ModelName, req.ModelName).
|
||||
Fields(fields).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m entity.ModelGatewayModel
|
||||
err = r.Struct(&m)
|
||||
return &m, err
|
||||
}
|
||||
|
||||
//// Get 按ID获取(带租户隔离,只查当前租户)
|
||||
//func (d *modelGatewayModelsDao) Get(ctx context.Context, req *entity.AsynchModel, fields ...string) (m *entity.AsynchModel, err error) {
|
||||
// var whereCondition strings.Builder
|
||||
// var queryParams []interface{}
|
||||
// if !g.IsEmpty(req.Id) {
|
||||
// whereCondition.WriteString(fmt.Sprintf(" AND %s = (?) ", entity.AsynchModelCol.Id))
|
||||
// queryParams = append(queryParams, req.Id)
|
||||
// }
|
||||
// if !g.IsEmpty(req.Creator) {
|
||||
// whereCondition.WriteString(fmt.Sprintf(" AND %s = (?) ", entity.AsynchModelCol.Creator))
|
||||
// queryParams = append(queryParams, req.Creator)
|
||||
// }
|
||||
// if !g.IsEmpty(req.IsChatModel) {
|
||||
// whereCondition.WriteString(fmt.Sprintf(" AND %s = (?) ", entity.AsynchModelCol.IsChatModel))
|
||||
// queryParams = append(queryParams, req.IsChatModel)
|
||||
// }
|
||||
// if !g.IsEmpty(req.ModelName) {
|
||||
// whereCondition.WriteString(fmt.Sprintf(" AND %s = (?) ", entity.AsynchModelCol.ModelName))
|
||||
// queryParams = append(queryParams, req.ModelName)
|
||||
// }
|
||||
// // 完整 SQL
|
||||
// sql := `SELECT * FROM "asynch_models" WHERE "deleted_at" IS NULL` + whereCondition.String()
|
||||
// r, err := gfdb.DB(ctx, public.DbNameModelGateway).GetAll(ctx, sql, queryParams...)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// var i []*entity.AsynchModel
|
||||
// if err = r.Structs(&i); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// for _, item := range i {
|
||||
// m = item
|
||||
// }
|
||||
// return
|
||||
//}
|
||||
|
||||
// GetByAcrossTenant 跨租户查询
|
||||
func (d *modelGatewayModelsDao) GetByAcrossTenant(ctx context.Context, req *entity.ModelGatewayModel, fields ...string) (*entity.ModelGatewayModel, error) {
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameModel).
|
||||
NoTenantId(ctx).
|
||||
OmitEmpty().
|
||||
Where(entity.ModelGatewayModelCol.Id, req.Id).
|
||||
Where(entity.ModelGatewayModelCol.Creator, req.Creator).
|
||||
Where(entity.ModelGatewayModelCol.ModelName, req.ModelName).
|
||||
Fields(fields).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m entity.ModelGatewayModel
|
||||
err = r.Struct(&m)
|
||||
return &m, err
|
||||
}
|
||||
|
||||
// GetByCreatorAndPlatform 按创建者、平台获取
|
||||
func (d *modelGatewayModelsDao) GetByCreatorAndPlatform(ctx context.Context, req *dto.ListModelReq) (list []*entity.ModelGatewayModel, total int, err error) {
|
||||
sql := `
|
||||
SELECT DISTINCT ON (model_name) *
|
||||
FROM asynch_models
|
||||
WHERE deleted_at IS NULL
|
||||
AND (? = '' OR model_name LIKE ?)
|
||||
`
|
||||
args := []any{
|
||||
req.ModelName, "%" + req.ModelName + "%",
|
||||
}
|
||||
|
||||
// modelType: 传 6 模糊匹配 6%
|
||||
if req.ModelType > 0 {
|
||||
prefix := strconv.Itoa(req.ModelType)[:1] // 截取第一位
|
||||
sql += ` AND model_type::text LIKE ? `
|
||||
args = append(args, prefix+"%")
|
||||
}
|
||||
|
||||
if !g.IsEmpty(req.IsPrivate) {
|
||||
sql += ` AND is_private = ? `
|
||||
args = append(args, req.IsPrivate)
|
||||
}
|
||||
|
||||
if req.IsOwner != nil && *req.IsOwner == 0 {
|
||||
if req.Enabled != nil && *req.Enabled == 1 {
|
||||
sql += ` AND creator = ? AND is_owner = ? AND enabled=1 `
|
||||
} else if req.Enabled != nil && *req.Enabled == 0 {
|
||||
sql += ` AND creator = ? AND is_owner = ? AND enabled=0 `
|
||||
} else {
|
||||
sql += ` AND creator = ? AND is_owner = ? `
|
||||
}
|
||||
args = append(args, req.Creator, req.IsOwner)
|
||||
} else if req.IsOwner != nil && *req.IsOwner == 1 {
|
||||
if req.Enabled != nil && *req.Enabled == 1 {
|
||||
sql += ` AND ((creator = ? AND is_owner = ? AND enabled=1) OR (is_owner = 0 AND enabled=1)) `
|
||||
} else if req.Enabled != nil && *req.Enabled == 0 {
|
||||
sql += ` AND ((creator = ? AND is_owner = ? AND enabled=0) OR (is_owner = 0 AND enabled=1)) `
|
||||
} else {
|
||||
sql += ` AND ((creator = ? AND is_owner = ?) OR (is_owner = 0 AND enabled=1)) `
|
||||
}
|
||||
args = append(args, req.Creator, req.IsOwner)
|
||||
}
|
||||
|
||||
sql += ` ORDER BY model_name, is_owner DESC, created_at DESC`
|
||||
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).GetAll(ctx, sql, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = r.Structs(&list)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
total = len(list)
|
||||
return
|
||||
}
|
||||
|
||||
// GetByModelNameForTenant 后台任务使用:按 tenant_id + model_name 查询,不依赖 gfdb Hook/Trace/用户上下文
|
||||
func (d *modelGatewayModelsDao) GetByModelNameForTenant(ctx context.Context, tenantId uint64, modelName string) (*entity.ModelGatewayModel, error) {
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).GetAll(ctx,
|
||||
"SELECT * FROM "+public.TableNameModel+" WHERE tenant_id=? AND model_name=? AND deleted_at IS NULL LIMIT 1",
|
||||
tenantId, modelName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.IsEmpty() {
|
||||
return nil, nil
|
||||
}
|
||||
var list []*entity.ModelGatewayModel
|
||||
if err := r.Structs(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return list[0], nil
|
||||
}
|
||||
159
dao/model_gateway_task_dao.go
Normal file
159
dao/model_gateway_task_dao.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"model-gateway/consts/public"
|
||||
"model-gateway/model/entity"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var ModelGatewayTask = &modelGatewayTaskDao{}
|
||||
|
||||
type modelGatewayTaskDao struct{}
|
||||
|
||||
// Insert 插入
|
||||
func (d *modelGatewayTaskDao) Insert(ctx context.Context, req *entity.ModelGatewayTask) (id int64, err error) {
|
||||
m := new(entity.ModelGatewayTask)
|
||||
err = gconv.Struct(req, &m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).Insert(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.LastInsertId()
|
||||
}
|
||||
|
||||
// Update 更新(按ID)
|
||||
func (d *modelGatewayTaskDao) Update(ctx context.Context, req *entity.ModelGatewayTask) (rows int64, err error) {
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).
|
||||
OmitEmpty().
|
||||
Data(req).
|
||||
Where(entity.ModelGatewayTaskCol.Id, req.Id).
|
||||
Update()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.RowsAffected()
|
||||
}
|
||||
|
||||
// Get 获取(按TaskID 或 ID)
|
||||
func (d *modelGatewayTaskDao) Get(ctx context.Context, req *entity.ModelGatewayTask) (m *entity.ModelGatewayTask, err error) {
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).
|
||||
OmitEmpty().
|
||||
Where(entity.ModelGatewayTaskCol.TaskID, req.TaskID).
|
||||
Where(entity.ModelGatewayTaskCol.Id, req.Id).
|
||||
One()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Struct(&m)
|
||||
return
|
||||
}
|
||||
|
||||
// List 分页查询
|
||||
func (d *modelGatewayTaskDao) List(ctx context.Context, pageNum, pageSize int, req *entity.ModelGatewayTask) (list []*entity.ModelGatewayTask, total int64, err error) {
|
||||
model := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).
|
||||
OmitEmpty().
|
||||
Where(entity.ModelGatewayTaskCol.Creator, req.Creator).
|
||||
Where(entity.ModelGatewayTaskCol.ModelName, "%"+req.ModelName+"%").
|
||||
Where(entity.ModelGatewayTaskCol.BizName, req.BizName).
|
||||
Where(entity.ModelGatewayTaskCol.State, req.State).
|
||||
Where(entity.ModelGatewayTaskCol.TaskID, req.TaskID).
|
||||
OrderDesc(entity.ModelGatewayTaskCol.CreatedAt)
|
||||
if pageNum > 0 && pageSize > 0 {
|
||||
model = model.Page(pageNum, pageSize)
|
||||
}
|
||||
r, totalInt, err := model.AllAndCount(false)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total = gconv.Int64(totalInt)
|
||||
err = r.Structs(&list)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除(软删,按ID)
|
||||
func (d *modelGatewayTaskDao) Delete(ctx context.Context, req *entity.ModelGatewayTask) (rows int64, err error) {
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).
|
||||
Where(entity.ModelGatewayTaskCol.Id, req.Id).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.RowsAffected()
|
||||
}
|
||||
|
||||
// ListByTaskIDs 批量查询
|
||||
func (d *modelGatewayTaskDao) ListByTaskIDs(ctx context.Context, taskIDs []string) (list []*entity.ModelGatewayTask, err error) {
|
||||
if len(taskIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).
|
||||
WhereIn(entity.ModelGatewayTaskCol.TaskID, taskIDs).
|
||||
All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = r.Structs(&list)
|
||||
return
|
||||
}
|
||||
|
||||
// MarkDownloadedByID 标记已下载
|
||||
func (d *modelGatewayTaskDao) MarkDownloadedByID(ctx context.Context, id int64) error {
|
||||
_, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).
|
||||
Where(entity.ModelGatewayTaskCol.Id, id).
|
||||
Where(entity.ModelGatewayTaskCol.State, 2).
|
||||
Data(map[string]any{entity.ModelGatewayTaskCol.State: 4}).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPendingAsyncTasks 获取进行中的异步任务
|
||||
func (d *modelGatewayTaskDao) GetPendingAsyncTasks(ctx context.Context, limit int) ([]*entity.ModelGatewayTask, error) {
|
||||
var tasks []*entity.ModelGatewayTask
|
||||
err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameTask).
|
||||
Where(entity.ModelGatewayTaskCol.State, 1).
|
||||
Limit(limit).
|
||||
Scan(&tasks)
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
// ======================== 事务抢占 ========================
|
||||
|
||||
// ClaimByID 按主键抢占,返回抢占后的任务
|
||||
func (d *modelGatewayTaskDao) ClaimByID(ctx context.Context, id int64) (*entity.ModelGatewayTask, error) {
|
||||
var task entity.ModelGatewayTask
|
||||
err := gfdb.DB(ctx, public.DbNameModelGateway).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
r, err := tx.Model(public.TableNameTask).
|
||||
Where(entity.ModelGatewayTaskCol.Id, id).
|
||||
Where(entity.ModelGatewayTaskCol.State, public.TaskStatusPending).
|
||||
Limit(1).
|
||||
LockUpdate().
|
||||
One()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.IsEmpty() {
|
||||
return fmt.Errorf("任务已被抢占或不存在: id=%d", id)
|
||||
}
|
||||
if err := r.Struct(&task); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Model(public.TableNameTask).
|
||||
Data(&entity.ModelGatewayTask{State: public.TaskStatusRunning}).
|
||||
Where(entity.ModelGatewayTaskCol.Id, id).
|
||||
OmitEmpty().
|
||||
Update()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
92
go.mod
Normal file
92
go.mod
Normal file
@@ -0,0 +1,92 @@
|
||||
module model-gateway
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
gitea.redpowerfuture.com/red-future/common v0.0.23
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2
|
||||
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/tidwall/gjson v1.19.0
|
||||
)
|
||||
|
||||
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.0 // 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/r3labs/diff/v2 v2.15.1 // indirect
|
||||
github.com/redis/go-redis/v9 v9.12.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tiger1103/gfast-token v1.0.10 // indirect
|
||||
github.com/vcaesar/cedar v0.30.0 // indirect
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible // 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.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // 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
|
||||
)
|
||||
467
go.sum
Normal file
467
go.sum
Normal file
@@ -0,0 +1,467 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
gitea.redpowerfuture.com/red-future/common v0.0.23 h1:xieoA00iKOCDm5SO9iXn+cSyMKBAlZwI0fuEVPWrHLg=
|
||||
gitea.redpowerfuture.com/red-future/common v0.0.23/go.mod h1:50U1Xi+Ie56z09S5LQbZvaken0Mxv3OeS9LgR7U/ZRY=
|
||||
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.2 h1:u8EpP24GkprogROnJ7htMov9Fc66pTP1eVYrWxiCYOs=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2/go.mod h1:GmvM3r8GVByVMi4RD2+MCs5+CfxVXPMeT8mVDkAaAXE=
|
||||
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2 h1:iTQegT+lEg/wDKvj2mi3W1wrdrwFarjokf88EXVVgu4=
|
||||
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2/go.mod h1:ZRw3GNz5cq4uYrW4TPSVyrYWaoqzujKdWro/AOcGBaE=
|
||||
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.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8=
|
||||
github.com/gogf/gf/v2 v2.10.2/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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
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 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
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=
|
||||
68
main.go
Normal file
68
main.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"model-gateway/model/dto"
|
||||
"model-gateway/service/task"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"model-gateway/controller"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/http"
|
||||
"gitea.redpowerfuture.com/red-future/common/jaeger"
|
||||
_ "gitea.redpowerfuture.com/red-future/common/swagger"
|
||||
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
|
||||
_ "github.com/gogf/gf/contrib/nosql/redis/v2"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
defer jaeger.ShutDown(ctx)
|
||||
|
||||
// 注册路由
|
||||
http.RouteRegister([]interface{}{
|
||||
controller.ModelGatewayModels,
|
||||
controller.ModelGatewayTask,
|
||||
controller.ModelGatewayLogsStat,
|
||||
})
|
||||
|
||||
// 本地调试:可选自动触发 worker/cleaner(由配置文件控制)
|
||||
startAutoRunner(ctx)
|
||||
|
||||
// 监听退出信号,确保 Ctrl+C 能完整退出(停止 worker/cleaner 并关闭 gateway server)
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
g.Log().Infof(ctx, "[main] 收到退出信号,开始优雅退出...")
|
||||
cancel()
|
||||
// 关闭 gateway server(RouteRegister 内部是 go Httpserver.Run() 启动的)
|
||||
_ = http.Httpserver.Shutdown()
|
||||
}
|
||||
|
||||
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.ModelGatewayTask.QueryPendingTasks(ctx, &dto.QueryPendingTasksReq{Limit: limit}); err != nil {
|
||||
g.Log().Warningf(ctx, "[auto-queryPending] run once failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
20
model/dto/model_gateway_logs_stat_dto.go
Normal file
20
model/dto/model_gateway_logs_stat_dto.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package dto
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// ListModelStatReq 统计列表
|
||||
type ListModelStatReq struct {
|
||||
g.Meta `path:"/listModelStat" method:"get" tags:"统计" summary:"模型请求统计列表" dc:"按天统计模型请求次数,支持分页与条件筛选"`
|
||||
PageNum int `p:"pageNum" json:"pageNum" dc:"页码(默认1)"`
|
||||
PageSize int `p:"pageSize" json:"pageSize" dc:"每页条数(默认10)"`
|
||||
StartDay string `p:"startDay" json:"startDay" dc:"开始日期(YYYY-MM-DD,可选)"`
|
||||
EndDay string `p:"endDay" json:"endDay" dc:"结束日期(YYYY-MM-DD,可选)"`
|
||||
TenantID *int64 `p:"tenantId" json:"tenantId" dc:"租户ID(可选)"`
|
||||
Creator string `p:"creator" json:"creator" dc:"创建人(可选,模糊匹配)"`
|
||||
ModelName string `p:"modelName" json:"modelName" dc:"模型名称(可选,模糊匹配)"`
|
||||
}
|
||||
|
||||
type ListModelStatRes struct {
|
||||
List any `json:"list" dc:"列表数据"`
|
||||
Total int64 `json:"total" dc:"总数"`
|
||||
}
|
||||
186
model/dto/model_gateway_models_dto.go
Normal file
186
model/dto/model_gateway_models_dto.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"model-gateway/model/entity"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// CreateModelReq 添加模型配置
|
||||
type CreateModelReq struct {
|
||||
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-是"`
|
||||
CallModel *int `p:"callModel" json:"callModel" dc:"调用模式:0-同步 1-异步 2-流式"`
|
||||
RequiredFields []string `p:"requiredFields" json:"requiredFields" dc:"必填字段"`
|
||||
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:"返回映射"`
|
||||
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 {
|
||||
ID int64 `json:"id,string" dc:"配置ID"`
|
||||
}
|
||||
|
||||
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:"模型类型"`
|
||||
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-是"`
|
||||
CallModel *int `p:"callModel" json:"callModel" dc:"调用模式:0-同步 1-异步 2-流式"`
|
||||
RequiredFields []string `p:"requiredFields" json:"requiredFields" dc:"必填字段"`
|
||||
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:"返回映射"`
|
||||
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 {
|
||||
ID int64 `json:"id,string" dc:"配置ID"`
|
||||
}
|
||||
|
||||
// DeleteModelReq 删除模型配置
|
||||
type DeleteModelReq struct {
|
||||
g.Meta `path:"/deleteModel" method:"delete" tags:"模型管理" summary:"删除模型配置" dc:"删除指定ID的模型配置"`
|
||||
ID int64 `p:"id" json:"id,string" v:"required#id不能为空" dc:"配置ID"`
|
||||
}
|
||||
|
||||
type DeleteModelRes struct {
|
||||
ID int64 `json:"id,string" dc:"配置ID"`
|
||||
}
|
||||
|
||||
// GetModelReq 获取模型配置详情
|
||||
type GetModelReq struct {
|
||||
g.Meta `path:"/getModel" method:"get" tags:"模型管理" summary:"获取模型配置" dc:"根据模型ID获取配置详情"`
|
||||
ID int64 `p:"id" json:"id,string" dc:"配置ID"`
|
||||
Creator string `p:"creator" json:"creator" dc:"创建人"`
|
||||
IsChatModel *int `p:"isChatModel" json:"isChatModel" dc:"是否为聊天模型"`
|
||||
ModelName string `p:"modelName" json:"modelName" dc:"模型名称(唯一标识)"`
|
||||
}
|
||||
|
||||
type GetModelRes struct {
|
||||
Model *entity.ModelGatewayModel `json:"model" dc:"模型配置详情"`
|
||||
}
|
||||
|
||||
// ListModelReq 配置列表
|
||||
type ListModelReq struct {
|
||||
g.Meta `path:"/listModel" method:"get" tags:"模型管理" summary:"模型配置列表" dc:"分页获取模型配置列表"`
|
||||
Page *beans.Page `json:"page"`
|
||||
ModelName string `p:"modelName" json:"modelName" dc:"模型名称(模糊查询,可选)"`
|
||||
ModelType int `p:"modelType" json:"modelType" dc:"模型类型"`
|
||||
Enabled *int `p:"enabled" json:"enabled" dc:"是否启用:0-禁用,1-启用"`
|
||||
IsPrivate *int `p:"isPrivate" json:"isPrivate" dc:"是否私有化 0-私有 1-公共"`
|
||||
IsOwner *int `p:"isOwner" json:"isOwner" dc:"是否为所有者 0-否 1-是"`
|
||||
Creator string `p:"creator" json:"creator" dc:"创建人"`
|
||||
}
|
||||
|
||||
type ListModelRes struct {
|
||||
List any `json:"list" dc:"列表数据"`
|
||||
Total int `json:"total" dc:"总数"`
|
||||
}
|
||||
|
||||
// AutoTuneReq 动态调参(由上层定时任务每小时触发一次)
|
||||
type AutoTuneReq struct {
|
||||
g.Meta `path:"/autoTune" method:"post" tags:"模型管理" summary:"动态调参" dc:"按 model_name 维度统计指定时间窗口内执行耗时(P90),动态生成运行时 max_concurrency/queue_limit(不超过配置上限),写入 Redis 供 Worker/CreateTask 使用;windowSeconds 不传默认 3600"`
|
||||
WindowSeconds int `p:"windowSeconds" json:"windowSeconds" dc:"统计窗口秒数;不传/<=0 默认 3600(1小时)"`
|
||||
}
|
||||
|
||||
type AutoTuneRes struct {
|
||||
List any `json:"list" dc:"调参结果列表"`
|
||||
}
|
||||
|
||||
type ModelTypeModelItem struct {
|
||||
ID int64 `json:"id" dc:"模型主键ID"`
|
||||
Name string `json:"name" dc:"模型名称"`
|
||||
Form any `json:"form" dc:"动态表单配置(JSON数组),用于前端渲染"`
|
||||
}
|
||||
|
||||
// ListModelTypeReq 模型类型列表(分页)
|
||||
type ListTypeReq struct {
|
||||
g.Meta `path:"/listType" method:"get" tags:"模型类型列表" summary:"模型类型列表" dc:"分页获取模型类型列表"`
|
||||
}
|
||||
|
||||
type TypeItem struct {
|
||||
Type map[int]string `json:"type" dc:"模型类型ID到名称的映射"`
|
||||
}
|
||||
|
||||
type ListOperatorReq struct {
|
||||
g.Meta `path:"/listOperator" method:"get" tags:"模型管理" summary:"获取运营商列表" dc:"获取运营商列表"`
|
||||
}
|
||||
|
||||
type ListOperatorRes struct {
|
||||
List []string `json:"list" dc:"运营商名称到ID的映射"`
|
||||
}
|
||||
|
||||
type UpdateChatModelReq struct {
|
||||
g.Meta `path:"/updateChatModel" method:"post" tags:"模型管理" summary:"更新聊天模型" dc:"更新指定模型的聊天模型"`
|
||||
Id int64 `p:"id" json:"id" v:"required#model不能为空" dc:"模型id"`
|
||||
}
|
||||
type UpdateChatModelRes struct {
|
||||
ID int64 `json:"id,string" dc:"模型ID"`
|
||||
}
|
||||
|
||||
type GetIsChatModelReq struct {
|
||||
g.Meta `path:"/getIsChatModel" method:"get" tags:"模型管理" summary:"获取模型是否为聊天模型" dc:"根据模型ID获取是否为聊天模型"`
|
||||
}
|
||||
|
||||
type GetIsChatModelRes struct {
|
||||
Model any `json:"model" dc:"模型详情"`
|
||||
}
|
||||
|
||||
// NodeFormField 节点表单
|
||||
type NodeFormField struct {
|
||||
Value any `json:"value" dc:"字段值"`
|
||||
Field string `json:"field" dc:"字段标识"`
|
||||
Label string `json:"label" dc:"字段标签"`
|
||||
Type string `json:"type" dc:"字段类型"`
|
||||
Required bool `json:"required" dc:"是否必填"`
|
||||
Default any `json:"default,omitempty" dc:"默认值"`
|
||||
Options []SelectOption `json:"options" dc:"下拉选项列表"`
|
||||
FieldConstraint any `json:"fieldConstraint" dc:"字段约束"`
|
||||
}
|
||||
|
||||
type SelectOption struct {
|
||||
Label string `json:"label" dc:"选项标签"`
|
||||
Value string `json:"value" dc:"选项值"`
|
||||
}
|
||||
107
model/dto/model_gateway_task_dto.go
Normal file
107
model/dto/model_gateway_task_dto.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// CreateTaskReq 创建异步任务
|
||||
type CreateTaskReq struct {
|
||||
g.Meta `path:"/createTask" method:"post" tags:"任务管理" summary:"创建异步任务" dc:"创建异步任务并返回任务ID;创建成功后会立即异步尝试执行当前任务,执行成功后按回调配置触发钩子"`
|
||||
ModelName string `p:"modelName" json:"modelName" v:"required#modelName不能为空" dc:"模型名称"`
|
||||
BizName string `p:"bizName" json:"bizName" dc:"业务名称(调用方模块/系统,用于统计)"`
|
||||
CallbackUrl string `p:"callbackUrl" json:"callbackUrl" dc:"回调地址(可选,用于后续业务通知)"`
|
||||
RequestPayload map[string]any `p:"requestPayload" json:"requestPayload" dc:"请求负载(透传给模型服务)"`
|
||||
EpicycleId int64 `json:"epicycleId" dc:"轮次ID"`
|
||||
BuildType int64 `json:"buildType" dc:"构建类型:1-提示词构建 2-节点构建"`
|
||||
}
|
||||
|
||||
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地址)"`
|
||||
TaskID string `p:"taskId" json:"taskId" v:"required#taskwId不能为空" dc:"任务ID"`
|
||||
}
|
||||
|
||||
type GetTaskResultRes struct {
|
||||
OssFile string `json:"ossFile" dc:"结果文件OSS地址"`
|
||||
State int `json:"state" dc:"任务状态"`
|
||||
}
|
||||
|
||||
// GetTaskBatchReq 批量查询任务(并对成功任务标记为已下载)
|
||||
type GetTaskBatchReq struct {
|
||||
g.Meta `path:"/getTaskBatch" method:"post" tags:"任务管理" summary:"批量查询任务" dc:"批量查询任务状态与OSS地址;对成功(state=2)的任务自动标记为已下载(state=4),并写入保留到期时间"`
|
||||
TaskIDs []string `p:"taskIds" json:"taskIds" v:"required#taskIds不能为空" dc:"任务ID列表"`
|
||||
}
|
||||
|
||||
type GetTaskBatchRes struct {
|
||||
List []GetTaskBatchItem `json:"list" dc:"任务列表"`
|
||||
}
|
||||
|
||||
type GetTaskBatchItem struct {
|
||||
TaskID string `json:"taskId" dc:"任务ID"`
|
||||
State int `json:"state" dc:"任务状态"`
|
||||
OssFile string `json:"ossFile" dc:"结果文件OSS地址"`
|
||||
TextResult map[string]any `json:"textResult" dc:"文本结果"`
|
||||
}
|
||||
|
||||
// ListTaskReq 任务列表分页查询
|
||||
type ListTaskReq struct {
|
||||
g.Meta `path:"/listTask" method:"get" tags:"任务管理" summary:"任务列表" dc:"分页查询任务列表,支持按状态/模型名称/task_id过滤"`
|
||||
PageNum int `p:"pageNum" json:"pageNum" dc:"页码(默认1)"`
|
||||
PageSize int `p:"pageSize" json:"pageSize" dc:"每页条数(默认10)"`
|
||||
ModelName string `p:"modelName" json:"modelName" dc:"模型名称(模糊匹配)"`
|
||||
BizName string `p:"bizName" json:"bizName" dc:"业务名称"`
|
||||
TaskID string `p:"taskId" json:"taskId" dc:"任务ID(模糊匹配)"`
|
||||
State int `p:"state" json:"state" dc:"任务状态(0/1/2/3/4,可选)"`
|
||||
}
|
||||
|
||||
type ListTaskRes struct {
|
||||
List any `json:"list" dc:"列表数据"`
|
||||
Total int64 `json:"total" dc:"总数"`
|
||||
}
|
||||
|
||||
// RunWorkReq 手动触发 worker 执行一次(由上层定时任务调用)
|
||||
type RunWorkReq struct {
|
||||
g.Meta `path:"/runWork" method:"post" tags:"任务管理" summary:"执行一次Worker" dc:"手动触发一次Worker抢占并处理排队中的任务;适合处理 createTask 立即执行时未处理到的任务以及积压队列"`
|
||||
BatchSize int `p:"batchSize" json:"batchSize" dc:"本次抢占任务数量(默认10)"`
|
||||
Goroutines int `p:"goroutines" json:"goroutines" dc:"本次并发数(默认1)"`
|
||||
}
|
||||
|
||||
type RunWorkRes struct {
|
||||
Claimed int `json:"claimed" dc:"本次抢占并处理的任务数"`
|
||||
}
|
||||
56
model/entity/model_gateway_logs_op.go
Normal file
56
model/entity/model_gateway_logs_op.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package entity
|
||||
|
||||
import "gitea.redpowerfuture.com/red-future/common/beans"
|
||||
|
||||
// ModelGatewayLogsOpCol 字段常量
|
||||
type modelGatewayLogsOpCol struct {
|
||||
beans.SQLBaseCol
|
||||
IP string
|
||||
UserAgent string
|
||||
APIPath string
|
||||
HttpMethod string
|
||||
BizName string
|
||||
ModelName string
|
||||
TaskID string
|
||||
OpType string
|
||||
Success string
|
||||
ErrorMsg string
|
||||
CostMs string
|
||||
RequestPayload string
|
||||
ResponsePayload string
|
||||
}
|
||||
|
||||
var ModelGatewayLogsOpCol = modelGatewayLogsOpCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
IP: "ip",
|
||||
UserAgent: "user_agent",
|
||||
APIPath: "api_path",
|
||||
HttpMethod: "http_method",
|
||||
BizName: "biz_name",
|
||||
ModelName: "model_name",
|
||||
TaskID: "task_id",
|
||||
OpType: "op_type",
|
||||
Success: "success",
|
||||
ErrorMsg: "error_msg",
|
||||
CostMs: "cost_ms",
|
||||
RequestPayload: "request_payload",
|
||||
ResponsePayload: "response_payload",
|
||||
}
|
||||
|
||||
// ModelGatewayLogsOp 操作日志
|
||||
type ModelGatewayLogsOp struct {
|
||||
beans.SQLBaseDO `orm:",inline"`
|
||||
IP string `orm:"ip" json:"ip"`
|
||||
UserAgent string `orm:"user_agent" json:"userAgent"`
|
||||
APIPath string `orm:"api_path" json:"apiPath"`
|
||||
HttpMethod string `orm:"http_method" json:"httpMethod"`
|
||||
BizName string `orm:"biz_name" json:"bizName"`
|
||||
ModelName string `orm:"model_name" json:"modelName"`
|
||||
TaskID string `orm:"task_id" json:"taskId"`
|
||||
OpType string `orm:"op_type" json:"opType"`
|
||||
Success int `orm:"success" json:"success"`
|
||||
ErrorMsg string `orm:"error_msg" json:"errorMsg"`
|
||||
CostMs int64 `orm:"cost_ms" json:"costMs"`
|
||||
RequestPayload *RequestPayload `orm:"request_payload" json:"requestPayload"`
|
||||
ResponsePayload map[string]any `orm:"response_payload" json:"responsePayload"`
|
||||
}
|
||||
35
model/entity/model_gateway_logs_stat.go
Normal file
35
model/entity/model_gateway_logs_stat.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// ModelGatewayLogsStatCol 字段常量
|
||||
type ModelGatewayLogsStatCol struct {
|
||||
Day string
|
||||
TenantId string
|
||||
Creator string
|
||||
ModelName string
|
||||
RequestCount string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
var ModelGatewayLogsStatCols = ModelGatewayLogsStatCol{
|
||||
Day: "day",
|
||||
TenantId: "tenant_id",
|
||||
Creator: "creator",
|
||||
ModelName: "model_name",
|
||||
RequestCount: "request_count",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
}
|
||||
|
||||
// ModelGatewayLogsStat 按天统计
|
||||
type ModelGatewayLogsStat struct {
|
||||
Day *gtime.Time `orm:"day" json:"day"`
|
||||
TenantId uint64 `orm:"tenant_id" json:"tenantId"`
|
||||
Creator string `orm:"creator" json:"creator"`
|
||||
ModelName string `orm:"model_name" json:"modelName"`
|
||||
RequestCount int64 `orm:"request_count" json:"requestCount"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt"`
|
||||
}
|
||||
99
model/entity/model_gateway_model.go
Normal file
99
model/entity/model_gateway_model.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package entity
|
||||
|
||||
import "gitea.redpowerfuture.com/red-future/common/beans"
|
||||
|
||||
type modelGatewayModelCol struct {
|
||||
beans.SQLBaseCol
|
||||
ModelName string
|
||||
ModelType string
|
||||
BaseURL string
|
||||
HttpMethod string
|
||||
HeadMsg string
|
||||
FormJSON string
|
||||
RequestMapping string
|
||||
ResponseMapping string
|
||||
ResponseBody string
|
||||
RequiredFields string
|
||||
IsPrivate string
|
||||
IsChatModel string
|
||||
CallMode string
|
||||
ApiKey string
|
||||
Enabled string
|
||||
MaxConcurrency string
|
||||
TimeoutSeconds string
|
||||
RetryTimes string
|
||||
AutoCleanSeconds string
|
||||
IsOwner string
|
||||
OperatorName string
|
||||
TokenConfig string
|
||||
ExtendMapping string
|
||||
QueryConfig string
|
||||
StreamConfig string
|
||||
FirstFrame string
|
||||
LastFrame string
|
||||
}
|
||||
|
||||
var ModelGatewayModelCol = modelGatewayModelCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
ModelName: "model_name",
|
||||
ModelType: "model_type",
|
||||
BaseURL: "base_url",
|
||||
HttpMethod: "http_method",
|
||||
HeadMsg: "head_msg",
|
||||
FormJSON: "form_json",
|
||||
RequestMapping: "request_mapping",
|
||||
ResponseMapping: "response_mapping",
|
||||
RequiredFields: "required_fields",
|
||||
IsPrivate: "is_private",
|
||||
IsChatModel: "is_chat_model",
|
||||
CallMode: "call_mode",
|
||||
ApiKey: "api_key",
|
||||
Enabled: "enabled",
|
||||
MaxConcurrency: "max_concurrency",
|
||||
TimeoutSeconds: "timeout_seconds",
|
||||
RetryTimes: "retry_times",
|
||||
AutoCleanSeconds: "auto_clean_seconds",
|
||||
IsOwner: "is_owner",
|
||||
OperatorName: "operator_name",
|
||||
TokenConfig: "token_config",
|
||||
ExtendMapping: "extend_mapping",
|
||||
QueryConfig: "query_config",
|
||||
StreamConfig: "stream_config",
|
||||
FirstFrame: "first_frame",
|
||||
LastFrame: "last_frame",
|
||||
}
|
||||
|
||||
type ModelGatewayModel 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 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"`
|
||||
RequiredFields []string `orm:"required_fields" json:"requiredFields"`
|
||||
IsPrivate *int `orm:"is_private" json:"isPrivate"`
|
||||
IsChatModel *int `orm:"is_chat_model" json:"isChatModel"`
|
||||
CallMode *int `orm:"call_mode" json:"callMode"`
|
||||
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 `orm:"is_owner" json:"isOwner"`
|
||||
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"`
|
||||
}
|
||||
|
||||
const ( //ResponseMapping 下的字段
|
||||
ResponseBody = "response_body" //返回主体
|
||||
TotalTokens = "total_tokens" //总token数
|
||||
)
|
||||
76
model/entity/model_gateway_task.go
Normal file
76
model/entity/model_gateway_task.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||
)
|
||||
|
||||
type modelGatewayTaskCol struct {
|
||||
beans.SQLBaseCol
|
||||
ModelName string
|
||||
TaskID string
|
||||
BizName string
|
||||
CallbackURL string
|
||||
State string
|
||||
Phase string
|
||||
ErrorMsg string
|
||||
ResultFile string
|
||||
TextResult string
|
||||
ExpendTokens string
|
||||
DurationSeconds string
|
||||
RetryCount string
|
||||
TmpFile string
|
||||
RequestPayload string
|
||||
EpicycleId string
|
||||
}
|
||||
|
||||
var ModelGatewayTaskCol = modelGatewayTaskCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
ModelName: "model_name",
|
||||
TaskID: "task_id",
|
||||
BizName: "biz_name",
|
||||
CallbackURL: "callback_url",
|
||||
State: "state",
|
||||
Phase: "phase",
|
||||
ErrorMsg: "error_msg",
|
||||
ResultFile: "result_file",
|
||||
TextResult: "text_result",
|
||||
ExpendTokens: "expend_tokens",
|
||||
DurationSeconds: "duration_seconds",
|
||||
RetryCount: "retry_count",
|
||||
TmpFile: "tmp_file",
|
||||
RequestPayload: "request_payload",
|
||||
EpicycleId: "epicycle_id",
|
||||
}
|
||||
|
||||
// ModelGatewayTask 模型网关任务
|
||||
type ModelGatewayTask struct {
|
||||
beans.SQLBaseDO `orm:",inline"`
|
||||
ModelName string `orm:"model_name" json:"modelName"`
|
||||
TaskID string `orm:"task_id" json:"taskId"`
|
||||
BizName string `orm:"biz_name" json:"bizName"`
|
||||
CallbackURL string `orm:"callback_url" json:"callbackUrl"`
|
||||
State int `orm:"state" json:"state"`
|
||||
Phase int `orm:"phase" json:"phase"`
|
||||
ErrorMsg string `orm:"error_msg" json:"errorMsg"`
|
||||
ResultFile *ResultFile `orm:"result_file" json:"resultFile"`
|
||||
TextResult map[string]any `orm:"text_result" json:"text"`
|
||||
ExpendTokens int64 `orm:"expend_tokens" json:"expendTokens"`
|
||||
DurationSeconds int64 `orm:"duration_seconds" json:"durationSeconds"`
|
||||
RetryCount int `orm:"retry_count" json:"retryCount"`
|
||||
TmpFile string `orm:"tmp_file" json:"tmpFile"`
|
||||
RequestPayload *RequestPayload `orm:"request_payload" json:"requestPayload"`
|
||||
EpicycleId int64 `orm:"epicycle_id" json:"epicycleId"`
|
||||
}
|
||||
|
||||
// ResultFile OSS 结果文件
|
||||
type ResultFile struct {
|
||||
OssFile string `json:"ossFile"`
|
||||
FileType string `json:"fileType"`
|
||||
FileSize int64 `json:"fileSize"`
|
||||
}
|
||||
|
||||
// RequestPayload 请求参数结构体
|
||||
type RequestPayload struct {
|
||||
Headers map[string]string `json:"headers"`
|
||||
Body map[string]any `json:"body"`
|
||||
}
|
||||
194
service/gateway/gateway_http_service.go
Normal file
194
service/gateway/gateway_http_service.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"model-gateway/common/util"
|
||||
"model-gateway/model/entity"
|
||||
"time"
|
||||
|
||||
commonHttp "gitea.redpowerfuture.com/red-future/common/http"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/guid"
|
||||
)
|
||||
|
||||
type UploadFileResponse struct {
|
||||
FileURL string `json:"fileURL"` // 文件 URL
|
||||
FileSize int `json:"fileSize"` // 文件大小(字节)
|
||||
FileName string `json:"fileName"` // 文件名
|
||||
FileFormat string `json:"fileFormat"` // 文件格式
|
||||
FileAddressPrefix string `json:"fileAddressPrefix"` // 文件地址前缀
|
||||
}
|
||||
|
||||
// UploadByTask 通过任务上传文件
|
||||
func UploadByTask(ctx context.Context, data []byte, fileExt string) (oss *UploadFileResponse, err error) {
|
||||
// multipart
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
ext := fileExt
|
||||
if ext == "" {
|
||||
ext = ".bin"
|
||||
}
|
||||
if ext[0] != '.' {
|
||||
ext = "." + ext
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("asynch_%d_%s%s", time.Now().Unix(), guid.S(), ext)
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := part.Write(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentType := writer.FormDataContentType()
|
||||
if err = writer.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers := util.ForwardHeaders(ctx)
|
||||
headers["Content-Type"] = contentType
|
||||
fullURL := "oss/file/uploadFile"
|
||||
g.Log().Infof(ctx, "[OSS] upload start url=%s filename=%s size=%d", fullURL, filename, len(data))
|
||||
|
||||
var resp UploadFileResponse
|
||||
if err = commonHttp.Post(ctx, fullURL, headers, &resp, body.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if &resp == nil {
|
||||
return nil, errors.New("[OSS] 上传文件失败")
|
||||
}
|
||||
g.Log().Infof(ctx, "[OSS] 上传成功 url=%s size=%d format=%s", resp.FileURL, resp.FileSize, resp.FileFormat)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CallbackPayload 回调请求体
|
||||
type CallbackPayload struct {
|
||||
TaskId string `json:"task_id"`
|
||||
State int `json:"state"`
|
||||
OssFile string `json:"oss_file"`
|
||||
FileType string `json:"file_type"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
}
|
||||
|
||||
// TriggerCallback 任务的回调
|
||||
func TriggerCallback(ctx context.Context, t *entity.ModelGatewayTask) {
|
||||
headers := util.ForwardHeaders(ctx)
|
||||
var resp struct{}
|
||||
payload := CallbackPayload{
|
||||
TaskId: t.TaskID,
|
||||
State: t.State,
|
||||
OssFile: t.ResultFile.OssFile,
|
||||
FileType: t.ResultFile.FileType,
|
||||
ErrorMsg: t.ErrorMsg,
|
||||
}
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "[回调] JSON序列化失败 taskId=%s 错误=%v", t.TaskID, err)
|
||||
return
|
||||
}
|
||||
g.Log().Infof(ctx, "[回调] 开始发送 taskId=%s 回调地址=%s 请求头数量=%d 消息体大小=%d字节",
|
||||
t.TaskID, t.CallbackURL, len(headers), len(jsonData))
|
||||
|
||||
err = commonHttp.Post(ctx, t.CallbackURL, headers, &resp, jsonData)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "[回调] 发送失败 taskId=%s 回调地址=%s 错误=%v", t.TaskID, t.CallbackURL, err)
|
||||
return
|
||||
}
|
||||
g.Log().Infof(ctx, "[回调] 发送成功 taskId=%s 回调地址=%s 消息体大小=%d字节", t.TaskID, t.CallbackURL, len(jsonData))
|
||||
}
|
||||
|
||||
// PromptsCallbackPayload 提示词回调请求体
|
||||
type PromptsCallbackPayload struct {
|
||||
EpicycleId int64 `json:"epicycleId"`
|
||||
Messages map[string]any `json:"messages"`
|
||||
}
|
||||
|
||||
// TriggerPromptsCallback 任务成功后的提示词回调
|
||||
func TriggerPromptsCallback(ctx context.Context, t *entity.ModelGatewayTask, epicycleId int64) {
|
||||
callbackURL := "prompts-core/session/callback"
|
||||
headers := util.ForwardHeaders(ctx)
|
||||
var resp struct{}
|
||||
payload := PromptsCallbackPayload{
|
||||
EpicycleId: epicycleId,
|
||||
Messages: t.TextResult,
|
||||
}
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "[提示词回调] JSON序列化失败 epicycleId=%d 错误=%v", epicycleId, err)
|
||||
return
|
||||
}
|
||||
g.Log().Infof(ctx, "[提示词回调] 开始发送 epicycleId=%d 回调地址=%s 请求头数量=%d 消息体大小=%d字节",
|
||||
t.EpicycleId, callbackURL, len(headers), len(jsonData))
|
||||
|
||||
err = commonHttp.Post(ctx, callbackURL, headers, &resp, jsonData)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "[提示词回调] 发送失败 epicycleId=%d 回调地址=%s 错误=%v", t.EpicycleId, callbackURL, err)
|
||||
return
|
||||
}
|
||||
g.Log().Infof(ctx, "[提示词回调] 发送成功 epicycleId=%d 回调地址=%s 消息体大小=%d字节", t.EpicycleId, callbackURL, len(jsonData))
|
||||
}
|
||||
|
||||
// IsSuperAdmin 调用admin-go服务检查是否是超级管理员
|
||||
func IsSuperAdmin(ctx context.Context) (res bool, err error) {
|
||||
headers := util.ForwardHeaders(ctx)
|
||||
var r = make(map[string]bool)
|
||||
if err = commonHttp.Get(ctx, "admin-go/api/v1/system/user/checkIsSuperAdmin", headers, &r); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return r["isSuperAdmin"], err
|
||||
}
|
||||
|
||||
//// callback 向回调地址 POST 任务结果(与查询接口 GetTaskRes 出参一致)
|
||||
//func (s *audioTaskService) callback(ctx context.Context, taskID, status, errMsg, callbackURL string) {
|
||||
// if callbackURL == "" {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// task, _ := dao.TranscribeTask.GetByTaskID(ctx, taskID)
|
||||
// if task == nil {
|
||||
// g.Log().Errorf(ctx, "[回调 %s] 任务不存在", taskID)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// detailList, _ := dao.TranscribeTaskDetail.ListByTaskID(ctx, taskID)
|
||||
// detailItems := make([]dto.TranscribeTaskDetailItem, 0, len(detailList))
|
||||
// for i := range detailList {
|
||||
// detailItems = append(detailItems, dao.DetailEntityToItem(&detailList[i]))
|
||||
// }
|
||||
//
|
||||
// // 构建与查询接口一致的 taskInfo
|
||||
// taskInfo := dao.EntityToItem(task)
|
||||
//
|
||||
// // 兼容历史数据: 从 result 中补全 scenes 等字段
|
||||
// detailItems = enrichDetailsFromResult(task.Result, detailItems)
|
||||
//
|
||||
// payload := dto.CallbackPayload{
|
||||
// TaskInfo: taskInfo,
|
||||
// DetailList: detailItems,
|
||||
// }
|
||||
//
|
||||
// body, _ := json.Marshal(payload)
|
||||
//
|
||||
// // 透传调用方的用户信息
|
||||
// userJSON, _ := json.Marshal(beans.User{UserName: "admin", TenantId: 1})
|
||||
//
|
||||
// req, _ := http.NewRequest("POST", callbackURL, bytes.NewReader(body))
|
||||
// req.Header.Set("Content-Type", "application/json")
|
||||
// req.Header.Set("X-User-Info", string(userJSON))
|
||||
//
|
||||
// resp, reqErr := http.DefaultClient.Do(req)
|
||||
// if reqErr != nil {
|
||||
// g.Log().Errorf(ctx, "[回调 %s] 请求失败: %v", taskID, reqErr)
|
||||
// return
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
//
|
||||
// respBody, _ := io.ReadAll(resp.Body)
|
||||
// g.Log().Infof(ctx, "[回调 %s] 响应 status=%d, body=%s", taskID, resp.StatusCode, string(respBody))
|
||||
//}
|
||||
256
service/model/model_service.go
Normal file
256
service/model/model_service.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"model-gateway/common/util"
|
||||
"model-gateway/consts/public"
|
||||
"model-gateway/dao"
|
||||
"model-gateway/model/dto"
|
||||
"model-gateway/model/entity"
|
||||
"model-gateway/service/gateway"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||
"gitea.redpowerfuture.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var ModelGatewayModels = &modelService{}
|
||||
|
||||
type modelService struct{}
|
||||
|
||||
// Create 创建模型
|
||||
func (s *modelService) Create(ctx context.Context, req *dto.CreateModelReq) (*dto.CreateModelRes, error) {
|
||||
// 1)如果设为会话模型,先把该用户旧会话模型取消
|
||||
if !g.IsEmpty(req.IsChatModel) && *req.IsChatModel == 1 {
|
||||
if err := s.clearUserChatModel(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// 2)判断是否超管,决定 isOwner
|
||||
req.IsOwner = gconv.PtrInt(1)
|
||||
if isAdmin, _ := gateway.IsSuperAdmin(ctx); isAdmin {
|
||||
req.IsOwner = gconv.PtrInt(0)
|
||||
}
|
||||
|
||||
// 3)入库
|
||||
id, err := dao.ModelGatewayModels.Insert(ctx, util.ConvertTo[entity.ModelGatewayModel](req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.CreateModelRes{ID: id}, nil
|
||||
}
|
||||
|
||||
// Update 更新模型配置
|
||||
func (s *modelService) Update(ctx context.Context, req *dto.UpdateModelReq) error {
|
||||
// 1)会话模型唯一性校验
|
||||
if req.IsChatModel != nil && *req.IsChatModel == 1 {
|
||||
if err := s.checkChatModelUnique(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 2)超管创建/普通用户更新
|
||||
req.IsOwner = gconv.PtrInt(1)
|
||||
if isAdmin, _ := gateway.IsSuperAdmin(ctx); isAdmin {
|
||||
req.IsOwner = gconv.PtrInt(0)
|
||||
_, err := dao.ModelGatewayModels.Update(ctx, util.ConvertTo[entity.ModelGatewayModel](req))
|
||||
return err
|
||||
}
|
||||
// 3)跨租户判断:超管的模型不允许直接修改,走插入新记录
|
||||
model, err := dao.ModelGatewayModels.GetByAcrossTenant(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{Id: req.ID},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if model.TenantId == 1 {
|
||||
_, err = dao.ModelGatewayModels.Insert(ctx, util.ConvertTo[entity.ModelGatewayModel](req))
|
||||
return err
|
||||
}
|
||||
_, err = dao.ModelGatewayModels.Update(ctx, util.ConvertTo[entity.ModelGatewayModel](req))
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete 删除模型
|
||||
func (s *modelService) Delete(ctx context.Context, req *dto.DeleteModelReq) error {
|
||||
_, err := dao.ModelGatewayModels.Delete(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{Id: req.ID},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Get 获取模型详情
|
||||
func (s *modelService) Get(ctx context.Context, req *dto.GetModelReq) (*dto.GetModelRes, error) {
|
||||
user, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if g.IsEmpty(req.ID) {
|
||||
req.Creator = user.UserName
|
||||
}
|
||||
model, err := dao.ModelGatewayModels.Get(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{
|
||||
Id: req.ID,
|
||||
Creator: user.UserName,
|
||||
},
|
||||
ModelName: req.ModelName,
|
||||
IsChatModel: req.IsChatModel,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.GetModelRes{
|
||||
Model: model,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List 获取模型列表
|
||||
func (s *modelService) List(ctx context.Context, req *dto.ListModelReq) (*dto.ListModelRes, error) {
|
||||
// 1)判断超管
|
||||
req.IsOwner = gconv.PtrInt(1)
|
||||
if isAdmin, _ := gateway.IsSuperAdmin(ctx); isAdmin {
|
||||
req.IsOwner = gconv.PtrInt(0)
|
||||
}
|
||||
|
||||
// 2)获取当前用户
|
||||
user, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Creator = user.UserName
|
||||
|
||||
// 3)查询
|
||||
models, total, err := dao.ModelGatewayModels.GetByCreatorAndPlatform(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ListModelRes{List: models, Total: total}, nil
|
||||
}
|
||||
|
||||
// UpdateChatModel 设置会话模型
|
||||
func (s *modelService) UpdateChatModel(ctx context.Context, req *dto.UpdateChatModelReq) error {
|
||||
// 1)校验新模型存在
|
||||
newModel, err := dao.ModelGatewayModels.GetByAcrossTenant(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{Id: req.Id},
|
||||
})
|
||||
if err != nil || newModel == nil {
|
||||
return errors.New("新会话模型不存在")
|
||||
}
|
||||
|
||||
// 2)获取当前用户的会话模型
|
||||
user, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentModel, err := dao.ModelGatewayModels.Get(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{Creator: user.UserName},
|
||||
IsChatModel: gconv.PtrInt(1),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3)事务:取消旧的 + 设置新的
|
||||
return gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if !g.IsEmpty(currentModel) {
|
||||
if currentModel.ModelType != public.ModelTypeInference {
|
||||
return errors.New("当前模型为非推理模型,不能设置为会话模型")
|
||||
}
|
||||
if currentModel.Id != req.Id {
|
||||
_, err = dao.ModelGatewayModels.Update(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{Id: currentModel.Id},
|
||||
IsChatModel: gconv.PtrInt(0),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dao.ModelGatewayModels.Update(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{Id: req.Id},
|
||||
IsChatModel: gconv.PtrInt(1),
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// GetIsChatModel 获取当前用户会话模型
|
||||
func (s *modelService) GetIsChatModel(ctx context.Context) (*dto.GetIsChatModelRes, error) {
|
||||
user, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
model, err := dao.ModelGatewayModels.Get(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{Creator: user.UserName},
|
||||
IsChatModel: gconv.PtrInt(1),
|
||||
})
|
||||
if err != nil || model == nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.GetIsChatModelRes{Model: model}, nil
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
// clearUserChatModel 清除当前用户旧会话模型
|
||||
func (s *modelService) clearUserChatModel(ctx context.Context) error {
|
||||
user, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
model, err := dao.ModelGatewayModels.Get(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{Creator: user.UserName},
|
||||
IsChatModel: gconv.PtrInt(1),
|
||||
})
|
||||
if err != nil || model == nil {
|
||||
return nil
|
||||
}
|
||||
_, err = dao.ModelGatewayModels.Update(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{Id: model.Id},
|
||||
IsChatModel: gconv.PtrInt(0),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// checkChatModelUnique 校验用户是否已有会话模型
|
||||
func (s *modelService) checkChatModelUnique(ctx context.Context) error {
|
||||
user, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
model, err := dao.ModelGatewayModels.Get(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{Creator: user.UserName},
|
||||
IsChatModel: gconv.PtrInt(1),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if model != nil {
|
||||
return errors.New("用户已存在会话模型")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetModelTypesFromConfig 从配置文件读取模型类型
|
||||
func GetModelTypesFromConfig() (res *dto.TypeItem, err error) {
|
||||
// 返回副本,避免外部修改
|
||||
types := make(map[int]string, len(public.ModelTypeName))
|
||||
for k, v := range public.ModelTypeName {
|
||||
types[k] = v
|
||||
}
|
||||
return &dto.TypeItem{
|
||||
Type: types,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetOperatorList 获取运营商列表
|
||||
func GetOperatorList() (res *dto.ListOperatorRes, err error) {
|
||||
return &dto.ListOperatorRes{
|
||||
List: public.OperatorList,
|
||||
}, nil
|
||||
}
|
||||
198
service/queue/auto_tune.go
Normal file
198
service/queue/auto_tune.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"model-gateway/model/dto"
|
||||
|
||||
"model-gateway/consts/public"
|
||||
"model-gateway/model/entity"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// AutoTuneResult 单次调参结果(按 model_name)
|
||||
type AutoTuneResult struct {
|
||||
ModelName string `json:"modelName"` // 模型名称(asynch_models.model_name)
|
||||
Samples int `json:"samples"` // 统计样本数(窗口内 state=2/3 且 started_at/finished_at 非空的任务数量)
|
||||
P90Exec float64 `json:"p90ExecSeconds"` // 执行耗时 P90(秒),口径:finished_at - started_at
|
||||
|
||||
CapMaxConcurrency int `json:"capMaxConcurrency"` // 配置上限:asynch_models.max_concurrency(cap,不会被动态调参覆盖)
|
||||
OldMaxConcurrency int `json:"oldMaxConcurrency"` // 调参前运行时值(Redis),若无则等于 cap
|
||||
NewMaxConcurrency int `json:"newMaxConcurrency"` // 本次计算出的运行时值(将写入 Redis),受 ±50% 约束且不超过 cap
|
||||
|
||||
CapQueueLimit int `json:"capQueueLimit"` // 配置上限:asynch_models.queue_limit(cap,不会被动态调参覆盖)
|
||||
OldQueueLimit int `json:"oldQueueLimit"` // 调参前运行时值(Redis),若无则等于 cap
|
||||
NewQueueLimit int `json:"newQueueLimit"` // 本次计算出的运行时值(将写入 Redis),受 ±50% 约束且不超过 cap
|
||||
|
||||
}
|
||||
|
||||
// AutoTune 由上层定时任务通过接口触发:
|
||||
// - 统计指定时间窗口内该模型任务的执行耗时(finished_at - started_at,取 P90)
|
||||
// - 基于吞吐与 P90 执行耗时估算 max_concurrency 的运行时值(不超过 cap)
|
||||
// - queue_limit 与 expected_seconds 绑定(允许排队时间 = expected_seconds * 2),生成运行时值(不超过 cap)
|
||||
// - 单次调整幅度限制 ±50%,写入 Redis(带 TTL)
|
||||
func AutoTune(ctx context.Context, req *dto.AutoTuneReq) (res *dto.AutoTuneRes, err error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("request cannot be nil")
|
||||
}
|
||||
if req.WindowSeconds <= 0 {
|
||||
req.WindowSeconds = 3600 // 默认1小时
|
||||
}
|
||||
// 1) 读取模型配置(cap),按 model_name 聚合去重(如果表里有多租户重复数据,取较大上限)
|
||||
var modelRows []*entity.ModelGatewayModel
|
||||
if err := gfdb.DB(ctx).Model(ctx, public.TableNameModel).
|
||||
Where("deleted_at IS NULL").
|
||||
Where(entity.ModelGatewayModelCol.Enabled, 1).
|
||||
Scan(&modelRows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modelMap := make(map[string]*entity.ModelGatewayModel)
|
||||
for _, m := range modelRows {
|
||||
if m == nil || m.ModelName == "" {
|
||||
continue
|
||||
}
|
||||
cur := modelMap[m.ModelName]
|
||||
if cur == nil {
|
||||
modelMap[m.ModelName] = m
|
||||
continue
|
||||
}
|
||||
// 取更大的 cap
|
||||
if m.MaxConcurrency > cur.MaxConcurrency {
|
||||
cur.MaxConcurrency = m.MaxConcurrency
|
||||
}
|
||||
if m.MaxConcurrency*2 > cur.MaxConcurrency*2 {
|
||||
cur.MaxConcurrency = m.MaxConcurrency
|
||||
}
|
||||
if m.TimeoutSeconds > cur.TimeoutSeconds {
|
||||
cur.TimeoutSeconds = m.TimeoutSeconds
|
||||
}
|
||||
}
|
||||
if len(modelMap) == 0 {
|
||||
return nil, errors.New("no models found")
|
||||
}
|
||||
|
||||
// 2) 统计指定窗口:按 model_name 计算 cnt 和 P90 执行耗时
|
||||
type statRow struct {
|
||||
ModelName string
|
||||
Cnt int
|
||||
P90Exec float64
|
||||
}
|
||||
var stats []statRow
|
||||
sql := fmt.Sprintf(`
|
||||
SELECT model_name,
|
||||
COUNT(1) AS cnt,
|
||||
COALESCE(percentile_cont(0.9) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (finished_at - started_at))), 0) AS p90_exec
|
||||
FROM %s
|
||||
WHERE deleted_at IS NULL
|
||||
AND state IN (2,3)
|
||||
AND started_at IS NOT NULL
|
||||
AND finished_at IS NOT NULL
|
||||
AND finished_at >= (NOW() - (? || ' seconds')::interval)
|
||||
GROUP BY model_name`, public.TableNameTask)
|
||||
r, err := gfdb.DB(ctx).GetAll(ctx, sql, req.WindowSeconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = r.Structs(&stats)
|
||||
statMap := make(map[string]statRow, len(stats))
|
||||
for _, s := range stats {
|
||||
statMap[s.ModelName] = s
|
||||
}
|
||||
|
||||
// 3) 调参计算
|
||||
const utilization = 0.8
|
||||
const maxChangeRatio = 0.5 // ±50%
|
||||
const queueFactor = 2.0 // 与 expected_seconds 绑定:W_target = expected_seconds * 2
|
||||
|
||||
out := make([]AutoTuneResult, 0, len(modelMap))
|
||||
for modelName, m := range modelMap {
|
||||
s := statMap[modelName]
|
||||
capMax := m.MaxConcurrency
|
||||
capQueue := m.MaxConcurrency * 2
|
||||
oldMax := GetRuntimeMaxConcurrency(ctx, modelName, capMax)
|
||||
oldQueue := GetRuntimeQueueLimit(ctx, modelName, capQueue)
|
||||
|
||||
// 默认:无样本则不调整
|
||||
if s.Cnt <= 0 || s.P90Exec <= 0 {
|
||||
out = append(out, AutoTuneResult{
|
||||
ModelName: modelName,
|
||||
Samples: s.Cnt,
|
||||
P90Exec: s.P90Exec,
|
||||
CapMaxConcurrency: capMax,
|
||||
OldMaxConcurrency: oldMax,
|
||||
NewMaxConcurrency: oldMax,
|
||||
CapQueueLimit: capQueue,
|
||||
OldQueueLimit: oldQueue,
|
||||
NewQueueLimit: oldQueue,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// arrival_rate ≈ 完成数/3600
|
||||
arrivalRate := float64(s.Cnt) / 3600.0
|
||||
|
||||
// desiredMax = ceil(arrivalRate * p90 / utilization)
|
||||
desiredMax := int(math.Ceil(arrivalRate * s.P90Exec / utilization))
|
||||
if desiredMax < 1 {
|
||||
desiredMax = 1
|
||||
}
|
||||
// 单次变化幅度限制
|
||||
minMax := int(math.Floor(float64(oldMax) * (1 - maxChangeRatio)))
|
||||
maxMax := int(math.Ceil(float64(oldMax) * (1 + maxChangeRatio)))
|
||||
if minMax < 1 {
|
||||
minMax = 1
|
||||
}
|
||||
newMax := clampInt(desiredMax, minMax, maxMax)
|
||||
if capMax > 0 {
|
||||
newMax = clampInt(newMax, 1, capMax)
|
||||
}
|
||||
setRuntimeInt(ctx, runtimeMaxConcurrencyKey(modelName), newMax)
|
||||
|
||||
// queue_limit:W_target = expected_seconds * queueFactor
|
||||
exp := m.TimeoutSeconds
|
||||
if exp <= 0 {
|
||||
exp = 60
|
||||
}
|
||||
wTarget := float64(exp) * queueFactor
|
||||
desiredQueue := int(math.Ceil(arrivalRate*wTarget)) + newMax
|
||||
if desiredQueue < newMax {
|
||||
desiredQueue = newMax
|
||||
}
|
||||
|
||||
newQueue := oldQueue
|
||||
if capQueue > 0 {
|
||||
minQ := int(math.Floor(float64(oldQueue) * (1 - maxChangeRatio)))
|
||||
maxQ := int(math.Ceil(float64(oldQueue) * (1 + maxChangeRatio)))
|
||||
if minQ < newMax {
|
||||
minQ = newMax
|
||||
}
|
||||
if maxQ < minQ {
|
||||
maxQ = minQ
|
||||
}
|
||||
newQueue = clampInt(desiredQueue, minQ, maxQ)
|
||||
newQueue = clampInt(newQueue, newMax, capQueue)
|
||||
setRuntimeInt(ctx, runtimeQueueLimitKey(modelName), newQueue)
|
||||
}
|
||||
|
||||
out = append(out, AutoTuneResult{
|
||||
ModelName: modelName,
|
||||
Samples: s.Cnt,
|
||||
P90Exec: s.P90Exec,
|
||||
CapMaxConcurrency: capMax,
|
||||
OldMaxConcurrency: oldMax,
|
||||
NewMaxConcurrency: newMax,
|
||||
CapQueueLimit: capQueue,
|
||||
OldQueueLimit: oldQueue,
|
||||
NewQueueLimit: newQueue,
|
||||
})
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "[auto_tune] done models=%d windowSeconds=%d", len(out), req.WindowSeconds)
|
||||
return &dto.AutoTuneRes{
|
||||
List: out,
|
||||
}, nil
|
||||
}
|
||||
107
service/queue/queue_gate.go
Normal file
107
service/queue/queue_gate.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// ===== 严格 queue_limit:Redis 原子闸门 =====
|
||||
//
|
||||
// 背景:原来的 queue_limit 通过“Count + Insert”做近似控制,分布式并发创建时会短暂超限。
|
||||
// 目标:以 Redis Lua 脚本实现原子校验 + 入队占位,做到严格不超限。
|
||||
//
|
||||
// 计数口径与原逻辑保持一致:只统计 state=0/1(排队中/执行中)。
|
||||
// - CreateTask 成功入库后占用 1 个 slot
|
||||
// - 任务成功/失败(state->2/3)释放 slot
|
||||
// - 失败任务重试(state 3->0)需要再次占用 slot,若占位失败则暂不重试(留在 state=3,下次 cleaner 再尝试)
|
||||
//
|
||||
// 说明:为避免极端情况下“占位泄漏”导致永久占满,采用 ZSET + 过期时间的方式自动回收。
|
||||
// 只要任务实际生命周期远小于 gateTTLSeconds,就可保持严格。
|
||||
|
||||
const (
|
||||
queueGateKeyPrefix = "asynch:qgate:" // asynch:qgate:{modelName}
|
||||
)
|
||||
|
||||
// Lua:清理过期 slot,然后按 limit 做原子判定并占位
|
||||
var queueGateAcquireLua = `
|
||||
local key = KEYS[1]
|
||||
local now = tonumber(ARGV[1])
|
||||
local limit = tonumber(ARGV[2])
|
||||
local expireAt = tonumber(ARGV[3])
|
||||
local member = ARGV[4]
|
||||
local keyTTL = tonumber(ARGV[5])
|
||||
|
||||
-- 先清理过期的占位
|
||||
redis.call("ZREMRANGEBYSCORE", key, "-inf", now)
|
||||
|
||||
local current = tonumber(redis.call("ZCARD", key) or "0")
|
||||
if current >= limit then
|
||||
return 0
|
||||
end
|
||||
redis.call("ZADD", key, expireAt, member)
|
||||
redis.call("EXPIRE", key, keyTTL)
|
||||
return 1
|
||||
`
|
||||
|
||||
// Lua:释放 slot(幂等)
|
||||
var queueGateReleaseLua = `
|
||||
local key = KEYS[1]
|
||||
local member = ARGV[1]
|
||||
redis.call("ZREM", key, member)
|
||||
return 1
|
||||
`
|
||||
|
||||
func queueGateKey(modelName string) string {
|
||||
return fmt.Sprintf("%s%s", queueGateKeyPrefix, modelName)
|
||||
}
|
||||
|
||||
// calcGateTTLSeconds 计算闸门占位的“自动回收 TTL”
|
||||
// 取 expectedSeconds 的倍数并做上下限,避免任务异常导致永久占位。
|
||||
func calcGateTTLSeconds(expectedSeconds int) int {
|
||||
// 默认至少 1 小时;最多 24 小时
|
||||
minTTL := 3600
|
||||
maxTTL := 24 * 3600
|
||||
if expectedSeconds <= 0 {
|
||||
return minTTL
|
||||
}
|
||||
ttl := int(math.Ceil(float64(expectedSeconds) * 10)) // 预计耗时 * 10 做兜底
|
||||
if ttl < minTTL {
|
||||
ttl = minTTL
|
||||
}
|
||||
if ttl > maxTTL {
|
||||
ttl = maxTTL
|
||||
}
|
||||
return ttl
|
||||
}
|
||||
|
||||
// AcquireQueueSlot 严格入队:原子占位(成功返回 true)
|
||||
func AcquireQueueSlot(ctx context.Context, modelName, taskId string, limit int, expectedSeconds int) (bool, error) {
|
||||
if limit <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
key := queueGateKey(modelName)
|
||||
now := time.Now().Unix()
|
||||
ttl := calcGateTTLSeconds(expectedSeconds)
|
||||
expireAt := now + int64(ttl)
|
||||
// keyTTL 要略大于 member TTL,避免 key 先过期导致计数丢失
|
||||
keyTTL := ttl + 60
|
||||
r, err := g.Redis().Do(ctx, "EVAL", queueGateAcquireLua, 1, key, now, limit, expireAt, taskId, keyTTL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("queue gate acquire failed: %w", err)
|
||||
}
|
||||
return gconv.Int(r) == 1, nil
|
||||
}
|
||||
|
||||
// ReleaseQueueSlot 释放占位(幂等)
|
||||
func ReleaseQueueSlot(ctx context.Context, modelName, taskId string) {
|
||||
if taskId == "" || modelName == "" {
|
||||
return
|
||||
}
|
||||
key := queueGateKey(modelName)
|
||||
_, _ = g.Redis().Do(ctx, "EVAL", queueGateReleaseLua, 1, key, taskId)
|
||||
}
|
||||
82
service/queue/runtime_tune.go
Normal file
82
service/queue/runtime_tune.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// 运行时调参存储在 Redis,不修改 asynch_models 中的 cap(最大上限)。
|
||||
// 上层每小时调用 /model/autoTune 写入运行时值;Worker/CreateTask 读取运行时值生效。
|
||||
|
||||
const (
|
||||
runtimeMaxCKeyPrefix = "asynch:runtime:max_concurrency:" // + model_name
|
||||
runtimeQueueKeyPrefix = "asynch:runtime:queue_limit:" // + model_name
|
||||
runtimeTTLSeconds = 2 * 3600 // 2小时,避免一次调参失败导致立即回退
|
||||
)
|
||||
|
||||
func runtimeMaxConcurrencyKey(modelName string) string {
|
||||
return runtimeMaxCKeyPrefix + modelName
|
||||
}
|
||||
func runtimeQueueLimitKey(modelName string) string {
|
||||
return runtimeQueueKeyPrefix + modelName
|
||||
}
|
||||
|
||||
func getRuntimeInt(ctx context.Context, key string) (int, bool) {
|
||||
v, err := g.Redis().Do(ctx, "GET", key)
|
||||
if err != nil || v == nil {
|
||||
return 0, false
|
||||
}
|
||||
iv := gconv.Int(v)
|
||||
if iv <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
return iv, true
|
||||
}
|
||||
|
||||
func setRuntimeInt(ctx context.Context, key string, val int) {
|
||||
if val <= 0 {
|
||||
return
|
||||
}
|
||||
// SETEX key ttl val
|
||||
_, _ = g.Redis().Do(ctx, "SETEX", key, runtimeTTLSeconds, val)
|
||||
}
|
||||
|
||||
// GetRuntimeMaxConcurrency 返回运行时并发上限(<= cap)。若不存在运行时值,则返回 cap。
|
||||
func GetRuntimeMaxConcurrency(ctx context.Context, modelName string, cap int) int {
|
||||
if cap <= 0 {
|
||||
return cap
|
||||
}
|
||||
if v, ok := getRuntimeInt(ctx, runtimeMaxConcurrencyKey(modelName)); ok {
|
||||
if v > cap {
|
||||
return cap
|
||||
}
|
||||
return v
|
||||
}
|
||||
return cap
|
||||
}
|
||||
|
||||
// GetRuntimeQueueLimit 返回运行时队列上限(<= cap)。若不存在运行时值,则返回 cap。
|
||||
func GetRuntimeQueueLimit(ctx context.Context, modelName string, cap int) int {
|
||||
if cap <= 0 {
|
||||
return cap
|
||||
}
|
||||
if v, ok := getRuntimeInt(ctx, runtimeQueueLimitKey(modelName)); ok {
|
||||
if v > cap {
|
||||
return cap
|
||||
}
|
||||
return v
|
||||
}
|
||||
return cap
|
||||
}
|
||||
|
||||
func clampInt(v, minV, maxV int) int {
|
||||
if v < minV {
|
||||
return minV
|
||||
}
|
||||
if v > maxV {
|
||||
return maxV
|
||||
}
|
||||
return v
|
||||
}
|
||||
57
service/queue/semaphore.go
Normal file
57
service/queue/semaphore.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var acquireLua = `
|
||||
local current = tonumber(redis.call("GET", KEYS[1]) or "0")
|
||||
local max = tonumber(ARGV[1])
|
||||
local ttl = tonumber(ARGV[2])
|
||||
if current >= max then
|
||||
return 0
|
||||
end
|
||||
current = redis.call("INCR", KEYS[1])
|
||||
if current == 1 then
|
||||
redis.call("EXPIRE", KEYS[1], ttl)
|
||||
end
|
||||
if current > max then
|
||||
redis.call("DECR", KEYS[1])
|
||||
return 0
|
||||
end
|
||||
return 1
|
||||
`
|
||||
|
||||
var releaseLua = `
|
||||
local current = tonumber(redis.call("DECR", KEYS[1]) or "0")
|
||||
if current <= 0 then
|
||||
redis.call("DEL", KEYS[1])
|
||||
end
|
||||
return 1
|
||||
`
|
||||
|
||||
// AcquireSemaphore 获取并发令牌
|
||||
func AcquireSemaphore(ctx context.Context, key string, max int, ttlSeconds int64) (bool, error) {
|
||||
if max <= 0 {
|
||||
// 不限制
|
||||
return true, nil
|
||||
}
|
||||
if ttlSeconds <= 0 {
|
||||
ttlSeconds = 3600
|
||||
}
|
||||
r, err := g.Redis().Do(ctx, "EVAL", acquireLua, 1, key, max, ttlSeconds)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("获取并发令牌失败: %w", err)
|
||||
}
|
||||
return gconv.Int(r) == 1, nil
|
||||
}
|
||||
|
||||
// ReleaseSemaphore 释放并发令牌
|
||||
func ReleaseSemaphore(ctx context.Context, key string) error {
|
||||
_, err := g.Redis().Do(ctx, "EVAL", releaseLua, 1, key)
|
||||
return err
|
||||
}
|
||||
34
service/stat/stat_service.go
Normal file
34
service/stat/stat_service.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package stat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"model-gateway/model/entity"
|
||||
|
||||
"model-gateway/dao"
|
||||
"model-gateway/model/dto"
|
||||
)
|
||||
|
||||
var ModelGatewayLogsStat = &logsStatService{}
|
||||
|
||||
type logsStatService struct{}
|
||||
|
||||
func (s *logsStatService) List(ctx context.Context, req *dto.ListModelStatReq) (*dto.ListModelStatRes, error) {
|
||||
if req == nil {
|
||||
req = &dto.ListModelStatReq{}
|
||||
}
|
||||
if req.PageNum <= 0 {
|
||||
req.PageNum = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
list, total, err := dao.ModelGatewayLogsStat.List(ctx, req.PageNum, req.PageSize, &entity.ModelGatewayLogsStat{
|
||||
Creator: req.Creator,
|
||||
ModelName: req.ModelName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.ListModelStatRes{List: list, Total: total}, nil
|
||||
}
|
||||
283
service/task/task_service.go
Normal file
283
service/task/task_service.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"model-gateway/common/util"
|
||||
"model-gateway/consts/public"
|
||||
"model-gateway/service/queue"
|
||||
"time"
|
||||
|
||||
"model-gateway/dao"
|
||||
"model-gateway/model/dto"
|
||||
"model-gateway/model/entity"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||
"gitea.redpowerfuture.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var ModelGatewayTask = &taskService{}
|
||||
|
||||
type taskService struct{}
|
||||
|
||||
// Create 创建任务
|
||||
func (s *taskService) Create(ctx context.Context, req *dto.CreateTaskReq) (res *dto.CreateTaskRes, err error) {
|
||||
taskID := uuid.NewString()
|
||||
|
||||
// 1) 检查模型配置,并且获取模型
|
||||
userInfo, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
model, err := dao.ModelGatewayModels.Get(ctx, &entity.ModelGatewayModel{
|
||||
SQLBaseDO: beans.SQLBaseDO{
|
||||
TenantId: userInfo.TenantId,
|
||||
Creator: userInfo.UserName,
|
||||
},
|
||||
ModelName: req.ModelName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if model == nil || (model.Enabled != nil && *model.Enabled != 1) {
|
||||
return nil, errors.New("模型不存在或未启用")
|
||||
}
|
||||
|
||||
// 2) 排队上限(严格控制:Redis 原子闸门)
|
||||
limit := queue.GetRuntimeQueueLimit(ctx, req.ModelName, model.MaxConcurrency*2)
|
||||
if limit > 0 {
|
||||
ok, err := queue.AcquireQueueSlot(ctx, req.ModelName, taskID, limit, model.TimeoutSeconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, errors.New("任务排队已满,请稍后再试")
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 插入任务记录
|
||||
requestPayload := entity.RequestPayload{
|
||||
Body: req.RequestPayload,
|
||||
Headers: util.ParseHeadMsgHeaders(model.HeadMsg),
|
||||
}
|
||||
id, err := dao.ModelGatewayTask.Insert(ctx, &entity.ModelGatewayTask{
|
||||
ModelName: req.ModelName,
|
||||
TaskID: taskID,
|
||||
State: public.TaskStatusPending,
|
||||
BizName: req.BizName,
|
||||
CallbackURL: req.CallbackUrl,
|
||||
RequestPayload: &requestPayload,
|
||||
EpicycleId: req.EpicycleId,
|
||||
})
|
||||
if err != nil { // 入库失败:回滚闸门占位
|
||||
queue.ReleaseQueueSlot(ctx, req.ModelName, taskID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4) 写操作日志(不影响主流程,失败忽略)
|
||||
ip := ""
|
||||
ua := ""
|
||||
apiPath := "/task/createTask"
|
||||
httpMethod := "POST"
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
ip = utils.GetLocalIP()
|
||||
ua = r.UserAgent()
|
||||
apiPath = r.URL.Path
|
||||
httpMethod = r.Method
|
||||
}
|
||||
_, _ = dao.ModelGatewayLogsOp.Insert(ctx, &entity.ModelGatewayLogsOp{
|
||||
IP: ip,
|
||||
UserAgent: ua,
|
||||
APIPath: apiPath,
|
||||
HttpMethod: httpMethod,
|
||||
BizName: req.BizName,
|
||||
ModelName: req.ModelName,
|
||||
TaskID: taskID,
|
||||
OpType: "createTask",
|
||||
Success: 1,
|
||||
CostMs: time.Since(time.Now()).Milliseconds(),
|
||||
RequestPayload: &requestPayload,
|
||||
ResponsePayload: gdb.Map{
|
||||
"taskId": taskID,
|
||||
},
|
||||
})
|
||||
|
||||
// 5) 获取任务信息
|
||||
task, err := dao.ModelGatewayTask.ClaimByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5) 创建成功后立即异步尝试执行当前任务
|
||||
go AsyncWorker.handleOne(util.AsyncCtx(ctx), task, model, req)
|
||||
|
||||
return &dto.CreateTaskRes{TaskID: taskID}, nil
|
||||
}
|
||||
|
||||
// GetResult 获取任务结果
|
||||
func (s *taskService) GetResult(ctx context.Context, taskID string) (res *dto.GetTaskResultRes, err error) {
|
||||
t, err := dao.ModelGatewayTask.Get(ctx, &entity.ModelGatewayTask{
|
||||
TaskID: taskID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t == nil {
|
||||
return nil, errors.New("任务不存在")
|
||||
}
|
||||
return &dto.GetTaskResultRes{
|
||||
OssFile: t.ResultFile.OssFile,
|
||||
State: t.State,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetBatch 批量查询任务;将成功(state=2)的任务更新为已下载(state=4),并写入过期时间
|
||||
func (s *taskService) GetBatch(ctx context.Context, req *dto.GetTaskBatchReq) (res *dto.GetTaskBatchRes, err error) {
|
||||
if req == nil || len(req.TaskIDs) == 0 {
|
||||
return &dto.GetTaskBatchRes{List: []dto.GetTaskBatchItem{}}, nil
|
||||
}
|
||||
// 1) 先查当前租户下的任务列表
|
||||
list, err := dao.ModelGatewayTask.ListByTaskIDs(ctx, req.TaskIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2) 对成功(state=2)的任务:标记为已下载(state=4)
|
||||
for _, t := range list {
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
if t.State != public.BuildTypeNode {
|
||||
continue
|
||||
}
|
||||
_ = dao.ModelGatewayTask.MarkDownloadedByID(ctx, t.Id)
|
||||
|
||||
// 为了本次返回一致性,内存里也更新
|
||||
t.State = public.TaskStatusDownloaded
|
||||
}
|
||||
|
||||
// 3) 组装返回
|
||||
items := make([]dto.GetTaskBatchItem, 0, len(list))
|
||||
for _, t := range list {
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, dto.GetTaskBatchItem{
|
||||
TaskID: t.TaskID,
|
||||
State: t.State,
|
||||
OssFile: t.ResultFile.OssFile,
|
||||
TextResult: t.TextResult,
|
||||
})
|
||||
}
|
||||
return &dto.GetTaskBatchRes{List: items}, nil
|
||||
}
|
||||
|
||||
// List 获取任务列表
|
||||
func (s *taskService) List(ctx context.Context, req *dto.ListTaskReq) (*dto.ListTaskRes, error) {
|
||||
if req.PageNum <= 0 {
|
||||
req.PageNum = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
user, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, total, err := dao.ModelGatewayTask.List(ctx, req.PageNum, req.PageSize, &entity.ModelGatewayTask{
|
||||
SQLBaseDO: beans.SQLBaseDO{
|
||||
Creator: user.UserName,
|
||||
},
|
||||
ModelName: req.ModelName,
|
||||
BizName: req.BizName,
|
||||
State: req.State,
|
||||
TaskID: req.TaskID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.ListTaskRes{List: list, Total: total}, nil
|
||||
}
|
||||
|
||||
// ModelTaskCallback 模型异步任务的回调通知
|
||||
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.ModelGatewayTask.Get(ctx, &entity.ModelGatewayTask{
|
||||
TaskID: req.TaskID,
|
||||
})
|
||||
if err != nil || task == nil {
|
||||
return nil, fmt.Errorf("任务不存在: %s", req.TaskID)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.ModelGatewayTask.GetPendingAsyncTasks(ctx, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 逐个查询
|
||||
var results []dto.QueryTaskItem
|
||||
for _, t := range tasks {
|
||||
// 拿到模型配置
|
||||
model, err := dao.ModelGatewayModels.GetByModelNameForTenant(ctx, t.TenantId, t.ModelName)
|
||||
if err != nil || model == nil || model.QueryConfig == nil {
|
||||
continue
|
||||
}
|
||||
result, err := util.PullTaskResult(ctx, nil, model.QueryConfig, model.HeadMsg)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "[轮询] 查询失败 taskID=%s err=%v", t.TaskID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
501
service/task/worker.go
Normal file
501
service/task/worker.go
Normal file
@@ -0,0 +1,501 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"model-gateway/common/util"
|
||||
"model-gateway/consts/public"
|
||||
"model-gateway/dao"
|
||||
"model-gateway/model/dto"
|
||||
"model-gateway/model/entity"
|
||||
"model-gateway/service/gateway"
|
||||
"model-gateway/service/queue"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var AsyncWorker = &asyncWorker{}
|
||||
|
||||
type asyncWorker struct {
|
||||
}
|
||||
|
||||
// handleOne 执行一次完整的任务
|
||||
func (w *asyncWorker) handleOne(ctx context.Context, task *entity.ModelGatewayTask, model *entity.ModelGatewayModel, req *dto.CreateTaskReq) {
|
||||
var (
|
||||
body = task.RequestPayload.Body
|
||||
maxRetry = model.RetryTimes
|
||||
startTime = time.Now()
|
||||
result map[string]any
|
||||
err error
|
||||
)
|
||||
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 {
|
||||
task.DurationSeconds = int64(time.Since(startTime).Seconds())
|
||||
w.failTask(ctx, task, startTime, err.Error())
|
||||
return
|
||||
}
|
||||
if !acquired {
|
||||
_, _ = dao.ModelGatewayTask.Update(ctx, &entity.ModelGatewayTask{
|
||||
SQLBaseDO: beans.SQLBaseDO{Id: task.Id},
|
||||
State: public.TaskStatusPending,
|
||||
})
|
||||
g.Log().Infof(ctx, "[执行任务][排队] 并发已满,放回队列 taskId=%s", task.TaskID)
|
||||
return
|
||||
}
|
||||
defer func() { _ = queue.ReleaseSemaphore(ctx, semKey) }()
|
||||
|
||||
// ============================================
|
||||
// 2) 调用模型
|
||||
// ============================================
|
||||
switch {
|
||||
case model.CallMode != nil && *model.CallMode == public.CallModeStream:
|
||||
rawBytes, streamErr := w.callModelStream(ctx, task, model, body)
|
||||
if streamErr != nil {
|
||||
w.failTask(ctx, task, startTime, streamErr.Error())
|
||||
return
|
||||
}
|
||||
result, err = util.ParseStreamResponse(rawBytes, model.StreamConfig)
|
||||
case model.CallMode != nil && *model.CallMode == public.CallModeAsync:
|
||||
result, err = w.callModel(ctx, task, model, body)
|
||||
if err == nil {
|
||||
result, err = util.PullTaskResult(ctx, result, model.QueryConfig, model.HeadMsg)
|
||||
}
|
||||
default:
|
||||
result, err = w.callModel(ctx, task, model, body)
|
||||
}
|
||||
if err != nil {
|
||||
w.failTask(ctx, task, startTime, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 3) 缓存临时文件
|
||||
// ============================================
|
||||
if tmpPath, tmpErr := util.SaveTempFileByType(task.TaskID, result, task.TmpFile); tmpErr == nil && tmpPath != "" {
|
||||
task.TmpFile = tmpPath
|
||||
task.Phase = 1
|
||||
_, _ = dao.ModelGatewayTask.Update(ctx, task)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 4) 解析校验 + 响应映射(可重试)
|
||||
// ============================================
|
||||
result, err = w.parseAndRetry(ctx, result, task, model, req, maxRetry, startTime)
|
||||
if err != nil {
|
||||
task.TextResult = result
|
||||
w.failTask(ctx, task, startTime, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 5) 上传 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, task.TaskID)
|
||||
}
|
||||
oss, err = gateway.UploadByTask(ctx, gjson.New(result).MustToJson(), "json")
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
g.Log().Errorf(ctx, "[执行任务][失败] OSS上传失败 taskId=%s attempt=%d/%d err=%v", task.TaskID, attempt, maxRetry, err)
|
||||
if attempt == maxRetry {
|
||||
task.State = public.TaskStatusFailed
|
||||
task.ErrorMsg = err.Error()
|
||||
task.Phase = 1
|
||||
_, _ = dao.ModelGatewayTask.Update(ctx, task)
|
||||
w.failTask(ctx, task, startTime, fmt.Sprintf("OSS上传重试耗尽: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 6) 成功收尾
|
||||
// ============================================
|
||||
task.State = public.TaskStatusSuccess
|
||||
task.DurationSeconds = int64(time.Since(startTime).Seconds())
|
||||
task.ResultFile = &entity.ResultFile{
|
||||
OssFile: oss.FileAddressPrefix + oss.FileURL,
|
||||
FileType: oss.FileFormat,
|
||||
FileSize: int64(oss.FileSize),
|
||||
}
|
||||
task.TextResult = result
|
||||
if _, err = dao.ModelGatewayTask.Update(ctx, task); err != nil {
|
||||
g.Log().Errorf(ctx, "[执行任务][失败] 更新数据库失败 taskId=%s err=%v", task.TaskID, err)
|
||||
return
|
||||
}
|
||||
|
||||
queue.ReleaseQueueSlot(ctx, task.ModelName, task.TaskID)
|
||||
go gateway.TriggerCallback(context.WithoutCancel(ctx), task)
|
||||
if req.EpicycleId != 0 {
|
||||
go gateway.TriggerPromptsCallback(context.WithoutCancel(ctx), task, req.EpicycleId)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "[执行任务][成功] taskId=%s duration=%ds fileType=%s",
|
||||
task.TaskID, task.DurationSeconds, oss.FileFormat)
|
||||
|
||||
_ = os.Remove(task.TmpFile)
|
||||
}
|
||||
|
||||
// callModelStream 调用模型,返回原始字节(不做响应映射,用于流式输出)
|
||||
func (w *asyncWorker) callModelStream(ctx context.Context, task *entity.ModelGatewayTask, model *entity.ModelGatewayModel, body map[string]any) ([]byte, error) {
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
if task.Phase == 1 && strings.TrimSpace(task.TmpFile) != "" {
|
||||
data, err = os.ReadFile(task.TmpFile)
|
||||
if err != nil || len(data) == 0 {
|
||||
data = nil
|
||||
}
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
data, err = InvokeModel(ctx, model, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmpPath, tmpErr := util.SaveTmpResult(task.TaskID, data, "")
|
||||
if tmpErr == nil && tmpPath != "" {
|
||||
task.TmpFile = tmpPath
|
||||
task.Phase = 1
|
||||
_, err = dao.ModelGatewayTask.Update(ctx, task)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "[执行任务][失败] 临时文件保存失败 taskId=%s err=%v", task.TaskID, tmpErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.ModelGatewayTask, model *entity.ModelGatewayModel, body map[string]any) (map[string]any, error) {
|
||||
// 1. 提交异步任务
|
||||
body, err := w.callModel(ctx, task, model, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 2. 拿到 task_id
|
||||
taskID := gjson.New(body).Get(entity.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}
|
||||
}
|
||||
}
|
||||
|
||||
// callModel 调用模型 + 检测文件类型 + 保存临时文件
|
||||
// 返回: 解析后的响应体, error
|
||||
func (w *asyncWorker) callModel(ctx context.Context, task *entity.ModelGatewayTask, model *entity.ModelGatewayModel, body map[string]any) (map[string]any, error) {
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
// 1) 如果已有临时文件且 phase=1,直接读取
|
||||
if task.Phase == 1 && strings.TrimSpace(task.TmpFile) != "" {
|
||||
data, err = os.ReadFile(task.TmpFile)
|
||||
if err != nil || len(data) == 0 {
|
||||
g.Log().Warningf(ctx, "[callModel] 读取临时文件失败,重新调用模型 taskId=%s err=%v", task.TaskID, err)
|
||||
data = nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 没有可用数据,调用模型
|
||||
if data == nil {
|
||||
data, err = InvokeModel(ctx, model, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3) 检测文件类型,保存临时文件
|
||||
_, ext := util.DetectFileType(data)
|
||||
tmpPath, tmpErr := util.SaveTmpResult(task.TaskID, data, ext)
|
||||
if tmpErr == nil && tmpPath != "" {
|
||||
task.TmpFile = tmpPath
|
||||
task.Phase = 1
|
||||
_, err = dao.ModelGatewayTask.Update(ctx, task)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "[执行任务][失败] 临时文件保存失败 taskId=%s err=%v", task.TaskID, tmpErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 检测文件类型,提取文本结果
|
||||
contentType, _ := util.DetectFileType(data)
|
||||
var textResult string
|
||||
if utf8.Valid(data) && (strings.HasPrefix(contentType, "text/") || contentType == "application/json") {
|
||||
textResult = string(data)
|
||||
}
|
||||
|
||||
// 5) 非文本内容,返回错误
|
||||
if textResult == "" {
|
||||
return nil, fmt.Errorf("模型返回非文本内容,contentType=%s", contentType)
|
||||
}
|
||||
|
||||
// 6) 解析并返回
|
||||
return gjson.New(textResult).Map(), nil
|
||||
}
|
||||
|
||||
// parseAndRetry 解析模型返回结果,并重试
|
||||
func (w *asyncWorker) parseAndRetry(ctx context.Context, body map[string]any, task *entity.ModelGatewayTask, model *entity.ModelGatewayModel, req *dto.CreateTaskReq, maxRetry int, startTime time.Time) (map[string]any, error) {
|
||||
for attempt := 0; attempt <= maxRetry; attempt++ {
|
||||
if attempt > 0 {
|
||||
g.Log().Infof(ctx, "[执行任务][重试] JSON解析 第%d/%d次 taskId=%s", attempt, maxRetry, task.TaskID)
|
||||
}
|
||||
|
||||
// 1) 响应映射
|
||||
mapped, err := util.MapResponsePayload(model.ResponseMapping, body)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "[执行任务][映射失败] taskId=%s attempt=%d/%d err=%v", task.TaskID, attempt, maxRetry, err)
|
||||
if attempt == maxRetry {
|
||||
return nil, fmt.Errorf("响应映射重试耗尽: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 2) 先存 token 到数据库,防止后续失败丢失
|
||||
if _, ok := mapped[entity.TotalTokens]; ok {
|
||||
task.ExpendTokens = gconv.Int64(mapped[entity.TotalTokens])
|
||||
_, err = dao.ModelGatewayTask.Update(ctx, &entity.ModelGatewayTask{
|
||||
SQLBaseDO: beans.SQLBaseDO{Id: task.Id},
|
||||
ExpendTokens: task.ExpendTokens,
|
||||
})
|
||||
}
|
||||
|
||||
// 3) 解析 + 校验
|
||||
var parsed map[string]any
|
||||
switch req.BuildType {
|
||||
case public.BuildTypePrompt, public.BuildTypeNode:
|
||||
parsed, err = util.ParseAndValidate(mapped, model)
|
||||
if err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
case public.BuildTypeStruct:
|
||||
parsed = util.ParseStructResult(mapped, entity.ResponseBody)
|
||||
return parsed, nil
|
||||
default:
|
||||
return mapped, nil
|
||||
}
|
||||
|
||||
g.Log().Warningf(ctx, "[执行任务][解析失败] taskId=%s attempt=%d/%d err=%v", task.TaskID, attempt, maxRetry, err)
|
||||
|
||||
if attempt == maxRetry {
|
||||
return nil, fmt.Errorf("JSON解析重试耗尽: %w", err)
|
||||
}
|
||||
|
||||
// 4) 重新调模型(直接调,不走缓存)
|
||||
task.RetryCount++
|
||||
_, _ = dao.ModelGatewayTask.Update(ctx, task)
|
||||
rawData, callErr := InvokeModel(ctx, model, task.RequestPayload.Body)
|
||||
|
||||
if callErr != nil {
|
||||
g.Log().Warningf(ctx, "[执行任务][重调模型失败] taskId=%s attempt=%d/%d err=%v", task.TaskID, attempt, maxRetry, callErr)
|
||||
continue
|
||||
}
|
||||
|
||||
// 5) 解析原始响应,覆盖 body 进入下一轮
|
||||
var rawResp map[string]any
|
||||
if err = json.Unmarshal(rawData, &rawResp); err != nil {
|
||||
g.Log().Warningf(ctx, "[执行任务][Unmarshal失败] taskId=%s err=%v", task.TaskID, err)
|
||||
continue
|
||||
}
|
||||
body = rawResp
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// InvokeModel 调用模型服务,返回二进制结果
|
||||
// modelKey 用于覆盖/补充模型配置 head_msg(例如每次请求携带不同的 X-API-Key)
|
||||
func InvokeModel(ctx context.Context, model *entity.ModelGatewayModel, body map[string]any) ([]byte, error) {
|
||||
// 1) 记录模型调用次数
|
||||
_ = dao.ModelGatewayLogsStat.IncRequestCount(ctx, time.Now(), model.TenantId, model.Creator, model.ModelName)
|
||||
|
||||
// 2)请求参数映射:将标准 payload 按模型配置的 requestMapping 转为模型需要的格式
|
||||
//—— 请求映射实际处理为提示词构建请求,因为有附加字段及其他字段的拼接。这里不方便做请求映射
|
||||
//mappedPayload := util.ReverseMap(model.RequestMapping, payload)
|
||||
|
||||
// 3)构建请求 URL 和超时
|
||||
baseURL := strings.TrimRight(model.BaseURL, "/")
|
||||
timeout := time.Duration(model.TimeoutSeconds) * time.Second
|
||||
client := &http.Client{Timeout: timeout}
|
||||
method := strings.ToUpper(strings.TrimSpace(model.HttpMethod))
|
||||
|
||||
// 4)构建 HTTP 请求
|
||||
var req *http.Request
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
q, err := util.BodyToQuery(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(q) > 0 {
|
||||
if strings.Contains(baseURL, "?") {
|
||||
baseURL = baseURL + "&" + q.Encode()
|
||||
} else {
|
||||
baseURL = baseURL + "?" + q.Encode()
|
||||
}
|
||||
}
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)
|
||||
default:
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader(bodyBytes))
|
||||
}
|
||||
|
||||
// 5)注入请求头:先模型静态配置,再动态 modelKey(后者可覆盖前者)
|
||||
for hk, hv := range util.ParseHeadMsgHeaders(model.HeadMsg) {
|
||||
req.Header.Set(hk, hv)
|
||||
}
|
||||
if model.ApiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+model.ApiKey)
|
||||
}
|
||||
if method != http.MethodGet {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
// 6)发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 7)读取响应体
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 8)检查 HTTP 状态码
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
msg := string(b)
|
||||
return nil, fmt.Errorf("模型服务返回非2xx: %d, body=%s", resp.StatusCode, msg)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// // InvokeModel 调用模型服务,返回二进制结果
|
||||
//
|
||||
// func InvokeModel(ctx context.Context, m *entity.AsynchModel, payload any, modelKey string) ([]byte, error) {
|
||||
// if m == nil || m.BaseURL == "" {
|
||||
// return nil, fmt.Errorf("模型配置不完整")
|
||||
// }
|
||||
// // 请求参数映射
|
||||
// mappedPayload, err := mapRequestPayload(m.RequestMapping, payload)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("请求参数映射失败: %w", err)
|
||||
// }
|
||||
// // 合并请求头
|
||||
// headers := util.ForwardHeaders(ctx)
|
||||
// for hk, hv := range parseHeadMsgHeaders(m.HeadMsg) {
|
||||
// headers[hk] = hv
|
||||
// }
|
||||
// for hk, hv := range parseHeadMsgHeaders(modelKey) {
|
||||
// headers[hk] = hv
|
||||
// }
|
||||
//
|
||||
// // 设置超时
|
||||
// timeout := time.Duration(m.TimeoutSeconds) * time.Second
|
||||
// if timeout <= 0 {
|
||||
// timeout = 600 * time.Second
|
||||
// }
|
||||
// ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
// defer cancel()
|
||||
//
|
||||
// invokeUrl := strings.TrimRight(m.BaseURL, "/")
|
||||
// method := strings.ToUpper(strings.TrimSpace(m.HttpMethod))
|
||||
// if method == "" {
|
||||
// method = http.MethodPost
|
||||
// }
|
||||
//
|
||||
// var respBytes []byte
|
||||
//
|
||||
// switch method {
|
||||
// case http.MethodGet:
|
||||
// err = commonHttp.Get(ctx, invokeUrl, headers, &respBytes, mappedPayload)
|
||||
// default:
|
||||
// err = commonHttp.Post(ctx, invokeUrl, headers, &respBytes, mappedPayload)
|
||||
// }
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// // 响应参数映射
|
||||
// mappedResponse, err := mapResponsePayload(m.ResponseMapping, respBytes)
|
||||
// if err != nil {
|
||||
// g.Log().Warningf(ctx, "响应参数映射失败: %v,返回原始数据", err)
|
||||
// return respBytes, nil
|
||||
// }
|
||||
// return mappedResponse, nil
|
||||
// }
|
||||
|
||||
// failTask 任务失败统一处理:更新数据库 + 释放排队 + 回调
|
||||
func (w *asyncWorker) failTask(ctx context.Context, t *entity.ModelGatewayTask, startTime time.Time, errMsg string) {
|
||||
t.State = 3
|
||||
t.ErrorMsg = errMsg
|
||||
t.DurationSeconds = int64(time.Since(startTime).Seconds())
|
||||
_, err := dao.ModelGatewayTask.Update(ctx, t)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "[执行任务][更新数据库失败] taskId=%s err=%v", t.TaskID, err)
|
||||
}
|
||||
queue.ReleaseQueueSlot(ctx, t.ModelName, t.TaskID)
|
||||
go gateway.TriggerCallback(context.WithoutCancel(ctx), t)
|
||||
}
|
||||
230
update.sql
Normal file
230
update.sql
Normal file
@@ -0,0 +1,230 @@
|
||||
-- =========================
|
||||
-- model_gateway_models
|
||||
-- =========================
|
||||
CREATE TABLE IF NOT EXISTS model_gateway_models (
|
||||
id int8 PRIMARY KEY,
|
||||
tenant_id int8 NOT NULL DEFAULT 0,
|
||||
creator varchar(64) NOT NULL,
|
||||
created_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater varchar(64) NOT NULL,
|
||||
updated_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at timestamp(6),
|
||||
model_name varchar(128) NOT NULL,
|
||||
model_type int2 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 '{}',
|
||||
api_key varchar(256) NOT NULL DEFAULT '',
|
||||
is_private int2 NOT NULL DEFAULT 0,
|
||||
enabled int2 NOT NULL DEFAULT 1,
|
||||
is_chat_model int2 NOT NULL DEFAULT 0,
|
||||
is_owner int2 NOT NULL DEFAULT 99,
|
||||
form_json jsonb NOT NULL DEFAULT '{}',
|
||||
request_mapping jsonb NOT NULL DEFAULT '{}',
|
||||
response_mapping jsonb NOT NULL DEFAULT '{}',
|
||||
response_body varchar(128) NOT NULL DEFAULT '',
|
||||
token_config jsonb NOT NULL DEFAULT '{}',
|
||||
extend_mapping jsonb NOT NULL DEFAULT '{}',
|
||||
query_config jsonb NOT NULL DEFAULT '{}',
|
||||
stream_config jsonb NOT NULL DEFAULT '{}',
|
||||
first_frame varchar(128) NOT NULL DEFAULT '',
|
||||
last_frame varchar(128) NOT NULL DEFAULT '',
|
||||
max_concurrency int4 NOT NULL DEFAULT 10,
|
||||
timeout_seconds int4 NOT NULL DEFAULT 600,
|
||||
retry_times int2 NOT NULL DEFAULT 3,
|
||||
response_token_field varchar(128) NOT NULL DEFAULT '',
|
||||
call_mode int2 NOT NULL DEFAULT 0,
|
||||
required_fields jsonb NOT NULL DEFAULT '[]',
|
||||
max_tokens int4 DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_model_gateway_models_tenant_creator_model ON model_gateway_models (tenant_id, creator, model_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_models_model_name ON model_gateway_models (model_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_models_model_type ON model_gateway_models (model_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_models_tenant_id ON model_gateway_models (tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_models_deleted_at ON model_gateway_models (deleted_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_models_enabled ON model_gateway_models (enabled);
|
||||
|
||||
COMMENT ON TABLE model_gateway_models IS '模型配置表';
|
||||
COMMENT ON COLUMN model_gateway_models.id IS '主键ID(非自增)';
|
||||
COMMENT ON COLUMN model_gateway_models.tenant_id IS '租户ID';
|
||||
COMMENT ON COLUMN model_gateway_models.creator IS '创建人';
|
||||
COMMENT ON COLUMN model_gateway_models.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN model_gateway_models.updater IS '更新人';
|
||||
COMMENT ON COLUMN model_gateway_models.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN model_gateway_models.deleted_at IS '删除时间(软删)';
|
||||
|
||||
COMMENT ON COLUMN model_gateway_models.model_name IS '模型名称';
|
||||
COMMENT ON COLUMN model_gateway_models.model_type IS '模型类型';
|
||||
COMMENT ON COLUMN model_gateway_models.operator_name IS '运营商名称';
|
||||
COMMENT ON COLUMN model_gateway_models.base_url IS '模型地址';
|
||||
COMMENT ON COLUMN model_gateway_models.http_method IS '请求方式 GET/POST';
|
||||
COMMENT ON COLUMN model_gateway_models.head_msg IS '请求头信息';
|
||||
COMMENT ON COLUMN model_gateway_models.api_key IS '调用凭证/密钥';
|
||||
COMMENT ON COLUMN model_gateway_models.is_private IS '是否私有化:0-私有 1-公共';
|
||||
COMMENT ON COLUMN model_gateway_models.enabled IS '是否启用:0-停用 1-启用';
|
||||
COMMENT ON COLUMN model_gateway_models.is_chat_model IS '是否为对话模型:0-否 1-是';
|
||||
COMMENT ON COLUMN model_gateway_models.is_owner IS '1=当前用户创建 0=超级管理员';
|
||||
COMMENT ON COLUMN model_gateway_models.call_mode IS '调用模式:0-同步 1-异步 2-流式';
|
||||
COMMENT ON COLUMN model_gateway_models.form_json IS '动态表单结构';
|
||||
COMMENT ON COLUMN model_gateway_models.request_mapping IS '请求映射';
|
||||
COMMENT ON COLUMN model_gateway_models.response_mapping IS '返回映射';
|
||||
COMMENT ON COLUMN model_gateway_models.response_body IS '返回主体';
|
||||
COMMENT ON COLUMN model_gateway_models.token_config IS 'Token计算配置';
|
||||
COMMENT ON COLUMN model_gateway_models.extend_mapping IS '附加映射';
|
||||
COMMENT ON COLUMN model_gateway_models.query_config IS '查询/回调配置';
|
||||
COMMENT ON COLUMN model_gateway_models.stream_config IS '流式输出配置';
|
||||
COMMENT ON COLUMN model_gateway_models.first_frame IS '首帧图片参数';
|
||||
COMMENT ON COLUMN model_gateway_models.last_frame IS '尾帧图片参数';
|
||||
COMMENT ON COLUMN model_gateway_models.max_concurrency IS '最大并发数';
|
||||
COMMENT ON COLUMN model_gateway_models.timeout_seconds IS '调用模型超时(秒)';
|
||||
COMMENT ON COLUMN model_gateway_models.retry_times IS '失败重试次数';
|
||||
COMMENT ON COLUMN model_gateway_models.response_token_field IS '响应中消耗token的字段映射';
|
||||
COMMENT ON COLUMN model_gateway_models.required_fields IS '必选字段列表';
|
||||
COMMENT ON COLUMN model_gateway_models.max_tokens IS '最大 token 数,0 表示不传';
|
||||
|
||||
|
||||
|
||||
-- =========================
|
||||
-- model_gateway_task
|
||||
-- =========================
|
||||
CREATE TABLE IF NOT EXISTS model_gateway_task (
|
||||
id int8 PRIMARY KEY,
|
||||
tenant_id int8 NOT NULL DEFAULT 0,
|
||||
creator varchar(64) NOT NULL,
|
||||
created_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater varchar(64) NOT NULL,
|
||||
updated_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at timestamp(6),
|
||||
model_name varchar(128) NOT NULL,
|
||||
task_id varchar(64) NOT NULL,
|
||||
biz_name varchar(128) NOT NULL DEFAULT '',
|
||||
callback_url varchar(512) DEFAULT '',
|
||||
state int2 NOT NULL DEFAULT 0,
|
||||
retry_count int4 NOT NULL DEFAULT 0,
|
||||
phase int2 NOT NULL DEFAULT 0,
|
||||
tmp_file text DEFAULT '',
|
||||
error_msg text DEFAULT '',
|
||||
result_file jsonb NOT NULL DEFAULT '{}',
|
||||
request_payload jsonb NOT NULL DEFAULT '{}',
|
||||
text_result jsonb NOT NULL DEFAULT '{}',
|
||||
expend_tokens int8 NOT NULL DEFAULT 0,
|
||||
duration_seconds int8 NOT NULL DEFAULT 0,
|
||||
epicycle_id varchar(64) NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_model_gateway_task_tenant_creator_task_id ON model_gateway_task (tenant_id, creator, task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_task_task_id ON model_gateway_task (task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_task_state ON model_gateway_task (state);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_task_deleted_at ON model_gateway_task (deleted_at);
|
||||
|
||||
COMMENT ON TABLE model_gateway_task IS '模型网关任务表';
|
||||
COMMENT ON COLUMN model_gateway_task.id IS '主键ID';
|
||||
COMMENT ON COLUMN model_gateway_task.tenant_id IS '租户ID';
|
||||
COMMENT ON COLUMN model_gateway_task.creator IS '创建人';
|
||||
COMMENT ON COLUMN model_gateway_task.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN model_gateway_task.updater IS '更新人';
|
||||
COMMENT ON COLUMN model_gateway_task.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN model_gateway_task.deleted_at IS '删除时间(软删)';
|
||||
COMMENT ON COLUMN model_gateway_task.model_name IS '模型名称';
|
||||
COMMENT ON COLUMN model_gateway_task.task_id IS '任务ID(对外返回)';
|
||||
COMMENT ON COLUMN model_gateway_task.biz_name IS '业务名称(调用方模块/系统)';
|
||||
COMMENT ON COLUMN model_gateway_task.callback_url IS '回调地址';
|
||||
COMMENT ON COLUMN model_gateway_task.state IS '0排队中/1执行中/2成功/3失败/4已下载';
|
||||
COMMENT ON COLUMN model_gateway_task.retry_count IS '已重试次数';
|
||||
COMMENT ON COLUMN model_gateway_task.phase IS '执行阶段:0模型阶段/1OSS阶段';
|
||||
COMMENT ON COLUMN model_gateway_task.tmp_file IS '临时结果文件路径';
|
||||
COMMENT ON COLUMN model_gateway_task.error_msg IS '错误信息';
|
||||
COMMENT ON COLUMN model_gateway_task.result_file IS '结果文件:{oss_file, file_type, file_size}';
|
||||
COMMENT ON COLUMN model_gateway_task.request_payload IS '请求参数(JSON)';
|
||||
COMMENT ON COLUMN model_gateway_task.text_result IS '文本类结果';
|
||||
COMMENT ON COLUMN model_gateway_task.expend_tokens IS '消耗token数';
|
||||
COMMENT ON COLUMN model_gateway_task.duration_seconds IS '耗时(秒)';
|
||||
COMMENT ON COLUMN model_gateway_task.epicycle_id IS '轮次ID';
|
||||
|
||||
|
||||
|
||||
-- =========================
|
||||
-- model_gateway_log_stat
|
||||
-- =========================
|
||||
CREATE TABLE IF NOT EXISTS model_gateway_log_stat (
|
||||
day date NOT NULL,
|
||||
tenant_id int8 NOT NULL DEFAULT 0,
|
||||
creator varchar(64) NOT NULL DEFAULT '',
|
||||
model_name varchar(128) NOT NULL DEFAULT '',
|
||||
request_count int8 NOT NULL DEFAULT 0,
|
||||
created_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (day, tenant_id, creator, model_name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_log_stat_day ON model_gateway_log_stat (day);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_log_stat_creator ON model_gateway_log_stat (creator);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_log_stat_model_name ON model_gateway_log_stat (model_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_log_stat_tenant_day ON model_gateway_log_stat (tenant_id, day);
|
||||
|
||||
COMMENT ON TABLE model_gateway_log_stat IS '按天统计表';
|
||||
COMMENT ON COLUMN model_gateway_log_stat.day IS '天(YYYY-MM-DD)';
|
||||
COMMENT ON COLUMN model_gateway_log_stat.tenant_id IS '租户ID';
|
||||
COMMENT ON COLUMN model_gateway_log_stat.creator IS '创建人';
|
||||
COMMENT ON COLUMN model_gateway_log_stat.model_name IS '模型名称';
|
||||
COMMENT ON COLUMN model_gateway_log_stat.request_count IS '请求次数';
|
||||
COMMENT ON COLUMN model_gateway_log_stat.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN model_gateway_log_stat.updated_at IS '更新时间';
|
||||
|
||||
|
||||
-- =========================
|
||||
-- model_gateway_logs_op
|
||||
-- =========================
|
||||
CREATE TABLE IF NOT EXISTS model_gateway_logs_op (
|
||||
id int8 PRIMARY KEY,
|
||||
tenant_id int8 NOT NULL DEFAULT 0,
|
||||
creator varchar(64) NOT NULL,
|
||||
created_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater varchar(64) NOT NULL,
|
||||
updated_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at timestamp(6),
|
||||
ip varchar(64) DEFAULT '',
|
||||
user_agent varchar(256) DEFAULT '',
|
||||
api_path varchar(256) DEFAULT '',
|
||||
http_method varchar(16) DEFAULT '',
|
||||
biz_name varchar(128) NOT NULL DEFAULT '',
|
||||
model_name varchar(128) NOT NULL DEFAULT '',
|
||||
task_id varchar(64) NOT NULL DEFAULT '',
|
||||
op_type varchar(64) NOT NULL DEFAULT 'createTask',
|
||||
success int2 NOT NULL DEFAULT 1,
|
||||
error_msg text DEFAULT '',
|
||||
cost_ms int8 NOT NULL DEFAULT 0,
|
||||
request_payload jsonb,
|
||||
response_payload jsonb
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_logs_op_task_id ON model_gateway_logs_op (task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_logs_op_biz_name ON model_gateway_logs_op (biz_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_logs_op_model_name ON model_gateway_logs_op (model_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_logs_op_op_type ON model_gateway_logs_op (op_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_logs_op_deleted_at ON model_gateway_logs_op (deleted_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_gateway_logs_op_tenant_time ON model_gateway_logs_op (tenant_id, created_at);
|
||||
|
||||
COMMENT ON TABLE model_gateway_logs_op IS '操作日志表';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.id IS '主键ID(非自增)';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.tenant_id IS '租户ID';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.creator IS '创建人';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.updater IS '更新人';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.deleted_at IS '删除时间(软删)';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.ip IS '客户端IP';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.user_agent IS 'User-Agent';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.api_path IS '接口路径';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.http_method IS 'HTTP方法';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.biz_name IS '业务名称(调用方模块/系统)';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.model_name IS '模型名称';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.task_id IS '任务ID';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.op_type IS '操作类型';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.success IS '是否成功:1成功/0失败';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.error_msg IS '错误信息(失败时)';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.cost_ms IS '耗时(毫秒)';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.request_payload IS '请求 JSON';
|
||||
COMMENT ON COLUMN model_gateway_logs_op.response_payload IS '响应 JSON';
|
||||
Reference in New Issue
Block a user